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}