hoomd_mc/translate/
hyperboloid.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::{Rng, distr::Distribution};
7
8use crate::{LocalTrial, Translate};
9use hoomd_manifold::{Hyperbolic, HyperbolicDisk};
10use hoomd_microstate::property::{Orientation, OrientedHyperbolicPoint, Point, Position};
11use hoomd_utility::valid::PositiveReal;
12use hoomd_vector::Angle;
13
14impl LocalTrial<Point<Hyperbolic<3>>> for Translate<Point<Hyperbolic<3>>> {
15    /// Propose local trial moves for a body on a hyperbolic surface.
16    ///
17    /// # Example
18    /// ```
19    /// use approxim::assert_relative_eq;
20    /// use hoomd_manifold::{Hyperbolic, Minkowski};
21    /// use hoomd_mc::{LocalTrial, Translate};
22    /// use hoomd_microstate::property::{Point, Position};
23    /// use hoomd_vector::Metric;
24    /// use rand::{SeedableRng, rngs::StdRng};
25    ///
26    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
27    /// let mut rng = StdRng::seed_from_u64(13);
28    /// let body_properties = Point::new(Hyperbolic::from_minkowski_coordinates(
29    ///     [1.0, -1.0, (3.0_f64).sqrt()].into(),
30    /// ));
31    /// let d = 0.1;
32    /// let translate = Translate::with_maximum_distance(d.try_into()?);
33    ///
34    /// let new_body_properties = translate.propose(&mut rng, body_properties);
35    ///
36    /// // Translation move keeps the point on the Hyperboloid
37    /// assert_relative_eq!(
38    ///     new_body_properties
39    ///         .position()
40    ///         .point()
41    ///         .distance_squared(&Minkowski::from([0.0, 0.0, 0.0])),
42    ///     -1.0_f64,
43    ///     epsilon = 1e-12
44    /// );
45    ///
46    /// // Translation move does not move the point more than a distance d
47    /// assert!(
48    ///     d > new_body_properties.position().distance(
49    ///         &Hyperbolic::from_minkowski_coordinates(Minkowski::from([
50    ///             1.0,
51    ///             -1.0,
52    ///             (3.0_f64).sqrt()
53    ///         ]))
54    ///     )
55    /// );
56    /// # Ok(())
57    /// # }
58    /// ```
59    #[inline]
60    fn propose<R: Rng>(
61        &self,
62        rng: &mut R,
63        body_properties: Point<Hyperbolic<3>>,
64    ) -> Point<Hyperbolic<3>> {
65        let mut trial = body_properties;
66        let max_distance: PositiveReal = self.maximum_distance
67            * PositiveReal::try_from(0.9).expect("hard-coded positive number");
68        let disk = HyperbolicDisk {
69            disk_radius: max_distance,
70            point: *trial.position_mut(),
71        };
72        let trial_sample: Hyperbolic<3> = disk.sample(rng);
73        let new = trial_sample.coordinates();
74        // store in polar coordinates to ensure point is on disk
75        let theta = new[1].atan2(new[0]);
76        let boost = (new[2]).acosh();
77        *trial.position_mut() = Hyperbolic::<3>::from_polar_coordinates(boost, theta);
78        trial
79    }
80}
81
82impl LocalTrial<OrientedHyperbolicPoint<3, Angle>>
83    for Translate<OrientedHyperbolicPoint<3, Angle>>
84{
85    /// Propose local trial moves for an oriented body on a hyperbolic surface.
86    #[inline]
87    fn propose<R: Rng>(
88        &self,
89        rng: &mut R,
90        body_properties: OrientedHyperbolicPoint<3, Angle>,
91    ) -> OrientedHyperbolicPoint<3, Angle> {
92        let mut trial = body_properties;
93        let original_orientation = body_properties.orientation.theta;
94        let max_distance: PositiveReal = self.maximum_distance
95            * PositiveReal::try_from(0.9).expect("hard-coded positive number");
96        let disk = HyperbolicDisk {
97            disk_radius: max_distance,
98            point: *trial.position_mut(),
99        };
100        let (trial_sample, boost, rotation) =
101            OrientedHyperbolicPoint::<3, Angle>::sample(&disk, rng);
102        // compute change in orientation
103        let del_phi = OrientedHyperbolicPoint::<3, Angle>::deck_transform(
104            boost,
105            rotation,
106            &body_properties.position,
107        );
108        *trial.orientation_mut() = Angle::from(original_orientation + del_phi);
109        // push point back onto Hyperboloid
110        let new = trial_sample.coordinates();
111        let theta = new[1].atan2(new[0]);
112        let boost = (new[2]).acosh();
113        *trial.position_mut() = Hyperbolic::<3>::from_polar_coordinates(boost, theta);
114        trial
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use approxim::assert_relative_eq;
122    use hoomd_manifold::{Hyperbolic, Minkowski};
123    use hoomd_microstate::property::{OrientedHyperbolicPoint, Point, Position};
124    use hoomd_vector::{Angle, Metric};
125    use rand::{SeedableRng, rngs::StdRng};
126    use rstest::*;
127
128    /// Number of trial moves to test
129    const N: usize = 256;
130    const NSTEPS: usize = 1000;
131
132    #[rstest]
133    fn translate_hyperbolic_point(#[values(0.01, 0.1, 1.0)] d: f64) {
134        let mut rng = StdRng::seed_from_u64(42);
135        let body_properties = Point::new(Hyperbolic::from_minkowski_coordinates(
136            [1.0, 0.0, (2.0_f64).sqrt()].into(),
137        ));
138        let translate =
139            Translate::with_maximum_distance(d.try_into().expect("hard-coded positive real"));
140
141        for _ in 0..N {
142            let new_body_properties = translate.propose(&mut rng, body_properties);
143
144            // Translation move keeps the point on the Hyperboloid
145            assert_relative_eq!(
146                new_body_properties
147                    .position()
148                    .point()
149                    .distance_squared(&Minkowski::from([0.0, 0.0, 0.0])),
150                -1.0,
151                epsilon = 1e-8
152            );
153
154            // Translation move does not move the point more than a distance d
155            let dist =
156                new_body_properties
157                    .position()
158                    .distance(&Hyperbolic::from_minkowski_coordinates(Minkowski::from([
159                        1.0,
160                        0.0,
161                        (2.0_f64).sqrt(),
162                    ])));
163            assert!(d > dist);
164        }
165    }
166
167    #[rstest]
168    fn translate_hyperbolic_point_chain(#[values(0.001, 0.01, 0.1, 0.25)] d: f64) {
169        let mut rng = StdRng::seed_from_u64(42);
170        let mut body_properties = Point::new(Hyperbolic::from_minkowski_coordinates(
171            [-1.0, 1.0, (3.0_f64).sqrt()].into(),
172        ));
173        let translate =
174            Translate::with_maximum_distance(d.try_into().expect("hard-coded positive real"));
175
176        for _ in 0..NSTEPS {
177            let new_body_properties = translate.propose(&mut rng, body_properties);
178
179            // Translation move keeps the point on the Hyperboloid
180            #[allow(
181                clippy::cast_possible_truncation,
182                reason = "float is truncated before converting to i32"
183            )]
184            let tolerance = 8_i32
185                - (new_body_properties.position.coordinates()[2]
186                    .log10()
187                    .trunc() as i32);
188            assert_relative_eq!(
189                new_body_properties
190                    .position()
191                    .point()
192                    .distance_squared(&Minkowski::from([0.0, 0.0, 0.0])),
193                -1.0,
194                epsilon = 10.0_f64.powi(-tolerance)
195            );
196
197            // Translation move does not move the point more than a distance d
198            let dist = new_body_properties
199                .position()
200                .distance(&body_properties.position);
201            assert!(d > dist);
202
203            body_properties.position = new_body_properties.position;
204        }
205    }
206
207    #[rstest]
208    fn translate_oriented_hyperbolic_point(#[values(0.001, 0.01, 0.1, 1.0)] d: f64) {
209        let mut rng = StdRng::seed_from_u64(42);
210        let body_properties = OrientedHyperbolicPoint {
211            position: Hyperbolic::from_minkowski_coordinates([1.0, 0.0, (2.0_f64).sqrt()].into()),
212            orientation: Angle::from(0.0),
213        };
214        let translate =
215            Translate::with_maximum_distance(d.try_into().expect("hard-coded positive real"));
216
217        for _ in 0..N {
218            let new_body_properties = translate.propose(&mut rng, body_properties);
219
220            // Translation move keeps the point on the Hyperboloid
221            #[allow(
222                clippy::cast_possible_truncation,
223                reason = "float is truncated before converting to i32"
224            )]
225            let tolerance = 8_i32
226                - (new_body_properties.position.coordinates()[2]
227                    .log10()
228                    .trunc() as i32);
229            assert_relative_eq!(
230                new_body_properties
231                    .position()
232                    .point()
233                    .distance_squared(&Minkowski::from([0.0, 0.0, 0.0])),
234                -1.0,
235                epsilon = 10.0_f64.powi(-tolerance)
236            );
237
238            // Translation move does not move the point more than a distance d
239            assert!(
240                d > new_body_properties.position().distance(
241                    &Hyperbolic::from_minkowski_coordinates(Minkowski::from([
242                        1.0,
243                        0.0,
244                        (2.0_f64).sqrt()
245                    ]))
246                )
247            );
248        }
249    }
250
251    #[rstest]
252    fn translate_oriented_hyperbolic_point_chain(#[values(0.001, 0.01, 0.1, 0.25)] d: f64) {
253        let mut rng = StdRng::seed_from_u64(42);
254        let mut body_properties = OrientedHyperbolicPoint {
255            position: Hyperbolic::from_minkowski_coordinates([1.0, 0.0, (2.0_f64).sqrt()].into()),
256            orientation: Angle::from(0.0),
257        };
258        let translate =
259            Translate::with_maximum_distance(d.try_into().expect("hard-coded positive real"));
260
261        for _ in 0..NSTEPS {
262            let new_body_properties = translate.propose(&mut rng, body_properties);
263
264            // Translation move keeps the point on the Hyperboloid
265            #[allow(
266                clippy::cast_possible_truncation,
267                reason = "float is truncated before converting to i32"
268            )]
269            let tolerance = 8_i32
270                - (new_body_properties.position.coordinates()[2]
271                    .log10()
272                    .trunc() as i32);
273            assert_relative_eq!(
274                new_body_properties
275                    .position()
276                    .point()
277                    .distance_squared(&Minkowski::from([0.0, 0.0, 0.0])),
278                -1.0,
279                epsilon = 10.0_f64.powi(-tolerance)
280            );
281
282            // Translation move does not move the point more than a distance d
283            let dist = new_body_properties
284                .position()
285                .distance(&body_properties.position);
286            assert!(d > dist);
287            body_properties.position = new_body_properties.position;
288        }
289    }
290
291    #[rstest]
292    fn translate_hyperbolic_point_far_from_cusp(#[values(0.001)] d: f64) {
293        //, 0.01, 0.1, 0.25)] d: f64) {
294        let mut rng = StdRng::seed_from_u64(42);
295        let mut body_properties = Point::new(Hyperbolic::from_minkowski_coordinates(
296            [10_000.0, 10_000.0, (200_000_001.0_f64).sqrt()].into(),
297        ));
298        let translate =
299            Translate::with_maximum_distance(d.try_into().expect("hard-coded positive real"));
300
301        for _ in 0..NSTEPS {
302            let new_body_properties = translate.propose(&mut rng, body_properties);
303
304            // Translation move keeps the point on the Hyperboloid
305            #[allow(
306                clippy::cast_possible_truncation,
307                reason = "float is truncated before converting to i32"
308            )]
309            let tolerance = 8_i32
310                - (new_body_properties.position.coordinates()[2]
311                    .log10()
312                    .trunc() as i32);
313            assert_relative_eq!(
314                new_body_properties
315                    .position()
316                    .point()
317                    .distance_squared(&Minkowski::from([0.0, 0.0, 0.0])),
318                -1.0,
319                epsilon = 10.0_f64.powi(-tolerance)
320            );
321
322            // Translation move does not move the point more than a distance d
323            let dist = new_body_properties
324                .position()
325                .distance(&body_properties.position);
326            // when far away from cusp, small displacements are unstable
327            assert!(d > dist);
328            body_properties.position = new_body_properties.position;
329        }
330    }
331    #[rstest]
332    fn translate_oriented_hyperbolic_point_far_from_cusp(#[values(0.001, 0.01, 0.1)] d: f64) {
333        let mut rng = StdRng::seed_from_u64(42);
334        let mut body_properties = OrientedHyperbolicPoint {
335            position: Hyperbolic::from_minkowski_coordinates(
336                [10_000.0, 10_000.0, (200_000_001.0_f64).sqrt()].into(),
337            ),
338            orientation: Angle::default(),
339        };
340        let translate =
341            Translate::with_maximum_distance(d.try_into().expect("hard-coded positive real"));
342
343        for _ in 0..NSTEPS {
344            let new_body_properties = translate.propose(&mut rng, body_properties);
345
346            // Translation move keeps the point on the Hyperboloid
347            #[allow(
348                clippy::cast_possible_truncation,
349                reason = "float is truncated before converting to i32"
350            )]
351            let tolerance = 8_i32
352                - (new_body_properties.position.coordinates()[2]
353                    .log10()
354                    .trunc() as i32);
355            assert_relative_eq!(
356                new_body_properties
357                    .position()
358                    .point()
359                    .distance_squared(&Minkowski::from([0.0, 0.0, 0.0])),
360                -1.0,
361                epsilon = 10.0_f64.powi(-tolerance)
362            );
363
364            // Translation move does not move the point more than a distance d
365            let dist = new_body_properties
366                .position()
367                .distance(&body_properties.position);
368            // when far away from cusp, small displacements are unstable
369            assert!(d > dist);
370            body_properties.position = new_body_properties.position;
371        }
372    }
373}