1use 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 #[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 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 #[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 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 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 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 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 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 #[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 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 #[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 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 #[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 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 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 #[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 let dist = new_body_properties
324 .position()
325 .distance(&body_properties.position);
326 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 #[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 let dist = new_body_properties
366 .position()
367 .distance(&body_properties.position);
368 assert!(d > dist);
370 body_properties.position = new_body_properties.position;
371 }
372 }
373}