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, Minkowski};
10use hoomd_microstate::property::{Orientation, OrientedHyperbolicPoint, Point, Position};
11use hoomd_vector::Angle;
12
13impl LocalTrial<Point<Hyperbolic<3>>> for Translate<Point<Hyperbolic<3>>> {
14    /// Propose local trial moves for a body on a hyperbolic surface.
15    ///
16    /// # Example
17    /// ```
18    /// use approxim::assert_relative_eq;
19    /// use hoomd_manifold::{Hyperbolic, Minkowski};
20    /// use hoomd_mc::{LocalTrial, Translate};
21    /// use hoomd_microstate::property::{Point, Position};
22    /// use hoomd_vector::Metric;
23    /// use rand::{SeedableRng, rngs::StdRng};
24    ///
25    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
26    /// let mut rng = StdRng::seed_from_u64(13);
27    /// let body_properties = Point::new(Hyperbolic::from_minkowski_coordinates(
28    ///     [1.0, -1.0, (3.0_f64).sqrt()].into(),
29    /// ));
30    /// let d = 0.1;
31    /// let translate = Translate::with_maximum_distance(d.try_into()?);
32    ///
33    /// let new_body_properties = translate.propose(&mut rng, body_properties);
34    ///
35    /// // Translation move keeps the point on the Hyperboloid
36    /// assert_relative_eq!(
37    ///     new_body_properties
38    ///         .position()
39    ///         .point()
40    ///         .distance_squared(&Minkowski::from([0.0, 0.0, 0.0])),
41    ///     -1.0_f64,
42    ///     epsilon = 1e-12
43    /// );
44    ///
45    /// // Translation move does not move the point more than a distance d
46    /// assert!(
47    ///     d > new_body_properties.position().distance(
48    ///         &Hyperbolic::from_minkowski_coordinates(Minkowski::from([
49    ///             1.0,
50    ///             -1.0,
51    ///             (3.0_f64).sqrt()
52    ///         ]))
53    ///     )
54    /// );
55    /// # Ok(())
56    /// # }
57    /// ```
58    #[inline]
59    fn propose<R: Rng>(
60        &self,
61        rng: &mut R,
62        body_properties: Point<Hyperbolic<3>>,
63    ) -> Point<Hyperbolic<3>> {
64        let mut trial = body_properties;
65        let disk = HyperbolicDisk {
66            disk_radius: *self.maximum_distance(),
67            point: *trial.position_mut(),
68        };
69        let trial_sample: Hyperbolic<3> = disk.sample(rng);
70        // push point back onto Hyperboloid
71        *trial.position_mut() = Hyperbolic::from_minkowski_coordinates(Minkowski::from([
72            trial_sample.coordinates()[0],
73            trial_sample.coordinates()[1],
74            (trial_sample.point()[0].powi(2) + trial_sample.point()[1].powi(2) + 1.0_f64).sqrt(),
75        ]));
76        trial
77    }
78}
79
80impl LocalTrial<OrientedHyperbolicPoint<3, Angle>>
81    for Translate<OrientedHyperbolicPoint<3, Angle>>
82{
83    /// Propose local trial moves for an oriented body on a hyperbolic surface.
84    #[inline]
85    fn propose<R: Rng>(
86        &self,
87        rng: &mut R,
88        body_properties: OrientedHyperbolicPoint<3, Angle>,
89    ) -> OrientedHyperbolicPoint<3, Angle> {
90        let mut trial = body_properties;
91        let original_orientation = body_properties.orientation.theta;
92        let disk = HyperbolicDisk {
93            disk_radius: *self.maximum_distance(),
94            point: *trial.position_mut(),
95        };
96        let (trial_sample, boost, rotation) =
97            OrientedHyperbolicPoint::<3, Angle>::sample(&disk, rng);
98        // compute change in orientation
99        let del_phi = OrientedHyperbolicPoint::<3, Angle>::deck_transform(
100            boost,
101            rotation,
102            &body_properties.position,
103        );
104        *trial.orientation_mut() = Angle::from(original_orientation + del_phi);
105        // push point back onto Hyperboloid
106        *trial.position_mut() = Hyperbolic::from_minkowski_coordinates(Minkowski::from([
107            trial_sample.coordinates()[0],
108            trial_sample.coordinates()[1],
109            (trial_sample.point()[0].powi(2) + trial_sample.point()[1].powi(2) + 1.0_f64).sqrt(),
110        ]));
111        trial
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use approxim::assert_relative_eq;
119    use hoomd_manifold::{Hyperbolic, Minkowski};
120    use hoomd_microstate::property::{OrientedHyperbolicPoint, Point, Position};
121    use hoomd_vector::{Angle, Metric};
122    use rand::{SeedableRng, rngs::StdRng};
123    use rstest::*;
124
125    /// Number of trial moves to test
126    const N: usize = 256;
127
128    #[rstest]
129    fn translate_hyperbolic_point(#[values(0.01, 0.1, 1.0)] d: f64) {
130        let mut rng = StdRng::seed_from_u64(42);
131        let rho: f64 = 1.0;
132        let body_properties = Point::new(Hyperbolic::from_minkowski_coordinates(
133            [1.0, 0.0, (2.0_f64).sqrt()].into(),
134        ));
135        let translate =
136            Translate::with_maximum_distance(d.try_into().expect("hard-coded positive real"));
137
138        for _ in 0..N {
139            let new_body_properties = translate.propose(&mut rng, body_properties);
140
141            // Translation move keeps the point on the Hyperboloid
142            assert_relative_eq!(
143                new_body_properties
144                    .position()
145                    .point()
146                    .distance_squared(&Minkowski::from([0.0, 0.0, 0.0])),
147                -(rho.powi(2)),
148                epsilon = 1e-12
149            );
150
151            // Translation move does not move the point more than a distance d
152            assert!(
153                d > new_body_properties.position().distance(
154                    &Hyperbolic::from_minkowski_coordinates(Minkowski::from([
155                        1.0,
156                        0.0,
157                        (2.0_f64).sqrt()
158                    ]),)
159                )
160            );
161        }
162    }
163
164    #[rstest]
165    fn translate_oriented_hyperbolic_point(#[values(0.01, 0.1, 1.0)] d: f64) {
166        let mut rng = StdRng::seed_from_u64(42);
167        let body_properties = OrientedHyperbolicPoint {
168            position: Hyperbolic::from_minkowski_coordinates([1.0, 0.0, (2.0_f64).sqrt()].into()),
169            orientation: Angle::from(0.0),
170        };
171        let translate =
172            Translate::with_maximum_distance(d.try_into().expect("hard-coded positive real"));
173
174        for _ in 0..N {
175            let new_body_properties = translate.propose(&mut rng, body_properties);
176
177            // Translation move keeps the point on the Hyperboloid
178            assert_relative_eq!(
179                new_body_properties
180                    .position()
181                    .point()
182                    .distance_squared(&Minkowski::from([0.0, 0.0, 0.0])),
183                -1.0,
184                epsilon = 1e-12
185            );
186
187            // Translation move does not move the point more than a distance d
188            assert!(
189                d > new_body_properties.position().distance(
190                    &Hyperbolic::from_minkowski_coordinates(Minkowski::from([
191                        1.0,
192                        0.0,
193                        (2.0_f64).sqrt()
194                    ]))
195                )
196            );
197        }
198    }
199}