Skip to main content

hoomd_microstate/property/
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 Point
5
6use serde::{Deserialize, Serialize};
7
8use super::Position;
9use crate::Transform;
10use hoomd_manifold::{Hyperbolic, Minkowski, Spherical};
11use hoomd_vector::Cartesian;
12
13/// A position in space and nothing more.
14///
15/// Use [`Point`] as a [`Body`](crate::Body) or [`Site`](crate::Site) property type.
16///
17/// # Example
18///
19/// ```
20/// use hoomd_microstate::property::Point;
21/// use hoomd_vector::Cartesian;
22///
23/// let point = Point::new(Cartesian::from([1.0, -2.0, 3.0]));
24/// ```
25#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
26pub struct Point<P> {
27    /// The location of the point in space.
28    pub position: P,
29}
30
31impl<P> Point<P> {
32    /// Construct a new point at the given position.
33    ///
34    /// # Example
35    ///
36    /// ```
37    /// use hoomd_microstate::property::Point;
38    /// use hoomd_vector::Cartesian;
39    ///
40    /// let point = Point::new(Cartesian::from([1.0, -2.0, 3.0]));
41    /// ```
42    #[inline]
43    #[must_use]
44    pub fn new(position: P) -> Self {
45        Self { position }
46    }
47}
48
49impl<const N: usize> Transform<Point<Cartesian<N>>> for Point<Cartesian<N>> {
50    /// Points transform by vector addition.
51    ///
52    /// ```math
53    /// \vec{r} = \vec{r}_\mathrm{body} + \vec{r}_\mathrm{site}
54    /// ```
55    ///
56    /// ```
57    /// use hoomd_microstate::{Transform, property::Point};
58    /// use hoomd_vector::Cartesian;
59    ///
60    /// let body_properties = Point::new(Cartesian::from([1.0, -2.0, 3.0]));
61    /// let site_properties = Point::new(Cartesian::from([-3.0, 2.0, 1.0]));
62    ///
63    /// let system_site = body_properties.transform(&site_properties);
64    /// assert_eq!(system_site.position, [-2.0, 0.0, 4.0].into());
65    /// ```
66    #[inline]
67    fn transform(&self, site_properties: &Point<Cartesian<N>>) -> Point<Cartesian<N>> {
68        Point {
69            position: self.position + site_properties.position,
70        }
71    }
72}
73
74impl Transform<Point<Hyperbolic<3>>> for Point<Hyperbolic<3>> {
75    /// Move `Point<Hyperbolic<3>>` properties from the local body frame to the
76    /// system frame.
77    ///
78    /// All positions in hyperbolic space are associated with some $`SO(2,1)`$
79    /// transformation which translates the origin to that position. The local
80    /// body frame is the frame in which the body position is the origin. The
81    /// position of the sites in the system frame is obtained by applying the
82    /// transformation associated with the body's position to the sites in the
83    /// local body frame.
84    #[inline]
85    fn transform(&self, site_properties: &Point<Hyperbolic<3>>) -> Point<Hyperbolic<3>> {
86        let body_pos = self.position.coordinates();
87        let body_theta = body_pos[1].atan2(body_pos[0]);
88        let body_boost = (body_pos[2]).acosh();
89        let site_pos = site_properties.position.coordinates();
90        let transformed_point = Minkowski::from([
91            site_pos[0]
92                * ((body_boost.cosh()) * (body_theta.cos()).powi(2) + (body_theta.sin()).powi(2))
93                + site_pos[1]
94                    * (body_theta.sin())
95                    * (body_theta.cos())
96                    * ((body_boost.cosh()) - 1.0)
97                + site_pos[2] * (body_boost.sinh()) * (body_theta.cos()),
98            site_pos[0] * (body_theta.sin()) * (body_theta.cos()) * ((body_boost.cosh()) - 1.0)
99                + site_pos[1]
100                    * ((body_boost.cosh()) * (body_theta.sin()).powi(2)
101                        + (body_theta.cos()).powi(2))
102                + site_pos[2] * (body_boost.sinh()) * (body_theta.sin()),
103            site_pos[0] * (body_boost.sinh()) * (body_theta.cos())
104                + site_pos[1] * (body_boost.sinh()) * (body_theta.sin())
105                + site_pos[2] * (body_boost.cosh()),
106        ]);
107        let new_hyperbolic = Hyperbolic::from_minkowski_coordinates(transformed_point);
108        Point::new(new_hyperbolic)
109    }
110}
111
112impl Transform<Point<Hyperbolic<4>>> for Point<Hyperbolic<4>> {
113    /// Move `Point<Hyperbolic<4>>` properties from the local body frame to the
114    /// system frame.
115    ///
116    /// All positions in hyperbolic space are associated with some $`SO(3,1)`$
117    /// transformation which translates the origin to that position. The local
118    /// body frame is the frame in which the body position is the origin. The
119    /// position of the sites in the system frame is obtained by applying the
120    /// transformation associated with the body's position to the sites in the
121    /// local body frame.
122    #[inline]
123    fn transform(&self, site_properties: &Point<Hyperbolic<4>>) -> Point<Hyperbolic<4>> {
124        let body_point = self.position.coordinates();
125        let body_theta = (body_point[2].powi(2) + body_point[1].powi(2))
126            .sqrt()
127            .atan2(body_point[0]);
128        let body_phi = body_point[2].atan2(body_point[1]);
129        let body_boost = (body_point[3]).acosh();
130        let site_pos = site_properties.position.coordinates();
131        let transformed_point = Minkowski::from([
132            site_pos[0]
133                * ((body_boost.cosh()) * ((body_theta.cos()).powi(2))
134                    + ((body_theta.sin()).powi(2)))
135                + site_pos[1]
136                    * (body_phi.cos())
137                    * (body_theta.sin())
138                    * (body_theta.cos())
139                    * ((body_boost.cosh()) - 1.0)
140                + site_pos[2]
141                    * (body_phi.sin())
142                    * (body_theta.sin())
143                    * (body_theta.cos())
144                    * ((body_boost.cosh()) - 1.0)
145                + site_pos[3] * (body_boost.sinh()) * (body_theta.cos()),
146            site_pos[0]
147                * (body_phi.cos())
148                * (body_theta.sin())
149                * (body_theta.cos())
150                * ((body_boost.cosh()) - 1.0)
151                + site_pos[1]
152                    * (((body_phi.cos()).powi(2))
153                        * ((body_boost.cosh()) * ((body_theta.sin()).powi(2))
154                            + ((body_theta.cos()).powi(2)))
155                        + ((body_phi.sin()).powi(2)))
156                + site_pos[2]
157                    * (body_phi.sin())
158                    * (body_phi.cos())
159                    * ((body_boost.cosh()) * ((body_theta.sin()).powi(2))
160                        + ((body_theta.cos()).powi(2))
161                        - 1.0)
162                + site_pos[3] * (body_boost.sinh()) * (body_theta.sin()) * (body_phi.cos()),
163            site_pos[0]
164                * (body_theta.cos())
165                * (body_theta.sin())
166                * (body_phi.sin())
167                * ((body_boost.cosh()) - 1.0)
168                + site_pos[1]
169                    * (body_phi.cos())
170                    * (body_phi.sin())
171                    * ((body_boost.cosh()) * ((body_theta.sin()).powi(2))
172                        + ((body_theta.cos()).powi(2))
173                        - 1.0)
174                + site_pos[2]
175                    * (((body_phi.sin()).powi(2))
176                        * ((body_boost.cosh()) * ((body_theta.sin()).powi(2))
177                            + ((body_theta.cos()).powi(2)))
178                        + ((body_phi.cos()).powi(2)))
179                + site_pos[3] * (body_boost.sinh()) * (body_theta.sin()) * (body_phi.sin()),
180            site_pos[0] * (body_boost.sinh()) * (body_theta.cos())
181                + site_pos[1] * (body_boost.sinh()) * (body_phi.cos()) * (body_theta.sin())
182                + site_pos[2] * (body_boost.sinh()) * (body_phi.sin()) * (body_theta.sin())
183                + site_pos[3] * (body_boost.cosh()),
184        ]);
185        let new_hyperbolic = Hyperbolic::from_minkowski_coordinates(transformed_point);
186        Point::new(new_hyperbolic)
187    }
188}
189
190impl Transform<Point<Spherical<3>>> for Point<Spherical<3>> {
191    #[inline]
192    /// Move `Point<Sphere<3>>` properties from the local body frame to the
193    /// system frame.
194    ///
195    /// All positions on the 2-sphere are associated with some $`SO(3)`$
196    /// transformation which translates the origin to that position. The local
197    /// body frame is the frame in which the body position is the origin. The
198    /// position of the sites in the system frame is obtained by applying the
199    /// transformation associated with the body's position to the sites in the
200    /// local body frame.
201    fn transform(&self, site_properties: &Point<Spherical<3>>) -> Point<Spherical<3>> {
202        let body_point = self.position.coordinates();
203        let body_phi = body_point[1].atan2(body_point[0]);
204        let body_theta = (body_point[2]).acos();
205        let trial_coords = site_properties.position.coordinates();
206        let transformed_point = Cartesian::from([
207            trial_coords[0] * (body_theta.cos()) * (body_phi.cos())
208                - trial_coords[1] * (body_phi.sin())
209                + trial_coords[2] * (body_theta.sin()) * (body_phi.cos()),
210            trial_coords[0] * (body_theta.cos()) * (body_phi.sin())
211                + trial_coords[1] * (body_phi.cos())
212                + trial_coords[2] * (body_theta.sin()) * (body_phi.sin()),
213            -trial_coords[0] * (body_theta.sin()) + trial_coords[2] * (body_theta.cos()),
214        ]);
215        let new_sphere = Spherical::from_cartesian_coordinates(transformed_point);
216        Point::new(new_sphere)
217    }
218}
219
220impl Transform<Point<Spherical<4>>> for Point<Spherical<4>> {
221    /// Move `Point<Sphere<4>>` properties from the local body frame to the
222    /// system frame.
223    ///
224    /// All positions on the 3-sphere are associated with some $`SU(2)`$
225    /// transformation which translates the origin to that position. The local
226    /// body frame is the frame in which the body position is the origin. The
227    /// position of the sites in the system frame is obtained by applying the
228    /// transformation associated with the body's position to the sites in the
229    /// local body frame.
230    #[inline]
231    fn transform(&self, site_properties: &Point<Spherical<4>>) -> Point<Spherical<4>> {
232        let body_point = self.position.coordinates();
233        let body_phi_1 = (body_point[2].powi(2) + body_point[1].powi(2))
234            .sqrt()
235            .atan2(body_point[0]);
236        let body_theta = (body_point[0].powi(2) + body_point[1].powi(2) + body_point[2].powi(2))
237            .sqrt()
238            .atan2(body_point[3]);
239        let body_phi_2 = body_point[2].atan2(body_point[1]);
240        let trial_coords = site_properties.position.coordinates();
241        let transformed_point = Cartesian::from([
242            trial_coords[0] * (body_theta.cos()) * (body_phi_1.cos())
243                - trial_coords[1] * (body_phi_1.sin())
244                + trial_coords[3] * (body_theta.sin()) * (body_phi_1.cos()),
245            trial_coords[0] * (body_theta.cos()) * (body_phi_1.sin()) * (body_phi_2.cos())
246                + trial_coords[1] * (body_phi_1.cos()) * (body_phi_2.cos())
247                - trial_coords[2] * (body_phi_2.sin())
248                + trial_coords[3] * (body_theta.sin()) * (body_phi_1.sin()) * (body_phi_2.cos()),
249            trial_coords[0] * (body_theta.cos()) * (body_phi_1.sin()) * (body_phi_2.sin())
250                + trial_coords[1] * (body_phi_1.cos()) * (body_phi_2.sin())
251                + trial_coords[2] * (body_phi_2.cos())
252                + trial_coords[3] * (body_theta.sin()) * (body_phi_1.sin()) * (body_phi_2.sin()),
253            -trial_coords[0] * (body_theta.sin()) + trial_coords[3] * (body_theta.cos()),
254        ]);
255        let new_sphere = Spherical::from_cartesian_coordinates(transformed_point);
256        Point::new(new_sphere)
257    }
258}
259
260impl<P> Position for Point<P> {
261    type Position = P;
262
263    #[inline]
264    fn position(&self) -> &P {
265        &self.position
266    }
267
268    #[inline]
269    fn position_mut(&mut self) -> &mut P {
270        &mut self.position
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    use approxim::assert_relative_eq;
279    use hoomd_manifold::{Hyperbolic, Spherical};
280    use hoomd_vector::Cartesian;
281    use std::f64::consts::PI;
282
283    #[test]
284    fn transform_point() {
285        let body = Point::new(Cartesian::from([3.0, -4.0, 5.0]));
286        let site = Point::new(Cartesian::from([-1.0, 2.0, -3.0]));
287        let transformed_site = body.transform(&site);
288        assert_eq!(transformed_site.position, [2.0, -2.0, 2.0].into());
289    }
290
291    #[test]
292    fn transform_h2_point() {
293        let boost: f64 = 1.3;
294        let bump: f64 = 0.1;
295        let body = Point::new(Hyperbolic::<3>::from_polar_coordinates(boost, 0.0));
296        let site = Point::new(Hyperbolic::<3>::from_polar_coordinates(bump, PI / 2.0));
297        let transformed_site = body.transform(&site);
298        assert_relative_eq!(
299            *transformed_site.position().point(),
300            [
301                (boost.sinh()) * (bump.cosh()),
302                bump.sinh(),
303                (boost.cosh()) * (bump.cosh())
304            ]
305            .into(),
306            epsilon = 1e-12
307        );
308    }
309
310    #[test]
311    fn transform_h3_point() {
312        let boost: f64 = 1.4;
313        let bump: f64 = 0.7;
314        let body = Point::new(Hyperbolic::<4>::from_polar_coordinates(boost, 0.0, 0.0));
315        let site = Point::new(Hyperbolic::<4>::from_polar_coordinates(bump, PI / 2.0, 0.0));
316        let transformed_site = body.transform(&site);
317        assert_relative_eq!(
318            *transformed_site.position().point(),
319            [
320                (boost.sinh()) * (bump.cosh()),
321                bump.sinh(),
322                0.0,
323                (boost.cosh()) * (bump.cosh())
324            ]
325            .into(),
326            epsilon = 1e-12
327        );
328    }
329
330    #[test]
331    fn transform_s2_point() {
332        let theta = PI / 5.0;
333        let blip = PI / 10.0;
334        let body = Point::new(Spherical::<3>::from_polar_coordinates(theta, 0.0));
335        let site = Point::new(Spherical::<3>::from_polar_coordinates(blip, PI / 2.0));
336        let transformed_site = body.transform(&site);
337        assert_relative_eq!(
338            *transformed_site.position().point(),
339            [
340                (theta.sin()) * (blip.cos()),
341                blip.sin(),
342                (theta.cos()) * (blip.cos())
343            ]
344            .into()
345        );
346    }
347
348    #[test]
349    fn transform_s3_point() {
350        let theta = PI / 5.0;
351        let blip = PI / 10.0;
352        let body = Point::new(Spherical::<4>::from_polar_coordinates(theta, 0.0, 0.0));
353        let site = Point::new(Spherical::<4>::from_polar_coordinates(blip, PI / 2.0, 0.0));
354        let transformed_site = body.transform(&site);
355        assert_relative_eq!(
356            *transformed_site.position().point(),
357            [
358                (theta.sin()) * (blip.cos()),
359                blip.sin(),
360                0.0,
361                (theta.cos()) * (blip.cos())
362            ]
363            .into()
364        );
365    }
366}