1use std::sync::Arc;
8use super::bonds::{Bond, BondOrder};
9use super::info::constants::get_bonds_canonical20;
10use super::views::chain::ChainView;
11use super::views::residue::ResidueView;
12use crate::data::Segmentation;
13use crate::info::elements::Element;
14use crate::model::{AtomicConformation, AtomicHierarchy, Bonds, Model};
15use crate::model::tables::{AtomsTable, ChainsTable, ResidueGroup, ResiduesTable};
16use itertools::{Itertools, izip};
17
18#[derive(Clone)]
25pub struct AtomCollection {
26 size: usize,
27 coords: Vec<[f32; 3]>,
28 res_ids: Vec<i32>,
29 res_names: Vec<String>,
30 is_hetero: Vec<bool>,
31 elements: Vec<Element>,
32 atom_names: Vec<String>,
33 chain_ids: Vec<String>,
34 bonds: Option<Vec<Bond>>,
35 residue_start_indices: Option<Vec<usize>>,
36 chain_start_indices: Option<Vec<usize>>,
37}
38
39impl AtomCollection {
40 pub fn new(
41 size: usize,
42 coords: Vec<[f32; 3]>,
43 res_ids: Vec<i32>,
44 res_names: Vec<String>,
45 is_hetero: Vec<bool>,
46 elements: Vec<Element>,
47 atom_names: Vec<String>,
48 chain_ids: Vec<String>,
49 bonds: Option<Vec<Bond>>,
50 ) -> Self {
51 let mut ac = AtomCollection {
52 size,
53 coords,
54 res_ids,
55 res_names,
56 is_hetero,
57 elements,
58 atom_names,
59 chain_ids,
60 bonds,
61 residue_start_indices: None,
62 chain_start_indices: None,
63 };
64 ac.calculate_chain_indices();
65 ac
66 }
67 pub fn calculate_chain_indices(&mut self) {
69 if self.chain_start_indices.is_none() {
70 if self.residue_start_indices.is_none() {
71 let residue_starts = self.get_residue_starts();
72 self.residue_start_indices = Some(residue_starts);
73 }
74
75 let residue_starts = self.residue_start_indices.as_ref().unwrap();
77 let chain_starts: Vec<usize> = self
78 .get_chain_starts()
79 .iter()
80 .map(|&atom_idx| {
81 let residue_idx = residue_starts
83 .iter()
84 .enumerate()
85 .filter(|&(_, &res_start)| res_start <= atom_idx)
86 .last()
87 .map(|(i, _)| i)
88 .unwrap_or(0);
89 residue_idx
90 })
91 .collect();
92
93 self.chain_start_indices = Some(chain_starts);
94 }
95 }
96 pub fn connect_via_residue_names(&mut self) {
97 if self.bonds.is_some() {
98 println!("Bonds already in place. Not overwriting.");
99 return;
100 }
101 let aa_bond_info = get_bonds_canonical20();
102 let residue_starts = self.get_residue_starts();
103 let n_atoms = self.size;
104 let mut bonds = Vec::new();
105 for res_i in 0..residue_starts.len() - 1 {
106 let curr_start_i = residue_starts[res_i];
107 let next_start_i = residue_starts[res_i + 1];
108 if let Some(bond_dict_for_res) =
109 aa_bond_info.get(&self.res_names[curr_start_i].as_str())
110 {
111 for &(atom_name1, atom_name2, bond_type) in bond_dict_for_res {
112 let atom_indices1: Vec<usize> = (curr_start_i..next_start_i)
113 .filter(|&i| self.atom_names[i] == atom_name1)
114 .collect();
115 let atom_indices2: Vec<usize> = (curr_start_i..next_start_i)
116 .filter(|&i| self.atom_names[i] == atom_name2)
117 .collect();
118 for &i in &atom_indices1 {
119 for &j in &atom_indices2 {
120 bonds.push(Bond::new(i as i32, j as i32, bond_type));
121 }
122 }
123 }
124 }
125 }
126 for res_i in 0..residue_starts.len() - 1 {
128 let curr_start_i = residue_starts[res_i];
129 let next_start_i = residue_starts[res_i + 1];
130 if self.chain_ids[curr_start_i] != self.chain_ids[next_start_i] {
132 continue;
133 }
134 if self.is_hetero[curr_start_i] || self.is_hetero[next_start_i] {
136 continue;
137 }
138 let next_end_i = residue_starts
139 .get(res_i + 2)
140 .copied()
141 .unwrap_or(n_atoms);
142 let c_idx = (curr_start_i..next_start_i).find(|&i| self.atom_names[i] == "C");
143 let n_idx = (next_start_i..next_end_i).find(|&i| self.atom_names[i] == "N");
144 if let (Some(c), Some(n)) = (c_idx, n_idx) {
145 bonds.push(Bond::new(c as i32, n as i32, BondOrder::Single));
146 }
147 }
148 self.bonds = Some(bonds);
149 }
150 pub fn get_size(&self) -> usize {
151 self.size
152 }
153 pub fn get_atom_name(&self, idx: usize) -> &String {
154 &self.atom_names[idx]
155 }
156 pub fn get_bonds(&self) -> Option<&Vec<Bond>> {
157 self.bonds.as_ref()
158 }
159 pub fn get_chain_id(&self, idx: usize) -> &String {
160 &self.chain_ids[idx]
161 }
162 pub fn get_coord(&self, idx: usize) -> &[f32; 3] {
163 &self.coords[idx]
164 }
165 pub fn get_coords(&self) -> &Vec<[f32; 3]> {
166 self.coords.as_ref()
167 }
168 pub fn get_element(&self, idx: usize) -> &Element {
169 &self.elements[idx]
170 }
171 pub fn get_elements(&self) -> &Vec<Element> {
172 self.elements.as_ref()
173 }
174 pub fn get_is_hetero(&self, idx: usize) -> bool {
175 self.is_hetero[idx]
176 }
177 pub fn get_resnames(&self) -> &Vec<String> {
178 self.res_names.as_ref()
179 }
180 pub fn get_res_id(&self, idx: usize) -> &i32 {
181 &self.res_ids[idx]
182 }
183 pub fn get_resids(&self) -> &Vec<i32> {
184 self.res_ids.as_ref()
185 }
186 pub fn get_res_name(&self, idx: usize) -> &String {
187 &self.res_names[idx]
188 }
189 fn get_residue_starts(&self) -> Vec<usize> {
192 let mut starts = vec![0];
193
194 starts.extend(
195 izip!(&self.res_ids, &self.res_names, &self.chain_ids)
196 .tuple_windows()
197 .enumerate()
198 .filter_map(
199 |(i, ((res_id1, name1, chain1), (res_id2, name2, chain2)))| {
200 if res_id1 != res_id2 || name1 != name2 || chain1 != chain2 {
201 Some(i + 1)
202 } else {
203 None
204 }
205 },
206 ),
207 );
208 starts
209 }
210 pub fn get_residue_start_indices(&self) -> Option<&Vec<usize>> {
211 self.residue_start_indices.as_ref()
212 }
213 fn get_chain_starts(&self) -> Vec<usize> {
215 let mut starts = vec![0];
216 starts.extend(
217 self.chain_ids
218 .iter()
219 .tuple_windows()
220 .enumerate()
221 .filter_map(
222 |(i, (chain1, chain2))| {
223 if chain1 != chain2 { Some(i + 1) } else { None }
224 },
225 ),
226 );
227 starts
228 }
229
230 pub fn filter(&self, mask: &[bool]) -> AtomCollection {
238 assert_eq!(mask.len(), self.size, "mask length must equal atom count");
239
240 let mut next = 0usize;
242 let remap: Vec<Option<usize>> = mask
243 .iter()
244 .map(|&keep| {
245 if keep {
246 let idx = next;
247 next += 1;
248 Some(idx)
249 } else {
250 None
251 }
252 })
253 .collect();
254
255 let selected: Vec<usize> = remap.iter().enumerate().filter_map(|(i, r)| r.map(|_| i)).collect();
256
257 let coords: Vec<[f32; 3]> = selected.iter().map(|&i| self.coords[i]).collect();
258 let res_ids: Vec<i32> = selected.iter().map(|&i| self.res_ids[i]).collect();
259 let res_names: Vec<String> = selected.iter().map(|&i| self.res_names[i].clone()).collect();
260 let is_hetero: Vec<bool> = selected.iter().map(|&i| self.is_hetero[i]).collect();
261 let elements: Vec<Element> = selected.iter().map(|&i| self.elements[i].clone()).collect();
262 let atom_names: Vec<String> = selected.iter().map(|&i| self.atom_names[i].clone()).collect();
263 let chain_ids: Vec<String> = selected.iter().map(|&i| self.chain_ids[i].clone()).collect();
264
265 let bonds: Option<Vec<Bond>> = self.bonds.as_ref().map(|bonds| {
266 bonds
267 .iter()
268 .filter_map(|b| {
269 let (a, b_idx) = b.get_atom_indices();
270 match (remap[a as usize], remap[b_idx as usize]) {
271 (Some(new_a), Some(new_b)) => {
272 Some(Bond::new(new_a as i32, new_b as i32, b.get_order()))
273 }
274 _ => None,
275 }
276 })
277 .collect()
278 });
279
280 AtomCollection::new(next, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, bonds)
281 }
282
283 pub fn select_chain(&self, chain_id: &str) -> Vec<bool> {
285 self.chain_ids.iter().map(|c| c == chain_id).collect()
286 }
287
288 pub fn select_hetero(&self) -> Vec<bool> {
290 self.is_hetero.clone()
291 }
292
293 pub fn select_backbone(&self) -> Vec<bool> {
295 const BACKBONE: [&str; 4] = ["N", "CA", "C", "O"];
296 self.atom_names.iter().map(|n| BACKBONE.contains(&n.as_str())).collect()
297 }
298
299 pub fn select_atom_name(&self, atom_name: &str) -> Vec<bool> {
301 self.atom_names.iter().map(|n| n == atom_name).collect()
302 }
303
304 pub fn select_residue_name(&self, res_name: &str) -> Vec<bool> {
306 self.res_names.iter().map(|n| n == res_name).collect()
307 }
308
309 pub fn iter_coords_and_elements(&self) -> impl Iterator<Item = (&[f32; 3], &Element)> {
310 izip!(&self.coords, &self.elements)
311 }
312
313 pub fn iter_chains(&self) -> impl Iterator<Item = ChainView<'_>> {
314 let chain_starts = match &self.chain_start_indices {
316 Some(indices) => indices.clone(),
317 None => Vec::new(),
318 };
319
320 (0..chain_starts.len()).map(move |i| {
321 let start_residue_idx = chain_starts[i];
322 let end_residue_idx = if i + 1 < chain_starts.len() {
323 chain_starts[i + 1]
324 } else {
325 match &self.residue_start_indices {
327 Some(indices) => indices.len(),
328 None => self.size,
329 }
330 };
331
332 ChainView {
333 data: self,
334 start_residue_idx,
335 end_residue_idx,
336 }
337 })
338 }
339 pub fn iter_residues(&self) -> impl Iterator<Item = ResidueView<'_>> {
340 let residue_starts = self.get_residue_starts();
341 let atom_size = self.get_size();
342 let last_atom_idx = residue_starts.last().copied();
345 (0..residue_starts.len().saturating_sub(1))
346 .map(move |i| ResidueView::new(self, residue_starts[i], residue_starts[i + 1]))
347 .chain(
348 last_atom_idx
349 .map(|idx| ResidueView::new(self, idx, atom_size))
350 .into_iter(),
351 )
352 }
353 pub fn iter_residues_aminoacid(&self) -> impl Iterator<Item = ResidueView<'_>> {
357 self.iter_residues()
358 .filter(|residue| residue.is_amino_acid())
359 }
360
361 pub fn to_model(&self) -> Model {
366 let n_atoms = self.size;
367
368 let atoms = AtomsTable {
369 atom_name: self.atom_names.clone(),
370 element: self.elements.iter().map(|e| e.symbol().to_string()).collect(),
371 alt_loc: vec![None; n_atoms],
372 formal_charge: vec![None; n_atoms],
373 };
374
375 let residue_starts = self.get_residue_starts();
376 let n_residues = residue_starts.len();
377 let mut comp_id = Vec::with_capacity(n_residues);
378 let mut label_seq_id = Vec::with_capacity(n_residues);
379 let mut auth_seq_id = Vec::with_capacity(n_residues);
380 let mut ins_code = Vec::with_capacity(n_residues);
381 let mut group = Vec::with_capacity(n_residues);
382
383 for &start in &residue_starts {
384 comp_id.push(self.res_names[start].clone());
385 let res_id = self.res_ids[start];
386 label_seq_id.push(res_id);
387 auth_seq_id.push(res_id);
388 ins_code.push(None);
389 group.push(if self.is_hetero[start] {
390 ResidueGroup::NonPolymer
391 } else {
392 ResidueGroup::Polymer
393 });
394 }
395
396 let residues = ResiduesTable {
397 comp_id,
398 label_seq_id,
399 auth_seq_id,
400 ins_code,
401 group,
402 };
403
404 let chain_starts = self.get_chain_starts();
405 let n_chains = chain_starts.len();
406 let mut label_asym_id = Vec::with_capacity(n_chains);
407 let mut auth_asym_id = Vec::with_capacity(n_chains);
408 let mut entity_id = Vec::with_capacity(n_chains);
409
410 for &start in &chain_starts {
411 let chain_id = self.chain_ids[start].clone();
412 label_asym_id.push(chain_id.clone());
413 auth_asym_id.push(chain_id.clone());
414 entity_id.push(chain_id);
415 }
416
417 let chains = ChainsTable {
418 label_asym_id,
419 auth_asym_id,
420 entity_id,
421 };
422
423 let mut atom_offsets: Vec<u32> = residue_starts.iter().map(|&s| s as u32).collect();
424 atom_offsets.push(n_atoms as u32);
425 let atom_to_residue = Segmentation::from_offsets(atom_offsets);
426
427 let residue_offsets: Vec<u32> = chain_starts
428 .iter()
429 .map(|&atom_start| {
430 residue_starts
431 .iter()
432 .position(|&r| r == atom_start)
433 .unwrap_or(0) as u32
434 })
435 .chain(std::iter::once(n_residues as u32))
436 .collect();
437 let residue_to_chain = Segmentation::from_offsets(residue_offsets);
438
439 let bonds = Bonds::from_unsorted(vec![], vec![], vec![], n_atoms);
440
441 let hierarchy = Arc::new(AtomicHierarchy {
442 atoms,
443 residues,
444 chains,
445 atom_to_residue,
446 residue_to_chain,
447 bonds,
448 });
449
450 let x: Vec<f32> = self.coords.iter().map(|c| c[0]).collect();
451 let y: Vec<f32> = self.coords.iter().map(|c| c[1]).collect();
452 let z: Vec<f32> = self.coords.iter().map(|c| c[2]).collect();
453
454 let conformation = AtomicConformation {
455 x,
456 y,
457 z,
458 occupancy: None,
459 b_iso: None,
460 confidence: None,
461 };
462
463 Model::new(hierarchy, conformation)
464 }
465}
466
467impl From<&Model> for AtomCollection {
468 fn from(model: &Model) -> Self {
472 let n_atoms = model.n_atoms();
473 let hierarchy = &model.hierarchy;
474 let conformation = &model.conformation;
475
476 let coords: Vec<[f32; 3]> = (0..n_atoms)
477 .map(|i| [conformation.x[i], conformation.y[i], conformation.z[i]])
478 .collect();
479
480 let mut res_ids = Vec::with_capacity(n_atoms);
481 let mut res_names = Vec::with_capacity(n_atoms);
482 let mut is_hetero = Vec::with_capacity(n_atoms);
483 let mut chain_ids = Vec::with_capacity(n_atoms);
484
485 for atom_idx in 0..n_atoms {
486 let res_idx = hierarchy.residue_of_atom(atom_idx);
487 let chain_idx = hierarchy.chain_of_residue(res_idx);
488
489 res_ids.push(hierarchy.residues.auth_seq_id[res_idx]);
490 res_names.push(hierarchy.residues.comp_id[res_idx].clone());
491 is_hetero.push(hierarchy.residues.group[res_idx] == ResidueGroup::NonPolymer);
492 chain_ids.push(hierarchy.chains.auth_asym_id[chain_idx].clone());
493 }
494
495 let elements: Vec<Element> = hierarchy
496 .atoms
497 .element
498 .iter()
499 .map(|s| Element::from_symbol(s).unwrap_or(Element::C))
500 .collect();
501
502 let atom_names = hierarchy.atoms.atom_name.clone();
503
504 AtomCollection::new(
505 n_atoms,
506 coords,
507 res_ids,
508 res_names,
509 is_hetero,
510 elements,
511 atom_names,
512 chain_ids,
513 None,
514 )
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 fn make_test_atom_collection() -> AtomCollection {
523 let coords = vec![
524 [1.0, 2.0, 3.0],
525 [4.0, 5.0, 6.0],
526 [7.0, 8.0, 9.0],
527 [10.0, 11.0, 12.0],
528 [13.0, 14.0, 15.0],
529 ];
530 let res_ids = vec![1, 1, 1, 2, 2];
531 let res_names = vec![
532 "ALA".into(), "ALA".into(), "ALA".into(),
533 "GLY".into(), "GLY".into(),
534 ];
535 let is_hetero = vec![false, false, false, false, false];
536 let elements = vec![Element::N, Element::C, Element::C, Element::N, Element::C];
537 let atom_names = vec![
538 "N".into(), "CA".into(), "C".into(),
539 "N".into(), "CA".into(),
540 ];
541 let chain_ids = vec![
542 "A".into(), "A".into(), "A".into(),
543 "A".into(), "A".into(),
544 ];
545
546 AtomCollection::new(
547 5,
548 coords,
549 res_ids,
550 res_names,
551 is_hetero,
552 elements,
553 atom_names,
554 chain_ids,
555 None,
556 )
557 }
558
559 #[test]
560 fn test_atomcollection_to_model_roundtrip() {
561 let original = make_test_atom_collection();
562
563 let model = original.to_model();
564 let restored = AtomCollection::from(&model);
565
566 assert_eq!(original.get_size(), restored.get_size());
567
568 for i in 0..original.get_size() {
569 assert_eq!(original.get_coord(i), restored.get_coord(i), "coord mismatch at {}", i);
570 assert_eq!(original.get_res_id(i), restored.get_res_id(i), "res_id mismatch at {}", i);
571 assert_eq!(original.get_res_name(i), restored.get_res_name(i), "res_name mismatch at {}", i);
572 assert_eq!(original.get_atom_name(i), restored.get_atom_name(i), "atom_name mismatch at {}", i);
573 assert_eq!(original.get_chain_id(i), restored.get_chain_id(i), "chain_id mismatch at {}", i);
574 assert_eq!(original.get_is_hetero(i), restored.get_is_hetero(i), "is_hetero mismatch at {}", i);
575 }
576 }
577
578 #[test]
579 fn test_model_to_atomcollection_coords() {
580 let ac = make_test_atom_collection();
581 let model = ac.to_model();
582
583 let model_coords = model.coords_as_slice();
584 assert_eq!(model_coords.len(), ac.get_size());
585 for i in 0..ac.get_size() {
586 assert_eq!(model_coords[i], *ac.get_coord(i), "coord mismatch at {}", i);
587 }
588
589 assert_eq!(model.x(), &[1.0, 4.0, 7.0, 10.0, 13.0]);
590 assert_eq!(model.y(), &[2.0, 5.0, 8.0, 11.0, 14.0]);
591 assert_eq!(model.z(), &[3.0, 6.0, 9.0, 12.0, 15.0]);
592 }
593
594 #[test]
595 fn test_model_hierarchy_structure() {
596 let ac = make_test_atom_collection();
597 let model = ac.to_model();
598
599 assert_eq!(model.n_atoms(), 5);
600 assert_eq!(model.n_residues(), 2);
601 assert_eq!(model.n_chains(), 1);
602
603 assert_eq!(model.hierarchy.atoms_in_residue(0), 0..3);
604 assert_eq!(model.hierarchy.atoms_in_residue(1), 3..5);
605
606 assert_eq!(model.hierarchy.residues_in_chain(0), 0..2);
607 }
608
609 #[test]
610 fn test_multi_chain_roundtrip() {
611 let coords = vec![
612 [1.0, 1.0, 1.0],
613 [2.0, 2.0, 2.0],
614 [3.0, 3.0, 3.0],
615 ];
616 let res_ids = vec![1, 1, 10];
617 let res_names = vec!["ALA".into(), "ALA".into(), "GLY".into()];
618 let is_hetero = vec![false, false, true];
619 let elements = vec![Element::N, Element::C, Element::N];
620 let atom_names = vec!["N".into(), "CA".into(), "N".into()];
621 let chain_ids = vec!["A".into(), "A".into(), "B".into()];
622
623 let original = AtomCollection::new(
624 3, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, None,
625 );
626
627 let model = original.to_model();
628 assert_eq!(model.n_chains(), 2);
629 assert_eq!(model.n_residues(), 2);
630
631 let restored = AtomCollection::from(&model);
632 assert_eq!(restored.get_chain_id(0), "A");
633 assert_eq!(restored.get_chain_id(2), "B");
634 assert!(restored.get_is_hetero(2));
635 assert!(!restored.get_is_hetero(0));
636 }
637
638 fn make_two_chain_collection() -> AtomCollection {
639 let coords = vec![[1.,0.,0.],[2.,0.,0.],[3.,0.,0.],[4.,0.,0.],[5.,0.,0.]];
643 let res_ids = vec![1, 1, 1, 2, 2];
644 let res_names: Vec<String> = ["ALA","ALA","ALA","GLY","GLY"].iter().map(|s| s.to_string()).collect();
645 let is_hetero = vec![false, false, false, false, false];
646 let elements = vec![Element::N, Element::C, Element::C, Element::N, Element::C];
647 let atom_names: Vec<String> = ["N","CA","C","N","CA"].iter().map(|s| s.to_string()).collect();
648 let chain_ids: Vec<String> = ["A","A","A","B","B"].iter().map(|s| s.to_string()).collect();
649 let bonds = vec![
650 Bond::new(0, 1, BondOrder::Single),
651 Bond::new(1, 2, BondOrder::Single),
652 Bond::new(3, 4, BondOrder::Single),
653 ];
654 AtomCollection::new(5, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, Some(bonds))
655 }
656
657 #[test]
658 fn test_filter_keeps_selected_atoms() {
659 let ac = make_two_chain_collection();
660 let mask = vec![true, false, true, false, true];
661 let filtered = ac.filter(&mask);
662 assert_eq!(filtered.get_size(), 3);
663 assert_eq!(filtered.get_coord(0), &[1., 0., 0.]);
664 assert_eq!(filtered.get_coord(1), &[3., 0., 0.]);
665 assert_eq!(filtered.get_coord(2), &[5., 0., 0.]);
666 }
667
668 #[test]
669 fn test_filter_remaps_bonds() {
670 let ac = make_two_chain_collection();
671 let mask = vec![true, true, true, false, false];
673 let filtered = ac.filter(&mask);
674 let bonds = filtered.get_bonds().unwrap();
675 assert_eq!(bonds.len(), 2);
676 let indices: Vec<(i32, i32)> = bonds.iter().map(|b| b.get_atom_indices()).collect();
677 assert!(indices.contains(&(0, 1)));
678 assert!(indices.contains(&(1, 2)));
679 }
680
681 #[test]
682 fn test_filter_drops_cross_boundary_bonds() {
683 let ac = make_two_chain_collection();
684 let mask = vec![true, false, false, false, true];
686 let filtered = ac.filter(&mask);
687 assert_eq!(filtered.get_size(), 2);
688 let bonds = filtered.get_bonds().unwrap();
689 assert!(bonds.is_empty());
690 }
691
692 #[test]
693 fn test_select_chain() {
694 let ac = make_two_chain_collection();
695 let mask = ac.select_chain("B");
696 assert_eq!(mask, vec![false, false, false, true, true]);
697 let filtered = ac.filter(&mask);
698 assert_eq!(filtered.get_size(), 2);
699 }
700
701 #[test]
702 fn test_select_backbone() {
703 let ac = make_two_chain_collection();
704 let mask = ac.select_backbone();
705 assert_eq!(mask, vec![true, true, true, true, true]);
707 }
708
709 #[test]
710 fn test_select_atom_name() {
711 let ac = make_two_chain_collection();
712 let mask = ac.select_atom_name("CA");
713 assert_eq!(mask, vec![false, true, false, false, true]);
714 }
715
716 #[test]
717 fn test_select_residue_name() {
718 let ac = make_two_chain_collection();
719 let mask = ac.select_residue_name("ALA");
720 assert_eq!(mask, vec![true, true, true, false, false]);
721 }
722
723 #[test]
724 #[should_panic(expected = "mask length must equal atom count")]
725 fn test_filter_wrong_mask_length() {
726 let ac = make_test_atom_collection();
727 ac.filter(&[true, false]);
728 }
729}