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