1use crate::data::OrderedSet;
4use crate::model::{Model, ModelError, SymmetryOperator};
5
6#[derive(Clone)]
12pub struct Unit<'a> {
13 model: &'a Model,
14 atoms: OrderedSet,
15 operator: SymmetryOperator,
16}
17
18impl<'a> Unit<'a> {
19 pub fn from_mask(model: &'a Model, mask: &[bool]) -> Self {
27 assert_eq!(
28 mask.len(),
29 model.n_atoms(),
30 "mask length must equal number of atoms"
31 );
32 let indices: Vec<u32> = mask
33 .iter()
34 .enumerate()
35 .filter_map(|(i, &selected)| if selected { Some(i as u32) } else { None })
36 .collect();
37
38 let atoms = if indices.is_empty() {
39 OrderedSet::interval(0, 0)
40 } else {
41 OrderedSet::from_sorted(indices)
42 };
43
44 Self {
45 model,
46 atoms,
47 operator: SymmetryOperator::identity(),
48 }
49 }
50
51 pub fn all(model: &'a Model) -> Self {
53 let n = model.n_atoms() as u32;
54 Self {
55 model,
56 atoms: OrderedSet::interval(0, n),
57 operator: SymmetryOperator::identity(),
58 }
59 }
60
61 pub fn from_indices(model: &'a Model, indices: OrderedSet) -> Self {
63 Self::try_from_indices(model, indices).expect("unit atom index is out of range")
64 }
65
66 pub fn try_from_indices(model: &'a Model, indices: OrderedSet) -> Result<Self, ModelError> {
68 if let Some(index) = indices
69 .iter()
70 .find(|&index| index as usize >= model.n_atoms())
71 {
72 return Err(ModelError::new(format!(
73 "unit atom index {index} is out of range for {} atoms",
74 model.n_atoms()
75 )));
76 }
77 Ok(Self {
78 model,
79 atoms: indices,
80 operator: SymmetryOperator::identity(),
81 })
82 }
83
84 pub fn try_from_indices_with_operator(
86 model: &'a Model,
87 indices: OrderedSet,
88 operator: SymmetryOperator,
89 ) -> Result<Self, ModelError> {
90 let mut unit = Self::try_from_indices(model, indices)?;
91 unit.operator = operator;
92 Ok(unit)
93 }
94
95 pub fn len(&self) -> usize {
97 self.atoms.len()
98 }
99
100 pub fn is_empty(&self) -> bool {
102 self.atoms.is_empty()
103 }
104
105 pub fn model(&self) -> &Model {
107 self.model
108 }
109
110 pub fn operator(&self) -> &SymmetryOperator {
111 &self.operator
112 }
113
114 pub fn atom_indices(&self) -> impl Iterator<Item = u32> + '_ {
116 self.atoms.iter()
117 }
118
119 pub fn coords(&self) -> impl Iterator<Item = [f32; 3]> + '_ {
121 self.atoms
122 .iter()
123 .map(|i| self.operator.apply(self.model.coord(i as usize)))
124 }
125
126 pub fn union(&self, other: &Unit<'a>) -> Self {
131 assert!(
132 std::ptr::eq(self.model, other.model),
133 "cannot union Units from different Models"
134 );
135 assert_eq!(
136 self.operator, other.operator,
137 "cannot union Units with different operators"
138 );
139 Self {
140 model: self.model,
141 atoms: self.atoms.union(&other.atoms),
142 operator: self.operator.clone(),
143 }
144 }
145
146 pub fn intersection(&self, other: &Unit<'a>) -> Self {
151 assert!(
152 std::ptr::eq(self.model, other.model),
153 "cannot intersect Units from different Models"
154 );
155 assert_eq!(
156 self.operator, other.operator,
157 "cannot intersect Units with different operators"
158 );
159 Self {
160 model: self.model,
161 atoms: self.atoms.intersection(&other.atoms),
162 operator: self.operator.clone(),
163 }
164 }
165
166 pub fn difference(&self, other: &Unit<'a>) -> Self {
171 assert!(
172 std::ptr::eq(self.model, other.model),
173 "cannot difference Units from different Models"
174 );
175 assert_eq!(
176 self.operator, other.operator,
177 "cannot difference Units with different operators"
178 );
179 Self {
180 model: self.model,
181 atoms: self.atoms.difference(&other.atoms),
182 operator: self.operator.clone(),
183 }
184 }
185
186 pub fn chain(model: &'a Model, chain_id: &str) -> Option<Self> {
190 let chain_idx = model
191 .hierarchy
192 .chains
193 .label_asym_id
194 .iter()
195 .position(|c| c == chain_id)?;
196
197 let res_range = model.hierarchy.residue_to_chain.segment(chain_idx);
198 if res_range.is_empty() {
199 return Some(Self {
200 model,
201 atoms: OrderedSet::interval(0, 0),
202 operator: SymmetryOperator::identity(),
203 });
204 }
205 let atom_start = model
206 .hierarchy
207 .atom_to_residue
208 .segment(res_range.start)
209 .start;
210 let atom_end = model
211 .hierarchy
212 .atom_to_residue
213 .segment(res_range.end - 1)
214 .end;
215
216 Some(Self {
217 model,
218 atoms: OrderedSet::interval(atom_start as u32, atom_end as u32),
219 operator: SymmetryOperator::identity(),
220 })
221 }
222
223 pub fn backbone(model: &'a Model) -> Self {
225 let backbone_names = ["N", "CA", "C", "O"];
226 let indices: Vec<u32> = model
227 .hierarchy
228 .atoms
229 .atom_name
230 .iter()
231 .enumerate()
232 .filter_map(|(i, name)| {
233 if backbone_names.contains(&name.as_str()) {
234 Some(i as u32)
235 } else {
236 None
237 }
238 })
239 .collect();
240
241 let atoms = if indices.is_empty() {
242 OrderedSet::interval(0, 0)
243 } else {
244 OrderedSet::from_sorted(indices)
245 };
246
247 Self {
248 model,
249 atoms,
250 operator: SymmetryOperator::identity(),
251 }
252 }
253}
254
255impl Model {
256 pub fn filter(&self, mask: &[bool]) -> Unit<'_> {
261 Unit::from_mask(self, mask)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::data::Segmentation;
269 use crate::info::elements::Element;
270 use crate::model::bonds::Bonds;
271 use crate::model::conformation::AtomicConformation;
272 use crate::model::hierarchy::AtomicHierarchy;
273 use crate::model::tables::{AtomsTable, ChainsTable, ResidueGroup, ResiduesTable};
274 use std::sync::Arc;
275
276 fn make_test_model(n_atoms: usize) -> Model {
277 let atoms = AtomsTable {
278 atom_name: (0..n_atoms).map(|i| format!("A{}", i)).collect(),
279 auth_atom_name: (0..n_atoms).map(|i| format!("A{}", i)).collect(),
280 element: vec![Element::C; n_atoms],
281 alt_loc: vec![None; n_atoms],
282 formal_charge: vec![None; n_atoms],
283 };
284 let residues = ResiduesTable {
285 comp_id: vec!["ALA".into()],
286 auth_comp_id: vec!["ALA".into()],
287 label_seq_id: vec![0],
288 auth_seq_id: vec![1],
289 ins_code: vec![None],
290 group: vec![ResidueGroup::Polymer],
291 };
292 let chains = ChainsTable {
293 label_asym_id: vec!["A".into()],
294 auth_asym_id: vec!["A".into()],
295 entity_id: vec!["1".into()],
296 };
297 let atom_to_residue = Segmentation::from_offsets(vec![0, n_atoms as u32]);
298 let residue_to_chain = Segmentation::from_offsets(vec![0, 1]);
299 let bonds = Bonds::from_unsorted(vec![], vec![], vec![], n_atoms);
300
301 let hierarchy = Arc::new(AtomicHierarchy {
302 atoms,
303 residues,
304 chains,
305 atom_to_residue,
306 residue_to_chain,
307 bonds,
308 });
309
310 let conformation = AtomicConformation {
311 x: (0..n_atoms).map(|i| i as f32).collect(),
312 y: (0..n_atoms).map(|i| i as f32 * 10.0).collect(),
313 z: (0..n_atoms).map(|_| 0.0).collect(),
314 occupancy: None,
315 b_iso: None,
316 confidence: None,
317 };
318
319 Model::new(hierarchy, conformation)
320 }
321
322 fn make_backbone_model() -> Model {
323 let atom_names = vec!["N".into(), "CA".into(), "C".into(), "O".into(), "CB".into()];
324 let n_atoms = atom_names.len();
325 let atoms = AtomsTable {
326 auth_atom_name: atom_names.clone(),
327 atom_name: atom_names,
328 element: vec![Element::C; n_atoms],
329 alt_loc: vec![None; n_atoms],
330 formal_charge: vec![None; n_atoms],
331 };
332 let residues = ResiduesTable {
333 comp_id: vec!["ALA".into()],
334 auth_comp_id: vec!["ALA".into()],
335 label_seq_id: vec![0],
336 auth_seq_id: vec![1],
337 ins_code: vec![None],
338 group: vec![ResidueGroup::Polymer],
339 };
340 let chains = ChainsTable {
341 label_asym_id: vec!["A".into()],
342 auth_asym_id: vec!["A".into()],
343 entity_id: vec!["1".into()],
344 };
345 let atom_to_residue = Segmentation::from_offsets(vec![0, n_atoms as u32]);
346 let residue_to_chain = Segmentation::from_offsets(vec![0, 1]);
347 let bonds = Bonds::from_unsorted(vec![], vec![], vec![], n_atoms);
348
349 let hierarchy = Arc::new(AtomicHierarchy {
350 atoms,
351 residues,
352 chains,
353 atom_to_residue,
354 residue_to_chain,
355 bonds,
356 });
357
358 let conformation = AtomicConformation {
359 x: vec![0.0, 1.0, 2.0, 3.0, 4.0],
360 y: vec![0.0, 0.0, 0.0, 0.0, 0.0],
361 z: vec![0.0, 0.0, 0.0, 0.0, 0.0],
362 occupancy: None,
363 b_iso: None,
364 confidence: None,
365 };
366
367 Model::new(hierarchy, conformation)
368 }
369
370 #[test]
371 fn test_unit_from_mask_no_copy() {
372 let model = make_test_model(10);
373 let mask: Vec<bool> = (0..10).map(|i| i % 2 == 0).collect();
374
375 let unit = Unit::from_mask(&model, &mask);
376
377 assert_eq!(unit.len(), 5);
378 assert!(std::ptr::eq(unit.model(), &model));
379
380 let indices: Vec<u32> = unit.atom_indices().collect();
381 assert_eq!(indices, vec![0, 2, 4, 6, 8]);
382 }
383
384 #[test]
385 fn test_unit_all() {
386 let model = make_test_model(5);
387 let unit = Unit::all(&model);
388
389 assert_eq!(unit.len(), 5);
390 let indices: Vec<u32> = unit.atom_indices().collect();
391 assert_eq!(indices, vec![0, 1, 2, 3, 4]);
392 }
393
394 #[test]
395 fn test_unit_from_indices_rejects_out_of_range_index() {
396 let model = make_test_model(5);
397 let result = Unit::try_from_indices(&model, OrderedSet::from_sorted(vec![1, 5]));
398 assert_eq!(
399 result.err().unwrap().to_string(),
400 "unit atom index 5 is out of range for 5 atoms"
401 );
402 }
403
404 #[test]
405 fn test_unit_iteration_lazy() {
406 let model = make_test_model(100);
407 let mask: Vec<bool> = (0..100).map(|i| i < 3).collect();
408
409 let unit = Unit::from_mask(&model, &mask);
410
411 let coords: Vec<[f32; 3]> = unit.coords().collect();
412 assert_eq!(coords.len(), 3);
413 assert_eq!(coords[0], [0.0, 0.0, 0.0]);
414 assert_eq!(coords[1], [1.0, 10.0, 0.0]);
415 assert_eq!(coords[2], [2.0, 20.0, 0.0]);
416 }
417
418 #[test]
419 fn test_unit_set_operations() {
420 let model = make_test_model(10);
421
422 let mask_a: Vec<bool> = (0..10).map(|i| i < 5).collect();
423 let mask_b: Vec<bool> = (0..10).map(|i| (3..8).contains(&i)).collect();
424
425 let unit_a = Unit::from_mask(&model, &mask_a);
426 let unit_b = Unit::from_mask(&model, &mask_b);
427
428 let union = unit_a.union(&unit_b);
429 let union_indices: Vec<u32> = union.atom_indices().collect();
430 assert_eq!(union_indices, vec![0, 1, 2, 3, 4, 5, 6, 7]);
431
432 let intersection = unit_a.intersection(&unit_b);
433 let int_indices: Vec<u32> = intersection.atom_indices().collect();
434 assert_eq!(int_indices, vec![3, 4]);
435
436 let difference = unit_a.difference(&unit_b);
437 let diff_indices: Vec<u32> = difference.atom_indices().collect();
438 assert_eq!(diff_indices, vec![0, 1, 2]);
439 }
440
441 #[test]
442 fn test_unit_empty() {
443 let model = make_test_model(5);
444 let mask = vec![false; 5];
445
446 let unit = Unit::from_mask(&model, &mask);
447 assert!(unit.is_empty());
448 assert_eq!(unit.len(), 0);
449 }
450
451 #[test]
452 fn test_model_filter() {
453 let model = make_test_model(5);
454 let mask = vec![true, false, true, false, true];
455
456 let unit = model.filter(&mask);
457 assert_eq!(unit.len(), 3);
458 }
459
460 #[test]
461 fn test_unit_chain() {
462 let model = make_test_model(5);
463
464 let chain_a = Unit::chain(&model, "A");
465 assert!(chain_a.is_some());
466 assert_eq!(chain_a.unwrap().len(), 5);
467
468 let chain_b = Unit::chain(&model, "B");
469 assert!(chain_b.is_none());
470 }
471
472 #[test]
473 fn test_unit_backbone() {
474 let model = make_backbone_model();
475
476 let backbone = Unit::backbone(&model);
477 assert_eq!(backbone.len(), 4);
478
479 let indices: Vec<u32> = backbone.atom_indices().collect();
480 assert_eq!(indices, vec![0, 1, 2, 3]);
481 }
482
483 #[test]
484 #[should_panic(expected = "mask length must equal number of atoms")]
485 fn test_unit_from_mask_wrong_length() {
486 let model = make_test_model(5);
487 let mask = vec![true, false, true];
488 Unit::from_mask(&model, &mask);
489 }
490}