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}