bio_forge/model/
structure.rs

1//! Representation of multi-chain biomolecular assemblies with geometric helpers.
2//!
3//! The `Structure` type aggregates polymer chains, provides fast lookup utilities, and
4//! offers derived properties such as geometric centers or mass-weighted centroids. It is
5//! the central container consumed by IO readers, cleaning operations, and solvation tools.
6
7use super::chain::Chain;
8use super::grid::Grid;
9use super::residue::Residue;
10use super::types::Point;
11use crate::utils::parallel::*;
12use std::fmt;
13
14/// High-level biomolecular assembly composed of zero or more chains.
15///
16/// A `Structure` wraps individual chains, tracks optional periodic box vectors, and offers
17/// convenience iterators for traversing chains, residues, and atoms alongside contextual
18/// metadata. Builders and operations mutate the structure to clean, solvate, or analyze
19/// biological systems.
20#[derive(Debug, Clone, Default)]
21pub struct Structure {
22    /// Internal collection of polymer chains preserving insertion order.
23    chains: Vec<Chain>,
24    /// Optional periodic box represented as crystallographic basis vectors.
25    pub box_vectors: Option<[[f64; 3]; 3]>,
26}
27
28impl Structure {
29    /// Creates an empty structure with no chains or box vectors.
30    ///
31    /// # Returns
32    ///
33    /// A new `Structure` identical to `Structure::default()`.
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Appends a chain to the structure, asserting unique chain IDs in debug builds.
39    ///
40    /// The chain is inserted at the end of the current collection and becomes visible to
41    /// iterator methods immediately.
42    ///
43    /// # Arguments
44    ///
45    /// * `chain` - Chain instance whose `id` must be unique within the structure.
46    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    /// Removes and returns a chain by identifier if it exists.
56    ///
57    /// # Arguments
58    ///
59    /// * `id` - Chain identifier to search for.
60    ///
61    /// # Returns
62    ///
63    /// `Some(Chain)` when a chain with the provided ID is present, otherwise `None`.
64    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    /// Drops every chain from the structure, leaving box vectors untouched.
73    pub fn clear(&mut self) {
74        self.chains.clear();
75    }
76
77    /// Retrieves an immutable chain by identifier.
78    ///
79    /// # Arguments
80    ///
81    /// * `id` - Chain identifier to search for.
82    ///
83    /// # Returns
84    ///
85    /// `Some(&Chain)` if found, otherwise `None`.
86    pub fn chain(&self, id: &str) -> Option<&Chain> {
87        self.chains.iter().find(|c| c.id == id)
88    }
89
90    /// Retrieves a mutable chain by identifier.
91    ///
92    /// # Arguments
93    ///
94    /// * `id` - Chain identifier to search for.
95    ///
96    /// # Returns
97    ///
98    /// `Some(&mut Chain)` if found, otherwise `None`.
99    pub fn chain_mut(&mut self, id: &str) -> Option<&mut Chain> {
100        self.chains.iter_mut().find(|c| c.id == id)
101    }
102
103    /// Finds a residue using chain ID, residue number, and optional insertion code.
104    ///
105    /// # Arguments
106    ///
107    /// * `chain_id` - Identifier of the chain to search.
108    /// * `residue_id` - Numeric residue index (typically PDB `resSeq`).
109    /// * `insertion_code` - Optional insertion code differentiating duplicate IDs.
110    ///
111    /// # Returns
112    ///
113    /// `Some(&Residue)` when the residue is located, otherwise `None`.
114    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    /// Finds a mutable residue reference using chain and residue identifiers.
125    ///
126    /// # Arguments
127    ///
128    /// * `chain_id` - Identifier of the chain to search.
129    /// * `residue_id` - Numeric residue index.
130    /// * `insertion_code` - Optional insertion code to disambiguate residues.
131    ///
132    /// # Returns
133    ///
134    /// `Some(&mut Residue)` when located, otherwise `None`.
135    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    /// Sorts chains lexicographically by their identifier.
146    pub fn sort_chains_by_id(&mut self) {
147        self.chains.sort_by(|a, b| a.id.cmp(&b.id));
148    }
149
150    /// Returns the number of chains currently stored.
151    ///
152    /// # Returns
153    ///
154    /// Chain count as `usize`.
155    pub fn chain_count(&self) -> usize {
156        self.chains.len()
157    }
158
159    /// Counts all residues across every chain.
160    ///
161    /// # Returns
162    ///
163    /// Total residue count as `usize`.
164    pub fn residue_count(&self) -> usize {
165        self.chains.iter().map(|c| c.residue_count()).sum()
166    }
167
168    /// Counts all atoms across every chain.
169    ///
170    /// # Returns
171    ///
172    /// Total atom count as `usize`.
173    pub fn atom_count(&self) -> usize {
174        self.chains.iter().map(|c| c.iter_atoms().count()).sum()
175    }
176
177    /// Indicates whether the structure contains zero chains.
178    ///
179    /// # Returns
180    ///
181    /// `true` if no chains are present.
182    pub fn is_empty(&self) -> bool {
183        self.chains.is_empty()
184    }
185
186    /// Provides an iterator over immutable chains.
187    ///
188    /// # Returns
189    ///
190    /// `std::slice::Iter<'_, Chain>` spanning all chains in insertion order.
191    pub fn iter_chains(&self) -> std::slice::Iter<'_, Chain> {
192        self.chains.iter()
193    }
194
195    /// Provides an iterator over mutable chains.
196    ///
197    /// # Returns
198    ///
199    /// `std::slice::IterMut<'_, Chain>` for in-place modification of chains.
200    pub fn iter_chains_mut(&mut self) -> std::slice::IterMut<'_, Chain> {
201        self.chains.iter_mut()
202    }
203
204    /// Provides a parallel iterator over immutable chains.
205    ///
206    /// # Returns
207    ///
208    /// A parallel iterator yielding `&Chain`.
209    #[cfg(feature = "parallel")]
210    pub fn par_chains(&self) -> impl IndexedParallelIterator<Item = &Chain> {
211        self.chains.par_iter()
212    }
213
214    /// Provides a parallel iterator over immutable chains (internal fallback).
215    #[cfg(not(feature = "parallel"))]
216    pub(crate) fn par_chains(&self) -> impl IndexedParallelIterator<Item = &Chain> {
217        self.chains.par_iter()
218    }
219
220    /// Provides a parallel iterator over mutable chains.
221    ///
222    /// # Returns
223    ///
224    /// A parallel iterator yielding `&mut Chain`.
225    #[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    /// Provides a parallel iterator over mutable chains (internal fallback).
231    #[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    /// Provides a parallel iterator over immutable residues across all chains.
237    ///
238    /// # Returns
239    ///
240    /// A parallel iterator yielding `&Residue`.
241    #[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    /// Provides a parallel iterator over immutable residues across all chains (internal fallback).
247    #[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    /// Provides a parallel iterator over mutable residues across all chains.
253    ///
254    /// # Returns
255    ///
256    /// A parallel iterator yielding `&mut Residue`.
257    #[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    /// Provides a parallel iterator over mutable residues across all chains (internal fallback).
265    #[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    /// Provides a parallel iterator over immutable atoms across all chains.
273    ///
274    /// # Returns
275    ///
276    /// A parallel iterator yielding `&Atom`.
277    #[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    /// Provides a parallel iterator over immutable atoms across all chains (internal fallback).
285    #[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    /// Provides a parallel iterator over mutable atoms across all chains.
293    ///
294    /// # Returns
295    ///
296    /// A parallel iterator yielding `&mut Atom`.
297    #[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    /// Provides a parallel iterator over mutable atoms across all chains (internal fallback).
305    #[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    /// Iterates over immutable atoms across all chains.
313    ///
314    /// # Returns
315    ///
316    /// An iterator yielding `&Atom` in chain/residue order.
317    pub fn iter_atoms(&self) -> impl Iterator<Item = &super::atom::Atom> {
318        self.chains.iter().flat_map(|c| c.iter_atoms())
319    }
320
321    /// Iterates over mutable atoms across all chains.
322    ///
323    /// # Returns
324    ///
325    /// An iterator yielding `&mut Atom` in chain/residue order.
326    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    /// Retains residues that satisfy a predicate, removing all others.
331    ///
332    /// The predicate receives the chain ID and a residue reference, enabling
333    /// context-sensitive filtering.
334    ///
335    /// # Arguments
336    ///
337    /// * `f` - Closure returning `true` to keep the residue.
338    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    /// Retains residues that satisfy a predicate, removing all others (Mutable version).
349    ///
350    /// The predicate receives the chain ID and a mutable residue reference.
351    ///
352    /// # Arguments
353    ///
354    /// * `f` - Closure returning `true` to keep the residue.
355    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    /// Retains residues that satisfy a predicate, removing all others (Parallel version).
366    ///
367    /// This method processes chains in parallel when the `parallel` feature is enabled.
368    /// The predicate must be thread-safe (`Sync` + `Send`) and immutable (`Fn`).
369    ///
370    /// # Arguments
371    ///
372    /// * `f` - Thread-safe closure returning `true` to keep the residue.
373    #[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    /// Retains residues that satisfy a predicate, removing all others (Sequential fallback).
385    #[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    /// Retains residues that satisfy a predicate, removing all others (Parallel Mutable version).
394    ///
395    /// # Arguments
396    ///
397    /// * `f` - Thread-safe closure returning `true` to keep the residue.
398    #[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    /// Retains residues that satisfy a predicate, removing all others (Sequential Mutable fallback).
410    #[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    /// Removes any chain that became empty after residue pruning.
419    pub fn prune_empty_chains(&mut self) {
420        self.chains.retain(|chain| !chain.is_empty());
421    }
422
423    /// Iterates over atoms while including chain and residue context.
424    ///
425    /// # Returns
426    ///
427    /// An iterator yielding triples `(&Chain, &Residue, &Atom)` for every atom.
428    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    /// Computes the geometric center of all atom coordinates.
439    ///
440    /// Falls back to the origin when the structure contains no atoms.
441    ///
442    /// # Returns
443    ///
444    /// A `Point` located at the unweighted centroid.
445    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    /// Computes the mass-weighted center of all atoms.
462    ///
463    /// Uses element atomic masses and returns the origin when the total mass is below
464    /// numerical tolerance.
465    ///
466    /// # Returns
467    ///
468    /// A `Point` representing the center of mass.
469    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    /// Constructs a spatial grid indexing all atoms in the structure.
487    ///
488    /// The grid stores `(chain_idx, residue_idx, atom_idx)` tuples, allowing efficient
489    /// retrieval of atoms within a spatial neighborhood.
490    ///
491    /// # Arguments
492    ///
493    /// * `cell_size` - The side length of each spatial bin.
494    ///
495    /// # Returns
496    ///
497    /// A [`Grid`] containing all atoms in the structure.
498    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(&center, 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(&center, 2.0).count();
1277        assert_eq!(coarse, 2);
1278
1279        let exact: Vec<_> = grid.neighbors(&center, 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(&center, 1e6).count(), structure.atom_count());
1317
1318        let center = Point::new(0.1, 0.1, 0.1);
1319        let count = grid.neighbors(&center, 0.5).count();
1320        assert_eq!(count, 50);
1321
1322        for &(c, r, a) in grid.neighbors(&center, 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}