hoomd_mc/translate/sphere.rs
1// Copyright (c) 2024-2026 The Regents of the University of Michigan.
2// Part of hoomd-rs, released under the BSD 3-Clause License.
3
4//! Implement Translation moves on curved surfaces
5
6use rand::{
7 Rng, RngExt,
8 distr::{Distribution, Uniform},
9};
10use rand_distr::StandardNormal;
11
12use crate::{LocalTrial, Translate};
13use hoomd_manifold::Spherical;
14use hoomd_microstate::property::{Point, Position};
15use hoomd_vector::{Cartesian, InnerProduct};
16
17impl LocalTrial<Point<Spherical<3>>> for Translate<Point<Spherical<3>>> {
18 /// Propose local trial moves for a body on the surface of a sphere
19 ///
20 /// # Example
21 /// ```
22 /// use approxim::assert_relative_eq;
23 /// use hoomd_manifold::{Spherical, SphericalDisk};
24 /// use hoomd_mc::{LocalTrial, Translate};
25 /// use hoomd_microstate::property::{Point, Position};
26 /// use hoomd_vector::{Cartesian, InnerProduct, Metric, Vector};
27 /// use rand::{Rng, SeedableRng, rngs::StdRng};
28 ///
29 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
30 /// let mut rng = StdRng::seed_from_u64(14);
31 /// let initial_point = Point::new(Spherical::from_cartesian_coordinates(
32 /// [0.5_f64.sqrt(), 0.5_f64.sqrt(), 0.0].into(),
33 /// ));
34 /// let d = 0.1;
35 /// let translate = Translate::with_maximum_distance(d.try_into()?);
36 ///
37 /// let new_body_properties = translate.propose(&mut rng, initial_point);
38 ///
39 /// // Translation move keeps point on the surface of the sphere
40 /// let new_body_radius = new_body_properties.position.point().norm();
41 /// assert_relative_eq!(new_body_radius, 1.0, epsilon = 1e-8);
42 ///
43 /// // Translation move does not translate the point more than a distance d away
44 /// assert!(
45 /// d > new_body_properties
46 /// .position()
47 /// .distance(&initial_point.position())
48 /// );
49 /// # Ok(())
50 /// # }
51 /// ```
52 #[inline]
53 fn propose<R: Rng>(
54 &self,
55 rng: &mut R,
56 body_properties: Point<Spherical<3>>,
57 ) -> Point<Spherical<3>> {
58 let mut trial = body_properties;
59 let dist = Uniform::new(0.0, self.maximum_distance().get())
60 .expect("max distance must be positive real");
61 let displacement = dist.sample(rng);
62 // let displacement = (self.maximum_distance().get()) * rng.sample::<f64, _>(StandardNormal);
63 let (sn, cs) = (displacement.sin(), displacement.cos());
64 let vec = Cartesian::<3>::from(std::array::from_fn(|_| rng.sample(StandardNormal)));
65 let proj = vec.dot(trial.position.point());
66 let tangent = Cartesian::from([
67 vec[0] - proj * trial.position.coordinates()[0],
68 vec[1] - proj * trial.position.coordinates()[1],
69 vec[2] - proj * trial.position.coordinates()[2],
70 ]);
71 let (unit, _norm) = tangent.to_unit().expect("cannot be null");
72 let new = Cartesian::from([
73 trial.position.coordinates()[0] * cs + unit.get().coordinates[0] * sn,
74 trial.position.coordinates()[1] * cs + unit.get().coordinates[1] * sn,
75 trial.position.coordinates()[2] * cs + unit.get().coordinates[2] * sn,
76 ]);
77 *trial.position_mut() = Spherical::from_cartesian_coordinates(new);
78 trial
79 }
80}
81
82impl LocalTrial<Point<Spherical<4>>> for Translate<Point<Spherical<4>>> {
83 /// Propose local trial moves for a body on the surface of a 3-sphere
84 ///
85 /// # Example
86 /// ```
87 /// use approxim::assert_relative_eq;
88 /// use hoomd_manifold::{Spherical, SphericalDisk};
89 /// use hoomd_mc::{LocalTrial, Translate};
90 /// use hoomd_microstate::property::{Point, Position};
91 /// use hoomd_vector::{Cartesian, InnerProduct, Metric, Vector};
92 /// use rand::{Rng, SeedableRng, rngs::StdRng};
93 /// use std::f64::consts::PI;
94 ///
95 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
96 /// let mut rng = StdRng::seed_from_u64(14);
97 /// let initial_point = Point::new(Spherical::<4>::from_polar_coordinates(
98 /// PI / 4.0,
99 /// PI / 10.0,
100 /// 5.0 * PI / 4.0,
101 /// ));
102 /// let d = 0.1;
103 /// let translate = Translate::with_maximum_distance(d.try_into()?);
104 ///
105 /// let new_body_properties = translate.propose(&mut rng, initial_point);
106 ///
107 /// // Translation move keeps point on the surface of the sphere
108 /// assert_relative_eq!(
109 /// new_body_properties.position().point().norm(),
110 /// 1.0_f64,
111 /// epsilon = 1e-8
112 /// );
113 ///
114 /// // Translation move does not translate the point more than a distance d away
115 /// assert!(
116 /// d > new_body_properties
117 /// .position()
118 /// .distance(&initial_point.position())
119 /// );
120 /// # Ok(())
121 /// # }
122 /// ```
123 #[inline]
124 fn propose<R: Rng>(
125 &self,
126 rng: &mut R,
127 body_properties: Point<Spherical<4>>,
128 ) -> Point<Spherical<4>> {
129 let mut trial = body_properties;
130 let dist = Uniform::new(0.0, self.maximum_distance().get())
131 .expect("max distance must be positive real");
132 let displacement = dist.sample(rng);
133 let (sn, cs) = ((displacement.abs()).sin(), (displacement.abs()).cos());
134 let vec = Cartesian::<4>::from(std::array::from_fn(|_| rng.sample(StandardNormal)));
135 let proj = vec.dot(trial.position.point());
136 let tangent = Cartesian::from([
137 vec[0] - proj * trial.position.coordinates()[0],
138 vec[1] - proj * trial.position.coordinates()[1],
139 vec[2] - proj * trial.position.coordinates()[2],
140 vec[3] - proj * trial.position.coordinates()[3],
141 ]);
142 let (unit, _norm) = tangent.to_unit().expect("cannot be null");
143 let new = Cartesian::from([
144 trial.position.coordinates()[0] * cs + unit.get().coordinates[0] * sn,
145 trial.position.coordinates()[1] * cs + unit.get().coordinates[1] * sn,
146 trial.position.coordinates()[2] * cs + unit.get().coordinates[2] * sn,
147 trial.position.coordinates()[3] * cs + unit.get().coordinates[3] * sn,
148 ]);
149 *trial.position_mut() = Spherical::from_cartesian_coordinates(new);
150 trial
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use approxim::assert_relative_eq;
158 use hoomd_vector::Metric;
159 use rand::{SeedableRng, rngs::StdRng};
160 use rstest::*;
161 use std::f64::consts::PI;
162
163 const N: usize = 256;
164
165 #[rstest]
166 fn translate_2spherical_point(#[values(0.01, 0.1, 1.0)] d: f64) {
167 let mut rng = StdRng::seed_from_u64(1459);
168 let initial_point = Point::new(Spherical::<3>::from_cartesian_coordinates(
169 [0.5_f64.sqrt(), 0.0, 0.5_f64.sqrt()].into(),
170 ));
171 let translate =
172 Translate::with_maximum_distance(d.try_into().expect("hard-coded positive value"));
173
174 for _ in 0..N {
175 let new_body_properties = translate.propose(&mut rng, initial_point);
176 // Translation move keeps point on the surface of the sphere
177 assert_relative_eq!(
178 new_body_properties.position().point().norm(),
179 1.0_f64,
180 epsilon = 1e-8
181 );
182 // Translation move does not translate the point more than a distance d away
183 assert!(
184 d > new_body_properties
185 .position()
186 .distance(initial_point.position())
187 );
188 }
189 }
190
191 #[rstest]
192 fn translate_3spherical_point(#[values(0.01, 0.1, 1.0)] d: f64) {
193 let mut rng = StdRng::seed_from_u64(1459);
194 let initial_point = Point::new(Spherical::<4>::from_polar_coordinates(
195 PI / 4.0,
196 PI / 10.0,
197 5.0 * PI / 4.0,
198 ));
199 let translate =
200 Translate::with_maximum_distance(d.try_into().expect("hard-coded positive value"));
201
202 for _ in 0..N {
203 let new_body_properties = translate.propose(&mut rng, initial_point);
204 // Translation move keeps point on the surface of the sphere
205 assert_relative_eq!(
206 new_body_properties.position().point().norm(),
207 1.0_f64,
208 epsilon = 1e-8
209 );
210 // Translation move does not translate the point more than a distance d away
211 assert!(
212 d > new_body_properties
213 .position()
214 .distance(initial_point.position())
215 );
216 }
217 }
218}