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