bio_forge/model/
topology.rs

1//! Graph-based description of bonded connectivity for `Structure` instances.
2//!
3//! The topology module stores canonicalized atom-to-atom bonds, exposes iterators for
4//! residue-level analysis, and provides helper utilities used by operations such as repair,
5//! hydrogen completion, and solvation to reason about neighboring atoms.
6
7use super::structure::Structure;
8use super::types::BondOrder;
9use std::fmt;
10
11/// Undirected bond connecting two atoms within a structure.
12///
13/// Bonds store canonical atom indices (ascending order) so equality, hashing, and sorting
14/// remain stable regardless of the order in which the connection was created.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct Bond {
17    /// Index of the first atom (always the lesser index after canonicalization).
18    pub a1_idx: usize,
19    /// Index of the second atom (greater-or-equal to `a1_idx`).
20    pub a2_idx: usize,
21    /// Chemical multiplicity assigned to the bond.
22    pub order: BondOrder,
23}
24
25impl Bond {
26    /// Creates a new bond while canonicalizing the endpoint ordering.
27    ///
28    /// The smaller atom index is stored in `a1_idx` to keep hashing and equality symmetric,
29    /// ensuring duplicated bonds collapse in sets and maps.
30    ///
31    /// # Arguments
32    ///
33    /// * `idx1` - Index of one bonded atom within the owning `Structure`.
34    /// * `idx2` - Index of the partner atom.
35    /// * `order` - Chemical bond order describing multiplicity or aromaticity.
36    ///
37    /// # Returns
38    ///
39    /// A `Bond` whose indices are sorted so `a1_idx <= a2_idx`.
40    pub fn new(idx1: usize, idx2: usize, order: BondOrder) -> Self {
41        if idx1 <= idx2 {
42            Self {
43                a1_idx: idx1,
44                a2_idx: idx2,
45                order,
46            }
47        } else {
48            Self {
49                a1_idx: idx2,
50                a2_idx: idx1,
51                order,
52            }
53        }
54    }
55}
56
57/// Bond graph overlay for a [`Structure`].
58///
59/// A `Topology` pairs structural coordinates with explicit bonds, enabling neighbor queries,
60/// validation routines, and format writers that require connectivity information.
61#[derive(Debug, Clone)]
62pub struct Topology {
63    structure: Structure,
64    bonds: Vec<Bond>,
65}
66
67impl Topology {
68    /// Builds a topology from a structure and its associated bonds.
69    ///
70    /// Indices are assumed to reference atoms within the provided structure. A debug assert
71    /// validates this assumption in development builds to catch mismatched templates.
72    ///
73    /// # Arguments
74    ///
75    /// * `structure` - Fully instantiated structure containing all atoms.
76    /// * `bonds` - Canonical bond list describing connectivity.
77    ///
78    /// # Returns
79    ///
80    /// A `Topology` ready for neighbor queries and downstream processing.
81    pub fn new(structure: Structure, bonds: Vec<Bond>) -> Self {
82        debug_assert!(
83            bonds.iter().all(|b| b.a2_idx < structure.atom_count()),
84            "Bond index out of bounds"
85        );
86        Self { structure, bonds }
87    }
88
89    /// Exposes the underlying structure.
90    ///
91    /// # Returns
92    ///
93    /// Immutable reference to the wrapped [`Structure`].
94    pub fn structure(&self) -> &Structure {
95        &self.structure
96    }
97
98    /// Returns all bonds present in the topology.
99    ///
100    /// # Returns
101    ///
102    /// Slice containing every [`Bond`], preserving insertion order.
103    pub fn bonds(&self) -> &[Bond] {
104        &self.bonds
105    }
106
107    /// Counts the number of stored bonds.
108    ///
109    /// # Returns
110    ///
111    /// Total number of bonds in the topology.
112    pub fn bond_count(&self) -> usize {
113        self.bonds.len()
114    }
115
116    /// Counts the atoms tracked by the underlying structure.
117    ///
118    /// # Returns
119    ///
120    /// Number of atoms derived from the wrapped structure.
121    pub fn atom_count(&self) -> usize {
122        self.structure.atom_count()
123    }
124
125    /// Iterates over all bonds incident to a specific atom.
126    ///
127    /// # Arguments
128    ///
129    /// * `atom_idx` - Index of the atom whose incident bonds should be returned.
130    ///
131    /// # Returns
132    ///
133    /// Iterator yielding references to [`Bond`] instances connected to `atom_idx`.
134    pub fn bonds_of(&self, atom_idx: usize) -> impl Iterator<Item = &Bond> {
135        self.bonds
136            .iter()
137            .filter(move |b| b.a1_idx == atom_idx || b.a2_idx == atom_idx)
138    }
139
140    /// Enumerates the neighboring atom indices for the provided atom.
141    ///
142    /// # Arguments
143    ///
144    /// * `atom_idx` - Index of the atom whose neighbors will be traversed.
145    ///
146    /// # Returns
147    ///
148    /// Iterator producing the indices of atoms bonded to `atom_idx`.
149    pub fn neighbors_of(&self, atom_idx: usize) -> impl Iterator<Item = usize> + '_ {
150        self.bonds_of(atom_idx).map(move |b| {
151            if b.a1_idx == atom_idx {
152                b.a2_idx
153            } else {
154                b.a1_idx
155            }
156        })
157    }
158}
159
160impl fmt::Display for Topology {
161    /// Formats the topology by reporting the atom and bond counts.
162    ///
163    /// This user-friendly summary is leveraged in logs and debugging output.
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        write!(
166            f,
167            "Topology {{ atoms: {}, bonds: {} }}",
168            self.atom_count(),
169            self.bond_count()
170        )
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::model::atom::Atom;
178    use crate::model::chain::Chain;
179    use crate::model::residue::Residue;
180    use crate::model::types::{Element, Point, ResidueCategory, StandardResidue};
181
182    #[test]
183    fn bond_new_creates_bond_with_canonical_ordering() {
184        let bond = Bond::new(5, 2, BondOrder::Single);
185
186        assert_eq!(bond.a1_idx, 2);
187        assert_eq!(bond.a2_idx, 5);
188        assert_eq!(bond.order, BondOrder::Single);
189    }
190
191    #[test]
192    fn bond_new_preserves_order_when_already_canonical() {
193        let bond = Bond::new(2, 5, BondOrder::Double);
194
195        assert_eq!(bond.a1_idx, 2);
196        assert_eq!(bond.a2_idx, 5);
197        assert_eq!(bond.order, BondOrder::Double);
198    }
199
200    #[test]
201    fn bond_new_handles_same_indices() {
202        let bond = Bond::new(3, 3, BondOrder::Triple);
203
204        assert_eq!(bond.a1_idx, 3);
205        assert_eq!(bond.a2_idx, 3);
206        assert_eq!(bond.order, BondOrder::Triple);
207    }
208
209    #[test]
210    fn topology_new_creates_topology_correctly() {
211        let mut structure = Structure::new();
212        let mut chain = Chain::new("A");
213        let mut residue = Residue::new(
214            1,
215            None,
216            "ALA",
217            Some(StandardResidue::ALA),
218            ResidueCategory::Standard,
219        );
220        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
221        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
222        chain.add_residue(residue);
223        structure.add_chain(chain);
224
225        let bonds = vec![Bond::new(0, 1, BondOrder::Single)];
226        let topology = Topology::new(structure, bonds);
227
228        assert_eq!(topology.atom_count(), 2);
229        assert_eq!(topology.bond_count(), 1);
230    }
231
232    #[test]
233    fn topology_structure_returns_correct_reference() {
234        let structure = Structure::new();
235        let topology = Topology::new(structure, Vec::new());
236
237        let retrieved = topology.structure();
238
239        assert_eq!(retrieved.chain_count(), 0);
240    }
241
242    #[test]
243    fn topology_bonds_returns_correct_slice() {
244        let mut structure = Structure::new();
245        let mut chain = Chain::new("A");
246        let mut residue = Residue::new(
247            1,
248            None,
249            "ALA",
250            Some(StandardResidue::ALA),
251            ResidueCategory::Standard,
252        );
253        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
254        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
255        residue.add_atom(Atom::new("N", Element::N, Point::new(2.0, 0.0, 0.0)));
256        chain.add_residue(residue);
257        structure.add_chain(chain);
258
259        let bonds = vec![
260            Bond::new(0, 1, BondOrder::Single),
261            Bond::new(1, 2, BondOrder::Double),
262        ];
263        let topology = Topology::new(structure, bonds);
264
265        let retrieved = topology.bonds();
266
267        assert_eq!(retrieved.len(), 2);
268        assert_eq!(retrieved[0].order, BondOrder::Single);
269        assert_eq!(retrieved[1].order, BondOrder::Double);
270    }
271
272    #[test]
273    fn topology_bond_count_returns_correct_count() {
274        let mut structure = Structure::new();
275        let mut chain = Chain::new("A");
276        let mut residue = Residue::new(
277            1,
278            None,
279            "ALA",
280            Some(StandardResidue::ALA),
281            ResidueCategory::Standard,
282        );
283        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
284        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
285        chain.add_residue(residue);
286        structure.add_chain(chain);
287
288        let bonds = vec![Bond::new(0, 1, BondOrder::Single)];
289        let topology = Topology::new(structure, bonds);
290
291        assert_eq!(topology.bond_count(), 1);
292    }
293
294    #[test]
295    fn topology_bond_count_returns_zero_for_empty_topology() {
296        let structure = Structure::new();
297        let topology = Topology::new(structure, Vec::new());
298
299        assert_eq!(topology.bond_count(), 0);
300    }
301
302    #[test]
303    fn topology_atom_count_returns_correct_count() {
304        let mut structure = Structure::new();
305        let mut chain = Chain::new("A");
306        let mut residue = Residue::new(
307            1,
308            None,
309            "ALA",
310            Some(StandardResidue::ALA),
311            ResidueCategory::Standard,
312        );
313        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
314        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
315        chain.add_residue(residue);
316        structure.add_chain(chain);
317
318        let topology = Topology::new(structure, Vec::new());
319
320        assert_eq!(topology.atom_count(), 2);
321    }
322
323    #[test]
324    fn topology_atom_count_returns_zero_for_empty_structure() {
325        let structure = Structure::new();
326        let topology = Topology::new(structure, Vec::new());
327
328        assert_eq!(topology.atom_count(), 0);
329    }
330
331    #[test]
332    fn topology_bonds_of_returns_correct_bonds() {
333        let mut structure = Structure::new();
334        let mut chain = Chain::new("A");
335        let mut residue = Residue::new(
336            1,
337            None,
338            "ALA",
339            Some(StandardResidue::ALA),
340            ResidueCategory::Standard,
341        );
342        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
343        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
344        residue.add_atom(Atom::new("N", Element::N, Point::new(2.0, 0.0, 0.0)));
345        chain.add_residue(residue);
346        structure.add_chain(chain);
347
348        let bonds = vec![
349            Bond::new(0, 1, BondOrder::Single),
350            Bond::new(1, 2, BondOrder::Double),
351        ];
352        let topology = Topology::new(structure, bonds);
353
354        let bonds_of_1: Vec<_> = topology.bonds_of(1).collect();
355
356        assert_eq!(bonds_of_1.len(), 2);
357        assert!(bonds_of_1.iter().any(|b| b.a1_idx == 0 && b.a2_idx == 1));
358        assert!(bonds_of_1.iter().any(|b| b.a1_idx == 1 && b.a2_idx == 2));
359    }
360
361    #[test]
362    fn topology_bonds_of_returns_empty_for_atom_with_no_bonds() {
363        let mut structure = Structure::new();
364        let mut chain = Chain::new("A");
365        let mut residue = Residue::new(
366            1,
367            None,
368            "ALA",
369            Some(StandardResidue::ALA),
370            ResidueCategory::Standard,
371        );
372        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
373        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
374        chain.add_residue(residue);
375        structure.add_chain(chain);
376
377        let bonds = vec![Bond::new(0, 1, BondOrder::Single)];
378        let topology = Topology::new(structure, bonds);
379
380        let bonds_of_5: Vec<_> = topology.bonds_of(5).collect();
381
382        assert!(bonds_of_5.is_empty());
383    }
384
385    #[test]
386    fn topology_neighbors_of_returns_correct_neighbors() {
387        let mut structure = Structure::new();
388        let mut chain = Chain::new("A");
389        let mut residue = Residue::new(
390            1,
391            None,
392            "ALA",
393            Some(StandardResidue::ALA),
394            ResidueCategory::Standard,
395        );
396        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
397        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
398        residue.add_atom(Atom::new("N", Element::N, Point::new(2.0, 0.0, 0.0)));
399        residue.add_atom(Atom::new("O", Element::O, Point::new(3.0, 0.0, 0.0)));
400        chain.add_residue(residue);
401        structure.add_chain(chain);
402
403        let bonds = vec![
404            Bond::new(0, 1, BondOrder::Single),
405            Bond::new(1, 2, BondOrder::Double),
406            Bond::new(1, 3, BondOrder::Triple),
407        ];
408        let topology = Topology::new(structure, bonds);
409
410        let neighbors: Vec<_> = topology.neighbors_of(1).collect();
411
412        assert_eq!(neighbors.len(), 3);
413        assert!(neighbors.contains(&0));
414        assert!(neighbors.contains(&2));
415        assert!(neighbors.contains(&3));
416    }
417
418    #[test]
419    fn topology_neighbors_of_returns_empty_for_isolated_atom() {
420        let mut structure = Structure::new();
421        let mut chain = Chain::new("A");
422        let mut residue = Residue::new(
423            1,
424            None,
425            "ALA",
426            Some(StandardResidue::ALA),
427            ResidueCategory::Standard,
428        );
429        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
430        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
431        chain.add_residue(residue);
432        structure.add_chain(chain);
433
434        let bonds = vec![Bond::new(0, 1, BondOrder::Single)];
435        let topology = Topology::new(structure, bonds);
436
437        let neighbors: Vec<_> = topology.neighbors_of(5).collect();
438
439        assert!(neighbors.is_empty());
440    }
441
442    #[test]
443    fn topology_display_formats_correctly() {
444        let mut structure = Structure::new();
445        let mut chain = Chain::new("A");
446        let mut residue = Residue::new(
447            1,
448            None,
449            "ALA",
450            Some(StandardResidue::ALA),
451            ResidueCategory::Standard,
452        );
453        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
454        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
455        chain.add_residue(residue);
456        structure.add_chain(chain);
457
458        let bonds = vec![Bond::new(0, 1, BondOrder::Single)];
459        let topology = Topology::new(structure, bonds);
460
461        let display = format!("{}", topology);
462        let expected = "Topology { atoms: 2, bonds: 1 }";
463
464        assert_eq!(display, expected);
465    }
466
467    #[test]
468    fn topology_display_formats_empty_topology_correctly() {
469        let structure = Structure::new();
470        let topology = Topology::new(structure, Vec::new());
471
472        let display = format!("{}", topology);
473        let expected = "Topology { atoms: 0, bonds: 0 }";
474
475        assert_eq!(display, expected);
476    }
477
478    #[test]
479    fn topology_clone_creates_identical_copy() {
480        let mut structure = Structure::new();
481        let mut chain = Chain::new("A");
482        let mut residue = Residue::new(
483            1,
484            None,
485            "ALA",
486            Some(StandardResidue::ALA),
487            ResidueCategory::Standard,
488        );
489        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
490        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
491        chain.add_residue(residue);
492        structure.add_chain(chain);
493
494        let bonds = vec![Bond::new(0, 1, BondOrder::Single)];
495        let topology = Topology::new(structure, bonds);
496
497        let cloned = topology.clone();
498
499        assert_eq!(topology.atom_count(), cloned.atom_count());
500        assert_eq!(topology.bond_count(), cloned.bond_count());
501        assert_eq!(topology.bonds(), cloned.bonds());
502    }
503
504    #[test]
505    fn bond_partial_eq_compares_correctly() {
506        let bond1 = Bond::new(0, 1, BondOrder::Single);
507        let bond2 = Bond::new(1, 0, BondOrder::Single);
508        let bond3 = Bond::new(0, 2, BondOrder::Single);
509        let bond4 = Bond::new(0, 1, BondOrder::Double);
510
511        assert_eq!(bond1, bond2);
512        assert_ne!(bond1, bond3);
513        assert_ne!(bond1, bond4);
514    }
515
516    #[test]
517    fn bond_hash_considers_canonical_ordering() {
518        use std::collections::HashSet;
519
520        let bond1 = Bond::new(0, 1, BondOrder::Single);
521        let bond2 = Bond::new(1, 0, BondOrder::Single);
522        let mut set = HashSet::new();
523
524        set.insert(bond1);
525        set.insert(bond2);
526
527        assert_eq!(set.len(), 1);
528    }
529
530    #[test]
531    fn topology_with_complex_structure() {
532        let mut structure = Structure::new();
533        let mut chain = Chain::new("A");
534        let mut residue = Residue::new(
535            1,
536            None,
537            "HOH",
538            Some(StandardResidue::HOH),
539            ResidueCategory::Standard,
540        );
541        residue.add_atom(Atom::new("O", Element::O, Point::new(0.0, 0.0, 0.0)));
542        residue.add_atom(Atom::new("H1", Element::H, Point::new(0.96, 0.0, 0.0)));
543        residue.add_atom(Atom::new("H2", Element::H, Point::new(-0.24, 0.93, 0.0)));
544        chain.add_residue(residue);
545        structure.add_chain(chain);
546
547        let bonds = vec![
548            Bond::new(0, 1, BondOrder::Single),
549            Bond::new(0, 2, BondOrder::Single),
550        ];
551        let topology = Topology::new(structure, bonds);
552
553        assert_eq!(topology.atom_count(), 3);
554        assert_eq!(topology.bond_count(), 2);
555
556        let o_neighbors: Vec<_> = topology.neighbors_of(0).collect();
557        assert_eq!(o_neighbors.len(), 2);
558        assert!(o_neighbors.contains(&1));
559        assert!(o_neighbors.contains(&2));
560
561        let h1_neighbors: Vec<_> = topology.neighbors_of(1).collect();
562        assert_eq!(h1_neighbors, vec![0]);
563
564        let h2_neighbors: Vec<_> = topology.neighbors_of(2).collect();
565        assert_eq!(h2_neighbors, vec![0]);
566    }
567
568    #[test]
569    fn topology_bonds_of_with_aromatic_bond() {
570        let mut structure = Structure::new();
571        let mut chain = Chain::new("A");
572        let mut residue = Residue::new(
573            1,
574            None,
575            "ALA",
576            Some(StandardResidue::ALA),
577            ResidueCategory::Standard,
578        );
579        residue.add_atom(Atom::new("CA", Element::C, Point::new(0.0, 0.0, 0.0)));
580        residue.add_atom(Atom::new("CB", Element::C, Point::new(1.0, 0.0, 0.0)));
581        chain.add_residue(residue);
582        structure.add_chain(chain);
583
584        let bonds = vec![Bond::new(0, 1, BondOrder::Aromatic)];
585        let topology = Topology::new(structure, bonds);
586
587        let bonds_of_0: Vec<_> = topology.bonds_of(0).collect();
588
589        assert_eq!(bonds_of_0.len(), 1);
590        assert_eq!(bonds_of_0[0].order, BondOrder::Aromatic);
591    }
592}