hoomd_microstate/
append.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 `AppendMicrostate` for built-in site and boundary types.
5
6use hoomd_geometry::shape::Hypercuboid;
7use hoomd_gsd::hoomd::{AppendError, Dimensions, Frame, HoomdGsdFile};
8use hoomd_manifold::{Hyperbolic, Spherical};
9use hoomd_vector::{Angle, Cartesian, Versor};
10
11use crate::{
12    AppendMicrostate, Microstate,
13    boundary::{Closed, Periodic},
14    property::{OrientedHyperbolicPoint, OrientedPoint, Point},
15};
16
17impl<B, X> AppendMicrostate<B, Point<Cartesian<2>>, X, Closed<Hypercuboid<2>>> for HoomdGsdFile {
18    #[inline]
19    fn append_microstate(
20        &mut self,
21        microstate: &Microstate<B, Point<Cartesian<2>>, X, Closed<Hypercuboid<2>>>,
22    ) -> Result<Frame<'_>, AppendError> {
23        self.append_frame(microstate.step())?
24            .configuration_box(microstate.boundary().0.to_gsd_box())?
25            .configuration_dimensions(Dimensions::Two)?
26            .particles_position(
27                microstate
28                    .iter_sites_tag_order()
29                    .map(|s| s.properties.position)
30                    .map(|p| [p[0], p[1], 0.0].into()),
31            )
32    }
33}
34
35impl<B, X> AppendMicrostate<B, Point<Cartesian<2>>, X, Periodic<Hypercuboid<2>>> for HoomdGsdFile {
36    #[inline]
37    fn append_microstate(
38        &mut self,
39        microstate: &Microstate<B, Point<Cartesian<2>>, X, Periodic<Hypercuboid<2>>>,
40    ) -> Result<Frame<'_>, AppendError> {
41        self.append_frame(microstate.step())?
42            .configuration_box(microstate.boundary().shape().to_gsd_box())?
43            .configuration_dimensions(Dimensions::Two)?
44            .particles_position(
45                microstate
46                    .iter_sites_tag_order()
47                    .map(|s| s.properties.position)
48                    .map(|p| [p[0], p[1], 0.0].into()),
49            )
50    }
51}
52
53impl<B, X> AppendMicrostate<B, OrientedPoint<Cartesian<2>, Angle>, X, Closed<Hypercuboid<2>>>
54    for HoomdGsdFile
55{
56    #[inline]
57    fn append_microstate(
58        &mut self,
59        microstate: &Microstate<B, OrientedPoint<Cartesian<2>, Angle>, X, Closed<Hypercuboid<2>>>,
60    ) -> Result<Frame<'_>, AppendError> {
61        self.append_frame(microstate.step())?
62            .configuration_box(microstate.boundary().0.to_gsd_box())?
63            .configuration_dimensions(Dimensions::Two)?
64            .particles_position(
65                microstate
66                    .iter_sites_tag_order()
67                    .map(|s| s.properties.position)
68                    .map(|p| [p[0], p[1], 0.0].into()),
69            )?
70            .particles_orientation(
71                microstate
72                    .iter_sites_tag_order()
73                    .map(|s| s.properties.orientation.theta)
74                    .map(|a| {
75                        Versor::from_axis_angle(
76                            [0.0, 0.0, 1.0]
77                                .try_into()
78                                .expect("hard-coded vector can be normalized"),
79                            a,
80                        )
81                    }),
82            )
83    }
84}
85
86impl<B, X> AppendMicrostate<B, OrientedPoint<Cartesian<2>, Angle>, X, Periodic<Hypercuboid<2>>>
87    for HoomdGsdFile
88{
89    #[inline]
90    fn append_microstate(
91        &mut self,
92        microstate: &Microstate<B, OrientedPoint<Cartesian<2>, Angle>, X, Periodic<Hypercuboid<2>>>,
93    ) -> Result<Frame<'_>, AppendError> {
94        self.append_frame(microstate.step())?
95            .configuration_box(microstate.boundary().shape().to_gsd_box())?
96            .configuration_dimensions(Dimensions::Two)?
97            .particles_position(
98                microstate
99                    .iter_sites_tag_order()
100                    .map(|s| s.properties.position)
101                    .map(|p| [p[0], p[1], 0.0].into()),
102            )?
103            .particles_orientation(
104                microstate
105                    .iter_sites_tag_order()
106                    .map(|s| s.properties.orientation.theta)
107                    .map(|a| {
108                        Versor::from_axis_angle(
109                            [0.0, 0.0, 1.0]
110                                .try_into()
111                                .expect("hard-coded vector can be normalized"),
112                            a,
113                        )
114                    }),
115            )
116    }
117}
118
119impl<B, X> AppendMicrostate<B, Point<Cartesian<3>>, X, Closed<Hypercuboid<3>>> for HoomdGsdFile {
120    #[inline]
121    fn append_microstate(
122        &mut self,
123        microstate: &Microstate<B, Point<Cartesian<3>>, X, Closed<Hypercuboid<3>>>,
124    ) -> Result<Frame<'_>, AppendError> {
125        self.append_frame(microstate.step())?
126            .configuration_box(microstate.boundary().0.to_gsd_box())?
127            .configuration_dimensions(Dimensions::Three)?
128            .particles_position(
129                microstate
130                    .iter_sites_tag_order()
131                    .map(|s| s.properties.position),
132            )
133    }
134}
135
136impl<B, X> AppendMicrostate<B, Point<Cartesian<3>>, X, Periodic<Hypercuboid<3>>> for HoomdGsdFile {
137    #[inline]
138    fn append_microstate(
139        &mut self,
140        microstate: &Microstate<B, Point<Cartesian<3>>, X, Periodic<Hypercuboid<3>>>,
141    ) -> Result<Frame<'_>, AppendError> {
142        self.append_frame(microstate.step())?
143            .configuration_box(microstate.boundary().shape().to_gsd_box())?
144            .configuration_dimensions(Dimensions::Three)?
145            .particles_position(
146                microstate
147                    .iter_sites_tag_order()
148                    .map(|s| s.properties.position),
149            )
150    }
151}
152
153impl<B, X> AppendMicrostate<B, OrientedPoint<Cartesian<3>, Versor>, X, Closed<Hypercuboid<3>>>
154    for HoomdGsdFile
155{
156    #[inline]
157    fn append_microstate(
158        &mut self,
159        microstate: &Microstate<B, OrientedPoint<Cartesian<3>, Versor>, X, Closed<Hypercuboid<3>>>,
160    ) -> Result<Frame<'_>, AppendError> {
161        self.append_frame(microstate.step())?
162            .configuration_box(microstate.boundary().0.to_gsd_box())?
163            .configuration_dimensions(Dimensions::Three)?
164            .particles_position(
165                microstate
166                    .iter_sites_tag_order()
167                    .map(|s| s.properties.position),
168            )?
169            .particles_orientation(
170                microstate
171                    .iter_sites_tag_order()
172                    .map(|s| s.properties.orientation),
173            )
174    }
175}
176
177impl<B, X> AppendMicrostate<B, OrientedPoint<Cartesian<3>, Versor>, X, Periodic<Hypercuboid<3>>>
178    for HoomdGsdFile
179{
180    #[inline]
181    fn append_microstate(
182        &mut self,
183        microstate: &Microstate<
184            B,
185            OrientedPoint<Cartesian<3>, Versor>,
186            X,
187            Periodic<Hypercuboid<3>>,
188        >,
189    ) -> Result<Frame<'_>, AppendError> {
190        self.append_frame(microstate.step())?
191            .configuration_box(microstate.boundary().shape().to_gsd_box())?
192            .configuration_dimensions(Dimensions::Three)?
193            .particles_position(
194                microstate
195                    .iter_sites_tag_order()
196                    .map(|s| s.properties.position),
197            )?
198            .particles_orientation(
199                microstate
200                    .iter_sites_tag_order()
201                    .map(|s| s.properties.orientation),
202            )
203    }
204}
205
206impl<B, X, C> AppendMicrostate<B, Point<Spherical<3>>, X, C> for HoomdGsdFile {
207    #[inline]
208    fn append_microstate(
209        &mut self,
210        microstate: &Microstate<B, Point<Spherical<3>>, X, C>,
211    ) -> Result<Frame<'_>, AppendError> {
212        self.append_frame(microstate.step())?
213            .configuration_box([2.0, 2.0, 2.0, 0.0, 0.0, 0.0])?
214            .configuration_dimensions(Dimensions::Three)?
215            .particles_position(
216                microstate
217                    .iter_sites_tag_order()
218                    .map(|s| *s.properties.position.point()),
219            )
220    }
221}
222
223impl<B, X, C> AppendMicrostate<B, Point<Spherical<4>>, X, C> for HoomdGsdFile {
224    #[inline]
225    fn append_microstate(
226        &mut self,
227        microstate: &Microstate<B, Point<Spherical<4>>, X, C>,
228    ) -> Result<Frame<'_>, AppendError> {
229        self.append_frame(microstate.step())?
230            .configuration_box([2.0, 2.0, 2.0, 0.0, 0.0, 0.0])?
231            .configuration_dimensions(Dimensions::Three)?
232            .particles_position(microstate.iter_sites_tag_order().map(|s| {
233                let proj: Vec<f64> = s.properties.position.stereographic_projection();
234                [proj[0], proj[1], proj[2]].into()
235            }))
236    }
237}
238
239impl<B, X, C> AppendMicrostate<B, OrientedHyperbolicPoint<3, Angle>, X, C> for HoomdGsdFile {
240    #[inline]
241    fn append_microstate(
242        &mut self,
243        microstate: &Microstate<B, OrientedHyperbolicPoint<3, Angle>, X, C>,
244    ) -> Result<Frame<'_>, AppendError> {
245        self.append_frame(microstate.step())?
246            .configuration_box([2.0, 2.0, 0.0, 0.0, 0.0, 0.0])?
247            .configuration_dimensions(Dimensions::Two)?
248            .particles_position(
249                microstate
250                    .iter_sites_tag_order()
251                    .map(|s| s.properties.position.to_poincare())
252                    .map(|p| [p[0], p[1], 0.0].into()),
253            )?
254            .particles_orientation(
255                microstate
256                    .iter_sites_tag_order()
257                    .map(|s| s.properties.orientation.theta)
258                    .map(|a| {
259                        Versor::from_axis_angle(
260                            [0.0, 0.0, 1.0]
261                                .try_into()
262                                .expect("hard-coded vector can be normalized"),
263                            a,
264                        )
265                    }),
266            )
267    }
268}
269
270impl<B, X, C> AppendMicrostate<B, Point<Hyperbolic<3>>, X, C> for HoomdGsdFile {
271    #[inline]
272    fn append_microstate(
273        &mut self,
274        microstate: &Microstate<B, Point<Hyperbolic<3>>, X, C>,
275    ) -> Result<Frame<'_>, AppendError> {
276        self.append_frame(microstate.step())?
277            .configuration_box([2.0, 2.0, 0.0, 0.0, 0.0, 0.0])?
278            .configuration_dimensions(Dimensions::Two)?
279            .particles_position(
280                microstate
281                    .iter_sites_tag_order()
282                    .map(|s| s.properties.position.to_poincare())
283                    .map(|p| [p[0], p[1], 0.0].into()),
284            )
285    }
286}
287
288#[cfg(test)]
289mod test {
290    use approxim::assert_relative_eq;
291    use assert2::assert;
292    use std::f64::consts::PI;
293    use tempfile::tempdir;
294
295    use super::*;
296    use crate::{Body, boundary::Open};
297    use hoomd_geometry::shape::{EightEight, Rectangle};
298    use hoomd_gsd::file_layer::{GsdFile, Mode};
299
300    #[test]
301    fn point_closed_rectangle_2d() -> anyhow::Result<()> {
302        let boundary = Closed(Rectangle {
303            edge_lengths: [12.0.try_into()?, 18.0.try_into()?],
304        });
305
306        let microstate = Microstate::builder()
307            .boundary(boundary)
308            .bodies([
309                Body::point(Cartesian::from([1.0, 0.0])),
310                Body::point(Cartesian::from([-1.0, 2.0])),
311            ])
312            .step(1234)
313            .try_build()?;
314
315        let tmp_dir = tempdir()?;
316        let path = tmp_dir.path().join("test.gsd");
317        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
318        hoomd_gsd_file.append_microstate(&microstate)?;
319
320        drop(hoomd_gsd_file);
321
322        let gsd_file = GsdFile::open(path, Mode::Read)?;
323
324        assert!(gsd_file.n_frames() == 1);
325
326        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
327        itertools::assert_equal(step, [1234]);
328
329        let positions = gsd_file.iter_arrays::<f32, 3>(0, "particles/position")?;
330        itertools::assert_equal(positions, [[1.0, 0.0, 0.0], [-1.0, 2.0, 0.0]]);
331
332        let dimensions = gsd_file.iter_scalars::<u8>(0, "configuration/dimensions")?;
333        itertools::assert_equal(dimensions, [2]);
334
335        let box_ = gsd_file.iter_scalars::<f32>(0, "configuration/box")?;
336        itertools::assert_equal(box_, [12.0_f32, 18.0, 0.0, 0.0, 0.0, 0.0]);
337
338        Ok(())
339    }
340
341    #[test]
342    fn point_periodic_rectangle_2d() -> anyhow::Result<()> {
343        let boundary = Periodic::new(
344            0.0,
345            Rectangle {
346                edge_lengths: [12.0.try_into()?, 18.0.try_into()?],
347            },
348        )?;
349
350        let microstate = Microstate::builder()
351            .boundary(boundary)
352            .bodies([
353                Body::point(Cartesian::from([1.0, 0.0])),
354                Body::point(Cartesian::from([-1.0, 2.0])),
355            ])
356            .step(1234)
357            .try_build()?;
358
359        let tmp_dir = tempdir()?;
360        let path = tmp_dir.path().join("test.gsd");
361        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
362        hoomd_gsd_file.append_microstate(&microstate)?;
363
364        drop(hoomd_gsd_file);
365
366        let gsd_file = GsdFile::open(path, Mode::Read)?;
367
368        assert!(gsd_file.n_frames() == 1);
369
370        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
371        itertools::assert_equal(step, [1234]);
372
373        let positions = gsd_file.iter_arrays::<f32, 3>(0, "particles/position")?;
374        itertools::assert_equal(positions, [[1.0, 0.0, 0.0], [-1.0, 2.0, 0.0]]);
375
376        let dimensions = gsd_file.iter_scalars::<u8>(0, "configuration/dimensions")?;
377        itertools::assert_equal(dimensions, [2]);
378
379        let box_ = gsd_file.iter_scalars::<f32>(0, "configuration/box")?;
380        itertools::assert_equal(box_, [12.0_f32, 18.0, 0.0, 0.0, 0.0, 0.0]);
381
382        Ok(())
383    }
384
385    #[test]
386    fn point_spherical_2d() -> anyhow::Result<()> {
387        let microstate = Microstate::builder()
388            .boundary(Open)
389            .bodies([
390                Body::point(Spherical::from_cartesian_coordinates(Cartesian::from([
391                    -1.0, 0.0, 0.0,
392                ]))),
393                Body::point(Spherical::from_cartesian_coordinates(Cartesian::from([
394                    0.0,
395                    f64::sqrt(0.5),
396                    f64::sqrt(0.5),
397                ]))),
398                Body::point(Spherical::from_cartesian_coordinates(Cartesian::from([
399                    -f64::sqrt(0.25),
400                    0.0,
401                    f64::sqrt(0.75),
402                ]))),
403            ])
404            .step(1234)
405            .try_build()?;
406
407        let tmp_dir = tempdir()?;
408        let path = tmp_dir.path().join("test.gsd");
409        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
410        hoomd_gsd_file.append_microstate(&microstate)?;
411
412        drop(hoomd_gsd_file);
413
414        let gsd_file = GsdFile::open(path, Mode::Read)?;
415
416        assert!(gsd_file.n_frames() == 1);
417
418        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
419        itertools::assert_equal(step, [1234]);
420
421        let positions: Vec<[f32; 3]> = gsd_file
422            .iter_arrays::<f32, 3>(0, "particles/position")?
423            .collect();
424        assert_relative_eq!(positions[0][0], -1.0);
425        assert_relative_eq!(positions[0][1], 0.0);
426        assert_relative_eq!(positions[0][2], 0.0);
427        assert_relative_eq!(positions[1][0], 0.0);
428        assert_relative_eq!(positions[1][1], f32::sqrt(0.5));
429        assert_relative_eq!(positions[1][2], f32::sqrt(0.5));
430        assert_relative_eq!(positions[2][0], -f32::sqrt(0.25));
431        assert_relative_eq!(positions[2][1], 0.0);
432        assert_relative_eq!(positions[2][2], f32::sqrt(0.75));
433        Ok(())
434    }
435
436    #[test]
437    fn point_spherical_3d() -> anyhow::Result<()> {
438        let microstate = Microstate::builder()
439            .boundary(Open)
440            .bodies([
441                Body::point(Spherical::from_cartesian_coordinates(Cartesian::from([
442                    1.0, 0.0, 0.0, 0.0,
443                ]))),
444                Body::point(Spherical::from_cartesian_coordinates(Cartesian::from([
445                    f64::sqrt(1.0 / 3.0),
446                    -f64::sqrt(1.0 / 3.0),
447                    f64::sqrt(1.0 / 3.0),
448                    0.0,
449                ]))),
450                Body::point(Spherical::from_cartesian_coordinates(Cartesian::from([
451                    0.0,
452                    0.0,
453                    f64::sqrt(0.5),
454                    f64::sqrt(0.5),
455                ]))),
456            ])
457            .step(1234)
458            .try_build()?;
459
460        let tmp_dir = tempdir()?;
461        let path = tmp_dir.path().join("test.gsd");
462        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
463        hoomd_gsd_file.append_microstate(&microstate)?;
464
465        drop(hoomd_gsd_file);
466
467        let gsd_file = GsdFile::open(path, Mode::Read)?;
468
469        assert!(gsd_file.n_frames() == 1);
470
471        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
472        itertools::assert_equal(step, [1234]);
473
474        let positions: Vec<[f32; 3]> = gsd_file
475            .iter_arrays::<f32, 3>(0, "particles/position")?
476            .collect();
477        assert_relative_eq!(positions[0][0], 1.0);
478        assert_relative_eq!(positions[0][1], 0.0);
479        assert_relative_eq!(positions[0][2], 0.0);
480        assert_relative_eq!(positions[1][0], f32::sqrt(1.0 / 3.0));
481        assert_relative_eq!(positions[1][1], -f32::sqrt(1.0 / 3.0));
482        assert_relative_eq!(positions[1][2], f32::sqrt(1.0 / 3.0));
483        assert_relative_eq!(positions[2][0], 0.0);
484        assert_relative_eq!(positions[2][1], 0.0);
485        assert_relative_eq!(positions[2][2], f32::sqrt(0.5) / (1.0 - f32::sqrt(0.5)));
486        Ok(())
487    }
488
489    #[test]
490    fn point_hyperbolic_2d_open() -> anyhow::Result<()> {
491        let microstate = Microstate::builder()
492            .boundary(Open)
493            .bodies([
494                Body::point(Hyperbolic::<3>::from_polar_coordinates(1.2, 0.0)),
495                Body::point(Hyperbolic::<3>::from_polar_coordinates(0.6, 1.5)),
496            ])
497            .step(1234)
498            .try_build()?;
499
500        let tmp_dir = tempdir()?;
501        let path = tmp_dir.path().join("test.gsd");
502        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
503        hoomd_gsd_file.append_microstate(&microstate)?;
504
505        drop(hoomd_gsd_file);
506
507        let gsd_file = GsdFile::open(path, Mode::Read)?;
508
509        assert!(gsd_file.n_frames() == 1);
510
511        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
512        itertools::assert_equal(step, [1234]);
513
514        let positions: Vec<[f32; 3]> = gsd_file
515            .iter_arrays::<f32, 3>(0, "particles/position")?
516            .collect();
517        assert_relative_eq!(positions[0][0], (f32::sinh(1.2)) / (1.0 + f32::cosh(1.2)));
518        assert_relative_eq!(positions[0][1], 0.0);
519        assert_relative_eq!(
520            positions[1][0],
521            (f32::sinh(0.6) * f32::cos(1.5)) / (1.0 + f32::cosh(0.6))
522        );
523        assert_relative_eq!(
524            positions[1][1],
525            (f32::sinh(0.6) * f32::sin(1.5)) / (1.0 + f32::cosh(0.6))
526        );
527        Ok(())
528    }
529
530    #[test]
531    fn point_hyperbolic_2d_eighteight_periodic() -> anyhow::Result<()> {
532        let boundary = Periodic::new(0.6, EightEight {})?;
533        let microstate = Microstate::builder()
534            .boundary(boundary)
535            .bodies([
536                Body::point(Hyperbolic::<3>::from_polar_coordinates(0.2, 0.3)),
537                Body::point(Hyperbolic::<3>::from_polar_coordinates(0.6, 0.7)),
538            ])
539            .step(1234)
540            .try_build()?;
541
542        let tmp_dir = tempdir()?;
543        let path = tmp_dir.path().join("test.gsd");
544        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
545        hoomd_gsd_file.append_microstate(&microstate)?;
546
547        drop(hoomd_gsd_file);
548
549        let gsd_file = GsdFile::open(path, Mode::Read)?;
550
551        assert!(gsd_file.n_frames() == 1);
552
553        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
554        itertools::assert_equal(step, [1234]);
555
556        let positions: Vec<[f32; 3]> = gsd_file
557            .iter_arrays::<f32, 3>(0, "particles/position")?
558            .collect();
559        assert_relative_eq!(
560            positions[0][0],
561            (f32::sinh(0.2) * f32::cos(0.3)) / (1.0 + f32::cosh(0.2))
562        );
563        assert_relative_eq!(
564            positions[0][1],
565            (f32::sinh(0.2) * f32::sin(0.3)) / (1.0 + f32::cosh(0.2))
566        );
567        assert_relative_eq!(
568            positions[1][0],
569            (f32::sinh(0.6) * f32::cos(0.7)) / (1.0 + f32::cosh(0.6))
570        );
571        assert_relative_eq!(
572            positions[1][1],
573            (f32::sinh(0.6) * f32::sin(0.7)) / (1.0 + f32::cosh(0.6))
574        );
575        Ok(())
576    }
577
578    #[test]
579    fn oriented_point_hyperbolic_2d_open() -> anyhow::Result<()> {
580        let microstate = Microstate::builder()
581            .boundary(Open)
582            .bodies([
583                Body {
584                    properties: OrientedHyperbolicPoint {
585                        position: Hyperbolic::<3>::from_polar_coordinates(0.5, 0.6),
586                        orientation: Angle::from(0.3),
587                    },
588                    sites: vec![OrientedHyperbolicPoint {
589                        position: Hyperbolic::<3>::default(),
590                        orientation: Angle::default(),
591                    }],
592                },
593                Body {
594                    properties: OrientedHyperbolicPoint {
595                        position: Hyperbolic::<3>::from_polar_coordinates(0.9, 0.3),
596                        orientation: Angle::from(1.2),
597                    },
598                    sites: vec![OrientedHyperbolicPoint {
599                        position: Hyperbolic::<3>::default(),
600                        orientation: Angle::default(),
601                    }],
602                },
603            ])
604            .step(1234)
605            .try_build()?;
606
607        let tmp_dir = tempdir()?;
608        let path = tmp_dir.path().join("test.gsd");
609        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
610        hoomd_gsd_file.append_microstate(&microstate)?;
611
612        drop(hoomd_gsd_file);
613
614        let gsd_file = GsdFile::open(path, Mode::Read)?;
615
616        assert!(gsd_file.n_frames() == 1);
617
618        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
619        itertools::assert_equal(step, [1234]);
620
621        let positions: Vec<[f32; 3]> = gsd_file
622            .iter_arrays::<f32, 3>(0, "particles/position")?
623            .collect();
624        assert_relative_eq!(
625            positions[0][0],
626            (f32::sinh(0.5) * f32::cos(0.6)) / (1.0 + f32::cosh(0.5))
627        );
628        assert_relative_eq!(
629            positions[0][1],
630            (f32::sinh(0.5) * f32::sin(0.6)) / (1.0 + f32::cosh(0.5))
631        );
632        assert_relative_eq!(
633            positions[1][0],
634            (f32::sinh(0.9) * f32::cos(0.3)) / (1.0 + f32::cosh(0.9))
635        );
636        assert_relative_eq!(
637            positions[1][1],
638            (f32::sinh(0.9) * f32::sin(0.3)) / (1.0 + f32::cosh(0.9))
639        );
640        Ok(())
641    }
642
643    #[test]
644    fn oriented_point_hyperbolic_2d_periodic_eighteight() -> anyhow::Result<()> {
645        let boundary = Periodic::new(0.6, EightEight {})?;
646        let microstate = Microstate::builder()
647            .boundary(boundary)
648            .bodies([
649                Body {
650                    properties: OrientedHyperbolicPoint {
651                        position: Hyperbolic::<3>::from_polar_coordinates(0.5, 0.6),
652                        orientation: Angle::from(0.3),
653                    },
654                    sites: vec![OrientedHyperbolicPoint {
655                        position: Hyperbolic::<3>::default(),
656                        orientation: Angle::default(),
657                    }],
658                },
659                Body {
660                    properties: OrientedHyperbolicPoint {
661                        position: Hyperbolic::<3>::from_polar_coordinates(0.9, 0.3),
662                        orientation: Angle::from(1.2),
663                    },
664                    sites: vec![OrientedHyperbolicPoint {
665                        position: Hyperbolic::<3>::default(),
666                        orientation: Angle::default(),
667                    }],
668                },
669            ])
670            .step(1234)
671            .try_build()?;
672
673        let tmp_dir = tempdir()?;
674        let path = tmp_dir.path().join("test.gsd");
675        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
676        hoomd_gsd_file.append_microstate(&microstate)?;
677
678        drop(hoomd_gsd_file);
679
680        let gsd_file = GsdFile::open(path, Mode::Read)?;
681
682        assert!(gsd_file.n_frames() == 1);
683
684        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
685        itertools::assert_equal(step, [1234]);
686
687        let positions: Vec<[f32; 3]> = gsd_file
688            .iter_arrays::<f32, 3>(0, "particles/position")?
689            .collect();
690        assert_relative_eq!(
691            positions[0][0],
692            (f32::sinh(0.5) * f32::cos(0.6)) / (1.0 + f32::cosh(0.5))
693        );
694        assert_relative_eq!(
695            positions[0][1],
696            (f32::sinh(0.5) * f32::sin(0.6)) / (1.0 + f32::cosh(0.5))
697        );
698        assert_relative_eq!(
699            positions[1][0],
700            (f32::sinh(0.9) * f32::cos(0.3)) / (1.0 + f32::cosh(0.9))
701        );
702        assert_relative_eq!(
703            positions[1][1],
704            (f32::sinh(0.9) * f32::sin(0.3)) / (1.0 + f32::cosh(0.9))
705        );
706        Ok(())
707    }
708
709    #[test]
710    fn oriented_point_closed_rectangle_2d() -> anyhow::Result<()> {
711        let boundary = Closed(Rectangle {
712            edge_lengths: [12.0.try_into()?, 18.0.try_into()?],
713        });
714
715        let site = OrientedPoint {
716            position: Cartesian::from([0.0, 0.0]),
717            orientation: Angle::default(),
718        };
719        let a = OrientedPoint {
720            position: Cartesian::from([1.0, 0.0]),
721            orientation: Angle::from(PI / 2.0),
722        };
723        let b = OrientedPoint {
724            position: Cartesian::from([-1.0, 2.0]),
725            orientation: Angle::from(PI),
726        };
727        let body_a = Body {
728            properties: a,
729            sites: [site].into(),
730        };
731        let body_b = Body {
732            properties: b,
733            sites: [site].into(),
734        };
735
736        let microstate = Microstate::builder()
737            .boundary(boundary)
738            .bodies([body_a, body_b])
739            .step(1234)
740            .try_build()?;
741
742        let tmp_dir = tempdir()?;
743        let path = tmp_dir.path().join("test.gsd");
744        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
745        hoomd_gsd_file.append_microstate(&microstate)?;
746
747        drop(hoomd_gsd_file);
748
749        let gsd_file = GsdFile::open(path, Mode::Read)?;
750
751        assert!(gsd_file.n_frames() == 1);
752
753        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
754        itertools::assert_equal(step, [1234]);
755
756        let positions = gsd_file.iter_arrays::<f32, 3>(0, "particles/position")?;
757        itertools::assert_equal(positions, [[1.0, 0.0, 0.0], [-1.0, 2.0, 0.0]]);
758
759        assert!(
760            gsd_file
761                .iter_arrays::<f32, 4>(0, "particles/orientation")?
762                .count()
763                == 2
764        );
765
766        let dimensions = gsd_file.iter_scalars::<u8>(0, "configuration/dimensions")?;
767        itertools::assert_equal(dimensions, [2]);
768
769        let box_ = gsd_file.iter_scalars::<f32>(0, "configuration/box")?;
770        itertools::assert_equal(box_, [12.0_f32, 18.0, 0.0, 0.0, 0.0, 0.0]);
771
772        Ok(())
773    }
774
775    #[test]
776    fn oriented_point_periodic_rectangle_2d() -> anyhow::Result<()> {
777        let boundary = Periodic::new(
778            0.0,
779            Rectangle {
780                edge_lengths: [12.0.try_into()?, 18.0.try_into()?],
781            },
782        )?;
783
784        let site = OrientedPoint {
785            position: Cartesian::from([0.0, 0.0]),
786            orientation: Angle::default(),
787        };
788        let a = OrientedPoint {
789            position: Cartesian::from([1.0, 0.0]),
790            orientation: Angle::from(PI / 2.0),
791        };
792        let b = OrientedPoint {
793            position: Cartesian::from([-1.0, 2.0]),
794            orientation: Angle::from(PI),
795        };
796        let body_a = Body {
797            properties: a,
798            sites: [site].into(),
799        };
800        let body_b = Body {
801            properties: b,
802            sites: [site].into(),
803        };
804
805        let microstate = Microstate::builder()
806            .boundary(boundary)
807            .bodies([body_a, body_b])
808            .step(1234)
809            .try_build()?;
810
811        let tmp_dir = tempdir()?;
812        let path = tmp_dir.path().join("test.gsd");
813        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
814        hoomd_gsd_file.append_microstate(&microstate)?;
815
816        drop(hoomd_gsd_file);
817
818        let gsd_file = GsdFile::open(path, Mode::Read)?;
819
820        assert!(gsd_file.n_frames() == 1);
821
822        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
823        itertools::assert_equal(step, [1234]);
824
825        let positions = gsd_file.iter_arrays::<f32, 3>(0, "particles/position")?;
826        itertools::assert_equal(positions, [[1.0, 0.0, 0.0], [-1.0, 2.0, 0.0]]);
827
828        assert!(
829            gsd_file
830                .iter_arrays::<f32, 4>(0, "particles/orientation")?
831                .count()
832                == 2
833        );
834
835        let dimensions = gsd_file.iter_scalars::<u8>(0, "configuration/dimensions")?;
836        itertools::assert_equal(dimensions, [2]);
837
838        let box_ = gsd_file.iter_scalars::<f32>(0, "configuration/box")?;
839        itertools::assert_equal(box_, [12.0_f32, 18.0, 0.0, 0.0, 0.0, 0.0]);
840
841        Ok(())
842    }
843
844    #[test]
845    fn point_closed_cuboid_3d() -> anyhow::Result<()> {
846        let boundary = Closed(Hypercuboid {
847            edge_lengths: [12.0.try_into()?, 18.0.try_into()?, 24.0.try_into()?],
848        });
849
850        let microstate = Microstate::builder()
851            .boundary(boundary)
852            .bodies([
853                Body::point(Cartesian::from([1.0, 0.0, 4.0])),
854                Body::point(Cartesian::from([-1.0, 2.0, -2.0])),
855            ])
856            .step(1234)
857            .try_build()?;
858
859        let tmp_dir = tempdir()?;
860        let path = tmp_dir.path().join("test.gsd");
861        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
862        hoomd_gsd_file.append_microstate(&microstate)?;
863
864        drop(hoomd_gsd_file);
865
866        let gsd_file = GsdFile::open(path, Mode::Read)?;
867
868        assert!(gsd_file.n_frames() == 1);
869
870        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
871        itertools::assert_equal(step, [1234]);
872
873        let positions = gsd_file.iter_arrays::<f32, 3>(0, "particles/position")?;
874        itertools::assert_equal(positions, [[1.0, 0.0, 4.0], [-1.0, 2.0, -2.0]]);
875
876        let dimensions = gsd_file.iter_scalars::<u8>(0, "configuration/dimensions")?;
877        itertools::assert_equal(dimensions, [3]);
878
879        let box_ = gsd_file.iter_scalars::<f32>(0, "configuration/box")?;
880        itertools::assert_equal(box_, [12.0_f32, 18.0, 24.0, 0.0, 0.0, 0.0]);
881
882        Ok(())
883    }
884
885    #[test]
886    fn point_periodic_cuboid_3d() -> anyhow::Result<()> {
887        let boundary = Periodic::new(
888            0.0,
889            Hypercuboid {
890                edge_lengths: [12.0.try_into()?, 18.0.try_into()?, 24.0.try_into()?],
891            },
892        )?;
893
894        let microstate = Microstate::builder()
895            .boundary(boundary)
896            .bodies([
897                Body::point(Cartesian::from([1.0, 0.0, 4.0])),
898                Body::point(Cartesian::from([-1.0, 2.0, -2.0])),
899            ])
900            .step(1234)
901            .try_build()?;
902
903        let tmp_dir = tempdir()?;
904        let path = tmp_dir.path().join("test.gsd");
905        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
906        hoomd_gsd_file.append_microstate(&microstate)?;
907
908        drop(hoomd_gsd_file);
909
910        let gsd_file = GsdFile::open(path, Mode::Read)?;
911
912        assert!(gsd_file.n_frames() == 1);
913
914        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
915        itertools::assert_equal(step, [1234]);
916
917        let positions = gsd_file.iter_arrays::<f32, 3>(0, "particles/position")?;
918        itertools::assert_equal(positions, [[1.0, 0.0, 4.0], [-1.0, 2.0, -2.0]]);
919
920        let dimensions = gsd_file.iter_scalars::<u8>(0, "configuration/dimensions")?;
921        itertools::assert_equal(dimensions, [3]);
922
923        let box_ = gsd_file.iter_scalars::<f32>(0, "configuration/box")?;
924        itertools::assert_equal(box_, [12.0_f32, 18.0, 24.0, 0.0, 0.0, 0.0]);
925
926        Ok(())
927    }
928
929    #[test]
930    fn oriented_point_closed_cuboid_3d() -> anyhow::Result<()> {
931        let boundary = Closed(Hypercuboid {
932            edge_lengths: [12.0.try_into()?, 18.0.try_into()?, 24.0.try_into()?],
933        });
934
935        let site = OrientedPoint {
936            position: Cartesian::default(),
937            orientation: Versor::default(),
938        };
939        let a = OrientedPoint {
940            position: Cartesian::from([1.0, 0.0, 4.0]),
941            orientation: Versor::from_axis_angle([1.0, 0.0, 0.0].try_into()?, PI / 2.0),
942        };
943        let b = OrientedPoint {
944            position: Cartesian::from([-1.0, 2.0, -2.0]),
945            orientation: Versor::from_axis_angle([0.0, 1.0, 0.0].try_into()?, PI / 2.0),
946        };
947        let body_a = Body {
948            properties: a,
949            sites: [site].into(),
950        };
951        let body_b = Body {
952            properties: b,
953            sites: [site].into(),
954        };
955
956        let microstate = Microstate::builder()
957            .boundary(boundary)
958            .bodies([body_a, body_b])
959            .step(1234)
960            .try_build()?;
961
962        let tmp_dir = tempdir()?;
963        let path = tmp_dir.path().join("test.gsd");
964        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
965        hoomd_gsd_file.append_microstate(&microstate)?;
966
967        drop(hoomd_gsd_file);
968
969        let gsd_file = GsdFile::open(path, Mode::Read)?;
970
971        assert!(gsd_file.n_frames() == 1);
972
973        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
974        itertools::assert_equal(step, [1234]);
975
976        let positions = gsd_file.iter_arrays::<f32, 3>(0, "particles/position")?;
977        itertools::assert_equal(positions, [[1.0, 0.0, 4.0], [-1.0, 2.0, -2.0]]);
978
979        assert!(
980            gsd_file
981                .iter_arrays::<f32, 4>(0, "particles/orientation")?
982                .count()
983                == 2
984        );
985
986        let dimensions = gsd_file.iter_scalars::<u8>(0, "configuration/dimensions")?;
987        itertools::assert_equal(dimensions, [3]);
988
989        let box_ = gsd_file.iter_scalars::<f32>(0, "configuration/box")?;
990        itertools::assert_equal(box_, [12.0_f32, 18.0, 24.0, 0.0, 0.0, 0.0]);
991
992        Ok(())
993    }
994
995    #[test]
996    fn oriented_point_periodic_cuboid_3d() -> anyhow::Result<()> {
997        let boundary = Periodic::new(
998            0.0,
999            Hypercuboid {
1000                edge_lengths: [12.0.try_into()?, 18.0.try_into()?, 24.0.try_into()?],
1001            },
1002        )?;
1003
1004        let site = OrientedPoint {
1005            position: Cartesian::from([0.0, 0.0, 0.0]),
1006            orientation: Versor::default(),
1007        };
1008        let a = OrientedPoint {
1009            position: Cartesian::from([1.0, 0.0, 4.0]),
1010            orientation: Versor::from_axis_angle([1.0, 0.0, 0.0].try_into()?, PI / 2.0),
1011        };
1012        let b = OrientedPoint {
1013            position: Cartesian::from([-1.0, 2.0, -2.0]),
1014            orientation: Versor::from_axis_angle([0.0, 1.0, 0.0].try_into()?, PI / 2.0),
1015        };
1016        let body_a = Body {
1017            properties: a,
1018            sites: [site].into(),
1019        };
1020        let body_b = Body {
1021            properties: b,
1022            sites: [site].into(),
1023        };
1024
1025        let microstate = Microstate::builder()
1026            .boundary(boundary)
1027            .bodies([body_a, body_b])
1028            .step(1234)
1029            .try_build()?;
1030
1031        let tmp_dir = tempdir()?;
1032        let path = tmp_dir.path().join("test.gsd");
1033        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1034        hoomd_gsd_file.append_microstate(&microstate)?;
1035
1036        drop(hoomd_gsd_file);
1037
1038        let gsd_file = GsdFile::open(path, Mode::Read)?;
1039
1040        assert!(gsd_file.n_frames() == 1);
1041
1042        let step = gsd_file.iter_scalars::<u64>(0, "configuration/step")?;
1043        itertools::assert_equal(step, [1234]);
1044
1045        let positions = gsd_file.iter_arrays::<f32, 3>(0, "particles/position")?;
1046        itertools::assert_equal(positions, [[1.0, 0.0, 4.0], [-1.0, 2.0, -2.0]]);
1047
1048        assert!(
1049            gsd_file
1050                .iter_arrays::<f32, 4>(0, "particles/orientation")?
1051                .count()
1052                == 2
1053        );
1054
1055        let dimensions = gsd_file.iter_scalars::<u8>(0, "configuration/dimensions")?;
1056        itertools::assert_equal(dimensions, [3]);
1057
1058        let box_ = gsd_file.iter_scalars::<f32>(0, "configuration/box")?;
1059        itertools::assert_equal(box_, [12.0_f32, 18.0, 24.0, 0.0, 0.0, 0.0]);
1060
1061        Ok(())
1062    }
1063}