1use super::bonds::{Bond, BondOrder};
8use super::info::constants::get_bonds_canonical20;
9use super::views::chain::ChainView;
10use super::views::residue::ResidueView;
11use crate::data::Segmentation;
12use crate::info::elements::Element;
13use crate::model::tables::{AtomsTable, ChainsTable, ResidueGroup, ResiduesTable};
14use crate::model::{AtomicConformation, AtomicHierarchy, Bonds, Model};
15use compact_str::CompactString;
16use itertools::{Itertools, izip};
17use std::sync::Arc;
18
19#[derive(Clone)]
26pub struct AtomCollection {
27 size: usize,
28 coords: Vec<[f32; 3]>,
29 res_ids: Vec<i32>,
30 res_names: Vec<CompactString>,
34 is_hetero: Vec<bool>,
35 elements: Vec<Element>,
36 atom_names: Vec<CompactString>,
37 chain_ids: Vec<CompactString>,
38 bonds: Option<Vec<Bond>>,
39 residue_start_indices: Option<Vec<usize>>,
40 chain_start_indices: Option<Vec<usize>>,
41 b_factor: Option<Vec<f32>>,
44 occupancy: Option<Vec<f32>>,
45}
46
47impl AtomCollection {
48 #[allow(clippy::too_many_arguments)] pub fn new(
50 size: usize,
51 coords: Vec<[f32; 3]>,
52 res_ids: Vec<i32>,
53 res_names: Vec<String>,
54 is_hetero: Vec<bool>,
55 elements: Vec<Element>,
56 atom_names: Vec<String>,
57 chain_ids: Vec<String>,
58 bonds: Option<Vec<Bond>>,
59 ) -> Self {
60 Self::from_compact(
61 size,
62 coords,
63 res_ids,
64 res_names.into_iter().map(CompactString::from).collect(),
65 is_hetero,
66 elements,
67 atom_names.into_iter().map(CompactString::from).collect(),
68 chain_ids.into_iter().map(CompactString::from).collect(),
69 bonds,
70 )
71 }
72 #[allow(clippy::too_many_arguments)] fn from_compact(
78 size: usize,
79 coords: Vec<[f32; 3]>,
80 res_ids: Vec<i32>,
81 res_names: Vec<CompactString>,
82 is_hetero: Vec<bool>,
83 elements: Vec<Element>,
84 atom_names: Vec<CompactString>,
85 chain_ids: Vec<CompactString>,
86 bonds: Option<Vec<Bond>>,
87 ) -> Self {
88 let mut ac = AtomCollection {
89 size,
90 coords,
91 res_ids,
92 res_names,
93 is_hetero,
94 elements,
95 atom_names,
96 chain_ids,
97 bonds,
98 residue_start_indices: None,
99 chain_start_indices: None,
100 b_factor: None,
101 occupancy: None,
102 };
103 ac.calculate_chain_indices();
104 ac
105 }
106
107 pub fn with_b_factor(mut self, values: Vec<f32>) -> Self {
114 assert_eq!(
115 values.len(),
116 self.size,
117 "b_factor length must equal atom count"
118 );
119 self.b_factor = Some(values);
120 self
121 }
122
123 pub fn with_occupancy(mut self, values: Vec<f32>) -> Self {
128 assert_eq!(
129 values.len(),
130 self.size,
131 "occupancy length must equal atom count"
132 );
133 self.occupancy = Some(values);
134 self
135 }
136
137 pub fn get_b_factor(&self, idx: usize) -> Option<f32> {
138 self.b_factor.as_ref().map(|v| v[idx])
139 }
140
141 pub fn get_b_factors(&self) -> Option<&[f32]> {
142 self.b_factor.as_deref()
143 }
144
145 pub fn get_occupancy(&self, idx: usize) -> Option<f32> {
146 self.occupancy.as_ref().map(|v| v[idx])
147 }
148
149 pub fn get_occupancies(&self) -> Option<&[f32]> {
150 self.occupancy.as_deref()
151 }
152 pub fn calculate_chain_indices(&mut self) {
154 if self.chain_start_indices.is_none() {
155 if self.residue_start_indices.is_none() {
156 let residue_starts = self.get_residue_starts();
157 self.residue_start_indices = Some(residue_starts);
158 }
159
160 let residue_starts = self.residue_start_indices.as_ref().unwrap();
162 let chain_starts: Vec<usize> = self
163 .get_chain_starts()
164 .iter()
165 .map(|&atom_idx| {
166 residue_starts
169 .iter()
170 .enumerate()
171 .rfind(|&(_, &res_start)| res_start <= atom_idx)
172 .map(|(i, _)| i)
173 .unwrap_or(0)
174 })
175 .collect();
176
177 self.chain_start_indices = Some(chain_starts);
178 }
179 }
180 pub fn connect_via_residue_names(&mut self) {
181 if self.bonds.is_some() {
182 println!("Bonds already in place. Not overwriting.");
183 return;
184 }
185 let aa_bond_info = get_bonds_canonical20();
186 let residue_starts = self.get_residue_starts();
187 let n_atoms = self.size;
188 let mut bonds = Vec::new();
189 for res_i in 0..residue_starts.len() - 1 {
190 let curr_start_i = residue_starts[res_i];
191 let next_start_i = residue_starts[res_i + 1];
192 if let Some(bond_dict_for_res) =
193 aa_bond_info.get(&self.res_names[curr_start_i].as_str())
194 {
195 for &(atom_name1, atom_name2, bond_type) in bond_dict_for_res {
196 let atom_indices1: Vec<usize> = (curr_start_i..next_start_i)
197 .filter(|&i| self.atom_names[i] == atom_name1)
198 .collect();
199 let atom_indices2: Vec<usize> = (curr_start_i..next_start_i)
200 .filter(|&i| self.atom_names[i] == atom_name2)
201 .collect();
202 for &i in &atom_indices1 {
203 for &j in &atom_indices2 {
204 bonds.push(Bond::new(i as i32, j as i32, bond_type));
205 }
206 }
207 }
208 }
209 }
210 for res_i in 0..residue_starts.len() - 1 {
212 let curr_start_i = residue_starts[res_i];
213 let next_start_i = residue_starts[res_i + 1];
214 if self.chain_ids[curr_start_i] != self.chain_ids[next_start_i] {
216 continue;
217 }
218 if self.is_hetero[curr_start_i] || self.is_hetero[next_start_i] {
220 continue;
221 }
222 let next_end_i = residue_starts.get(res_i + 2).copied().unwrap_or(n_atoms);
223 let c_idx = (curr_start_i..next_start_i).find(|&i| self.atom_names[i] == "C");
224 let n_idx = (next_start_i..next_end_i).find(|&i| self.atom_names[i] == "N");
225 if let (Some(c), Some(n)) = (c_idx, n_idx) {
226 bonds.push(Bond::new(c as i32, n as i32, BondOrder::Single));
227 }
228 }
229 self.bonds = Some(bonds);
230 }
231 pub fn get_size(&self) -> usize {
232 self.size
233 }
234 pub fn get_atom_name(&self, idx: usize) -> &str {
235 &self.atom_names[idx]
236 }
237 pub fn get_bonds(&self) -> Option<&Vec<Bond>> {
238 self.bonds.as_ref()
239 }
240 pub fn get_chain_id(&self, idx: usize) -> &str {
241 &self.chain_ids[idx]
242 }
243 pub fn get_coord(&self, idx: usize) -> &[f32; 3] {
244 &self.coords[idx]
245 }
246 pub fn get_coords(&self) -> &Vec<[f32; 3]> {
247 self.coords.as_ref()
248 }
249 pub fn get_element(&self, idx: usize) -> &Element {
250 &self.elements[idx]
251 }
252 pub fn get_elements(&self) -> &Vec<Element> {
253 self.elements.as_ref()
254 }
255 pub fn get_is_hetero(&self, idx: usize) -> bool {
256 self.is_hetero[idx]
257 }
258 pub fn get_resnames(&self) -> &[CompactString] {
259 self.res_names.as_ref()
260 }
261 pub fn get_res_id(&self, idx: usize) -> &i32 {
262 &self.res_ids[idx]
263 }
264 pub fn get_resids(&self) -> &Vec<i32> {
265 self.res_ids.as_ref()
266 }
267 pub fn get_res_name(&self, idx: usize) -> &str {
268 &self.res_names[idx]
269 }
270 fn get_residue_starts(&self) -> Vec<usize> {
273 let mut starts = vec![0];
274
275 starts.extend(
276 izip!(&self.res_ids, &self.res_names, &self.chain_ids)
277 .tuple_windows()
278 .enumerate()
279 .filter_map(
280 |(i, ((res_id1, name1, chain1), (res_id2, name2, chain2)))| {
281 if res_id1 != res_id2 || name1 != name2 || chain1 != chain2 {
282 Some(i + 1)
283 } else {
284 None
285 }
286 },
287 ),
288 );
289 starts
290 }
291 pub fn get_residue_start_indices(&self) -> Option<&Vec<usize>> {
292 self.residue_start_indices.as_ref()
293 }
294 fn get_chain_starts(&self) -> Vec<usize> {
296 let mut starts = vec![0];
297 starts.extend(
298 self.chain_ids
299 .iter()
300 .tuple_windows()
301 .enumerate()
302 .filter_map(
303 |(i, (chain1, chain2))| {
304 if chain1 != chain2 { Some(i + 1) } else { None }
305 },
306 ),
307 );
308 starts
309 }
310
311 pub fn filter(&self, mask: &[bool]) -> AtomCollection {
319 assert_eq!(mask.len(), self.size, "mask length must equal atom count");
320
321 let mut next = 0usize;
323 let remap: Vec<Option<usize>> = mask
324 .iter()
325 .map(|&keep| {
326 if keep {
327 let idx = next;
328 next += 1;
329 Some(idx)
330 } else {
331 None
332 }
333 })
334 .collect();
335
336 let selected: Vec<usize> = remap
337 .iter()
338 .enumerate()
339 .filter_map(|(i, r)| r.map(|_| i))
340 .collect();
341
342 let coords: Vec<[f32; 3]> = selected.iter().map(|&i| self.coords[i]).collect();
343 let res_ids: Vec<i32> = selected.iter().map(|&i| self.res_ids[i]).collect();
344 let res_names: Vec<CompactString> = selected
345 .iter()
346 .map(|&i| self.res_names[i].clone())
347 .collect();
348 let is_hetero: Vec<bool> = selected.iter().map(|&i| self.is_hetero[i]).collect();
349 let elements: Vec<Element> = selected.iter().map(|&i| self.elements[i]).collect();
350 let atom_names: Vec<CompactString> = selected
351 .iter()
352 .map(|&i| self.atom_names[i].clone())
353 .collect();
354 let chain_ids: Vec<CompactString> = selected
355 .iter()
356 .map(|&i| self.chain_ids[i].clone())
357 .collect();
358
359 let bonds: Option<Vec<Bond>> = self.bonds.as_ref().map(|bonds| {
360 bonds
361 .iter()
362 .filter_map(|b| {
363 let (a, b_idx) = b.get_atom_indices();
364 match (remap[a as usize], remap[b_idx as usize]) {
365 (Some(new_a), Some(new_b)) => {
366 Some(Bond::new(new_a as i32, new_b as i32, b.get_order()))
367 }
368 _ => None,
369 }
370 })
371 .collect()
372 });
373
374 let mut filtered = AtomCollection::from_compact(
375 next, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, bonds,
376 );
377 if let Some(values) = &self.b_factor {
378 filtered.b_factor = Some(selected.iter().map(|&i| values[i]).collect());
379 }
380 if let Some(values) = &self.occupancy {
381 filtered.occupancy = Some(selected.iter().map(|&i| values[i]).collect());
382 }
383 filtered
384 }
385
386 pub fn select_chain(&self, chain_id: &str) -> Vec<bool> {
388 self.chain_ids.iter().map(|c| c == chain_id).collect()
389 }
390
391 pub fn select_hetero(&self) -> Vec<bool> {
393 self.is_hetero.clone()
394 }
395
396 pub fn select_water(&self) -> Vec<bool> {
398 const WATER: [&str; 3] = ["HOH", "WAT", "H2O"];
399 self.res_names
400 .iter()
401 .map(|n| WATER.contains(&n.as_str()))
402 .collect()
403 }
404
405 pub fn select_ion(&self) -> Vec<bool> {
407 let mut mask = vec![false; self.size];
408 for residue in self.iter_residues() {
409 if residue.is_ion() {
410 #[allow(clippy::needless_range_loop)] for idx in residue.start_atom_idx..residue.end_atom_idx {
412 mask[idx] = true;
413 }
414 }
415 }
416 mask
417 }
418
419 pub fn select_backbone(&self) -> Vec<bool> {
421 const BACKBONE: [&str; 4] = ["N", "CA", "C", "O"];
422 self.atom_names
423 .iter()
424 .map(|n| BACKBONE.contains(&n.as_str()))
425 .collect()
426 }
427
428 pub fn select_atom_name(&self, atom_name: &str) -> Vec<bool> {
430 self.atom_names.iter().map(|n| n == atom_name).collect()
431 }
432
433 pub fn select_residue_name(&self, res_name: &str) -> Vec<bool> {
435 self.res_names.iter().map(|n| n == res_name).collect()
436 }
437
438 pub fn iter_coords_and_elements(&self) -> impl Iterator<Item = (&[f32; 3], &Element)> {
439 izip!(&self.coords, &self.elements)
440 }
441
442 pub fn iter_chains(&self) -> impl Iterator<Item = ChainView<'_>> {
443 let chain_starts = match &self.chain_start_indices {
445 Some(indices) => indices.clone(),
446 None => Vec::new(),
447 };
448
449 (0..chain_starts.len()).map(move |i| {
450 let start_residue_idx = chain_starts[i];
451 let end_residue_idx = if i + 1 < chain_starts.len() {
452 chain_starts[i + 1]
453 } else {
454 match &self.residue_start_indices {
456 Some(indices) => indices.len(),
457 None => self.size,
458 }
459 };
460
461 ChainView {
462 data: self,
463 start_residue_idx,
464 end_residue_idx,
465 }
466 })
467 }
468 pub fn iter_residues(&self) -> impl Iterator<Item = ResidueView<'_>> {
469 let residue_starts = self.get_residue_starts();
470 let atom_size = self.get_size();
471 let last_atom_idx = residue_starts.last().copied();
474 (0..residue_starts.len().saturating_sub(1))
475 .map(move |i| ResidueView::new(self, residue_starts[i], residue_starts[i + 1]))
476 .chain(last_atom_idx.map(|idx| ResidueView::new(self, idx, atom_size)))
477 }
478 pub fn iter_residues_aminoacid(&self) -> impl Iterator<Item = ResidueView<'_>> {
482 self.iter_residues()
483 .filter(|residue| residue.is_amino_acid())
484 }
485
486 pub fn to_model(&self) -> Model {
491 let n_atoms = self.size;
492
493 let atoms = AtomsTable {
494 atom_name: self.atom_names.iter().map(|s| s.to_string()).collect(),
495 auth_atom_name: self.atom_names.iter().map(|s| s.to_string()).collect(),
496 element: self.elements.clone(),
497 alt_loc: vec![None; n_atoms],
498 formal_charge: vec![None; n_atoms],
499 };
500
501 let residue_starts = self.get_residue_starts();
502 let n_residues = residue_starts.len();
503 let mut comp_id = Vec::with_capacity(n_residues);
504 let mut label_seq_id = Vec::with_capacity(n_residues);
505 let mut auth_seq_id = Vec::with_capacity(n_residues);
506 let mut ins_code = Vec::with_capacity(n_residues);
507 let mut group = Vec::with_capacity(n_residues);
508
509 for &start in &residue_starts {
510 comp_id.push(self.res_names[start].to_string());
511 let res_id = self.res_ids[start];
512 label_seq_id.push(res_id);
513 auth_seq_id.push(res_id);
514 ins_code.push(None);
515 group.push(if self.is_hetero[start] {
516 ResidueGroup::NonPolymer
517 } else {
518 ResidueGroup::Polymer
519 });
520 }
521
522 let residues = ResiduesTable {
523 comp_id,
524 auth_comp_id: self
525 .get_residue_starts()
526 .iter()
527 .map(|&start| self.res_names[start].to_string())
528 .collect(),
529 label_seq_id,
530 auth_seq_id,
531 ins_code,
532 group,
533 };
534
535 let chain_starts = self.get_chain_starts();
536 let n_chains = chain_starts.len();
537 let mut label_asym_id = Vec::with_capacity(n_chains);
538 let mut auth_asym_id = Vec::with_capacity(n_chains);
539 let mut entity_id = Vec::with_capacity(n_chains);
540
541 for &start in &chain_starts {
542 let chain_id = self.chain_ids[start].to_string();
543 label_asym_id.push(chain_id.clone());
544 auth_asym_id.push(chain_id.clone());
545 entity_id.push(chain_id);
546 }
547
548 let chains = ChainsTable {
549 label_asym_id,
550 auth_asym_id,
551 entity_id,
552 };
553
554 let mut atom_offsets: Vec<u32> = residue_starts.iter().map(|&s| s as u32).collect();
555 atom_offsets.push(n_atoms as u32);
556 let atom_to_residue = Segmentation::from_offsets(atom_offsets);
557
558 let residue_offsets: Vec<u32> = chain_starts
559 .iter()
560 .map(|&atom_start| {
561 residue_starts
562 .iter()
563 .position(|&r| r == atom_start)
564 .unwrap_or(0) as u32
565 })
566 .chain(std::iter::once(n_residues as u32))
567 .collect();
568 let residue_to_chain = Segmentation::from_offsets(residue_offsets);
569
570 let bonds = Bonds::from_unsorted(vec![], vec![], vec![], n_atoms);
571
572 let hierarchy = Arc::new(AtomicHierarchy {
573 atoms,
574 residues,
575 chains,
576 atom_to_residue,
577 residue_to_chain,
578 bonds,
579 });
580
581 let x: Vec<f32> = self.coords.iter().map(|c| c[0]).collect();
582 let y: Vec<f32> = self.coords.iter().map(|c| c[1]).collect();
583 let z: Vec<f32> = self.coords.iter().map(|c| c[2]).collect();
584
585 let conformation = AtomicConformation {
586 x,
587 y,
588 z,
589 occupancy: self.occupancy.clone(),
590 b_iso: self.b_factor.clone(),
591 confidence: None,
592 };
593
594 Model::new(hierarchy, conformation)
595 }
596}
597
598impl From<&Model> for AtomCollection {
599 fn from(model: &Model) -> Self {
603 let n_atoms = model.n_atoms();
604 let hierarchy = &model.hierarchy;
605 let conformation = &model.conformation;
606
607 let coords: Vec<[f32; 3]> = (0..n_atoms)
608 .map(|i| [conformation.x[i], conformation.y[i], conformation.z[i]])
609 .collect();
610
611 let mut res_ids = Vec::with_capacity(n_atoms);
612 let mut res_names = Vec::with_capacity(n_atoms);
613 let mut is_hetero = Vec::with_capacity(n_atoms);
614 let mut chain_ids = Vec::with_capacity(n_atoms);
615
616 for atom_idx in 0..n_atoms {
617 let res_idx = hierarchy.residue_of_atom(atom_idx);
618 let chain_idx = hierarchy.chain_of_residue(res_idx);
619
620 res_ids.push(hierarchy.residues.auth_seq_id[res_idx]);
621 res_names.push(hierarchy.residues.comp_id[res_idx].clone());
622 is_hetero.push(hierarchy.residues.group[res_idx] == ResidueGroup::NonPolymer);
623 chain_ids.push(hierarchy.chains.auth_asym_id[chain_idx].clone());
624 }
625
626 let elements = hierarchy.atoms.element.clone();
627
628 let atom_names = hierarchy.atoms.atom_name.clone();
629
630 let mut ac = AtomCollection::new(
631 n_atoms, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, None,
632 );
633
634 if let Some(values) = conformation
638 .confidence
639 .clone()
640 .or_else(|| conformation.b_iso.clone())
641 {
642 ac = ac.with_b_factor(values);
643 }
644 if let Some(values) = conformation.occupancy.clone() {
645 ac = ac.with_occupancy(values);
646 }
647
648 ac
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 fn make_test_atom_collection() -> AtomCollection {
657 let coords = vec![
658 [1.0, 2.0, 3.0],
659 [4.0, 5.0, 6.0],
660 [7.0, 8.0, 9.0],
661 [10.0, 11.0, 12.0],
662 [13.0, 14.0, 15.0],
663 ];
664 let res_ids = vec![1, 1, 1, 2, 2];
665 let res_names = vec![
666 "ALA".into(),
667 "ALA".into(),
668 "ALA".into(),
669 "GLY".into(),
670 "GLY".into(),
671 ];
672 let is_hetero = vec![false, false, false, false, false];
673 let elements = vec![Element::N, Element::C, Element::C, Element::N, Element::C];
674 let atom_names = vec!["N".into(), "CA".into(), "C".into(), "N".into(), "CA".into()];
675 let chain_ids = vec!["A".into(), "A".into(), "A".into(), "A".into(), "A".into()];
676
677 AtomCollection::new(
678 5, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, None,
679 )
680 }
681
682 #[test]
683 fn test_b_factor_and_occupancy_roundtrip_through_model() {
684 let original = make_test_atom_collection()
685 .with_b_factor(vec![10.0, 20.0, 30.0, 40.0, 50.0])
686 .with_occupancy(vec![1.0, 1.0, 0.5, 1.0, 0.3]);
687
688 let model = original.to_model();
689 assert_eq!(
690 model.conformation.b_iso.as_deref(),
691 Some(&[10.0, 20.0, 30.0, 40.0, 50.0][..])
692 );
693 assert_eq!(
694 model.conformation.occupancy.as_deref(),
695 Some(&[1.0, 1.0, 0.5, 1.0, 0.3][..])
696 );
697
698 let restored = AtomCollection::from(&model);
699 assert_eq!(
700 restored.get_b_factors(),
701 Some(&[10.0, 20.0, 30.0, 40.0, 50.0][..])
702 );
703 assert_eq!(
704 restored.get_occupancies(),
705 Some(&[1.0, 1.0, 0.5, 1.0, 0.3][..])
706 );
707 }
708
709 #[test]
710 fn test_confidence_preferred_over_b_iso_when_converting_from_model() {
711 let mut model = make_test_atom_collection().to_model();
712 model.conformation.b_iso = Some(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
713 model.conformation.confidence = Some(vec![90.0, 91.0, 92.0, 93.0, 94.0]);
714
715 let ac = AtomCollection::from(&model);
716 assert_eq!(
717 ac.get_b_factors(),
718 Some(&[90.0, 91.0, 92.0, 93.0, 94.0][..])
719 );
720 }
721
722 #[test]
723 fn test_filter_remaps_b_factor_and_occupancy() {
724 let ac = make_two_chain_collection()
725 .with_b_factor(vec![1.0, 2.0, 3.0, 4.0, 5.0])
726 .with_occupancy(vec![0.1, 0.2, 0.3, 0.4, 0.5]);
727 let mask = vec![true, false, true, false, true];
728 let filtered = ac.filter(&mask);
729 assert_eq!(filtered.get_b_factors(), Some(&[1.0, 3.0, 5.0][..]));
730 assert_eq!(filtered.get_occupancies(), Some(&[0.1, 0.3, 0.5][..]));
731 }
732
733 #[test]
734 fn test_atomcollection_to_model_roundtrip() {
735 let original = make_test_atom_collection();
736
737 let model = original.to_model();
738 let restored = AtomCollection::from(&model);
739
740 assert_eq!(original.get_size(), restored.get_size());
741
742 for i in 0..original.get_size() {
743 assert_eq!(
744 original.get_coord(i),
745 restored.get_coord(i),
746 "coord mismatch at {}",
747 i
748 );
749 assert_eq!(
750 original.get_res_id(i),
751 restored.get_res_id(i),
752 "res_id mismatch at {}",
753 i
754 );
755 assert_eq!(
756 original.get_res_name(i),
757 restored.get_res_name(i),
758 "res_name mismatch at {}",
759 i
760 );
761 assert_eq!(
762 original.get_atom_name(i),
763 restored.get_atom_name(i),
764 "atom_name mismatch at {}",
765 i
766 );
767 assert_eq!(
768 original.get_chain_id(i),
769 restored.get_chain_id(i),
770 "chain_id mismatch at {}",
771 i
772 );
773 assert_eq!(
774 original.get_is_hetero(i),
775 restored.get_is_hetero(i),
776 "is_hetero mismatch at {}",
777 i
778 );
779 }
780 }
781
782 #[test]
783 fn test_model_to_atomcollection_coords() {
784 let ac = make_test_atom_collection();
785 let model = ac.to_model();
786
787 let model_coords = model.coords_as_slice();
788 assert_eq!(model_coords.len(), ac.get_size());
789 #[allow(clippy::needless_range_loop)] for i in 0..ac.get_size() {
791 assert_eq!(model_coords[i], *ac.get_coord(i), "coord mismatch at {}", i);
792 }
793
794 assert_eq!(model.x(), &[1.0, 4.0, 7.0, 10.0, 13.0]);
795 assert_eq!(model.y(), &[2.0, 5.0, 8.0, 11.0, 14.0]);
796 assert_eq!(model.z(), &[3.0, 6.0, 9.0, 12.0, 15.0]);
797 }
798
799 #[test]
800 fn test_model_hierarchy_structure() {
801 let ac = make_test_atom_collection();
802 let model = ac.to_model();
803
804 assert_eq!(model.n_atoms(), 5);
805 assert_eq!(model.n_residues(), 2);
806 assert_eq!(model.n_chains(), 1);
807
808 assert_eq!(model.hierarchy.atoms_in_residue(0), 0..3);
809 assert_eq!(model.hierarchy.atoms_in_residue(1), 3..5);
810
811 assert_eq!(model.hierarchy.residues_in_chain(0), 0..2);
812 }
813
814 #[test]
815 fn test_multi_chain_roundtrip() {
816 let coords = vec![[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [3.0, 3.0, 3.0]];
817 let res_ids = vec![1, 1, 10];
818 let res_names = vec!["ALA".into(), "ALA".into(), "GLY".into()];
819 let is_hetero = vec![false, false, true];
820 let elements = vec![Element::N, Element::C, Element::N];
821 let atom_names = vec!["N".into(), "CA".into(), "N".into()];
822 let chain_ids = vec!["A".into(), "A".into(), "B".into()];
823
824 let original = AtomCollection::new(
825 3, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, None,
826 );
827
828 let model = original.to_model();
829 assert_eq!(model.n_chains(), 2);
830 assert_eq!(model.n_residues(), 2);
831
832 let restored = AtomCollection::from(&model);
833 assert_eq!(restored.get_chain_id(0), "A");
834 assert_eq!(restored.get_chain_id(2), "B");
835 assert!(restored.get_is_hetero(2));
836 assert!(!restored.get_is_hetero(0));
837 }
838
839 fn make_two_chain_collection() -> AtomCollection {
840 let coords = vec![
844 [1., 0., 0.],
845 [2., 0., 0.],
846 [3., 0., 0.],
847 [4., 0., 0.],
848 [5., 0., 0.],
849 ];
850 let res_ids = vec![1, 1, 1, 2, 2];
851 let res_names: Vec<String> = ["ALA", "ALA", "ALA", "GLY", "GLY"]
852 .iter()
853 .map(|s| s.to_string())
854 .collect();
855 let is_hetero = vec![false, false, false, false, false];
856 let elements = vec![Element::N, Element::C, Element::C, Element::N, Element::C];
857 let atom_names: Vec<String> = ["N", "CA", "C", "N", "CA"]
858 .iter()
859 .map(|s| s.to_string())
860 .collect();
861 let chain_ids: Vec<String> = ["A", "A", "A", "B", "B"]
862 .iter()
863 .map(|s| s.to_string())
864 .collect();
865 let bonds = vec![
866 Bond::new(0, 1, BondOrder::Single),
867 Bond::new(1, 2, BondOrder::Single),
868 Bond::new(3, 4, BondOrder::Single),
869 ];
870 AtomCollection::new(
871 5,
872 coords,
873 res_ids,
874 res_names,
875 is_hetero,
876 elements,
877 atom_names,
878 chain_ids,
879 Some(bonds),
880 )
881 }
882
883 #[test]
884 fn test_filter_keeps_selected_atoms() {
885 let ac = make_two_chain_collection();
886 let mask = vec![true, false, true, false, true];
887 let filtered = ac.filter(&mask);
888 assert_eq!(filtered.get_size(), 3);
889 assert_eq!(filtered.get_coord(0), &[1., 0., 0.]);
890 assert_eq!(filtered.get_coord(1), &[3., 0., 0.]);
891 assert_eq!(filtered.get_coord(2), &[5., 0., 0.]);
892 }
893
894 #[test]
895 fn test_filter_remaps_bonds() {
896 let ac = make_two_chain_collection();
897 let mask = vec![true, true, true, false, false];
899 let filtered = ac.filter(&mask);
900 let bonds = filtered.get_bonds().unwrap();
901 assert_eq!(bonds.len(), 2);
902 let indices: Vec<(i32, i32)> = bonds.iter().map(|b| b.get_atom_indices()).collect();
903 assert!(indices.contains(&(0, 1)));
904 assert!(indices.contains(&(1, 2)));
905 }
906
907 #[test]
908 fn test_filter_drops_cross_boundary_bonds() {
909 let ac = make_two_chain_collection();
910 let mask = vec![true, false, false, false, true];
912 let filtered = ac.filter(&mask);
913 assert_eq!(filtered.get_size(), 2);
914 let bonds = filtered.get_bonds().unwrap();
915 assert!(bonds.is_empty());
916 }
917
918 #[test]
919 fn test_select_chain() {
920 let ac = make_two_chain_collection();
921 let mask = ac.select_chain("B");
922 assert_eq!(mask, vec![false, false, false, true, true]);
923 let filtered = ac.filter(&mask);
924 assert_eq!(filtered.get_size(), 2);
925 }
926
927 #[test]
928 fn test_select_backbone() {
929 let ac = make_two_chain_collection();
930 let mask = ac.select_backbone();
931 assert_eq!(mask, vec![true, true, true, true, true]);
933 }
934
935 #[test]
936 fn test_select_atom_name() {
937 let ac = make_two_chain_collection();
938 let mask = ac.select_atom_name("CA");
939 assert_eq!(mask, vec![false, true, false, false, true]);
940 }
941
942 #[test]
943 fn test_select_residue_name() {
944 let ac = make_two_chain_collection();
945 let mask = ac.select_residue_name("ALA");
946 assert_eq!(mask, vec![true, true, true, false, false]);
947 }
948
949 #[test]
950 #[should_panic(expected = "mask length must equal atom count")]
951 fn test_filter_wrong_mask_length() {
952 let ac = make_test_atom_collection();
953 ac.filter(&[true, false]);
954 }
955
956 fn make_water_ion_collection() -> AtomCollection {
958 let coords = vec![
959 [1.0, 0.0, 0.0],
960 [2.0, 0.0, 0.0],
961 [3.0, 0.0, 0.0],
962 [4.0, 0.0, 0.0],
963 [5.0, 0.0, 0.0],
964 ];
965 let res_ids = vec![1, 1, 2, 3, 4];
966 let res_names: Vec<String> = vec!["ALA", "ALA", "HOH", "ZN", "WAT"]
967 .into_iter()
968 .map(|s| s.to_string())
969 .collect();
970 let is_hetero = vec![false, false, true, true, true];
971 let elements = vec![Element::N, Element::C, Element::O, Element::Zn, Element::O];
972 let atom_names: Vec<String> = vec!["N", "CA", "O", "ZN", "O"]
973 .into_iter()
974 .map(|s| s.to_string())
975 .collect();
976 let chain_ids: Vec<String> = vec!["A"; 5].into_iter().map(|s| s.to_string()).collect();
977 AtomCollection::new(
978 5, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, None,
979 )
980 }
981
982 #[test]
983 fn test_is_water_hoh() {
984 let ac = make_water_ion_collection();
985 let mut residues = ac.iter_residues();
986 let _ala = residues.next().unwrap();
987 let hoh = residues.next().unwrap();
988 assert!(hoh.is_water());
989 }
990
991 #[test]
992 fn test_is_water_wat() {
993 let ac = make_water_ion_collection();
994 let mut residues = ac.iter_residues();
995 residues.next(); residues.next(); residues.next(); let wat = residues.next().unwrap();
999 assert!(wat.is_water());
1000 }
1001
1002 #[test]
1003 fn test_is_water_negative() {
1004 let ac = make_water_ion_collection();
1005 let ala = ac.iter_residues().next().unwrap();
1006 assert!(!ala.is_water());
1007 }
1008
1009 #[test]
1010 fn test_is_ion_single_hetero_nonwater() {
1011 let ac = make_water_ion_collection();
1012 let mut residues = ac.iter_residues();
1013 residues.next(); residues.next(); let zn = residues.next().unwrap();
1016 assert!(zn.is_ion());
1017 }
1018
1019 #[test]
1020 fn test_is_ion_water_not_ion() {
1021 let ac = make_water_ion_collection();
1022 let mut residues = ac.iter_residues();
1023 residues.next(); let hoh = residues.next().unwrap();
1025 assert!(!hoh.is_ion(), "water should not be classified as ion");
1026 }
1027
1028 #[test]
1029 fn test_is_ion_protein_residue() {
1030 let ac = make_water_ion_collection();
1031 let ala = ac.iter_residues().next().unwrap();
1032 assert!(!ala.is_ion());
1033 }
1034
1035 #[test]
1036 fn test_is_ion_multi_atom_hetero() {
1037 let coords = vec![[0.0; 3], [1.0; 3]];
1039 let res_ids = vec![1, 1];
1040 let res_names: Vec<String> = vec!["LIG", "LIG"]
1041 .into_iter()
1042 .map(|s| s.to_string())
1043 .collect();
1044 let is_hetero = vec![true, true];
1045 let elements = vec![Element::C, Element::O];
1046 let atom_names: Vec<String> = vec!["C1", "O1"]
1047 .into_iter()
1048 .map(|s| s.to_string())
1049 .collect();
1050 let chain_ids: Vec<String> = vec!["A", "A"].into_iter().map(|s| s.to_string()).collect();
1051 let ac = AtomCollection::new(
1052 2, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, None,
1053 );
1054 let lig = ac.iter_residues().next().unwrap();
1055 assert!(
1056 !lig.is_ion(),
1057 "multi-atom hetero residue should not be an ion"
1058 );
1059 }
1060
1061 #[test]
1062 fn test_select_water_mask() {
1063 let ac = make_water_ion_collection();
1064 let mask = ac.select_water();
1065 assert_eq!(mask, vec![false, false, true, false, true]);
1067 }
1068
1069 #[test]
1070 fn test_select_ion_mask() {
1071 let ac = make_water_ion_collection();
1072 let mask = ac.select_ion();
1073 assert_eq!(mask, vec![false, false, false, true, false]);
1075 }
1076
1077 #[test]
1078 fn test_long_names_preserved() {
1079 let coords = vec![[0.0; 3], [1.0; 3]];
1083 let res_ids = vec![1, 1];
1084 let res_names: Vec<String> = vec!["AABCD", "AABCD"]
1085 .into_iter()
1086 .map(String::from)
1087 .collect();
1088 let is_hetero = vec![false, false];
1089 let elements = vec![Element::C, Element::O];
1090 let atom_names: Vec<String> = vec!["C1", "OXT"].into_iter().map(String::from).collect();
1091 let chain_ids: Vec<String> = vec!["AA", "AA"].into_iter().map(String::from).collect();
1092 let ac = AtomCollection::new(
1093 2, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, None,
1094 );
1095
1096 assert_eq!(ac.get_res_name(0), "AABCD");
1097 assert_eq!(ac.get_chain_id(0), "AA");
1098 assert_eq!(ac.get_atom_name(1), "OXT");
1099
1100 let subset = ac.filter(&[false, true]);
1102 assert_eq!(subset.get_size(), 1);
1103 assert_eq!(subset.get_res_name(0), "AABCD");
1104 assert_eq!(subset.get_atom_name(0), "OXT");
1105 assert_eq!(subset.get_chain_id(0), "AA");
1106 }
1107
1108 #[test]
1109 fn test_compact_string_is_inline() {
1110 assert!(!CompactString::from("CA").is_heap_allocated());
1113 assert!(!CompactString::from("AABCD").is_heap_allocated());
1114 }
1115}