ferritin_core/trajectory/
array_trajectory.rs1use std::borrow::Cow;
4use std::sync::Arc;
5use crate::model::Model;
6use super::Trajectory;
7
8#[derive(Debug, Clone)]
10pub enum TrajectoryError {
11 Empty,
13 MixedTopologies,
15}
16
17impl std::fmt::Display for TrajectoryError {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 TrajectoryError::Empty => write!(f, "trajectory must contain at least one frame"),
21 TrajectoryError::MixedTopologies => {
22 write!(f, "all frames must share the same Arc<AtomicHierarchy>")
23 }
24 }
25 }
26}
27
28impl std::error::Error for TrajectoryError {}
29
30pub struct ArrayTrajectory {
36 models: Vec<Model>,
37}
38
39impl ArrayTrajectory {
40 pub fn new(models: Vec<Model>) -> Result<Self, TrajectoryError> {
46 if models.is_empty() {
47 return Err(TrajectoryError::Empty);
48 }
49 let first = Arc::as_ptr(&models[0].hierarchy);
50 for m in &models[1..] {
51 if Arc::as_ptr(&m.hierarchy) != first {
52 return Err(TrajectoryError::MixedTopologies);
53 }
54 }
55 Ok(Self { models })
56 }
57
58 pub fn models(&self) -> &[Model] {
60 &self.models
61 }
62}
63
64impl Trajectory for ArrayTrajectory {
65 fn frame_count(&self) -> usize {
66 self.models.len()
67 }
68
69 fn representative(&self) -> &Model {
70 &self.models[0]
71 }
72
73 fn frame(&self, index: usize) -> Cow<'_, Model> {
74 Cow::Borrowed(&self.models[index])
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use std::borrow::Cow;
82 use crate::data::Segmentation;
83 use crate::model::{AtomicConformation, AtomicHierarchy, Bonds};
84 use crate::model::tables::{AtomsTable, ChainsTable, ResidueGroup, ResiduesTable};
85
86 fn make_simple_hierarchy(n_residues: usize) -> Arc<AtomicHierarchy> {
87 let n_atoms = n_residues;
88 let atoms = AtomsTable {
89 atom_name: (0..n_atoms).map(|_| "CA".to_string()).collect(),
90 element: (0..n_atoms).map(|_| "C".to_string()).collect(),
91 alt_loc: vec![None; n_atoms],
92 formal_charge: vec![None; n_atoms],
93 };
94 let residues = ResiduesTable {
95 comp_id: (0..n_residues).map(|i| format!("R{}", i)).collect(),
96 label_seq_id: (0..n_residues as i32).collect(),
97 auth_seq_id: (1..=n_residues as i32).collect(),
98 ins_code: vec![None; n_residues],
99 group: vec![ResidueGroup::Polymer; n_residues],
100 };
101 let chains = ChainsTable {
102 label_asym_id: vec!["A".into()],
103 auth_asym_id: vec!["A".into()],
104 entity_id: vec!["1".into()],
105 };
106 let atom_offsets: Vec<u32> = (0..=n_residues as u32).collect();
107 let atom_to_residue = Segmentation::from_offsets(atom_offsets);
108 let residue_to_chain = Segmentation::from_offsets(vec![0, n_residues as u32]);
109 let bonds = Bonds::from_unsorted(vec![], vec![], vec![], n_atoms);
110 Arc::new(AtomicHierarchy { atoms, residues, chains, atom_to_residue, residue_to_chain, bonds })
111 }
112
113 fn make_conformation(n: usize, offset: f32) -> AtomicConformation {
114 AtomicConformation {
115 x: (0..n).map(|i| i as f32 + offset).collect(),
116 y: (0..n).map(|i| i as f32 * 10.0).collect(),
117 z: vec![0.0; n],
118 occupancy: None,
119 b_iso: None,
120 confidence: None,
121 }
122 }
123
124 #[test]
125 fn test_array_trajectory_frame_count() {
126 let hierarchy = make_simple_hierarchy(3);
127 let models: Vec<Model> = (0..3)
128 .map(|i| Model::new(Arc::clone(&hierarchy), make_conformation(3, i as f32 * 100.0)))
129 .collect();
130 let traj = ArrayTrajectory::new(models).unwrap();
131 assert_eq!(traj.frame_count(), 3);
132 }
133
134 #[test]
135 fn test_array_trajectory_borrowed() {
136 let hierarchy = make_simple_hierarchy(2);
137 let models: Vec<Model> = (0..3)
138 .map(|i| Model::new(Arc::clone(&hierarchy), make_conformation(2, i as f32 * 10.0)))
139 .collect();
140 let traj = ArrayTrajectory::new(models).unwrap();
141 for i in 0..3 {
142 let f = traj.frame(i);
143 assert!(matches!(f, Cow::Borrowed(_)), "frame({}) should be Cow::Borrowed", i);
144 }
145 }
146
147 #[test]
148 fn test_array_trajectory_arc_shared() {
149 let hierarchy = make_simple_hierarchy(4);
150 let models: Vec<Model> = (0..3)
151 .map(|i| Model::new(Arc::clone(&hierarchy), make_conformation(4, i as f32 * 5.0)))
152 .collect();
153 let traj = ArrayTrajectory::new(models).unwrap();
154 let h0 = &traj.frame(0).hierarchy;
155 let h2 = &traj.frame(2).hierarchy;
156 assert!(Arc::ptr_eq(h0, h2), "frame(0) and frame(2) must share the same Arc<AtomicHierarchy>");
157 }
158
159 #[test]
160 fn test_array_trajectory_empty_error() {
161 let result = ArrayTrajectory::new(vec![]);
162 assert!(matches!(result, Err(TrajectoryError::Empty)));
163 }
164
165 #[test]
166 fn test_array_trajectory_mixed_topologies_error() {
167 let h1 = make_simple_hierarchy(3);
168 let h2 = make_simple_hierarchy(3); let models = vec![
170 Model::new(Arc::clone(&h1), make_conformation(3, 0.0)),
171 Model::new(Arc::clone(&h2), make_conformation(3, 1.0)),
172 ];
173 let result = ArrayTrajectory::new(models);
174 assert!(matches!(result, Err(TrajectoryError::MixedTopologies)));
175 }
176
177 #[test]
178 fn test_array_trajectory_object_safe() {
179 let hierarchy = make_simple_hierarchy(2);
180 let models: Vec<Model> = (0..2)
181 .map(|i| Model::new(Arc::clone(&hierarchy), make_conformation(2, i as f32)))
182 .collect();
183 let traj = ArrayTrajectory::new(models).unwrap();
184 let boxed: Box<dyn Trajectory> = Box::new(traj);
185 assert_eq!(boxed.frame_count(), 2);
186 }
187}