bio_forge/model/
chain.rs

1//! Ordered residue collections representing biomolecular chains.
2//!
3//! Chains own a sequence of `Residue` records, enforce uniqueness on residue identifiers,
4//! and expose iterator helpers that cascade access down to atoms. Higher-level operations
5//! such as topology repair or solvation treat chains as the primary unit when traversing a
6//! structure.
7
8use super::residue::Residue;
9use crate::utils::parallel::*;
10use smol_str::SmolStr;
11use std::fmt;
12
13/// Polymer chain containing an ordered list of residues.
14///
15/// The chain preserves the order residues were added, mirrors the chain identifier seen in
16/// structure files (e.g., `"A"`), and provides iteration helpers for both residues and
17/// atoms while keeping the internal storage private.
18#[derive(Debug, Clone, PartialEq)]
19pub struct Chain {
20    /// Chain identifier matching the source structure (usually a single character).
21    pub id: SmolStr,
22    /// Internal storage preserving insertion order for residues.
23    residues: Vec<Residue>,
24}
25
26impl Chain {
27    /// Creates an empty chain with the provided identifier.
28    ///
29    /// Use this constructor when building structures procedurally or when importing from
30    /// file formats that enumerate chains.
31    ///
32    /// # Arguments
33    ///
34    /// * `id` - Label matching the original structure's chain identifier.
35    ///
36    /// # Returns
37    ///
38    /// A new `Chain` with no residues.
39    pub fn new(id: &str) -> Self {
40        Self {
41            id: SmolStr::new(id),
42            residues: Vec::new(),
43        }
44    }
45
46    /// Adds a residue to the chain while preventing duplicate identifiers.
47    ///
48    /// Residues are appended in insertion order. Duplicate `(id, insertion_code)` pairs are
49    /// rejected during debug builds to guard against malformed inputs.
50    ///
51    /// # Arguments
52    ///
53    /// * `residue` - The residue to append.
54    pub fn add_residue(&mut self, residue: Residue) {
55        // Avoids ambiguous lookups by ensuring each (id, insertion_code) pair is unique.
56        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    /// Reserves capacity for at least `additional` more residues to be inserted.
67    ///
68    /// Use this to avoid frequent reallocations when adding a known number of residues.
69    ///
70    /// # Arguments
71    ///
72    /// * `additional` - The number of residues to reserve space for.
73    pub fn reserve(&mut self, additional: usize) {
74        self.residues.reserve(additional);
75    }
76
77    /// Looks up a residue by identifier and optional insertion code.
78    ///
79    /// # Arguments
80    ///
81    /// * `id` - Residue sequence number.
82    /// * `insertion_code` - Optional insertion code used in PDB/mmCIF records.
83    ///
84    /// # Returns
85    ///
86    /// `Some(&Residue)` if a matching residue exists; otherwise `None`.
87    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    /// Fetches a mutable reference to a residue by identifier.
94    ///
95    /// # Arguments
96    ///
97    /// * `id` - Residue sequence number.
98    /// * `insertion_code` - Optional insertion code qualifier.
99    ///
100    /// # Returns
101    ///
102    /// `Some(&mut Residue)` when present; otherwise `None`.
103    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    /// Returns an immutable slice containing all residues in order.
110    ///
111    /// Useful for bulk analysis or when interfacing with APIs that operate on slices.
112    ///
113    /// # Returns
114    ///
115    /// Slice view of the underlying residue list.
116    pub fn residues(&self) -> &[Residue] {
117        &self.residues
118    }
119
120    /// Reports the number of residues stored in the chain.
121    ///
122    /// # Returns
123    ///
124    /// Count of residues currently tracked.
125    pub fn residue_count(&self) -> usize {
126        self.residues.len()
127    }
128
129    /// Counts all atoms across every residue in the chain.
130    ///
131    /// # Returns
132    ///
133    /// Total atom count as `usize`.
134    pub fn atom_count(&self) -> usize {
135        self.residues.iter().map(|r| r.atom_count()).sum()
136    }
137
138    /// Indicates whether the chain contains no residues.
139    ///
140    /// # Returns
141    ///
142    /// `true` when the chain is empty.
143    pub fn is_empty(&self) -> bool {
144        self.residues.is_empty()
145    }
146
147    /// Provides an iterator over immutable residue references.
148    ///
149    /// This mirrors `residues()` but avoids exposing slice internals and composes nicely with
150    /// iterator adaptors.
151    ///
152    /// # Returns
153    ///
154    /// A standard slice iterator over `Residue` references.
155    pub fn iter_residues(&self) -> std::slice::Iter<'_, Residue> {
156        self.residues.iter()
157    }
158
159    /// Provides an iterator over mutable residue references.
160    ///
161    /// # Returns
162    ///
163    /// A mutable slice iterator that allows in-place modifications.
164    pub fn iter_residues_mut(&mut self) -> std::slice::IterMut<'_, Residue> {
165        self.residues.iter_mut()
166    }
167
168    /// Provides a parallel iterator over immutable residues.
169    ///
170    /// # Returns
171    ///
172    /// A parallel iterator yielding `&Residue`.
173    #[cfg(feature = "parallel")]
174    pub fn par_residues(&self) -> impl IndexedParallelIterator<Item = &Residue> {
175        self.residues.par_iter()
176    }
177
178    /// Provides a parallel iterator over immutable residues (internal fallback).
179    #[cfg(not(feature = "parallel"))]
180    pub(crate) fn par_residues(&self) -> impl IndexedParallelIterator<Item = &Residue> {
181        self.residues.par_iter()
182    }
183
184    /// Provides a parallel iterator over mutable residues.
185    ///
186    /// # Returns
187    ///
188    /// A parallel iterator yielding `&mut Residue`.
189    #[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    /// Provides a parallel iterator over mutable residues (internal fallback).
195    #[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    /// Iterates over all atoms contained in the chain.
201    ///
202    /// Residues are traversed in order and their atom iterators flattened, yielding atoms in
203    /// the same relative ordering seen in the original structure.
204    ///
205    /// # Returns
206    ///
207    /// Iterator that yields immutable `Atom` references.
208    pub fn iter_atoms(&self) -> impl Iterator<Item = &super::atom::Atom> {
209        self.residues.iter().flat_map(|r| r.iter_atoms())
210    }
211
212    /// Iterates over all atoms with mutable access.
213    ///
214    /// # Returns
215    ///
216    /// Iterator producing mutable `Atom` references for bulk editing operations.
217    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    /// Retains only residues that satisfy the provided predicate.
222    ///
223    /// # Arguments
224    ///
225    /// * `f` - Predicate invoked for each residue; keep the residue when it returns `true`.
226    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    /// Retains only residues that satisfy the provided predicate, allowing mutation.
234    ///
235    /// # Arguments
236    ///
237    /// * `f` - Predicate invoked for each mutable residue; keep the residue when it returns `true`.
238    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    /// Removes a residue by identifier and returns ownership if found.
246    ///
247    /// # Arguments
248    ///
249    /// * `id` - Residue number to remove.
250    /// * `insertion_code` - Optional insertion qualifier.
251    ///
252    /// # Returns
253    ///
254    /// `Some(Residue)` containing the removed residue; otherwise `None`.
255    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}