Skip to main content

ferritin_core/trajectory/
coordinates.rs

1//! Coordinate data for trajectory frames.
2//!
3//! [`Frame`] holds a single snapshot of coordinates (topology-free), and
4//! [`Coordinates`] is an ordered collection of frames for trajectory storage.
5
6/// Unit cell parameters for periodic boundary condition systems.
7#[derive(Clone, Debug, PartialEq)]
8pub struct UnitCell {
9    /// Length of the a-axis in Å.
10    pub a: f32,
11    /// Length of the b-axis in Å.
12    pub b: f32,
13    /// Length of the c-axis in Å.
14    pub c: f32,
15    /// Angle between b and c axes in degrees.
16    pub alpha: f32,
17    /// Angle between a and c axes in degrees.
18    pub beta: f32,
19    /// Angle between a and b axes in degrees.
20    pub gamma: f32,
21}
22
23/// Single frame of coordinates — topology-free.
24///
25/// Stores Cartesian coordinates as parallel arrays (SoA layout). Optional
26/// fields are `None` when not relevant (e.g. non-periodic systems have no
27/// `cell`; single-structure files have no `time`).
28#[derive(Clone, Debug)]
29pub struct Frame {
30    /// X coordinates (Å), one per atom.
31    pub x: Vec<f32>,
32    /// Y coordinates (Å), one per atom.
33    pub y: Vec<f32>,
34    /// Z coordinates (Å), one per atom.
35    pub z: Vec<f32>,
36    /// Periodic box parameters, or `None` for non-periodic systems.
37    pub cell: Option<UnitCell>,
38    /// Simulation time in picoseconds, or `None` if not applicable.
39    pub time: Option<f64>,
40}
41
42/// Ordered collection of coordinate frames.
43///
44/// Frames are indexed by position. Use [`Coordinates::frame`] for random access.
45#[derive(Clone, Debug)]
46pub struct Coordinates {
47    frames: Vec<Frame>,
48}
49
50impl Coordinates {
51    /// Construct from a `Vec<Frame>`.
52    pub fn new(frames: Vec<Frame>) -> Self {
53        Self { frames }
54    }
55
56    /// Number of frames.
57    pub fn len(&self) -> usize {
58        self.frames.len()
59    }
60
61    /// Returns `true` if there are no frames.
62    pub fn is_empty(&self) -> bool {
63        self.frames.is_empty()
64    }
65
66    /// Returns a reference to the frame at `index`.
67    ///
68    /// # Panics
69    /// Panics if `index >= len()`.
70    pub fn frame(&self, index: usize) -> &Frame {
71        &self.frames[index]
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_coordinates_len() {
81        let frames: Vec<Frame> = (0..3)
82            .map(|i| Frame {
83                x: vec![i as f32],
84                y: vec![0.0],
85                z: vec![0.0],
86                cell: None,
87                time: None,
88            })
89            .collect();
90        let coords = Coordinates::new(frames);
91        assert_eq!(coords.len(), 3);
92        assert!(!coords.is_empty());
93    }
94
95    #[test]
96    fn test_frame_fields() {
97        let cell = UnitCell {
98            a: 10.0, b: 20.0, c: 30.0,
99            alpha: 90.0, beta: 90.0, gamma: 120.0,
100        };
101        let frame = Frame {
102            x: vec![1.0, 2.0],
103            y: vec![3.0, 4.0],
104            z: vec![5.0, 6.0],
105            cell: Some(cell.clone()),
106            time: Some(0.5),
107        };
108
109        assert_eq!(frame.x, vec![1.0, 2.0]);
110        assert_eq!(frame.y, vec![3.0, 4.0]);
111        assert_eq!(frame.z, vec![5.0, 6.0]);
112        assert_eq!(frame.time, Some(0.5));
113        let c = frame.cell.as_ref().unwrap();
114        assert_eq!(c.a, 10.0);
115        assert_eq!(c.b, 20.0);
116        assert_eq!(c.c, 30.0);
117        assert_eq!(c.alpha, 90.0);
118        assert_eq!(c.beta, 90.0);
119        assert_eq!(c.gamma, 120.0);
120    }
121}