Skip to main content

hoomd_microstate/property/
oriented_hyperbolic_point.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 `OrientedHyperbolicPoint`
5
6use super::{Orientation, Point, Position};
7use crate::Transform;
8use hoomd_manifold::{Hyperbolic, HyperbolicDisk, Minkowski};
9use hoomd_vector::Angle;
10use rand::{
11    Rng,
12    distr::{Distribution, Uniform},
13};
14use robust::{Coord, orient2d};
15use std::f64::consts::PI;
16
17/// The position and orientation of an extended body in hyperbolic space.
18///
19/// Use [`OrientedHyperbolicPoint`] as a [`Body`](crate::Body) or
20/// [`Site`](crate::Site) property type.
21///
22/// # Example
23///
24/// ```
25/// use hoomd_manifold::{Hyperbolic, Minkowski};
26/// use hoomd_microstate::property::OrientedHyperbolicPoint;
27/// use hoomd_vector::Angle;
28///
29/// let point = OrientedHyperbolicPoint {
30///     position: Hyperbolic::from_minkowski_coordinates(Minkowski::from([
31///         0.0, 0.0, 1.0,
32///     ])),
33///     orientation: Angle::from(2.39),
34/// };
35/// ```
36#[derive(Clone, Copy, Debug, Default, PartialEq)]
37pub struct OrientedHyperbolicPoint<const N: usize, R> {
38    /// The location of the extended body in the system frame.
39    pub position: Hyperbolic<N>,
40    /// Orientation of the body in the global frame.
41    pub orientation: R,
42}
43
44impl OrientedHyperbolicPoint<3, Angle> {
45    /// Compute the intersection of the geodesic arc passing through a point and the
46    /// x axis, given the polar coordinates of the point. Returns a tuple containing
47    /// the x coordinates of the intersection and the signed arc-angle of the path.
48    #[must_use]
49    #[inline]
50    pub fn intersection_point(theta: f64, boost: f64) -> (f64, f64) {
51        let rad = (boost.sinh()) / (1.0 + (boost.cosh())); //radius in poincare coordinates
52        let x_0 = (1.0 + rad.powi(2)
53            - ((1.0 + rad.powi(2)).powi(2) - 4.0 * (rad.powi(2)) * ((theta.cos()).powi(2))).sqrt())
54            / (2.0 * rad * (theta.cos()));
55        let p = Hyperbolic::<3>::from_polar_coordinates(boost, theta);
56        let intersect = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
57            2.0 * x_0 / (1.0 - x_0.powi(2)),
58            0.0,
59            (1.0 + x_0.powi(2)) / (1.0 - x_0.powi(2)),
60        ]));
61        let angle_change = Self::parallel_transport_angle(&p, &intersect);
62        let end_boost = (intersect.coordinates()[2]).acosh();
63        (end_boost, angle_change)
64    }
65    /// Compute the signed angle change when an oriented hyperbolic point is
66    /// translated to another point.
67    #[must_use]
68    #[inline]
69    pub fn parallel_transport_angle(start: &Hyperbolic<3>, destination: &Hyperbolic<3>) -> f64 {
70        let (b1, theta_1) = (
71            (start.coordinates()[2]).acosh(),
72            (start.coordinates()[1]).atan2(start.coordinates()[0]),
73        );
74        let (b2, theta_2) = (
75            (destination.coordinates()[2]).acosh(),
76            (destination.coordinates()[1]).atan2(destination.coordinates()[0]),
77        );
78        // rotate into non-pathological frame
79        let phi_1 = -(theta_2 - theta_1) / 2.0;
80        let phi_2 = (theta_2 - theta_1) / 2.0;
81
82        let p = [
83            (b1.sinh()) * (phi_1.cos()) / (1.0 + (b1.cosh())),
84            (b1.sinh()) * (phi_1.sin()) / (1.0 + (b1.cosh())),
85        ];
86        let q = [
87            (b2.sinh()) * (phi_2.cos()) / (1.0 + (b2.cosh())),
88            (b2.sinh()) * (phi_2.sin()) / (1.0 + (b2.cosh())),
89        ];
90        let center = [
91            ((p[0].powi(2) + p[1].powi(2) + 1.0) * q[1]
92                - (q[0].powi(2) + q[1].powi(2) + 1.0) * p[1])
93                / (2.0 * (p[0] * q[1] - q[0] * p[1])),
94            (-(p[0].powi(2) + p[1].powi(2) + 1.0) * q[0]
95                + (q[0].powi(2) + q[1].powi(2) + 1.0) * p[0])
96                / (2.0 * (p[0] * q[1] - q[0] * p[1])),
97        ];
98        let theta_1 = ((p[1] - center[1]).atan2(p[0] - center[0])).rem_euclid(2.0 * PI);
99        let theta_2 = ((q[1] - center[1]).atan2(q[0] - center[0])).rem_euclid(2.0 * PI);
100        let abs_delta_theta = ((theta_2 - theta_1).abs()).rem_euclid(2.0 * PI);
101        // get the sign of the angle change
102        let p_a = Coord { x: p[0], y: p[1] };
103        let p_b = Coord { x: q[0], y: q[1] };
104        let p_c = Coord {
105            x: center[0],
106            y: center[1],
107        };
108        let sign = orient2d(p_a, p_b, p_c).signum();
109        if sign.is_nan() {
110            0.0
111        } else {
112            sign * abs_delta_theta
113        }
114    }
115    /// Compute the change in orientation associated with a transformation from a given
116    /// position. Isometries of hyperbolic space can be expressed as a boost conjugated
117    /// by a rotation, i.e., $`R(\theta) T(\eta) R(-\theta)`$. Such a transformation can
118    /// be expressed in the Poincare disk representation as the Mobius transformation
119    /// ```math
120    /// g(z) = \begin{bmatrix} \cosh(\eta/2) & e^{i\theta} \sinh(\eta/2) \\ e^{-i\theta}\sinh(\eta/2) & \cosh(\eta/2) \end{bmatrix}
121    /// ```
122    /// This transformation induces a rotation of angle $`\Delta\phi`$ in the tangent
123    /// bundle, where
124    /// ```math
125    /// \Delta \phi = \operatorname{Arg} [g'(z)] = -2 \operatorname{Arg}[e^{-i\theta}\sinh(\eta/2)z + \cosh(\eta/2)]
126    /// ```
127    #[inline]
128    #[must_use]
129    pub fn deck_transform(boost: f64, rotation: f64, position: &Hyperbolic<3>) -> f64 {
130        let tau_over_two = boost / 2.0;
131        let poincare = position.to_poincare();
132        -2.0 * (((tau_over_two.sinh())
133            * ((rotation.cos()) * poincare[1] - (rotation.sin()) * poincare[0]))
134            .atan2(
135                (tau_over_two.cosh())
136                    + (tau_over_two.sinh())
137                        * ((rotation.cos()) * poincare[0] + (rotation.sin()) * poincare[1]),
138            ))
139    }
140    /// Generate a random `HyperbolicDisk` point with the corresponding boost rapidity
141    /// and rotation angle. Function returns a tuple with the random `Hyperbolic<3>`
142    /// point in the first entry, the associated rapidity and angle in the second and
143    /// third entries, respectively.
144    ///
145    /// # Panics
146    /// Panics when maximum boost is a non-positive number.
147    #[inline]
148    #[must_use]
149    pub fn sample<R: Rng + ?Sized>(
150        disk: &HyperbolicDisk,
151        rng: &mut R,
152    ) -> (Hyperbolic<3>, f64, f64) {
153        let max_boost = disk.disk_radius.get();
154        let point = disk.point;
155        let eta = (point.coordinates()[2]).acosh();
156        let phi = point.coordinates()[1].atan2(point.coordinates()[0]);
157        let trial_boost = Uniform::new(0.0, 1.0).expect("r is positive and real");
158        let trial_rotation =
159            Uniform::new(-PI, PI).expect("hard-coded distribution should be valid");
160        let theta = trial_rotation.sample(rng);
161        let v1: f64 = trial_boost.sample(rng);
162        let v = v1.sqrt() * max_boost;
163        let (v_sinh, eta_sinh, eta_cosh) = (v.sinh(), eta.sinh(), eta.cosh());
164        let (phi_sin, phi_cos) = (phi.sin(), phi.cos());
165        let trial_coords = [v_sinh * theta.cos(), v_sinh * theta.sin(), v.cosh()];
166        let transformed_point = Minkowski::from([
167            trial_coords[0] * (eta_cosh * (phi_cos.powi(2)) + phi_sin.powi(2))
168                + trial_coords[1] * phi_sin * phi_cos * (eta_cosh - 1.0)
169                + trial_coords[2] * eta_sinh * phi_cos,
170            trial_coords[0] * phi_sin * phi_cos * (eta_cosh - 1.0)
171                + trial_coords[1] * (eta_cosh * (phi_sin.powi(2)) + phi_cos.powi(2))
172                + trial_coords[2] * eta_sinh * phi_sin,
173            trial_coords[0] * eta_sinh * phi_cos
174                + trial_coords[1] * eta_sinh * phi_sin
175                + trial_coords[2] * eta_cosh,
176        ]);
177        (
178            Hyperbolic::from_minkowski_coordinates(transformed_point),
179            v,
180            theta,
181        )
182    }
183}
184
185/// Treat `Point<Hyperbolic<3>>` sites as constituents of oriented rigid bodies.
186impl Transform<Point<Hyperbolic<3>>> for OrientedHyperbolicPoint<3, Angle> {
187    /// Move `Point<Hyperbolic<3>>` properties from the local body frame to the system frame.
188    /// ```
189    /// use approxim::assert_relative_eq;
190    /// use hoomd_manifold::Hyperbolic;
191    /// use hoomd_microstate::{
192    ///     Transform,
193    ///     property::{OrientedHyperbolicPoint, Point},
194    /// };
195    /// use hoomd_vector::Angle;
196    /// use std::f64::consts::PI;
197    ///
198    /// let body_boost = 1.1;
199    /// let body_orientation = PI / 2.0;
200    /// let site_boost = 0.1;
201    /// let body = OrientedHyperbolicPoint {
202    ///     position: Hyperbolic::<3>::from_polar_coordinates(body_boost, 0.0),
203    ///     orientation: Angle::from(body_orientation),
204    /// };
205    /// let site = Point::new(Hyperbolic::<3>::from_polar_coordinates(
206    ///     site_boost,
207    ///     -PI / 4.0,
208    /// ));
209    /// let transformed_site = body.transform(&site);
210    /// assert_relative_eq!(
211    ///     *transformed_site.position.point(),
212    ///     [
213    ///         (body_boost.sinh()) * (site_boost.cosh())
214    ///             + ((PI / 4.0).cos())
215    ///                 * (body_boost.cosh())
216    ///                 * (site_boost.sinh()),
217    ///         ((PI / 4.0).sin()) * site_boost.sinh(),
218    ///         (body_boost.cosh()) * (site_boost.cosh())
219    ///             + ((PI / 4.0).cos())
220    ///                 * (body_boost.sinh())
221    ///                 * (site_boost.sinh()),
222    ///     ]
223    ///     .into(),
224    ///     epsilon = 1e-12
225    /// );
226    /// ```
227    #[inline]
228    fn transform(&self, site_properties: &Point<Hyperbolic<3>>) -> Point<Hyperbolic<3>> {
229        let body_pos = self.position.coordinates();
230        let body_pos_theta = (body_pos[1].atan2(body_pos[0])).rem_euclid(2.0 * PI);
231        let body_pos_boost = body_pos[2].acosh();
232        let body_angle_system = self.orientation.theta;
233        let body_angle_body =
234            Self::deck_transform(-body_pos_boost, body_pos_theta, self.position())
235                + body_angle_system;
236        let site_pos = site_properties.position.coordinates();
237        let (body_pos_boost_cosh, body_pos_boost_sinh) =
238            (body_pos_boost.cosh(), body_pos_boost.sinh());
239        let (bpb_cos, bpb_sin, diff_cos, diff_sin) = (
240            body_pos_theta.cos(),
241            body_pos_theta.sin(),
242            (body_angle_body - body_pos_theta).cos(),
243            (body_angle_body - body_pos_theta).sin(),
244        );
245        let transformed_point = Minkowski::from([
246            site_pos[0]
247                * (body_pos_boost_cosh * bpb_cos * diff_cos - diff_sin * (body_pos_theta).sin())
248                - site_pos[1] * (bpb_sin * diff_cos + body_pos_boost_cosh * bpb_cos * diff_sin)
249                + site_pos[2] * body_pos_boost_sinh * bpb_cos,
250            site_pos[0] * (body_pos_boost_cosh * bpb_sin * diff_cos + bpb_cos * diff_sin)
251                + site_pos[1] * (bpb_cos * diff_cos - body_pos_boost_cosh * bpb_sin * diff_sin)
252                + site_pos[2] * body_pos_boost_sinh * bpb_sin,
253            site_pos[0] * body_pos_boost_sinh * diff_cos
254                - site_pos[1] * body_pos_boost_sinh * diff_sin
255                + site_pos[2] * body_pos_boost_cosh,
256        ]);
257        let new_hyperbolic = Hyperbolic::from_minkowski_coordinates(transformed_point);
258        Point::new(new_hyperbolic)
259    }
260}
261
262impl Transform<OrientedHyperbolicPoint<3, Angle>> for OrientedHyperbolicPoint<3, Angle> {
263    #[inline]
264    fn transform(
265        &self,
266        site_properties: &OrientedHyperbolicPoint<3, Angle>,
267    ) -> OrientedHyperbolicPoint<3, Angle> {
268        let body_pos = self.position.coordinates();
269        let body_pos_theta = body_pos[1].atan2(body_pos[0]);
270        let body_pos_boost = body_pos[2].acosh();
271        let body_angle_system = self.orientation.theta;
272        let body_angle_body =
273            Self::deck_transform(-body_pos_boost, body_pos_theta, self.position())
274                + body_angle_system;
275        let (body_pos_boost_cosh, body_pos_boost_sinh) =
276            (body_pos_boost.cosh(), body_pos_boost.sinh());
277        let (bpb_cos, bpb_sin, diff_cos, diff_sin) = (
278            body_pos_theta.cos(),
279            body_pos_theta.sin(),
280            (body_angle_body - body_pos_theta).cos(),
281            (body_angle_body - body_pos_theta).sin(),
282        );
283        let site_pos = site_properties.position.coordinates();
284        let transformed_point = Minkowski::from([
285            site_pos[0] * (body_pos_boost_cosh * bpb_cos * diff_cos - diff_sin * bpb_sin)
286                - site_pos[1] * (bpb_sin * diff_cos + body_pos_boost_cosh * bpb_cos * diff_sin)
287                + site_pos[2] * body_pos_boost_sinh * bpb_cos,
288            site_pos[0] * (body_pos_boost_cosh * bpb_sin * diff_cos + bpb_cos * diff_sin)
289                + site_pos[1] * (bpb_cos * diff_cos - body_pos_boost_cosh * bpb_sin * diff_sin)
290                + site_pos[2] * body_pos_boost_sinh * bpb_sin,
291            site_pos[0] * body_pos_boost_sinh * diff_cos
292                - site_pos[1] * body_pos_boost_sinh * diff_sin
293                + site_pos[2] * body_pos_boost_cosh,
294        ]);
295        let new_hyperbolic = Hyperbolic::from_minkowski_coordinates(transformed_point);
296        let site_angle_system =
297            Self::deck_transform(body_pos_boost, body_pos_theta, &site_properties.position)
298                + site_properties.orientation.theta
299                + body_angle_system;
300        OrientedHyperbolicPoint {
301            position: new_hyperbolic,
302            orientation: Angle::from(site_angle_system),
303        }
304    }
305}
306
307impl<const N: usize, R> Position for OrientedHyperbolicPoint<N, R> {
308    type Position = Hyperbolic<N>;
309
310    #[inline]
311    fn position(&self) -> &Hyperbolic<N> {
312        &self.position
313    }
314
315    #[inline]
316    fn position_mut(&mut self) -> &mut Hyperbolic<N> {
317        &mut self.position
318    }
319}
320
321impl<const N: usize, R> Orientation for OrientedHyperbolicPoint<N, R> {
322    type Rotation = R;
323
324    #[inline]
325    fn orientation(&self) -> &R {
326        &self.orientation
327    }
328
329    #[inline]
330    fn orientation_mut(&mut self) -> &mut R {
331        &mut self.orientation
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use approxim::assert_relative_eq;
339    use hoomd_geometry::shape::EightEight;
340    use hoomd_vector::{Angle, Metric};
341    use std::f64::consts::PI;
342
343    #[test]
344    fn transform_oriented_h2_point() {
345        let body_boost = 1.1;
346        let body_orientation = PI / 2.0;
347        let site_boost = 0.1;
348        let body = OrientedHyperbolicPoint {
349            position: Hyperbolic::<3>::from_polar_coordinates(body_boost, 0.0),
350            orientation: Angle::from(body_orientation),
351        };
352        let site = Point::new(Hyperbolic::<3>::from_polar_coordinates(
353            site_boost,
354            -PI / 4.0,
355        ));
356        let transformed_site = body.transform(&site);
357        assert_relative_eq!(
358            *transformed_site.position().point(),
359            [
360                (body_boost.sinh()) * (site_boost.cosh())
361                    + ((PI / 4.0).cos()) * (body_boost.cosh()) * (site_boost.sinh()),
362                ((PI / 4.0).sin()) * site_boost.sinh(),
363                (body_boost.cosh()) * (site_boost.cosh())
364                    + ((PI / 4.0).cos()) * (body_boost.sinh()) * (site_boost.sinh()),
365            ]
366            .into(),
367            epsilon = 1e-12
368        );
369    }
370
371    #[test]
372    fn parallel_transport_method() {
373        let p = [-(2.0_f64).sqrt() / 2.0, (2.0_f64).sqrt() / 2.0];
374        let q = [(2.0_f64).sqrt() / 2.0, (2.0_f64).sqrt() / 2.0];
375        let center = [
376            ((p[0].powi(2) + p[1].powi(2) + 1.0) * q[1]
377                - (q[0].powi(2) + q[1].powi(2) + 1.0) * p[1])
378                / (2.0 * (p[0] * q[1] - q[0] * p[1])),
379            (-(p[0].powi(2) + p[1].powi(2) + 1.0) * q[0]
380                + (q[0].powi(2) + q[1].powi(2) + 1.0) * p[0])
381                / (2.0 * (p[0] * q[1] - q[0] * p[1])),
382        ];
383        let theta_1 = ((p[1] - center[1]).atan2(p[0] - center[0])).rem_euclid(2.0 * PI);
384        let theta_2 = ((q[1] - center[1]).atan2(q[0] - center[0])).rem_euclid(2.0 * PI);
385        let abs_delta_theta = ((theta_2 - theta_1).abs()).rem_euclid(2.0 * PI);
386        // get the sign of the angle change
387        let p_a = Coord { x: p[0], y: p[1] };
388        let p_b = Coord { x: q[0], y: q[1] };
389        let p_c = Coord {
390            x: center[0],
391            y: center[1],
392        };
393        let sign = orient2d(p_a, p_b, p_c).signum();
394        let del_angle = sign * abs_delta_theta;
395        let answer = PI / 2.0;
396
397        assert_relative_eq!(answer, del_angle, epsilon = 1e-8);
398    }
399
400    #[test]
401    fn parallel_transport() {
402        let p1 = Hyperbolic::<3>::from_polar_coordinates(EightEight::EIGHTEIGHT, -PI / 4.0);
403        let p2 = Hyperbolic::<3>::from_polar_coordinates(EightEight::EIGHTEIGHT, 0.0);
404        let del_angle = OrientedHyperbolicPoint::<3, Angle>::parallel_transport_angle(&p1, &p2);
405        assert_relative_eq!(-PI / 2.0, del_angle, epsilon = 1e-12);
406    }
407
408    #[test]
409    fn intersection_points() {
410        let (x_0, del_theta) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
411            PI / 8.0,
412            EightEight::EIGHTEIGHT,
413        );
414        assert_relative_eq!(EightEight::CUSP_TO_EDGE, x_0, epsilon = 1e-12);
415        assert_relative_eq!(PI / 4.0, del_theta, epsilon = 1e-12);
416    }
417
418    #[test]
419    fn shortest_distance() {
420        let offset_boost = 0.2;
421        let v: f64 = 2.448_452_447_678_076;
422        let point = Hyperbolic::<3>::from_polar_coordinates(v - offset_boost, 0.01);
423
424        // get nearest boundary point
425        let point_in_center = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
426            (v.cosh()) * point.coordinates()[0] - (v.sinh()) * point.coordinates()[2],
427            point.coordinates()[1],
428            -(v.sinh()) * point.coordinates()[0] + (v.cosh()) * point.coordinates()[2],
429        ]));
430        let (angle_c, boost_c) = (
431            point_in_center.coordinates()[1].atan2(point_in_center.coordinates()[0]),
432            (point_in_center.coordinates()[2]).acosh(),
433        );
434        let (int_o, _o_o) = OrientedHyperbolicPoint::<3, Angle>::intersection_point(
435            angle_c - 7.0 * PI / 8.0,
436            boost_c,
437        );
438        let intersection_o = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
439            (v.cosh()) * (int_o.sinh()) * ((7.0 * PI / 8.0).cos()) + (v.sinh()) * (int_o.cosh()),
440            (int_o.sinh()) * ((7.0 * PI / 8.0).sin()),
441            (v.sinh()) * (int_o.sinh()) * ((7.0 * PI / 8.0).cos()) + (v.cosh()) * (int_o.cosh()),
442        ]));
443        let blip = 0.05;
444        let intersection_plus = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
445            (v.cosh()) * ((blip + int_o).sinh()) * ((7.0 * PI / 8.0).cos())
446                + (v.sinh()) * ((blip + int_o).cosh()),
447            ((blip + int_o).sinh()) * ((7.0 * PI / 8.0).sin()),
448            (v.sinh()) * ((blip + int_o).sinh()) * ((7.0 * PI / 8.0).cos())
449                + (v.cosh()) * ((blip + int_o).cosh()),
450        ]));
451        let intersection_minus = Hyperbolic::<3>::from_minkowski_coordinates(Minkowski::from([
452            (v.cosh()) * ((blip - int_o).sinh()) * ((7.0 * PI / 8.0).cos())
453                + (v.sinh()) * ((blip - int_o).cosh()),
454            ((blip - int_o).sinh()) * ((7.0 * PI / 8.0).sin()),
455            (v.sinh()) * ((blip - int_o).sinh()) * ((7.0 * PI / 8.0).cos())
456                + (v.cosh()) * ((blip - int_o).cosh()),
457        ]));
458        assert!(point.distance(&intersection_plus) > point.distance(&intersection_o));
459        assert!(point.distance(&intersection_minus) > point.distance(&intersection_o));
460    }
461}