1use super::atom::Atom;
8use super::types::{ResidueCategory, ResiduePosition, StandardResidue};
9use smol_str::SmolStr;
10use std::fmt;
11
12#[derive(Debug, Clone, PartialEq)]
19pub struct Residue {
20 pub id: i32,
22 pub insertion_code: Option<char>,
24 pub name: SmolStr,
26 pub standard_name: Option<StandardResidue>,
28 pub category: ResidueCategory,
30 pub position: ResiduePosition,
32 atoms: Vec<Atom>,
34}
35
36impl Residue {
37 pub fn new(
54 id: i32,
55 insertion_code: Option<char>,
56 name: &str,
57 standard_name: Option<StandardResidue>,
58 category: ResidueCategory,
59 ) -> Self {
60 Self {
61 id,
62 insertion_code,
63 name: SmolStr::new(name),
64 standard_name,
65 category,
66 position: ResiduePosition::None,
67 atoms: Vec::new(),
68 }
69 }
70
71 pub fn is_standard(&self) -> bool {
77 self.standard_name.is_some()
78 }
79
80 pub fn add_atom(&mut self, atom: Atom) {
93 debug_assert!(
94 self.atom(&atom.name).is_none(),
95 "Attempted to add a duplicate atom name '{}' to residue '{}'",
96 atom.name,
97 self.name
98 );
99 self.atoms.push(atom);
100 }
101
102 pub fn remove_atom(&mut self, name: &str) -> Option<Atom> {
112 if let Some(index) = self.atoms.iter().position(|a| a.name == name) {
113 Some(self.atoms.remove(index))
114 } else {
115 None
116 }
117 }
118
119 pub fn atom(&self, name: &str) -> Option<&Atom> {
129 self.atoms.iter().find(|a| a.name == name)
130 }
131
132 pub fn atom_mut(&mut self, name: &str) -> Option<&mut Atom> {
142 self.atoms.iter_mut().find(|a| a.name == name)
143 }
144
145 pub fn has_atom(&self, name: &str) -> bool {
155 self.atom(name).is_some()
156 }
157
158 pub fn atoms(&self) -> &[Atom] {
166 &self.atoms
167 }
168
169 pub fn atom_count(&self) -> usize {
175 self.atoms.len()
176 }
177
178 pub fn is_empty(&self) -> bool {
184 self.atoms.is_empty()
185 }
186
187 pub fn iter_atoms(&self) -> std::slice::Iter<'_, Atom> {
196 self.atoms.iter()
197 }
198
199 pub fn iter_atoms_mut(&mut self) -> std::slice::IterMut<'_, Atom> {
205 self.atoms.iter_mut()
206 }
207
208 #[cfg(feature = "parallel")]
214 pub fn par_atoms(&self) -> impl crate::utils::parallel::IndexedParallelIterator<Item = &Atom> {
215 use crate::utils::parallel::IntoParallelRefIterator;
216 self.atoms.par_iter()
217 }
218
219 #[cfg(not(feature = "parallel"))]
221 pub(crate) fn par_atoms(
222 &self,
223 ) -> impl crate::utils::parallel::IndexedParallelIterator<Item = &Atom> {
224 use crate::utils::parallel::IntoParallelRefIterator;
225 self.atoms.par_iter()
226 }
227
228 #[cfg(feature = "parallel")]
234 pub fn par_atoms_mut(
235 &mut self,
236 ) -> impl crate::utils::parallel::IndexedParallelIterator<Item = &mut Atom> {
237 use crate::utils::parallel::IntoParallelRefMutIterator;
238 self.atoms.par_iter_mut()
239 }
240
241 #[cfg(not(feature = "parallel"))]
243 pub(crate) fn par_atoms_mut(
244 &mut self,
245 ) -> impl crate::utils::parallel::IndexedParallelIterator<Item = &mut Atom> {
246 use crate::utils::parallel::IntoParallelRefMutIterator;
247 self.atoms.par_iter_mut()
248 }
249
250 pub fn strip_hydrogens(&mut self) {
255 self.atoms
256 .retain(|a| a.element != crate::model::types::Element::H);
257 }
258}
259
260impl fmt::Display for Residue {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 let insertion_code_str = self
264 .insertion_code
265 .map(|c| format!(" (ic: {})", c))
266 .unwrap_or_default();
267
268 if let Some(std_name) = self.standard_name {
269 write!(
270 f,
271 "Residue {{ id: {}{}, name: \"{}\" ({}), category: {}, atoms: {} }}",
272 self.id,
273 insertion_code_str,
274 self.name,
275 std_name,
276 self.category,
277 self.atom_count()
278 )
279 } else {
280 write!(
281 f,
282 "Residue {{ id: {}{}, name: \"{}\", category: {}, atoms: {} }}",
283 self.id,
284 insertion_code_str,
285 self.name,
286 self.category,
287 self.atom_count()
288 )
289 }
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use crate::model::types::{Element, Point};
297
298 #[test]
299 fn residue_new_creates_correct_residue() {
300 let residue = Residue::new(
301 1,
302 None,
303 "ALA",
304 Some(StandardResidue::ALA),
305 ResidueCategory::Standard,
306 );
307
308 assert_eq!(residue.id, 1);
309 assert_eq!(residue.insertion_code, None);
310 assert_eq!(residue.name, "ALA");
311 assert_eq!(residue.standard_name, Some(StandardResidue::ALA));
312 assert_eq!(residue.category, ResidueCategory::Standard);
313 assert_eq!(residue.position, ResiduePosition::None);
314 assert!(residue.atoms.is_empty());
315 }
316
317 #[test]
318 fn residue_new_with_insertion_code() {
319 let residue = Residue::new(
320 1,
321 Some('A'),
322 "ALA",
323 Some(StandardResidue::ALA),
324 ResidueCategory::Standard,
325 );
326
327 assert_eq!(residue.id, 1);
328 assert_eq!(residue.insertion_code, Some('A'));
329 }
330
331 #[test]
332 fn residue_new_with_none_standard_name() {
333 let residue = Residue::new(2, None, "UNK", None, ResidueCategory::Hetero);
334
335 assert_eq!(residue.id, 2);
336 assert_eq!(residue.name, "UNK");
337 assert_eq!(residue.standard_name, None);
338 assert_eq!(residue.category, ResidueCategory::Hetero);
339 }
340
341 #[test]
342 fn residue_is_standard_returns_true_for_standard_residue() {
343 let residue = Residue::new(
344 1,
345 None,
346 "ALA",
347 Some(StandardResidue::ALA),
348 ResidueCategory::Standard,
349 );
350 assert!(residue.is_standard());
351 }
352
353 #[test]
354 fn residue_is_standard_returns_false_for_non_standard_residue() {
355 let residue = Residue::new(2, None, "UNK", None, ResidueCategory::Hetero);
356 assert!(!residue.is_standard());
357 }
358
359 #[test]
360 fn residue_add_atom_adds_atom_correctly() {
361 let mut residue = Residue::new(
362 1,
363 None,
364 "ALA",
365 Some(StandardResidue::ALA),
366 ResidueCategory::Standard,
367 );
368 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
369
370 residue.add_atom(atom);
371
372 assert_eq!(residue.atom_count(), 1);
373 assert!(residue.has_atom("CA"));
374 assert_eq!(residue.atom("CA").unwrap().name, "CA");
375 }
376
377 #[test]
378 fn residue_remove_atom_removes_existing_atom() {
379 let mut residue = Residue::new(
380 1,
381 None,
382 "ALA",
383 Some(StandardResidue::ALA),
384 ResidueCategory::Standard,
385 );
386 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
387 residue.add_atom(atom);
388
389 let removed = residue.remove_atom("CA");
390
391 assert!(removed.is_some());
392 assert_eq!(removed.unwrap().name, "CA");
393 assert_eq!(residue.atom_count(), 0);
394 assert!(!residue.has_atom("CA"));
395 }
396
397 #[test]
398 fn residue_remove_atom_returns_none_for_nonexistent_atom() {
399 let mut residue = Residue::new(
400 1,
401 None,
402 "ALA",
403 Some(StandardResidue::ALA),
404 ResidueCategory::Standard,
405 );
406
407 let removed = residue.remove_atom("NONEXISTENT");
408
409 assert!(removed.is_none());
410 }
411
412 #[test]
413 fn residue_atom_returns_correct_atom() {
414 let mut residue = Residue::new(
415 1,
416 None,
417 "ALA",
418 Some(StandardResidue::ALA),
419 ResidueCategory::Standard,
420 );
421 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
422 residue.add_atom(atom);
423
424 let retrieved = residue.atom("CA");
425
426 assert!(retrieved.is_some());
427 assert_eq!(retrieved.unwrap().name, "CA");
428 }
429
430 #[test]
431 fn residue_atom_returns_none_for_nonexistent_atom() {
432 let residue = Residue::new(
433 1,
434 None,
435 "ALA",
436 Some(StandardResidue::ALA),
437 ResidueCategory::Standard,
438 );
439
440 let retrieved = residue.atom("NONEXISTENT");
441
442 assert!(retrieved.is_none());
443 }
444
445 #[test]
446 fn residue_atom_mut_returns_correct_mutable_atom() {
447 let mut residue = Residue::new(
448 1,
449 None,
450 "ALA",
451 Some(StandardResidue::ALA),
452 ResidueCategory::Standard,
453 );
454 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
455 residue.add_atom(atom);
456
457 let retrieved = residue.atom_mut("CA");
458
459 assert!(retrieved.is_some());
460 assert_eq!(retrieved.unwrap().name, "CA");
461 }
462
463 #[test]
464 fn residue_atom_mut_returns_none_for_nonexistent_atom() {
465 let mut residue = Residue::new(
466 1,
467 None,
468 "ALA",
469 Some(StandardResidue::ALA),
470 ResidueCategory::Standard,
471 );
472
473 let retrieved = residue.atom_mut("NONEXISTENT");
474
475 assert!(retrieved.is_none());
476 }
477
478 #[test]
479 fn residue_has_atom_returns_true_for_existing_atom() {
480 let mut residue = Residue::new(
481 1,
482 None,
483 "ALA",
484 Some(StandardResidue::ALA),
485 ResidueCategory::Standard,
486 );
487 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
488 residue.add_atom(atom);
489
490 assert!(residue.has_atom("CA"));
491 }
492
493 #[test]
494 fn residue_has_atom_returns_false_for_nonexistent_atom() {
495 let residue = Residue::new(
496 1,
497 None,
498 "ALA",
499 Some(StandardResidue::ALA),
500 ResidueCategory::Standard,
501 );
502
503 assert!(!residue.has_atom("NONEXISTENT"));
504 }
505
506 #[test]
507 fn residue_atoms_returns_correct_slice() {
508 let mut residue = Residue::new(
509 1,
510 None,
511 "ALA",
512 Some(StandardResidue::ALA),
513 ResidueCategory::Standard,
514 );
515 let atom1 = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
516 let atom2 = Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0));
517 residue.add_atom(atom1);
518 residue.add_atom(atom2);
519
520 let atoms = residue.atoms();
521
522 assert_eq!(atoms.len(), 2);
523 assert_eq!(atoms[0].name, "CA");
524 assert_eq!(atoms[1].name, "CB");
525 }
526
527 #[test]
528 fn residue_atom_count_returns_correct_count() {
529 let mut residue = Residue::new(
530 1,
531 None,
532 "ALA",
533 Some(StandardResidue::ALA),
534 ResidueCategory::Standard,
535 );
536
537 assert_eq!(residue.atom_count(), 0);
538
539 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
540 residue.add_atom(atom);
541
542 assert_eq!(residue.atom_count(), 1);
543 }
544
545 #[test]
546 fn residue_is_empty_returns_true_for_empty_residue() {
547 let residue = Residue::new(
548 1,
549 None,
550 "ALA",
551 Some(StandardResidue::ALA),
552 ResidueCategory::Standard,
553 );
554
555 assert!(residue.is_empty());
556 }
557
558 #[test]
559 fn residue_is_empty_returns_false_for_non_empty_residue() {
560 let mut residue = Residue::new(
561 1,
562 None,
563 "ALA",
564 Some(StandardResidue::ALA),
565 ResidueCategory::Standard,
566 );
567 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
568 residue.add_atom(atom);
569
570 assert!(!residue.is_empty());
571 }
572
573 #[test]
574 fn residue_iter_atoms_iterates_correctly() {
575 let mut residue = Residue::new(
576 1,
577 None,
578 "ALA",
579 Some(StandardResidue::ALA),
580 ResidueCategory::Standard,
581 );
582 let atom1 = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
583 let atom2 = Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0));
584 residue.add_atom(atom1);
585 residue.add_atom(atom2);
586
587 let mut names = Vec::new();
588 for atom in residue.iter_atoms() {
589 names.push(atom.name.clone());
590 }
591
592 assert_eq!(names, vec!["CA", "CB"]);
593 }
594
595 #[test]
596 fn residue_iter_atoms_mut_iterates_correctly() {
597 let mut residue = Residue::new(
598 1,
599 None,
600 "ALA",
601 Some(StandardResidue::ALA),
602 ResidueCategory::Standard,
603 );
604 let atom1 = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
605 residue.add_atom(atom1);
606
607 for atom in residue.iter_atoms_mut() {
608 atom.translate_by(&nalgebra::Vector3::new(1.0, 0.0, 0.0));
609 }
610
611 assert!((residue.atom("CA").unwrap().pos.x - 1.0).abs() < 1e-10);
612 }
613
614 #[test]
615 fn residue_par_atoms_iterates_correctly() {
616 use crate::utils::parallel::ParallelIterator;
617
618 let mut residue = Residue::new(
619 1,
620 None,
621 "ALA",
622 Some(StandardResidue::ALA),
623 ResidueCategory::Standard,
624 );
625 let atom1 = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
626 let atom2 = Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0));
627 residue.add_atom(atom1);
628 residue.add_atom(atom2);
629
630 let count = residue.par_atoms().count();
631 assert_eq!(count, 2);
632
633 let names: Vec<String> = residue.par_atoms().map(|a| a.name.to_string()).collect();
634 assert_eq!(names, vec!["CA", "CB"]);
635 }
636
637 #[test]
638 fn residue_par_atoms_mut_iterates_correctly() {
639 use crate::utils::parallel::ParallelIterator;
640
641 let mut residue = Residue::new(
642 1,
643 None,
644 "ALA",
645 Some(StandardResidue::ALA),
646 ResidueCategory::Standard,
647 );
648 let atom1 = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
649 residue.add_atom(atom1);
650
651 residue.par_atoms_mut().for_each(|atom| {
652 atom.translate_by(&nalgebra::Vector3::new(1.0, 0.0, 0.0));
653 });
654
655 assert!((residue.atom("CA").unwrap().pos.x - 1.0).abs() < 1e-10);
656 }
657
658 #[test]
659 fn residue_strip_hydrogens_removes_hydrogen_atoms() {
660 let mut residue = Residue::new(
661 1,
662 None,
663 "ALA",
664 Some(StandardResidue::ALA),
665 ResidueCategory::Standard,
666 );
667 let carbon = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
668 let hydrogen1 = Atom::new("HA", Element::H, Point::new(1.0, 0.0, 0.0));
669 let hydrogen2 = Atom::new("HB", Element::H, Point::new(2.0, 0.0, 0.0));
670 residue.add_atom(carbon);
671 residue.add_atom(hydrogen1);
672 residue.add_atom(hydrogen2);
673
674 residue.strip_hydrogens();
675
676 assert_eq!(residue.atom_count(), 1);
677 assert!(residue.has_atom("CA"));
678 assert!(!residue.has_atom("HA"));
679 assert!(!residue.has_atom("HB"));
680 }
681
682 #[test]
683 fn residue_strip_hydrogens_preserves_non_hydrogen_atoms() {
684 let mut residue = Residue::new(
685 1,
686 None,
687 "ALA",
688 Some(StandardResidue::ALA),
689 ResidueCategory::Standard,
690 );
691 let carbon = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
692 let nitrogen = Atom::new("N", Element::N, Point::new(1.0, 0.0, 0.0));
693 let oxygen = Atom::new("O", Element::O, Point::new(2.0, 0.0, 0.0));
694 residue.add_atom(carbon);
695 residue.add_atom(nitrogen);
696 residue.add_atom(oxygen);
697
698 residue.strip_hydrogens();
699
700 assert_eq!(residue.atom_count(), 3);
701 assert!(residue.has_atom("CA"));
702 assert!(residue.has_atom("N"));
703 assert!(residue.has_atom("O"));
704 }
705
706 #[test]
707 fn residue_display_formats_standard_residue_correctly() {
708 let residue = Residue::new(
709 1,
710 None,
711 "ALA",
712 Some(StandardResidue::ALA),
713 ResidueCategory::Standard,
714 );
715
716 let display = format!("{}", residue);
717 let expected =
718 "Residue { id: 1, name: \"ALA\" (ALA), category: Standard Residue, atoms: 0 }";
719
720 assert_eq!(display, expected);
721 }
722
723 #[test]
724 fn residue_display_formats_residue_with_insertion_code_correctly() {
725 let residue = Residue::new(
726 1,
727 Some('A'),
728 "ALA",
729 Some(StandardResidue::ALA),
730 ResidueCategory::Standard,
731 );
732
733 let display = format!("{}", residue);
734 let expected =
735 "Residue { id: 1 (ic: A), name: \"ALA\" (ALA), category: Standard Residue, atoms: 0 }";
736
737 assert_eq!(display, expected);
738 }
739
740 #[test]
741 fn residue_display_formats_non_standard_residue_correctly() {
742 let residue = Residue::new(2, None, "UNK", None, ResidueCategory::Hetero);
743
744 let display = format!("{}", residue);
745 let expected = "Residue { id: 2, name: \"UNK\", category: Hetero Residue, atoms: 0 }";
746
747 assert_eq!(display, expected);
748 }
749
750 #[test]
751 fn residue_display_includes_atom_count() {
752 let mut residue = Residue::new(
753 1,
754 None,
755 "ALA",
756 Some(StandardResidue::ALA),
757 ResidueCategory::Standard,
758 );
759 let atom1 = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
760 let atom2 = Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0));
761 residue.add_atom(atom1);
762 residue.add_atom(atom2);
763
764 let display = format!("{}", residue);
765 let expected =
766 "Residue { id: 1, name: \"ALA\" (ALA), category: Standard Residue, atoms: 2 }";
767
768 assert_eq!(display, expected);
769 }
770
771 #[test]
772 fn residue_clone_creates_identical_copy() {
773 let mut residue = Residue::new(
774 1,
775 Some('A'),
776 "ALA",
777 Some(StandardResidue::ALA),
778 ResidueCategory::Standard,
779 );
780 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
781 residue.add_atom(atom);
782 residue.position = ResiduePosition::Internal;
783
784 let cloned = residue.clone();
785
786 assert_eq!(residue, cloned);
787 assert_eq!(residue.id, cloned.id);
788 assert_eq!(residue.insertion_code, cloned.insertion_code);
789 assert_eq!(residue.name, cloned.name);
790 assert_eq!(residue.standard_name, cloned.standard_name);
791 assert_eq!(residue.category, cloned.category);
792 assert_eq!(residue.position, cloned.position);
793 assert_eq!(residue.atoms, cloned.atoms);
794 }
795
796 #[test]
797 fn residue_partial_eq_compares_correctly() {
798 let mut residue1 = Residue::new(
799 1,
800 None,
801 "ALA",
802 Some(StandardResidue::ALA),
803 ResidueCategory::Standard,
804 );
805 let mut residue2 = Residue::new(
806 1,
807 None,
808 "ALA",
809 Some(StandardResidue::ALA),
810 ResidueCategory::Standard,
811 );
812 let atom = Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0));
813 residue1.add_atom(atom.clone());
814 residue2.add_atom(atom);
815
816 let residue3 = Residue::new(
817 2,
818 None,
819 "ALA",
820 Some(StandardResidue::ALA),
821 ResidueCategory::Standard,
822 );
823
824 assert_eq!(residue1, residue2);
825 assert_ne!(residue1, residue3);
826 }
827}