dreid_forge/model/
system.rs

1//! Molecular system representation with atoms, bonds, and metadata.
2//!
3//! This module provides the core data structures for representing
4//! complete molecular systems:
5//!
6//! - [`Bond`] — Connection between two atoms with associated bond order
7//! - [`System`] — Complete molecular system with atoms, bonds, periodicity,
8//!   and optional biological metadata
9
10use super::atom::Atom;
11use super::metadata::BioMetadata;
12use super::types::BondOrder;
13
14/// A chemical bond between two atoms.
15///
16/// Represents a covalent bond with canonical ordering (i ≤ j) to ensure
17/// consistent hashing and equality comparisons. This allows bonds to be
18/// stored in hash-based collections without duplicates regardless of the
19/// order atoms are specified.
20///
21/// # Fields
22///
23/// * `i` — Index of the first atom (always ≤ j)
24/// * `j` — Index of the second atom (always ≥ i)
25/// * `order` — Bond multiplicity ([`BondOrder`])
26///
27/// # Examples
28///
29/// ```
30/// use dreid_forge::{Bond, BondOrder};
31///
32/// // Order is automatically canonicalized
33/// let bond1 = Bond::new(5, 2, BondOrder::Single);
34/// let bond2 = Bond::new(2, 5, BondOrder::Single);
35///
36/// assert_eq!(bond1.i, 2); // smaller index
37/// assert_eq!(bond1.j, 5); // larger index
38/// assert_eq!(bond1, bond2); // equivalent regardless of input order
39/// ```
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41pub struct Bond {
42    /// Index of the first atom (canonically the smaller index).
43    pub i: usize,
44    /// Index of the second atom (canonically the larger or equal index).
45    pub j: usize,
46    /// Bond order (single, double, triple, or aromatic).
47    pub order: BondOrder,
48}
49
50impl Bond {
51    /// Creates a new bond with canonical atom ordering.
52    ///
53    /// Automatically orders atom indices so that `i ≤ j`, ensuring
54    /// consistent representation regardless of the order arguments
55    /// are provided.
56    ///
57    /// # Arguments
58    ///
59    /// * `idx1` — Index of the first atom
60    /// * `idx2` — Index of the second atom
61    /// * `order` — Bond multiplicity
62    ///
63    /// # Returns
64    ///
65    /// A new [`Bond`] with canonically ordered indices.
66    pub fn new(idx1: usize, idx2: usize, order: BondOrder) -> Self {
67        if idx1 <= idx2 {
68            Self {
69                i: idx1,
70                j: idx2,
71                order,
72            }
73        } else {
74            Self {
75                i: idx2,
76                j: idx1,
77                order,
78            }
79        }
80    }
81}
82
83/// A complete molecular system.
84///
85/// Contains all information needed to represent a molecular structure:
86/// atoms with their positions, connectivity through bonds, optional
87/// periodic boundary conditions, and optional biological metadata for
88/// biomolecular systems.
89///
90/// # Fields
91///
92/// * `atoms` — Vector of atoms with element types and positions
93/// * `bonds` — Vector of bonds defining molecular connectivity
94/// * `box_vectors` — Optional 3×3 matrix defining periodic cell vectors
95/// * `bio_metadata` — Optional biological annotation (residues, chains)
96///
97/// # Examples
98///
99/// ```
100/// use dreid_forge::{Atom, Bond, BondOrder, Element, System};
101///
102/// // Build a simple CO molecule
103/// let mut system = System::new();
104/// system.atoms.push(Atom::new(Element::C, [0.0, 0.0, 0.0]));
105/// system.atoms.push(Atom::new(Element::O, [1.128, 0.0, 0.0]));
106/// system.bonds.push(Bond::new(0, 1, BondOrder::Triple));
107///
108/// assert_eq!(system.atom_count(), 2);
109/// assert_eq!(system.bond_count(), 1);
110/// assert!(!system.is_periodic());
111/// ```
112#[derive(Debug, Clone, Default)]
113pub struct System {
114    /// Atoms in the system with element types and positions.
115    pub atoms: Vec<Atom>,
116    /// Bonds defining molecular connectivity.
117    pub bonds: Vec<Bond>,
118    /// Periodic cell vectors as row-major 3×3 matrix, if periodic.
119    pub box_vectors: Option<[[f64; 3]; 3]>,
120    /// Biological metadata (residue info, chains) for biomolecules.
121    pub bio_metadata: Option<BioMetadata>,
122}
123
124impl System {
125    /// Creates a new empty molecular system.
126    ///
127    /// Initializes a system with no atoms, no bonds, no periodic
128    /// boundaries, and no biological metadata.
129    ///
130    /// # Returns
131    ///
132    /// An empty [`System`] instance.
133    pub fn new() -> Self {
134        Self::default()
135    }
136
137    /// Returns the number of atoms in the system.
138    #[inline]
139    pub fn atom_count(&self) -> usize {
140        self.atoms.len()
141    }
142
143    /// Returns the number of bonds in the system.
144    #[inline]
145    pub fn bond_count(&self) -> usize {
146        self.bonds.len()
147    }
148
149    /// Returns `true` if the system has periodic boundary conditions.
150    ///
151    /// A system is periodic if [`box_vectors`](Self::box_vectors) is `Some`.
152    #[inline]
153    pub fn is_periodic(&self) -> bool {
154        self.box_vectors.is_some()
155    }
156
157    /// Returns `true` if the system has biological metadata attached.
158    ///
159    /// Biological metadata includes residue names, chain IDs, and
160    /// other annotations from PDB/mmCIF files.
161    #[inline]
162    pub fn has_bio_metadata(&self) -> bool {
163        self.bio_metadata.is_some()
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::model::atom::Atom;
171    use crate::model::metadata::BioMetadata;
172    use crate::model::types::BondOrder;
173    use crate::model::types::Element;
174    use std::collections::HashSet;
175
176    #[test]
177    fn bond_new_canonical_order() {
178        let b1 = Bond::new(5, 2, BondOrder::Single);
179        assert_eq!(b1.i, 2);
180        assert_eq!(b1.j, 5);
181
182        let b2 = Bond::new(2, 5, BondOrder::Single);
183        assert_eq!(b1, b2);
184    }
185
186    #[test]
187    fn bond_hashset_dedupes_equivalent_bonds() {
188        let b1 = Bond::new(3, 1, BondOrder::Double);
189        let b2 = Bond::new(1, 3, BondOrder::Double);
190        let mut hs = HashSet::new();
191        hs.insert(b1);
192        hs.insert(b2);
193        assert_eq!(hs.len(), 1);
194    }
195
196    #[test]
197    fn system_new_and_counts() {
198        let s = System::new();
199        assert_eq!(s.atom_count(), 0);
200        assert_eq!(s.bond_count(), 0);
201        assert!(!s.is_periodic());
202        assert!(!s.has_bio_metadata());
203    }
204
205    #[test]
206    fn system_add_atoms_and_bonds_and_flags() {
207        let mut s = System::new();
208        s.atoms.push(Atom::new(Element::C, [0.0, 0.0, 0.0]));
209        s.atoms.push(Atom::new(Element::O, [1.2, 0.0, 0.0]));
210        assert_eq!(s.atom_count(), 2);
211
212        s.bonds.push(Bond::new(0, 1, BondOrder::Double));
213        assert_eq!(s.bond_count(), 1);
214
215        assert!(!s.is_periodic());
216        s.box_vectors = Some([[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]]);
217        assert!(s.is_periodic());
218
219        assert!(!s.has_bio_metadata());
220        s.bio_metadata = Some(BioMetadata::new());
221        assert!(s.has_bio_metadata());
222    }
223
224    #[test]
225    fn debug_and_clone_system() {
226        let mut s = System::new();
227        s.atoms.push(Atom::new(Element::H, [0.0, 0.0, 0.0]));
228        s.bonds.push(Bond::new(0, 0, BondOrder::Single));
229        let dbg = format!("{:?}", s.clone());
230        assert!(dbg.contains("System"));
231        assert!(dbg.contains("atoms"));
232        assert!(dbg.contains("bonds"));
233    }
234}