1use super::residue::Residue;
9use crate::utils::parallel::*;
10use smol_str::SmolStr;
11use std::fmt;
12
13#[derive(Debug, Clone, PartialEq)]
19pub struct Chain {
20 pub id: SmolStr,
22 residues: Vec<Residue>,
24}
25
26impl Chain {
27 pub fn new(id: &str) -> Self {
40 Self {
41 id: SmolStr::new(id),
42 residues: Vec::new(),
43 }
44 }
45
46 pub fn add_residue(&mut self, residue: Residue) {
55 debug_assert!(
57 self.residue(residue.id, residue.insertion_code).is_none(),
58 "Attempted to add a duplicate residue ID '{}' (ic: {:?}) to chain '{}'",
59 residue.id,
60 residue.insertion_code,
61 self.id
62 );
63 self.residues.push(residue);
64 }
65
66 pub fn reserve(&mut self, additional: usize) {
74 self.residues.reserve(additional);
75 }
76
77 pub fn residue(&self, id: i32, insertion_code: Option<char>) -> Option<&Residue> {
88 self.residues
89 .iter()
90 .find(|r| r.id == id && r.insertion_code == insertion_code)
91 }
92
93 pub fn residue_mut(&mut self, id: i32, insertion_code: Option<char>) -> Option<&mut Residue> {
104 self.residues
105 .iter_mut()
106 .find(|r| r.id == id && r.insertion_code == insertion_code)
107 }
108
109 pub fn residues(&self) -> &[Residue] {
117 &self.residues
118 }
119
120 pub fn residue_count(&self) -> usize {
126 self.residues.len()
127 }
128
129 pub fn atom_count(&self) -> usize {
135 self.residues.iter().map(|r| r.atom_count()).sum()
136 }
137
138 pub fn is_empty(&self) -> bool {
144 self.residues.is_empty()
145 }
146
147 pub fn iter_residues(&self) -> std::slice::Iter<'_, Residue> {
156 self.residues.iter()
157 }
158
159 pub fn iter_residues_mut(&mut self) -> std::slice::IterMut<'_, Residue> {
165 self.residues.iter_mut()
166 }
167
168 #[cfg(feature = "parallel")]
174 pub fn par_residues(&self) -> impl IndexedParallelIterator<Item = &Residue> {
175 self.residues.par_iter()
176 }
177
178 #[cfg(not(feature = "parallel"))]
180 pub(crate) fn par_residues(&self) -> impl IndexedParallelIterator<Item = &Residue> {
181 self.residues.par_iter()
182 }
183
184 #[cfg(feature = "parallel")]
190 pub fn par_residues_mut(&mut self) -> impl IndexedParallelIterator<Item = &mut Residue> {
191 self.residues.par_iter_mut()
192 }
193
194 #[cfg(not(feature = "parallel"))]
196 pub(crate) fn par_residues_mut(&mut self) -> impl IndexedParallelIterator<Item = &mut Residue> {
197 self.residues.par_iter_mut()
198 }
199
200 pub fn iter_atoms(&self) -> impl Iterator<Item = &super::atom::Atom> {
209 self.residues.iter().flat_map(|r| r.iter_atoms())
210 }
211
212 pub fn iter_atoms_mut(&mut self) -> impl Iterator<Item = &mut super::atom::Atom> {
218 self.residues.iter_mut().flat_map(|r| r.iter_atoms_mut())
219 }
220
221 pub fn retain_residues<F>(&mut self, mut f: F)
227 where
228 F: FnMut(&Residue) -> bool,
229 {
230 self.residues.retain(|residue| f(residue));
231 }
232
233 pub fn retain_residues_mut<F>(&mut self, mut f: F)
239 where
240 F: FnMut(&mut Residue) -> bool,
241 {
242 self.residues.retain_mut(|residue| f(residue));
243 }
244
245 pub fn remove_residue(&mut self, id: i32, insertion_code: Option<char>) -> Option<Residue> {
256 if let Some(index) = self
257 .residues
258 .iter()
259 .position(|r| r.id == id && r.insertion_code == insertion_code)
260 {
261 Some(self.residues.remove(index))
262 } else {
263 None
264 }
265 }
266}
267
268impl fmt::Display for Chain {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 write!(
271 f,
272 "Chain {{ id: \"{}\", residues: {} }}",
273 self.id,
274 self.residue_count()
275 )
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::model::atom::Atom;
283 use crate::model::types::{Element, Point, ResidueCategory, StandardResidue};
284
285 fn sample_residue(id: i32, name: &str) -> Residue {
286 Residue::new(
287 id,
288 None,
289 name,
290 Some(StandardResidue::ALA),
291 ResidueCategory::Standard,
292 )
293 }
294
295 #[test]
296 fn chain_new_creates_correct_chain() {
297 let chain = Chain::new("A");
298
299 assert_eq!(chain.id, "A");
300 assert!(chain.residues.is_empty());
301 }
302
303 #[test]
304 fn chain_add_residue_adds_residue_correctly() {
305 let mut chain = Chain::new("A");
306 let residue = Residue::new(
307 1,
308 None,
309 "ALA",
310 Some(StandardResidue::ALA),
311 ResidueCategory::Standard,
312 );
313
314 chain.add_residue(residue);
315
316 assert_eq!(chain.residue_count(), 1);
317 assert!(chain.residue(1, None).is_some());
318 assert_eq!(chain.residue(1, None).unwrap().name, "ALA");
319 }
320
321 #[test]
322 fn chain_reserve_increases_capacity() {
323 let mut chain = Chain::new("A");
324 let initial_capacity = chain.residues.capacity();
325
326 chain.reserve(50);
327
328 assert!(chain.residues.capacity() >= initial_capacity + 50);
329 }
330
331 #[test]
332 fn chain_residue_returns_correct_residue() {
333 let mut chain = Chain::new("A");
334 let residue = Residue::new(
335 1,
336 None,
337 "ALA",
338 Some(StandardResidue::ALA),
339 ResidueCategory::Standard,
340 );
341 chain.add_residue(residue);
342
343 let retrieved = chain.residue(1, None);
344
345 assert!(retrieved.is_some());
346 assert_eq!(retrieved.unwrap().id, 1);
347 assert_eq!(retrieved.unwrap().name, "ALA");
348 }
349
350 #[test]
351 fn chain_residue_returns_none_for_nonexistent_residue() {
352 let chain = Chain::new("A");
353
354 let retrieved = chain.residue(999, None);
355
356 assert!(retrieved.is_none());
357 }
358
359 #[test]
360 fn chain_residue_mut_returns_correct_mutable_residue() {
361 let mut chain = Chain::new("A");
362 let residue = Residue::new(
363 1,
364 None,
365 "ALA",
366 Some(StandardResidue::ALA),
367 ResidueCategory::Standard,
368 );
369 chain.add_residue(residue);
370
371 let retrieved = chain.residue_mut(1, None);
372
373 assert!(retrieved.is_some());
374 assert_eq!(retrieved.unwrap().id, 1);
375 }
376
377 #[test]
378 fn chain_residue_mut_returns_none_for_nonexistent_residue() {
379 let mut chain = Chain::new("A");
380
381 let retrieved = chain.residue_mut(999, None);
382
383 assert!(retrieved.is_none());
384 }
385
386 #[test]
387 fn chain_residues_returns_correct_slice() {
388 let mut chain = Chain::new("A");
389 let residue1 = Residue::new(
390 1,
391 None,
392 "ALA",
393 Some(StandardResidue::ALA),
394 ResidueCategory::Standard,
395 );
396 let residue2 = Residue::new(
397 2,
398 None,
399 "GLY",
400 Some(StandardResidue::GLY),
401 ResidueCategory::Standard,
402 );
403 chain.add_residue(residue1);
404 chain.add_residue(residue2);
405
406 let residues = chain.residues();
407
408 assert_eq!(residues.len(), 2);
409 assert_eq!(residues[0].id, 1);
410 assert_eq!(residues[1].id, 2);
411 }
412
413 #[test]
414 fn chain_residue_count_returns_correct_count() {
415 let mut chain = Chain::new("A");
416
417 assert_eq!(chain.residue_count(), 0);
418
419 let residue = Residue::new(
420 1,
421 None,
422 "ALA",
423 Some(StandardResidue::ALA),
424 ResidueCategory::Standard,
425 );
426 chain.add_residue(residue);
427
428 assert_eq!(chain.residue_count(), 1);
429 }
430
431 #[test]
432 fn chain_atom_count_calculates_total_atoms() {
433 let mut chain = Chain::new("A");
434
435 let mut r1 = sample_residue(1, "ALA");
436 r1.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
437 r1.add_atom(Atom::new("CB", Element::C, Point::new(0.0, 0.0, 0.0)));
438
439 let mut r2 = sample_residue(2, "GLY");
440 r2.add_atom(Atom::new("N", Element::N, Point::new(0.0, 0.0, 0.0)));
441
442 chain.add_residue(r1);
443 chain.add_residue(r2);
444
445 assert_eq!(chain.atom_count(), 3);
446 }
447
448 #[test]
449 fn chain_is_empty_returns_true_for_empty_chain() {
450 let chain = Chain::new("A");
451
452 assert!(chain.is_empty());
453 }
454
455 #[test]
456 fn chain_is_empty_returns_false_for_non_empty_chain() {
457 let mut chain = Chain::new("A");
458 let residue = Residue::new(
459 1,
460 None,
461 "ALA",
462 Some(StandardResidue::ALA),
463 ResidueCategory::Standard,
464 );
465 chain.add_residue(residue);
466
467 assert!(!chain.is_empty());
468 }
469
470 #[test]
471 fn chain_iter_residues_iterates_correctly() {
472 let mut chain = Chain::new("A");
473 let residue1 = Residue::new(
474 1,
475 None,
476 "ALA",
477 Some(StandardResidue::ALA),
478 ResidueCategory::Standard,
479 );
480 let residue2 = Residue::new(
481 2,
482 None,
483 "GLY",
484 Some(StandardResidue::GLY),
485 ResidueCategory::Standard,
486 );
487 chain.add_residue(residue1);
488 chain.add_residue(residue2);
489
490 let mut ids = Vec::new();
491 for residue in chain.iter_residues() {
492 ids.push(residue.id);
493 }
494
495 assert_eq!(ids, vec![1, 2]);
496 }
497
498 #[test]
499 fn chain_iter_residues_mut_iterates_correctly() {
500 let mut chain = Chain::new("A");
501 let residue = Residue::new(
502 1,
503 None,
504 "ALA",
505 Some(StandardResidue::ALA),
506 ResidueCategory::Standard,
507 );
508 chain.add_residue(residue);
509
510 for residue in chain.iter_residues_mut() {
511 residue.position = crate::model::types::ResiduePosition::Internal;
512 }
513
514 assert_eq!(
515 chain.residue(1, None).unwrap().position,
516 crate::model::types::ResiduePosition::Internal
517 );
518 }
519
520 #[test]
521 fn chain_par_residues_iterates_correctly() {
522 let mut chain = Chain::new("A");
523 chain.add_residue(sample_residue(1, "ALA"));
524 chain.add_residue(sample_residue(2, "GLY"));
525
526 let ids: Vec<i32> = chain.par_residues().map(|r| r.id).collect();
527 assert_eq!(ids, vec![1, 2]);
528 }
529
530 #[test]
531 fn chain_par_residues_mut_iterates_correctly() {
532 let mut chain = Chain::new("A");
533 chain.add_residue(sample_residue(1, "ALA"));
534 chain.add_residue(sample_residue(2, "GLY"));
535
536 chain.par_residues_mut().for_each(|r| {
537 r.id += 10;
538 });
539
540 assert_eq!(chain.residue(11, None).unwrap().name, "ALA");
541 assert_eq!(chain.residue(12, None).unwrap().name, "GLY");
542 }
543
544 #[test]
545 fn chain_iter_atoms_iterates_over_all_atoms() {
546 let mut chain = Chain::new("A");
547 let mut residue1 = Residue::new(
548 1,
549 None,
550 "ALA",
551 Some(StandardResidue::ALA),
552 ResidueCategory::Standard,
553 );
554 let mut residue2 = Residue::new(
555 2,
556 None,
557 "GLY",
558 Some(StandardResidue::GLY),
559 ResidueCategory::Standard,
560 );
561
562 let atom1 = Atom::new("CA1", Element::C, Point::new(0.0, 0.0, 0.0));
563 let atom2 = Atom::new("CB1", Element::C, Point::new(1.0, 0.0, 0.0));
564 let atom3 = Atom::new("CA2", Element::C, Point::new(2.0, 0.0, 0.0));
565
566 residue1.add_atom(atom1);
567 residue1.add_atom(atom2);
568 residue2.add_atom(atom3);
569
570 chain.add_residue(residue1);
571 chain.add_residue(residue2);
572
573 let mut atom_names = Vec::new();
574 for atom in chain.iter_atoms() {
575 atom_names.push(atom.name.clone());
576 }
577
578 assert_eq!(atom_names, vec!["CA1", "CB1", "CA2"]);
579 }
580
581 #[test]
582 fn chain_iter_atoms_mut_iterates_over_all_atoms_mutably() {
583 let mut chain = Chain::new("A");
584 let mut residue = Residue::new(
585 1,
586 None,
587 "ALA",
588 Some(StandardResidue::ALA),
589 ResidueCategory::Standard,
590 );
591 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
592 residue.add_atom(atom);
593 chain.add_residue(residue);
594
595 for atom in chain.iter_atoms_mut() {
596 atom.translate_by(&nalgebra::Vector3::new(1.0, 0.0, 0.0));
597 }
598
599 let translated_atom = chain.residue(1, None).unwrap().atom("CA").unwrap();
600 assert!((translated_atom.pos.x - 1.0).abs() < 1e-10);
601 }
602
603 #[test]
604 fn chain_iter_atoms_returns_empty_iterator_for_empty_chain() {
605 let chain = Chain::new("A");
606
607 let count = chain.iter_atoms().count();
608
609 assert_eq!(count, 0);
610 }
611
612 #[test]
613 fn chain_iter_atoms_mut_returns_empty_iterator_for_empty_chain() {
614 let mut chain = Chain::new("A");
615
616 let count = chain.iter_atoms_mut().count();
617
618 assert_eq!(count, 0);
619 }
620
621 #[test]
622 fn chain_display_formats_correctly() {
623 let mut chain = Chain::new("A");
624 let residue = Residue::new(
625 1,
626 None,
627 "ALA",
628 Some(StandardResidue::ALA),
629 ResidueCategory::Standard,
630 );
631 chain.add_residue(residue);
632
633 let display = format!("{}", chain);
634 let expected = "Chain { id: \"A\", residues: 1 }";
635
636 assert_eq!(display, expected);
637 }
638
639 #[test]
640 fn chain_display_formats_empty_chain_correctly() {
641 let chain = Chain::new("B");
642
643 let display = format!("{}", chain);
644 let expected = "Chain { id: \"B\", residues: 0 }";
645
646 assert_eq!(display, expected);
647 }
648
649 #[test]
650 fn chain_clone_creates_identical_copy() {
651 let mut chain = Chain::new("A");
652 let residue = Residue::new(
653 1,
654 None,
655 "ALA",
656 Some(StandardResidue::ALA),
657 ResidueCategory::Standard,
658 );
659 chain.add_residue(residue);
660
661 let cloned = chain.clone();
662
663 assert_eq!(chain, cloned);
664 assert_eq!(chain.id, cloned.id);
665 assert_eq!(chain.residues, cloned.residues);
666 }
667
668 #[test]
669 fn chain_partial_eq_compares_correctly() {
670 let mut chain1 = Chain::new("A");
671 let mut chain2 = Chain::new("A");
672 let residue = Residue::new(
673 1,
674 None,
675 "ALA",
676 Some(StandardResidue::ALA),
677 ResidueCategory::Standard,
678 );
679 chain1.add_residue(residue.clone());
680 chain2.add_residue(residue);
681
682 let chain3 = Chain::new("B");
683
684 assert_eq!(chain1, chain2);
685 assert_ne!(chain1, chain3);
686 }
687
688 #[test]
689 fn chain_with_multiple_residues_and_atoms() {
690 let mut chain = Chain::new("A");
691
692 for i in 1..=3 {
693 let mut residue = Residue::new(
694 i,
695 None,
696 &format!("RES{}", i),
697 None,
698 ResidueCategory::Standard,
699 );
700 let atom = Atom::new(
701 &format!("ATOM{}", i),
702 Element::C,
703 Point::new(i as f64, 0.0, 0.0),
704 );
705 residue.add_atom(atom);
706 chain.add_residue(residue);
707 }
708
709 assert_eq!(chain.residue_count(), 3);
710 assert_eq!(chain.iter_atoms().count(), 3);
711
712 let residue = chain.residue(2, None).unwrap();
713 assert_eq!(residue.name, "RES2");
714 assert_eq!(residue.atom("ATOM2").unwrap().name, "ATOM2");
715 }
716
717 #[test]
718 fn chain_handles_insertion_codes_correctly() {
719 let mut chain = Chain::new("A");
720 let residue1 = Residue::new(
721 1,
722 None,
723 "ALA",
724 Some(StandardResidue::ALA),
725 ResidueCategory::Standard,
726 );
727 let residue2 = Residue::new(
728 1,
729 Some('A'),
730 "ALA",
731 Some(StandardResidue::ALA),
732 ResidueCategory::Standard,
733 );
734
735 chain.add_residue(residue1);
736 chain.add_residue(residue2);
737
738 assert_eq!(chain.residue_count(), 2);
739 assert!(chain.residue(1, None).is_some());
740 assert!(chain.residue(1, Some('A')).is_some());
741 assert_eq!(
742 chain.residue(1, Some('A')).unwrap().insertion_code,
743 Some('A')
744 );
745 }
746
747 #[test]
748 fn chain_retain_residues_filters_using_predicate() {
749 let mut chain = Chain::new("A");
750 chain.add_residue(sample_residue(1, "ALA"));
751 chain.add_residue(sample_residue(2, "GLY"));
752 chain.add_residue(sample_residue(3, "SER"));
753
754 chain.retain_residues(|residue| residue.id % 2 == 1);
755
756 let ids: Vec<i32> = chain.iter_residues().map(|r| r.id).collect();
757 assert_eq!(ids, vec![1, 3]);
758 }
759
760 #[test]
761 fn chain_retain_residues_mut_filters_and_modifies() {
762 let mut chain = Chain::new("A");
763 chain.add_residue(sample_residue(1, "ALA"));
764 chain.add_residue(sample_residue(2, "GLY"));
765 chain.add_residue(sample_residue(3, "SER"));
766
767 chain.retain_residues_mut(|residue| {
768 if residue.id % 2 == 1 {
769 residue.name = format!("{}_MOD", residue.name).into();
770 true
771 } else {
772 false
773 }
774 });
775
776 let ids: Vec<i32> = chain.iter_residues().map(|r| r.id).collect();
777 assert_eq!(ids, vec![1, 3]);
778 assert_eq!(chain.residue(1, None).unwrap().name, "ALA_MOD");
779 assert_eq!(chain.residue(3, None).unwrap().name, "SER_MOD");
780 }
781
782 #[test]
783 fn chain_remove_residue_returns_removed_value() {
784 let mut chain = Chain::new("A");
785 chain.add_residue(sample_residue(5, "ALA"));
786 chain.add_residue(sample_residue(6, "GLY"));
787
788 let removed = chain.remove_residue(5, None);
789
790 assert!(removed.is_some());
791 assert_eq!(removed.unwrap().id, 5);
792 assert!(chain.residue(5, None).is_none());
793 assert_eq!(chain.residue_count(), 1);
794 }
795
796 #[test]
797 fn chain_remove_residue_returns_none_for_missing_entry() {
798 let mut chain = Chain::new("A");
799 chain.add_residue(sample_residue(5, "ALA"));
800
801 assert!(chain.remove_residue(42, None).is_none());
802 assert_eq!(chain.residue_count(), 1);
803 }
804}