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}