1use super::chain::Chain;
8use super::grid::Grid;
9use super::residue::Residue;
10use super::types::Point;
11use crate::utils::parallel::*;
12use std::fmt;
13
14#[derive(Debug, Clone, Default)]
21pub struct Structure {
22 chains: Vec<Chain>,
24 pub box_vectors: Option<[[f64; 3]; 3]>,
26}
27
28impl Structure {
29 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn add_chain(&mut self, chain: Chain) {
47 debug_assert!(
48 self.chain(&chain.id).is_none(),
49 "Attempted to add a duplicate chain ID '{}'",
50 chain.id
51 );
52 self.chains.push(chain);
53 }
54
55 pub fn remove_chain(&mut self, id: &str) -> Option<Chain> {
65 if let Some(index) = self.chains.iter().position(|c| c.id == id) {
66 Some(self.chains.remove(index))
67 } else {
68 None
69 }
70 }
71
72 pub fn clear(&mut self) {
74 self.chains.clear();
75 }
76
77 pub fn chain(&self, id: &str) -> Option<&Chain> {
87 self.chains.iter().find(|c| c.id == id)
88 }
89
90 pub fn chain_mut(&mut self, id: &str) -> Option<&mut Chain> {
100 self.chains.iter_mut().find(|c| c.id == id)
101 }
102
103 pub fn find_residue(
115 &self,
116 chain_id: &str,
117 residue_id: i32,
118 insertion_code: Option<char>,
119 ) -> Option<&Residue> {
120 self.chain(chain_id)
121 .and_then(|c| c.residue(residue_id, insertion_code))
122 }
123
124 pub fn find_residue_mut(
136 &mut self,
137 chain_id: &str,
138 residue_id: i32,
139 insertion_code: Option<char>,
140 ) -> Option<&mut Residue> {
141 self.chain_mut(chain_id)
142 .and_then(|c| c.residue_mut(residue_id, insertion_code))
143 }
144
145 pub fn sort_chains_by_id(&mut self) {
147 self.chains.sort_by(|a, b| a.id.cmp(&b.id));
148 }
149
150 pub fn chain_count(&self) -> usize {
156 self.chains.len()
157 }
158
159 pub fn residue_count(&self) -> usize {
165 self.chains.iter().map(|c| c.residue_count()).sum()
166 }
167
168 pub fn atom_count(&self) -> usize {
174 self.chains.iter().map(|c| c.iter_atoms().count()).sum()
175 }
176
177 pub fn is_empty(&self) -> bool {
183 self.chains.is_empty()
184 }
185
186 pub fn iter_chains(&self) -> std::slice::Iter<'_, Chain> {
192 self.chains.iter()
193 }
194
195 pub fn iter_chains_mut(&mut self) -> std::slice::IterMut<'_, Chain> {
201 self.chains.iter_mut()
202 }
203
204 #[cfg(feature = "parallel")]
210 pub fn par_chains(&self) -> impl IndexedParallelIterator<Item = &Chain> {
211 self.chains.par_iter()
212 }
213
214 #[cfg(not(feature = "parallel"))]
216 pub(crate) fn par_chains(&self) -> impl IndexedParallelIterator<Item = &Chain> {
217 self.chains.par_iter()
218 }
219
220 #[cfg(feature = "parallel")]
226 pub fn par_chains_mut(&mut self) -> impl IndexedParallelIterator<Item = &mut Chain> {
227 self.chains.par_iter_mut()
228 }
229
230 #[cfg(not(feature = "parallel"))]
232 pub(crate) fn par_chains_mut(&mut self) -> impl IndexedParallelIterator<Item = &mut Chain> {
233 self.chains.par_iter_mut()
234 }
235
236 #[cfg(feature = "parallel")]
242 pub fn par_residues(&self) -> impl ParallelIterator<Item = &Residue> {
243 self.chains.par_iter().flat_map(|c| c.par_residues())
244 }
245
246 #[cfg(not(feature = "parallel"))]
248 pub(crate) fn par_residues(&self) -> impl ParallelIterator<Item = &Residue> {
249 self.chains.par_iter().flat_map(|c| c.par_residues())
250 }
251
252 #[cfg(feature = "parallel")]
258 pub fn par_residues_mut(&mut self) -> impl ParallelIterator<Item = &mut Residue> {
259 self.chains
260 .par_iter_mut()
261 .flat_map(|c| c.par_residues_mut())
262 }
263
264 #[cfg(not(feature = "parallel"))]
266 pub(crate) fn par_residues_mut(&mut self) -> impl ParallelIterator<Item = &mut Residue> {
267 self.chains
268 .par_iter_mut()
269 .flat_map(|c| c.par_residues_mut())
270 }
271
272 #[cfg(feature = "parallel")]
278 pub fn par_atoms(&self) -> impl ParallelIterator<Item = &super::atom::Atom> {
279 self.chains
280 .par_iter()
281 .flat_map(|c| c.par_residues().flat_map(|r| r.par_atoms()))
282 }
283
284 #[cfg(not(feature = "parallel"))]
286 pub(crate) fn par_atoms(&self) -> impl ParallelIterator<Item = &super::atom::Atom> {
287 self.chains
288 .par_iter()
289 .flat_map(|c| c.par_residues().flat_map(|r| r.par_atoms()))
290 }
291
292 #[cfg(feature = "parallel")]
298 pub fn par_atoms_mut(&mut self) -> impl ParallelIterator<Item = &mut super::atom::Atom> {
299 self.chains
300 .par_iter_mut()
301 .flat_map(|c| c.par_residues_mut().flat_map(|r| r.par_atoms_mut()))
302 }
303
304 #[cfg(not(feature = "parallel"))]
306 pub(crate) fn par_atoms_mut(&mut self) -> impl ParallelIterator<Item = &mut super::atom::Atom> {
307 self.chains
308 .par_iter_mut()
309 .flat_map(|c| c.par_residues_mut().flat_map(|r| r.par_atoms_mut()))
310 }
311
312 pub fn iter_atoms(&self) -> impl Iterator<Item = &super::atom::Atom> {
318 self.chains.iter().flat_map(|c| c.iter_atoms())
319 }
320
321 pub fn iter_atoms_mut(&mut self) -> impl Iterator<Item = &mut super::atom::Atom> {
327 self.chains.iter_mut().flat_map(|c| c.iter_atoms_mut())
328 }
329
330 pub fn retain_residues<F>(&mut self, mut f: F)
339 where
340 F: FnMut(&str, &Residue) -> bool,
341 {
342 for chain in &mut self.chains {
343 let chain_id = chain.id.clone();
344 chain.retain_residues(|residue| f(&chain_id, residue));
345 }
346 }
347
348 pub fn retain_residues_mut<F>(&mut self, mut f: F)
356 where
357 F: FnMut(&str, &mut Residue) -> bool,
358 {
359 for chain in &mut self.chains {
360 let chain_id = chain.id.clone();
361 chain.retain_residues_mut(|residue| f(&chain_id, residue));
362 }
363 }
364
365 #[cfg(feature = "parallel")]
374 pub fn par_retain_residues<F>(&mut self, f: F)
375 where
376 F: Fn(&str, &Residue) -> bool + Sync + Send,
377 {
378 self.chains.par_iter_mut().for_each(|chain| {
379 let chain_id = chain.id.clone();
380 chain.retain_residues(|residue| f(&chain_id, residue));
381 });
382 }
383
384 #[cfg(not(feature = "parallel"))]
386 pub fn par_retain_residues<F>(&mut self, f: F)
387 where
388 F: Fn(&str, &Residue) -> bool + Sync + Send,
389 {
390 self.retain_residues(f);
391 }
392
393 #[cfg(feature = "parallel")]
399 pub fn par_retain_residues_mut<F>(&mut self, f: F)
400 where
401 F: Fn(&str, &mut Residue) -> bool + Sync + Send,
402 {
403 self.chains.par_iter_mut().for_each(|chain| {
404 let chain_id = chain.id.clone();
405 chain.retain_residues_mut(|residue| f(&chain_id, residue));
406 });
407 }
408
409 #[cfg(not(feature = "parallel"))]
411 pub fn par_retain_residues_mut<F>(&mut self, f: F)
412 where
413 F: Fn(&str, &mut Residue) -> bool + Sync + Send,
414 {
415 self.retain_residues_mut(f);
416 }
417
418 pub fn prune_empty_chains(&mut self) {
420 self.chains.retain(|chain| !chain.is_empty());
421 }
422
423 pub fn iter_atoms_with_context(
429 &self,
430 ) -> impl Iterator<Item = (&Chain, &Residue, &super::atom::Atom)> {
431 self.chains.iter().flat_map(|chain| {
432 chain.iter_residues().flat_map(move |residue| {
433 residue.iter_atoms().map(move |atom| (chain, residue, atom))
434 })
435 })
436 }
437
438 pub fn geometric_center(&self) -> Point {
446 let mut sum = nalgebra::Vector3::zeros();
447 let mut count = 0;
448
449 for atom in self.iter_atoms() {
450 sum += atom.pos.coords;
451 count += 1;
452 }
453
454 if count > 0 {
455 Point::from(sum / (count as f64))
456 } else {
457 Point::origin()
458 }
459 }
460
461 pub fn center_of_mass(&self) -> Point {
470 let mut total_mass = 0.0;
471 let mut weighted_sum = nalgebra::Vector3::zeros();
472
473 for atom in self.iter_atoms() {
474 let mass = atom.element.atomic_mass();
475 weighted_sum += atom.pos.coords * mass;
476 total_mass += mass;
477 }
478
479 if total_mass > 1e-9 {
480 Point::from(weighted_sum / total_mass)
481 } else {
482 Point::origin()
483 }
484 }
485
486 pub fn spatial_grid(&self, cell_size: f64) -> Grid<(usize, usize, usize)> {
499 let items: Vec<_> = self
500 .par_chains()
501 .enumerate()
502 .flat_map(|(c_idx, chain)| {
503 chain
504 .par_residues()
505 .enumerate()
506 .flat_map_iter(move |(r_idx, residue)| {
507 residue
508 .iter_atoms()
509 .enumerate()
510 .map(move |(a_idx, atom)| (atom.pos, (c_idx, r_idx, a_idx)))
511 })
512 })
513 .collect();
514 Grid::new(items, cell_size)
515 }
516}
517
518impl fmt::Display for Structure {
519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520 write!(
521 f,
522 "Structure {{ chains: {}, residues: {}, atoms: {} }}",
523 self.chain_count(),
524 self.residue_count(),
525 self.atom_count()
526 )
527 }
528}
529
530impl FromIterator<Chain> for Structure {
531 fn from_iter<T: IntoIterator<Item = Chain>>(iter: T) -> Self {
532 Self {
533 chains: iter.into_iter().collect(),
534 box_vectors: None,
535 }
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use crate::model::atom::Atom;
543 use crate::model::types::{Element, ResidueCategory, StandardResidue};
544
545 fn make_residue(id: i32, name: &str) -> Residue {
546 Residue::new(
547 id,
548 None,
549 name,
550 Some(StandardResidue::ALA),
551 ResidueCategory::Standard,
552 )
553 }
554
555 #[test]
556 fn structure_new_creates_empty_structure() {
557 let structure = Structure::new();
558
559 assert!(structure.is_empty());
560 assert_eq!(structure.chain_count(), 0);
561 assert_eq!(structure.residue_count(), 0);
562 assert_eq!(structure.atom_count(), 0);
563 assert!(structure.box_vectors.is_none());
564 }
565
566 #[test]
567 fn structure_default_creates_empty_structure() {
568 let structure = Structure::default();
569
570 assert!(structure.is_empty());
571 assert!(structure.box_vectors.is_none());
572 }
573
574 #[test]
575 fn structure_add_chain_adds_chain_correctly() {
576 let mut structure = Structure::new();
577 let chain = Chain::new("A");
578
579 structure.add_chain(chain);
580
581 assert_eq!(structure.chain_count(), 1);
582 assert!(structure.chain("A").is_some());
583 assert_eq!(structure.chain("A").unwrap().id, "A");
584 }
585
586 #[test]
587 fn structure_remove_chain_removes_existing_chain() {
588 let mut structure = Structure::new();
589 let chain = Chain::new("A");
590 structure.add_chain(chain);
591
592 let removed = structure.remove_chain("A");
593
594 assert!(removed.is_some());
595 assert_eq!(removed.unwrap().id, "A");
596 assert_eq!(structure.chain_count(), 0);
597 assert!(structure.chain("A").is_none());
598 }
599
600 #[test]
601 fn structure_remove_chain_returns_none_for_nonexistent_chain() {
602 let mut structure = Structure::new();
603
604 let removed = structure.remove_chain("NONEXISTENT");
605
606 assert!(removed.is_none());
607 }
608
609 #[test]
610 fn structure_clear_removes_all_chains() {
611 let mut structure = Structure::new();
612 structure.add_chain(Chain::new("A"));
613 structure.add_chain(Chain::new("B"));
614
615 structure.clear();
616
617 assert!(structure.is_empty());
618 assert_eq!(structure.chain_count(), 0);
619 }
620
621 #[test]
622 fn structure_chain_returns_correct_chain() {
623 let mut structure = Structure::new();
624 let chain = Chain::new("A");
625 structure.add_chain(chain);
626
627 let retrieved = structure.chain("A");
628
629 assert!(retrieved.is_some());
630 assert_eq!(retrieved.unwrap().id, "A");
631 }
632
633 #[test]
634 fn structure_chain_returns_none_for_nonexistent_chain() {
635 let structure = Structure::new();
636
637 let retrieved = structure.chain("NONEXISTENT");
638
639 assert!(retrieved.is_none());
640 }
641
642 #[test]
643 fn structure_chain_mut_returns_correct_mutable_chain() {
644 let mut structure = Structure::new();
645 let chain = Chain::new("A");
646 structure.add_chain(chain);
647
648 let retrieved = structure.chain_mut("A");
649
650 assert!(retrieved.is_some());
651 assert_eq!(retrieved.unwrap().id, "A");
652 }
653
654 #[test]
655 fn structure_chain_mut_returns_none_for_nonexistent_chain() {
656 let mut structure = Structure::new();
657
658 let retrieved = structure.chain_mut("NONEXISTENT");
659
660 assert!(retrieved.is_none());
661 }
662
663 #[test]
664 fn structure_find_residue_finds_correct_residue() {
665 let mut structure = Structure::new();
666 let mut chain = Chain::new("A");
667 let residue = Residue::new(
668 1,
669 None,
670 "ALA",
671 Some(StandardResidue::ALA),
672 ResidueCategory::Standard,
673 );
674 chain.add_residue(residue);
675 structure.add_chain(chain);
676
677 let found = structure.find_residue("A", 1, None);
678
679 assert!(found.is_some());
680 assert_eq!(found.unwrap().id, 1);
681 assert_eq!(found.unwrap().name, "ALA");
682 }
683
684 #[test]
685 fn structure_find_residue_returns_none_for_nonexistent_chain() {
686 let structure = Structure::new();
687
688 let found = structure.find_residue("NONEXISTENT", 1, None);
689
690 assert!(found.is_none());
691 }
692
693 #[test]
694 fn structure_find_residue_returns_none_for_nonexistent_residue() {
695 let mut structure = Structure::new();
696 let chain = Chain::new("A");
697 structure.add_chain(chain);
698
699 let found = structure.find_residue("A", 999, None);
700
701 assert!(found.is_none());
702 }
703
704 #[test]
705 fn structure_find_residue_mut_finds_correct_mutable_residue() {
706 let mut structure = Structure::new();
707 let mut chain = Chain::new("A");
708 let residue = Residue::new(
709 1,
710 None,
711 "ALA",
712 Some(StandardResidue::ALA),
713 ResidueCategory::Standard,
714 );
715 chain.add_residue(residue);
716 structure.add_chain(chain);
717
718 let found = structure.find_residue_mut("A", 1, None);
719
720 assert!(found.is_some());
721 assert_eq!(found.unwrap().id, 1);
722 }
723
724 #[test]
725 fn structure_sort_chains_by_id_sorts_correctly() {
726 let mut structure = Structure::new();
727 structure.add_chain(Chain::new("C"));
728 structure.add_chain(Chain::new("A"));
729 structure.add_chain(Chain::new("B"));
730
731 structure.sort_chains_by_id();
732
733 let ids: Vec<&str> = structure.iter_chains().map(|c| c.id.as_str()).collect();
734 assert_eq!(ids, vec!["A", "B", "C"]);
735 }
736
737 #[test]
738 fn structure_chain_count_returns_correct_count() {
739 let mut structure = Structure::new();
740
741 assert_eq!(structure.chain_count(), 0);
742
743 structure.add_chain(Chain::new("A"));
744 assert_eq!(structure.chain_count(), 1);
745
746 structure.add_chain(Chain::new("B"));
747 assert_eq!(structure.chain_count(), 2);
748 }
749
750 #[test]
751 fn structure_residue_count_returns_correct_count() {
752 let mut structure = Structure::new();
753 let mut chain = Chain::new("A");
754 chain.add_residue(Residue::new(
755 1,
756 None,
757 "ALA",
758 Some(StandardResidue::ALA),
759 ResidueCategory::Standard,
760 ));
761 chain.add_residue(Residue::new(
762 2,
763 None,
764 "GLY",
765 Some(StandardResidue::GLY),
766 ResidueCategory::Standard,
767 ));
768 structure.add_chain(chain);
769
770 assert_eq!(structure.residue_count(), 2);
771 }
772
773 #[test]
774 fn structure_atom_count_returns_correct_count() {
775 let mut structure = Structure::new();
776 let mut chain = Chain::new("A");
777 let mut residue = Residue::new(
778 1,
779 None,
780 "ALA",
781 Some(StandardResidue::ALA),
782 ResidueCategory::Standard,
783 );
784 residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
785 residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
786 chain.add_residue(residue);
787 structure.add_chain(chain);
788
789 assert_eq!(structure.atom_count(), 2);
790 }
791
792 #[test]
793 fn structure_is_empty_returns_true_for_empty_structure() {
794 let structure = Structure::new();
795
796 assert!(structure.is_empty());
797 }
798
799 #[test]
800 fn structure_is_empty_returns_false_for_non_empty_structure() {
801 let mut structure = Structure::new();
802 structure.add_chain(Chain::new("A"));
803
804 assert!(!structure.is_empty());
805 }
806
807 #[test]
808 fn structure_iter_chains_iterates_correctly() {
809 let mut structure = Structure::new();
810 structure.add_chain(Chain::new("A"));
811 structure.add_chain(Chain::new("B"));
812
813 let mut ids = Vec::new();
814 for chain in structure.iter_chains() {
815 ids.push(chain.id.clone());
816 }
817
818 assert_eq!(ids, vec!["A", "B"]);
819 }
820
821 #[test]
822 fn structure_iter_chains_mut_iterates_correctly() {
823 let mut structure = Structure::new();
824 structure.add_chain(Chain::new("A"));
825
826 for chain in structure.iter_chains_mut() {
827 chain.id = "MODIFIED".into();
828 }
829
830 assert_eq!(structure.chain("MODIFIED").unwrap().id, "MODIFIED");
831 }
832
833 #[test]
834 fn structure_par_chains_iterates_correctly() {
835 let mut structure = Structure::new();
836 structure.add_chain(Chain::new("A"));
837 structure.add_chain(Chain::new("B"));
838
839 let ids: Vec<String> = structure.par_chains().map(|c| c.id.to_string()).collect();
840
841 assert_eq!(ids, vec!["A", "B"]);
842 }
843
844 #[test]
845 fn structure_par_chains_mut_iterates_correctly() {
846 let mut structure = Structure::new();
847 structure.add_chain(Chain::new("A"));
848 structure.add_chain(Chain::new("B"));
849
850 structure.par_chains_mut().for_each(|c| {
851 c.id = format!("{}_MOD", c.id).into();
852 });
853
854 assert_eq!(structure.chain("A_MOD").unwrap().id, "A_MOD");
855 assert_eq!(structure.chain("B_MOD").unwrap().id, "B_MOD");
856 }
857
858 #[test]
859 fn structure_par_residues_iterates_correctly() {
860 let mut structure = Structure::new();
861 let mut chain_a = Chain::new("A");
862 chain_a.add_residue(make_residue(1, "ALA"));
863 let mut chain_b = Chain::new("B");
864 chain_b.add_residue(make_residue(2, "GLY"));
865 structure.add_chain(chain_a);
866 structure.add_chain(chain_b);
867
868 let count = structure.par_residues().count();
869 assert_eq!(count, 2);
870
871 let names: Vec<String> = structure
872 .par_residues()
873 .map(|r| r.name.to_string())
874 .collect();
875 assert!(names.contains(&"ALA".to_string()));
876 assert!(names.contains(&"GLY".to_string()));
877 }
878
879 #[test]
880 fn structure_par_residues_mut_iterates_correctly() {
881 let mut structure = Structure::new();
882 let mut chain_a = Chain::new("A");
883 chain_a.add_residue(make_residue(1, "ALA"));
884 let mut chain_b = Chain::new("B");
885 chain_b.add_residue(make_residue(2, "GLY"));
886 structure.add_chain(chain_a);
887 structure.add_chain(chain_b);
888
889 structure.par_residues_mut().for_each(|r| {
890 r.name = format!("{}_MOD", r.name).into();
891 });
892
893 let chain_a = structure.chain("A").unwrap();
894 assert_eq!(chain_a.residue(1, None).unwrap().name, "ALA_MOD");
895
896 let chain_b = structure.chain("B").unwrap();
897 assert_eq!(chain_b.residue(2, None).unwrap().name, "GLY_MOD");
898 }
899
900 #[test]
901 fn structure_par_atoms_iterates_correctly() {
902 let mut structure = Structure::new();
903 let mut chain = Chain::new("A");
904 let mut r1 = make_residue(1, "ALA");
905 r1.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
906 let mut r2 = make_residue(2, "GLY");
907 r2.add_atom(Atom::new("N", Element::N, Point::new(1.0, 0.0, 0.0)));
908
909 chain.add_residue(r1);
910 chain.add_residue(r2);
911 structure.add_chain(chain);
912
913 let count = structure.par_atoms().count();
914 assert_eq!(count, 2);
915
916 let names: Vec<String> = structure.par_atoms().map(|a| a.name.to_string()).collect();
917 assert!(names.contains(&"CA".to_string()));
918 assert!(names.contains(&"N".to_string()));
919 }
920
921 #[test]
922 fn structure_par_atoms_mut_iterates_correctly() {
923 let mut structure = Structure::new();
924 let mut chain = Chain::new("A");
925 let mut r1 = make_residue(1, "ALA");
926 r1.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
927 chain.add_residue(r1);
928 structure.add_chain(chain);
929
930 structure.par_atoms_mut().for_each(|a| {
931 a.pos.x += 10.0;
932 });
933
934 let atom = structure
935 .chain("A")
936 .unwrap()
937 .residue(1, None)
938 .unwrap()
939 .atom("CA")
940 .unwrap();
941 assert!((atom.pos.x - 10.0).abs() < 1e-6);
942 }
943
944 #[test]
945 fn structure_iter_atoms_iterates_over_all_atoms() {
946 let mut structure = Structure::new();
947 let mut chain = Chain::new("A");
948 let mut residue = Residue::new(
949 1,
950 None,
951 "ALA",
952 Some(StandardResidue::ALA),
953 ResidueCategory::Standard,
954 );
955 residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
956 residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
957 chain.add_residue(residue);
958 structure.add_chain(chain);
959
960 let mut atom_names = Vec::new();
961 for atom in structure.iter_atoms() {
962 atom_names.push(atom.name.clone());
963 }
964
965 assert_eq!(atom_names, vec!["CA", "CB"]);
966 }
967
968 #[test]
969 fn structure_iter_atoms_mut_iterates_over_all_atoms_mutably() {
970 let mut structure = Structure::new();
971 let mut chain = Chain::new("A");
972 let mut residue = Residue::new(
973 1,
974 None,
975 "ALA",
976 Some(StandardResidue::ALA),
977 ResidueCategory::Standard,
978 );
979 residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
980 chain.add_residue(residue);
981 structure.add_chain(chain);
982
983 for atom in structure.iter_atoms_mut() {
984 atom.translate_by(&nalgebra::Vector3::new(1.0, 0.0, 0.0));
985 }
986
987 let translated_atom = structure
988 .find_residue("A", 1, None)
989 .unwrap()
990 .atom("CA")
991 .unwrap();
992 assert!((translated_atom.pos.x - 1.0).abs() < 1e-10);
993 }
994
995 #[test]
996 fn structure_retain_residues_filters_using_chain_context() {
997 let mut structure = Structure::new();
998 let mut chain_a = Chain::new("A");
999 chain_a.add_residue(make_residue(1, "ALA"));
1000 chain_a.add_residue(make_residue(2, "GLY"));
1001 let mut chain_b = Chain::new("B");
1002 chain_b.add_residue(make_residue(3, "SER"));
1003 structure.add_chain(chain_a);
1004 structure.add_chain(chain_b);
1005
1006 structure.retain_residues(|chain_id, residue| chain_id == "A" && residue.id == 1);
1007
1008 let chain_a = structure.chain("A").unwrap();
1009 assert_eq!(chain_a.residue_count(), 1);
1010 assert!(chain_a.residue(1, None).is_some());
1011 assert!(structure.chain("B").unwrap().is_empty());
1012 }
1013
1014 #[test]
1015 fn structure_retain_residues_mut_filters_and_modifies() {
1016 let mut structure = Structure::new();
1017 let mut chain_a = Chain::new("A");
1018 chain_a.add_residue(make_residue(1, "ALA"));
1019 chain_a.add_residue(make_residue(2, "GLY"));
1020 let mut chain_b = Chain::new("B");
1021 chain_b.add_residue(make_residue(3, "SER"));
1022 structure.add_chain(chain_a);
1023 structure.add_chain(chain_b);
1024
1025 structure.retain_residues_mut(|chain_id, residue| {
1026 if chain_id == "A" && residue.id == 1 {
1027 residue.name = format!("{}_MOD", residue.name).into();
1028 true
1029 } else {
1030 false
1031 }
1032 });
1033
1034 let chain_a = structure.chain("A").unwrap();
1035 assert_eq!(chain_a.residue_count(), 1);
1036 assert_eq!(chain_a.residue(1, None).unwrap().name, "ALA_MOD");
1037 assert!(structure.chain("B").unwrap().is_empty());
1038 }
1039
1040 #[test]
1041 fn structure_par_retain_residues_filters_correctly() {
1042 let mut structure = Structure::new();
1043 let mut chain_a = Chain::new("A");
1044 chain_a.add_residue(make_residue(1, "ALA"));
1045 chain_a.add_residue(make_residue(2, "GLY"));
1046 let mut chain_b = Chain::new("B");
1047 chain_b.add_residue(make_residue(3, "SER"));
1048 structure.add_chain(chain_a);
1049 structure.add_chain(chain_b);
1050
1051 structure.par_retain_residues(|chain_id, residue| chain_id == "A" && residue.id == 1);
1052
1053 let chain_a = structure.chain("A").unwrap();
1054 assert_eq!(chain_a.residue_count(), 1);
1055 assert!(chain_a.residue(1, None).is_some());
1056 assert!(structure.chain("B").unwrap().is_empty());
1057 }
1058
1059 #[test]
1060 fn structure_par_retain_residues_mut_filters_and_modifies() {
1061 let mut structure = Structure::new();
1062 let mut chain_a = Chain::new("A");
1063 chain_a.add_residue(make_residue(1, "ALA"));
1064 chain_a.add_residue(make_residue(2, "GLY"));
1065 let mut chain_b = Chain::new("B");
1066 chain_b.add_residue(make_residue(3, "SER"));
1067 structure.add_chain(chain_a);
1068 structure.add_chain(chain_b);
1069
1070 structure.par_retain_residues_mut(|chain_id, residue| {
1071 if chain_id == "A" && residue.id == 1 {
1072 residue.name = format!("{}_MOD", residue.name).into();
1073 true
1074 } else {
1075 false
1076 }
1077 });
1078
1079 let chain_a = structure.chain("A").unwrap();
1080 assert_eq!(chain_a.residue_count(), 1);
1081 assert_eq!(chain_a.residue(1, None).unwrap().name, "ALA_MOD");
1082 assert!(structure.chain("B").unwrap().is_empty());
1083 }
1084
1085 #[test]
1086 fn structure_prune_empty_chains_removes_them() {
1087 let mut structure = Structure::new();
1088 let mut chain_a = Chain::new("A");
1089 chain_a.add_residue(make_residue(1, "ALA"));
1090 let chain_b = Chain::new("B");
1091 structure.add_chain(chain_a);
1092 structure.add_chain(chain_b);
1093
1094 structure.prune_empty_chains();
1095
1096 assert!(structure.chain("A").is_some());
1097 assert!(structure.chain("B").is_none());
1098 assert_eq!(structure.chain_count(), 1);
1099 }
1100
1101 #[test]
1102 fn structure_iter_atoms_with_context_provides_correct_context() {
1103 let mut structure = Structure::new();
1104 let mut chain = Chain::new("A");
1105 let mut residue = Residue::new(
1106 1,
1107 None,
1108 "ALA",
1109 Some(StandardResidue::ALA),
1110 ResidueCategory::Standard,
1111 );
1112 residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
1113 chain.add_residue(residue);
1114 structure.add_chain(chain);
1115
1116 let mut contexts = Vec::new();
1117 for (chain, residue, atom) in structure.iter_atoms_with_context() {
1118 contexts.push((chain.id.clone(), residue.id, atom.name.clone()));
1119 }
1120
1121 assert_eq!(contexts, vec![("A".into(), 1, "CA".into())]);
1122 }
1123
1124 #[test]
1125 fn structure_geometric_center_calculates_correctly() {
1126 let mut structure = Structure::new();
1127 let mut chain = Chain::new("A");
1128 let mut residue = Residue::new(
1129 1,
1130 None,
1131 "ALA",
1132 Some(StandardResidue::ALA),
1133 ResidueCategory::Standard,
1134 );
1135 residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
1136 residue.add_atom(Atom::new("CB", Element::C, Point::new(2.0, 0.0, 0.0)));
1137 chain.add_residue(residue);
1138 structure.add_chain(chain);
1139
1140 let center = structure.geometric_center();
1141
1142 assert!((center.x - 1.0).abs() < 1e-10);
1143 assert!((center.y - 0.0).abs() < 1e-10);
1144 assert!((center.z - 0.0).abs() < 1e-10);
1145 }
1146
1147 #[test]
1148 fn structure_geometric_center_returns_origin_for_empty_structure() {
1149 let structure = Structure::new();
1150
1151 let center = structure.geometric_center();
1152
1153 assert_eq!(center, Point::origin());
1154 }
1155
1156 #[test]
1157 fn structure_center_of_mass_calculates_correctly() {
1158 let mut structure = Structure::new();
1159 let mut chain = Chain::new("A");
1160 let mut residue = Residue::new(
1161 1,
1162 None,
1163 "ALA",
1164 Some(StandardResidue::ALA),
1165 ResidueCategory::Standard,
1166 );
1167 residue.add_atom(Atom::new("H", Element::H, Point::new(0.0, 0.0, 0.0)));
1168 residue.add_atom(Atom::new("C", Element::C, Point::new(2.0, 0.0, 0.0)));
1169 chain.add_residue(residue);
1170 structure.add_chain(chain);
1171
1172 let com = structure.center_of_mass();
1173
1174 let expected_x = (0.0 * 1.00794 + 2.0 * 12.0107) / (1.00794 + 12.0107);
1175 assert!((com.x - expected_x).abs() < 1e-3);
1176 assert!((com.y - 0.0).abs() < 1e-10);
1177 assert!((com.z - 0.0).abs() < 1e-10);
1178 }
1179
1180 #[test]
1181 fn structure_center_of_mass_returns_origin_for_empty_structure() {
1182 let structure = Structure::new();
1183
1184 let com = structure.center_of_mass();
1185
1186 assert_eq!(com, Point::origin());
1187 }
1188
1189 #[test]
1190 fn structure_display_formats_correctly() {
1191 let mut structure = Structure::new();
1192 let mut chain = Chain::new("A");
1193 let mut residue = Residue::new(
1194 1,
1195 None,
1196 "ALA",
1197 Some(StandardResidue::ALA),
1198 ResidueCategory::Standard,
1199 );
1200 residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
1201 chain.add_residue(residue);
1202 structure.add_chain(chain);
1203
1204 let display = format!("{}", structure);
1205 let expected = "Structure { chains: 1, residues: 1, atoms: 1 }";
1206
1207 assert_eq!(display, expected);
1208 }
1209
1210 #[test]
1211 fn structure_display_formats_empty_structure_correctly() {
1212 let structure = Structure::new();
1213
1214 let display = format!("{}", structure);
1215 let expected = "Structure { chains: 0, residues: 0, atoms: 0 }";
1216
1217 assert_eq!(display, expected);
1218 }
1219
1220 #[test]
1221 fn structure_from_iterator_creates_structure_correctly() {
1222 let chains = vec![Chain::new("A"), Chain::new("B")];
1223 let structure: Structure = chains.into_iter().collect();
1224
1225 assert_eq!(structure.chain_count(), 2);
1226 assert!(structure.chain("A").is_some());
1227 assert!(structure.chain("B").is_some());
1228 assert!(structure.box_vectors.is_none());
1229 }
1230
1231 #[test]
1232 fn structure_clone_creates_identical_copy() {
1233 let mut structure = Structure::new();
1234 structure.add_chain(Chain::new("A"));
1235 structure.box_vectors = Some([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
1236
1237 let cloned = structure.clone();
1238
1239 assert_eq!(structure.chain_count(), cloned.chain_count());
1240 assert_eq!(structure.box_vectors, cloned.box_vectors);
1241 }
1242
1243 #[test]
1244 fn spatial_grid_bins_and_neighbor_queries_work() {
1245 let mut structure = Structure::new();
1246 let mut chain = Chain::new("A");
1247 let mut residue = Residue::new(
1248 1,
1249 None,
1250 "ALA",
1251 Some(StandardResidue::ALA),
1252 ResidueCategory::Standard,
1253 );
1254
1255 residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
1256 residue.add_atom(Atom::new("CB", Element::C, Point::new(1.5, 0.0, 0.0)));
1257 chain.add_residue(residue);
1258 structure.add_chain(chain);
1259
1260 let grid = structure.spatial_grid(1.0);
1261
1262 let big_center = structure.geometric_center();
1263 let all_count = grid.neighbors(&big_center, 1e6).count();
1264 assert_eq!(all_count, structure.atom_count());
1265
1266 let center = Point::new(0.0, 0.0, 0.0);
1267 let neighbors: Vec<_> = grid.neighbors(¢er, 0.1).collect();
1268 assert_eq!(neighbors.len(), 1);
1269
1270 let &(c_idx, r_idx, a_idx) = neighbors[0];
1271 let chain_ref = structure.iter_chains().nth(c_idx).unwrap();
1272 let residue_ref = chain_ref.iter_residues().nth(r_idx).unwrap();
1273 let atom_ref = residue_ref.iter_atoms().nth(a_idx).unwrap();
1274 assert_eq!(atom_ref.name, "CA");
1275
1276 let coarse = grid.neighbors(¢er, 2.0).count();
1277 assert_eq!(coarse, 2);
1278
1279 let exact: Vec<_> = grid.neighbors(¢er, 1.0).exact().collect();
1280 assert_eq!(exact.len(), 1);
1281 }
1282
1283 #[test]
1284 fn spatial_grid_empty_structure_is_empty() {
1285 let structure = Structure::new();
1286
1287 let grid = structure.spatial_grid(1.0);
1288 assert_eq!(grid.neighbors(&Point::origin(), 1.0).count(), 0);
1289 assert_eq!(grid.neighbors(&Point::origin(), 1.0).exact().count(), 0);
1290 }
1291
1292 #[test]
1293 fn spatial_grid_dense_packing_and_indices_are_consistent() {
1294 let mut structure = Structure::new();
1295 let mut chain = Chain::new("A");
1296 let mut residue = Residue::new(
1297 1,
1298 None,
1299 "ALA",
1300 Some(StandardResidue::ALA),
1301 ResidueCategory::Standard,
1302 );
1303
1304 for i in 0..50 {
1305 residue.add_atom(Atom::new(
1306 &format!("X{}", i),
1307 Element::C,
1308 Point::new(0.1, 0.1, 0.1),
1309 ));
1310 }
1311 chain.add_residue(residue);
1312 structure.add_chain(chain);
1313
1314 let grid = structure.spatial_grid(1.0);
1315 let center = Point::new(0.1, 0.1, 0.1);
1316 assert_eq!(grid.neighbors(¢er, 1e6).count(), structure.atom_count());
1317
1318 let center = Point::new(0.1, 0.1, 0.1);
1319 let count = grid.neighbors(¢er, 0.5).count();
1320 assert_eq!(count, 50);
1321
1322 for &(c, r, a) in grid.neighbors(¢er, 0.5) {
1323 let chain_ref = structure.iter_chains().nth(c).unwrap();
1324 let residue_ref = chain_ref.iter_residues().nth(r).unwrap();
1325 let atom_ref = residue_ref.iter_atoms().nth(a).unwrap();
1326 assert!(atom_ref.name.starts_with('X'));
1327 }
1328 }
1329}