bio_forge/model/
residue.rs

1//! Representation of a polymer residue and its atom inventory.
2//!
3//! Residues bridge raw template data, readers/writers, and structure-level operations by
4//! tracking naming metadata, chain position, and owned atoms. Higher-level components use
5//! this module to inspect or mutate residues while preserving biochemical context.
6
7use super::atom::Atom;
8use super::types::{ResidueCategory, ResiduePosition, StandardResidue};
9use smol_str::SmolStr;
10use std::fmt;
11
12/// A residue within a biomolecular structure.
13///
14/// Residues capture crystallographic identifiers (sequence number and insertion code), the
15/// original and normalized names, classification metadata, and the atoms that belong to the
16/// residue. They are the primary unit passed between IO routines, topology builders, and
17/// structure editing operations.
18#[derive(Debug, Clone, PartialEq)]
19pub struct Residue {
20    /// Author-provided sequence identifier (can be negative for certain files).
21    pub id: i32,
22    /// Optional insertion code differentiating records that share the same `id`.
23    pub insertion_code: Option<char>,
24    /// Original residue name as encountered during parsing.
25    pub name: SmolStr,
26    /// Canonical residue assignment if the name maps to a standard alphabet entry.
27    pub standard_name: Option<StandardResidue>,
28    /// Category describing whether the residue is standard, heterogen, or ion.
29    pub category: ResidueCategory,
30    /// Chain position annotation (terminus, internal, or nucleic end).
31    pub position: ResiduePosition,
32    /// Owned atoms that comprise the residue.
33    atoms: Vec<Atom>,
34}
35
36impl Residue {
37    /// Creates a residue with the provided identifiers and classification.
38    ///
39    /// The residue starts without atoms and defaults to `ResiduePosition::None` until the
40    /// caller updates it based on chain topology.
41    ///
42    /// # Arguments
43    ///
44    /// * `id` - Sequence number pulled from the original structure file.
45    /// * `insertion_code` - Optional insertion code for distinguishing alternate residues.
46    /// * `name` - Author-provided residue label (e.g., `"ALA"`, `"DN1"`).
47    /// * `standard_name` - Canonical assignment when the residue matches a standard entry.
48    /// * `category` - Classification describing standard, hetero, or ion behavior.
49    ///
50    /// # Returns
51    ///
52    /// A `Residue` ready for atoms to be added.
53    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    /// Reports whether the residue maps to a standard residue definition.
72    ///
73    /// # Returns
74    ///
75    /// `true` when `standard_name` is set.
76    pub fn is_standard(&self) -> bool {
77        self.standard_name.is_some()
78    }
79
80    /// Appends an atom to the residue.
81    ///
82    /// Duplicate atom names are guarded with a debug assertion to prevent inconsistent
83    /// topologies while still allowing release builds to proceed.
84    ///
85    /// # Arguments
86    ///
87    /// * `atom` - The atom to insert; ownership moves into the residue.
88    ///
89    /// # Panics
90    ///
91    /// Panics in debug builds if an atom with the same `name` already exists.
92    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    /// Removes an atom by name and returns ownership to the caller.
103    ///
104    /// # Arguments
105    ///
106    /// * `name` - Atom name to remove (case-sensitive).
107    ///
108    /// # Returns
109    ///
110    /// `Some(atom)` if the atom existed, otherwise `None`.
111    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    /// Retrieves an immutable reference to an atom by name.
120    ///
121    /// # Arguments
122    ///
123    /// * `name` - Atom name to search for.
124    ///
125    /// # Returns
126    ///
127    /// `Some(&Atom)` when the atom exists, otherwise `None`.
128    pub fn atom(&self, name: &str) -> Option<&Atom> {
129        self.atoms.iter().find(|a| a.name == name)
130    }
131
132    /// Retrieves a mutable reference to an atom by name.
133    ///
134    /// # Arguments
135    ///
136    /// * `name` - Atom name to search for.
137    ///
138    /// # Returns
139    ///
140    /// `Some(&mut Atom)` when the atom exists, otherwise `None`.
141    pub fn atom_mut(&mut self, name: &str) -> Option<&mut Atom> {
142        self.atoms.iter_mut().find(|a| a.name == name)
143    }
144
145    /// Checks whether an atom with the provided name exists.
146    ///
147    /// # Arguments
148    ///
149    /// * `name` - Atom name to test.
150    ///
151    /// # Returns
152    ///
153    /// `true` if the atom is present.
154    pub fn has_atom(&self, name: &str) -> bool {
155        self.atom(name).is_some()
156    }
157
158    /// Returns an immutable slice of all atoms contained in the residue.
159    ///
160    /// Enables zero-copy iteration when only read-only access is required.
161    ///
162    /// # Returns
163    ///
164    /// A slice view over the internal atom vector.
165    pub fn atoms(&self) -> &[Atom] {
166        &self.atoms
167    }
168
169    /// Counts the number of atoms owned by the residue.
170    ///
171    /// # Returns
172    ///
173    /// Number of atoms currently stored.
174    pub fn atom_count(&self) -> usize {
175        self.atoms.len()
176    }
177
178    /// Indicates whether the residue has no atoms.
179    ///
180    /// # Returns
181    ///
182    /// `true` if `atom_count()` equals zero.
183    pub fn is_empty(&self) -> bool {
184        self.atoms.is_empty()
185    }
186
187    /// Provides an iterator over immutable atom references.
188    ///
189    /// This mirrors `atoms()` but allows idiomatic `for` loops without exposing the backing
190    /// vector.
191    ///
192    /// # Returns
193    ///
194    /// An iterator yielding `&Atom` values.
195    pub fn iter_atoms(&self) -> std::slice::Iter<'_, Atom> {
196        self.atoms.iter()
197    }
198
199    /// Provides an iterator over mutable atom references.
200    ///
201    /// # Returns
202    ///
203    /// An iterator yielding `&mut Atom` values so callers can edit coordinates or metadata.
204    pub fn iter_atoms_mut(&mut self) -> std::slice::IterMut<'_, Atom> {
205        self.atoms.iter_mut()
206    }
207
208    /// Provides a parallel iterator over immutable atoms.
209    ///
210    /// # Returns
211    ///
212    /// A parallel iterator yielding `&Atom`.
213    #[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    /// Provides a parallel iterator over immutable atoms (internal fallback).
220    #[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    /// Provides a parallel iterator over mutable atoms.
229    ///
230    /// # Returns
231    ///
232    /// A parallel iterator yielding `&mut Atom`.
233    #[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    /// Provides a parallel iterator over mutable atoms (internal fallback).
242    #[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    /// Removes all hydrogen atoms from the residue.
251    ///
252    /// Used by cleaning operations when preparing structures for solvation or heavy-atom
253    /// analysis.
254    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    /// Formats the residue with identifier, canonical name, category, and atom count.
262    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}