bio_forge/model/
template.rs

1//! Canonical residue and ligand templates shared across topology-generating code.
2//!
3//! `Template` captures the authoritative atom list and bond connectivity for a residue so
4//! that topology builders, repair passes, and solvation routines can reason about expected
5//! atoms and their relationships before manipulating a structure.
6
7use super::types::BondOrder;
8use std::fmt;
9
10/// Describes the atom inventory and connectivity for a residue or ligand template.
11///
12/// Templates originate from the embedded parameter database and encapsulate the minimal
13/// information needed to validate coordinate files, seed missing atoms, or emit force-field
14/// compatible topologies.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Template {
17    /// External identifier such as a three-letter residue code.
18    pub name: String,
19    /// Ordered atom name list defining the template's expected atoms.
20    atom_names: Vec<String>,
21    /// Bond definitions captured as atom-name pairs with multiplicity information.
22    bonds: Vec<(String, String, BondOrder)>,
23}
24
25impl Template {
26    /// Constructs a template from explicit atom and bond data.
27    ///
28    /// The constructor enforces (in debug builds) that every bond references atoms present
29    /// in the provided `atom_names` list, preventing malformed templates from entering the
30    /// store.
31    ///
32    /// # Arguments
33    ///
34    /// * `name` - Short identifier for the template (e.g., three-letter code).
35    /// * `atom_names` - Complete list of atom names belonging to the template.
36    /// * `bonds` - Connectivity tuples describing bonded atom pairs and their `BondOrder`.
37    ///
38    /// # Returns
39    ///
40    /// A fully owned `Template` instance containing the supplied data.
41    ///
42    /// # Panics
43    ///
44    /// Panics in debug builds if any bond references an atom not listed in `atom_names`.
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use bio_forge::{BondOrder, Template};
50    ///
51    /// let atoms = vec!["O".into(), "H1".into(), "H2".into()];
52    /// let bonds = vec![
53    ///     ("O".into(), "H1".into(), BondOrder::Single),
54    ///     ("O".into(), "H2".into(), BondOrder::Single),
55    /// ];
56    /// let template = Template::new("HOH", atoms, bonds);
57    ///
58    /// assert_eq!(template.atom_count(), 3);
59    /// assert_eq!(template.bond_count(), 2);
60    /// ```
61    pub fn new<S: Into<String>>(
62        name: S,
63        atom_names: Vec<String>,
64        bonds: Vec<(String, String, BondOrder)>,
65    ) -> Self {
66        debug_assert!(
67            bonds
68                .iter()
69                .all(|(a1, a2, _)| { atom_names.contains(a1) && atom_names.contains(a2) }),
70            "Bond in template '{}' refers to an atom name that does not exist in the atom list.",
71            name.into()
72        );
73
74        Self {
75            name: name.into(),
76            atom_names,
77            bonds,
78        }
79    }
80
81    /// Reports whether the template defines a bond between the provided atom names.
82    ///
83    /// Lookup is order-independent, allowing callers to check connectivity without sorting
84    /// the names first.
85    ///
86    /// # Arguments
87    ///
88    /// * `name1` - Name of the first atom.
89    /// * `name2` - Name of the second atom.
90    ///
91    /// # Returns
92    ///
93    /// `true` if a bond exists between the atoms, otherwise `false`.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use bio_forge::{BondOrder, Template};
99    ///
100    /// let atoms = vec!["C1".into(), "C2".into()];
101    /// let bonds = vec![("C1".into(), "C2".into(), BondOrder::Single)];
102    /// let template = Template::new("ETH", atoms, bonds);
103    ///
104    /// assert!(template.has_bond("C1", "C2"));
105    /// assert!(template.has_bond("C2", "C1"));
106    /// assert!(!template.has_bond("C1", "H1"));
107    /// ```
108    pub fn has_bond(&self, name1: &str, name2: &str) -> bool {
109        self.bonds
110            .iter()
111            .any(|(a1, a2, _)| (a1 == name1 && a2 == name2) || (a1 == name2 && a2 == name1))
112    }
113
114    /// Checks whether an atom name is present in the template definition.
115    ///
116    /// # Arguments
117    ///
118    /// * `name` - Atom name to search for.
119    ///
120    /// # Returns
121    ///
122    /// `true` if the atom exists in `atom_names`.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use bio_forge::{BondOrder, Template};
128    ///
129    /// let atoms = vec!["N".into(), "CA".into(), "C".into()];
130    /// let template = Template::new("GLY", atoms, Vec::new());
131    ///
132    /// assert!(template.has_atom("CA"));
133    /// assert!(!template.has_atom("OXT"));
134    /// ```
135    pub fn has_atom(&self, name: &str) -> bool {
136        self.atom_names.contains(&name.to_string())
137    }
138
139    /// Returns the ordered slice of atom names defined for the template.
140    ///
141    /// # Returns
142    ///
143    /// Borrowed slice referencing the internal atom list.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use bio_forge::{BondOrder, Template};
149    ///
150    /// let atoms = vec!["P".into(), "O5'".into()];
151    /// let template = Template::new("PO4", atoms.clone(), Vec::new());
152    ///
153    /// assert_eq!(template.atom_names(), atoms.as_slice());
154    /// ```
155    pub fn atom_names(&self) -> &[String] {
156        &self.atom_names
157    }
158
159    /// Returns the list of bond definitions stored within the template.
160    ///
161    /// # Returns
162    ///
163    /// Borrowed slice of `(atom_a, atom_b, BondOrder)` tuples.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// use bio_forge::{BondOrder, Template};
169    ///
170    /// let atoms = vec!["C1".into(), "O1".into()];
171    /// let bonds = vec![("C1".into(), "O1".into(), BondOrder::Double)];
172    /// let template = Template::new("CO", atoms, bonds.clone());
173    ///
174    /// assert_eq!(template.bonds(), bonds.as_slice());
175    /// ```
176    pub fn bonds(&self) -> &[(String, String, BondOrder)] {
177        &self.bonds
178    }
179
180    /// Counts atoms described by the template.
181    ///
182    /// # Returns
183    ///
184    /// Number of atom entries in `atom_names`.
185    ///
186    /// # Examples
187    ///
188    /// ```
189    /// use bio_forge::{BondOrder, Template};
190    ///
191    /// let template = Template::new("ION", vec!["K".into()], Vec::new());
192    /// assert_eq!(template.atom_count(), 1);
193    /// ```
194    pub fn atom_count(&self) -> usize {
195        self.atom_names.len()
196    }
197
198    /// Counts bonds described by the template.
199    ///
200    /// # Returns
201    ///
202    /// Number of bond tuples stored in `bonds`.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// use bio_forge::{BondOrder, Template};
208    ///
209    /// let atoms = vec!["C".into(), "O".into()];
210    /// let bonds = vec![("C".into(), "O".into(), BondOrder::Double)];
211    /// let template = Template::new("CO", atoms, bonds);
212    ///
213    /// assert_eq!(template.bond_count(), 1);
214    /// ```
215    pub fn bond_count(&self) -> usize {
216        self.bonds.len()
217    }
218}
219
220impl fmt::Display for Template {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(
223            f,
224            "Template {{ name: \"{}\", atoms: {}, bonds: {} }}",
225            self.name,
226            self.atom_count(),
227            self.bond_count()
228        )
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn template_new_creates_correct_template() {
238        let atom_names = vec!["C1".to_string(), "C2".to_string(), "O1".to_string()];
239        let bonds = vec![
240            ("C1".to_string(), "C2".to_string(), BondOrder::Single),
241            ("C2".to_string(), "O1".to_string(), BondOrder::Double),
242        ];
243
244        let template = Template::new("LIG", atom_names.clone(), bonds.clone());
245
246        assert_eq!(template.name, "LIG");
247        assert_eq!(template.atom_names, atom_names);
248        assert_eq!(template.bonds, bonds);
249    }
250
251    #[test]
252    fn template_new_with_empty_atom_names() {
253        let atom_names = Vec::new();
254        let bonds = Vec::new();
255
256        let template = Template::new("EMPTY", atom_names, bonds);
257
258        assert_eq!(template.name, "EMPTY");
259        assert_eq!(template.atom_count(), 0);
260        assert_eq!(template.bond_count(), 0);
261    }
262
263    #[test]
264    fn template_new_with_empty_bonds() {
265        let atom_names = vec!["C1".to_string(), "C2".to_string()];
266        let bonds = Vec::new();
267
268        let template = Template::new("NO_BONDS", atom_names.clone(), bonds);
269
270        assert_eq!(template.name, "NO_BONDS");
271        assert_eq!(template.atom_names, atom_names);
272        assert_eq!(template.bonds, Vec::new());
273    }
274
275    #[test]
276    fn template_new_with_string_name() {
277        let atom_names = vec!["N1".to_string()];
278        let bonds = Vec::new();
279
280        let template = Template::new(String::from("ATP"), atom_names.clone(), bonds);
281
282        assert_eq!(template.name, "ATP");
283        assert_eq!(template.atom_names, atom_names);
284    }
285
286    #[test]
287    fn template_has_bond_returns_true_for_existing_bond() {
288        let atom_names = vec!["C1".to_string(), "C2".to_string(), "O1".to_string()];
289        let bonds = vec![
290            ("C1".to_string(), "C2".to_string(), BondOrder::Single),
291            ("C2".to_string(), "O1".to_string(), BondOrder::Double),
292        ];
293        let template = Template::new("LIG", atom_names, bonds);
294
295        assert!(template.has_bond("C1", "C2"));
296        assert!(template.has_bond("C2", "O1"));
297    }
298
299    #[test]
300    fn template_has_bond_returns_true_for_reverse_order() {
301        let atom_names = vec!["C1".to_string(), "C2".to_string()];
302        let bonds = vec![("C1".to_string(), "C2".to_string(), BondOrder::Single)];
303        let template = Template::new("LIG", atom_names, bonds);
304
305        assert!(template.has_bond("C2", "C1"));
306    }
307
308    #[test]
309    fn template_has_bond_returns_false_for_nonexistent_bond() {
310        let atom_names = vec!["C1".to_string(), "C2".to_string(), "O1".to_string()];
311        let bonds = vec![("C1".to_string(), "C2".to_string(), BondOrder::Single)];
312        let template = Template::new("LIG", atom_names, bonds);
313
314        assert!(!template.has_bond("C1", "O1"));
315        assert!(!template.has_bond("O1", "C2"));
316    }
317
318    #[test]
319    fn template_has_bond_returns_false_for_empty_template() {
320        let template = Template::new("EMPTY", Vec::new(), Vec::new());
321
322        assert!(!template.has_bond("C1", "C2"));
323    }
324
325    #[test]
326    fn template_has_atom_returns_true_for_existing_atom() {
327        let atom_names = vec!["C1".to_string(), "C2".to_string(), "O1".to_string()];
328        let bonds = Vec::new();
329        let template = Template::new("LIG", atom_names, bonds);
330
331        assert!(template.has_atom("C1"));
332        assert!(template.has_atom("C2"));
333        assert!(template.has_atom("O1"));
334    }
335
336    #[test]
337    fn template_has_atom_returns_false_for_nonexistent_atom() {
338        let atom_names = vec!["C1".to_string(), "C2".to_string()];
339        let bonds = Vec::new();
340        let template = Template::new("LIG", atom_names, bonds);
341
342        assert!(!template.has_atom("O1"));
343        assert!(!template.has_atom("N1"));
344    }
345
346    #[test]
347    fn template_has_atom_returns_false_for_empty_template() {
348        let template = Template::new("EMPTY", Vec::new(), Vec::new());
349
350        assert!(!template.has_atom("C1"));
351    }
352
353    #[test]
354    fn template_atom_names_returns_correct_slice() {
355        let atom_names = vec!["C1".to_string(), "C2".to_string(), "O1".to_string()];
356        let bonds = Vec::new();
357        let template = Template::new("LIG", atom_names.clone(), bonds);
358
359        let names = template.atom_names();
360
361        assert_eq!(names, atom_names.as_slice());
362        assert_eq!(names.len(), 3);
363        assert_eq!(names[0], "C1");
364        assert_eq!(names[1], "C2");
365        assert_eq!(names[2], "O1");
366    }
367
368    #[test]
369    fn template_atom_names_returns_empty_slice_for_empty_template() {
370        let template = Template::new("EMPTY", Vec::new(), Vec::new());
371
372        let names = template.atom_names();
373
374        assert_eq!(names, &[] as &[String]);
375        assert_eq!(names.len(), 0);
376    }
377
378    #[test]
379    fn template_bonds_returns_correct_slice() {
380        let atom_names = vec!["C1".to_string(), "C2".to_string(), "O1".to_string()];
381        let bonds = vec![
382            ("C1".to_string(), "C2".to_string(), BondOrder::Single),
383            ("C2".to_string(), "O1".to_string(), BondOrder::Double),
384        ];
385        let template = Template::new("LIG", atom_names, bonds.clone());
386
387        let template_bonds = template.bonds();
388
389        assert_eq!(template_bonds, bonds.as_slice());
390        assert_eq!(template_bonds.len(), 2);
391        assert_eq!(
392            template_bonds[0],
393            ("C1".to_string(), "C2".to_string(), BondOrder::Single)
394        );
395        assert_eq!(
396            template_bonds[1],
397            ("C2".to_string(), "O1".to_string(), BondOrder::Double)
398        );
399    }
400
401    #[test]
402    fn template_bonds_returns_empty_slice_for_empty_template() {
403        let template = Template::new("EMPTY", Vec::new(), Vec::new());
404
405        let bonds = template.bonds();
406
407        assert_eq!(bonds, &[]);
408        assert_eq!(bonds.len(), 0);
409    }
410
411    #[test]
412    fn template_atom_count_returns_correct_count() {
413        let atom_names = vec![
414            "C1".to_string(),
415            "C2".to_string(),
416            "O1".to_string(),
417            "N1".to_string(),
418        ];
419        let bonds = Vec::new();
420        let template = Template::new("LIG", atom_names, bonds);
421
422        assert_eq!(template.atom_count(), 4);
423    }
424
425    #[test]
426    fn template_atom_count_returns_zero_for_empty_template() {
427        let template = Template::new("EMPTY", Vec::new(), Vec::new());
428
429        assert_eq!(template.atom_count(), 0);
430    }
431
432    #[test]
433    fn template_bond_count_returns_correct_count() {
434        let atom_names = vec!["C1".to_string(), "C2".to_string(), "O1".to_string()];
435        let bonds = vec![
436            ("C1".to_string(), "C2".to_string(), BondOrder::Single),
437            ("C2".to_string(), "O1".to_string(), BondOrder::Double),
438            ("C1".to_string(), "O1".to_string(), BondOrder::Single),
439        ];
440        let template = Template::new("LIG", atom_names, bonds);
441
442        assert_eq!(template.bond_count(), 3);
443    }
444
445    #[test]
446    fn template_bond_count_returns_zero_for_empty_template() {
447        let template = Template::new("EMPTY", Vec::new(), Vec::new());
448
449        assert_eq!(template.bond_count(), 0);
450    }
451
452    #[test]
453    fn template_display_formats_correctly() {
454        let atom_names = vec!["C1".to_string(), "C2".to_string(), "O1".to_string()];
455        let bonds = vec![
456            ("C1".to_string(), "C2".to_string(), BondOrder::Single),
457            ("C2".to_string(), "O1".to_string(), BondOrder::Double),
458        ];
459        let template = Template::new("LIG", atom_names, bonds);
460
461        let display = format!("{}", template);
462        let expected = "Template { name: \"LIG\", atoms: 3, bonds: 2 }";
463
464        assert_eq!(display, expected);
465    }
466
467    #[test]
468    fn template_display_formats_empty_template_correctly() {
469        let template = Template::new("EMPTY", Vec::new(), Vec::new());
470
471        let display = format!("{}", template);
472        let expected = "Template { name: \"EMPTY\", atoms: 0, bonds: 0 }";
473
474        assert_eq!(display, expected);
475    }
476
477    #[test]
478    fn template_display_formats_template_with_special_characters() {
479        let atom_names = vec!["C1".to_string()];
480        let bonds = Vec::new();
481        let template = Template::new("LIG-123", atom_names, bonds);
482
483        let display = format!("{}", template);
484        let expected = "Template { name: \"LIG-123\", atoms: 1, bonds: 0 }";
485
486        assert_eq!(display, expected);
487    }
488
489    #[test]
490    fn template_clone_creates_identical_copy() {
491        let atom_names = vec!["C1".to_string(), "C2".to_string()];
492        let bonds = vec![("C1".to_string(), "C2".to_string(), BondOrder::Single)];
493        let template = Template::new("LIG", atom_names.clone(), bonds.clone());
494
495        let cloned = template.clone();
496
497        assert_eq!(template, cloned);
498        assert_eq!(template.name, cloned.name);
499        assert_eq!(template.atom_names, cloned.atom_names);
500        assert_eq!(template.bonds, cloned.bonds);
501    }
502
503    #[test]
504    fn template_partial_eq_compares_correctly() {
505        let atom_names = vec!["C1".to_string(), "C2".to_string()];
506        let bonds = vec![("C1".to_string(), "C2".to_string(), BondOrder::Single)];
507
508        let template1 = Template::new("LIG", atom_names.clone(), bonds.clone());
509        let template2 = Template::new("LIG", atom_names.clone(), bonds.clone());
510        let template3 = Template::new("DIF", atom_names.clone(), bonds.clone());
511
512        assert_eq!(template1, template2);
513        assert_ne!(template1, template3);
514    }
515
516    #[test]
517    fn template_with_complex_molecule() {
518        let atom_names = vec![
519            "C1".to_string(),
520            "C2".to_string(),
521            "O1".to_string(),
522            "H11".to_string(),
523            "H12".to_string(),
524            "H13".to_string(),
525            "H21".to_string(),
526            "H22".to_string(),
527            "H1".to_string(),
528        ];
529        let bonds = vec![
530            ("C1".to_string(), "C2".to_string(), BondOrder::Single),
531            ("C2".to_string(), "O1".to_string(), BondOrder::Single),
532            ("C1".to_string(), "H11".to_string(), BondOrder::Single),
533            ("C1".to_string(), "H12".to_string(), BondOrder::Single),
534            ("C1".to_string(), "H13".to_string(), BondOrder::Single),
535            ("C2".to_string(), "H21".to_string(), BondOrder::Single),
536            ("C2".to_string(), "H22".to_string(), BondOrder::Single),
537            ("O1".to_string(), "H1".to_string(), BondOrder::Single),
538        ];
539
540        let template = Template::new("ETOH", atom_names, bonds);
541
542        assert_eq!(template.name, "ETOH");
543        assert_eq!(template.atom_count(), 9);
544        assert_eq!(template.bond_count(), 8);
545
546        assert!(template.has_bond("C1", "C2"));
547        assert!(template.has_bond("C2", "O1"));
548        assert!(template.has_bond("O1", "H1"));
549
550        assert!(template.has_atom("C1"));
551        assert!(template.has_atom("H1"));
552        assert!(!template.has_atom("C3"));
553    }
554
555    #[test]
556    fn template_with_aromatic_bonds() {
557        let atom_names = vec!["C1".to_string(), "C2".to_string(), "C3".to_string()];
558        let bonds = vec![
559            ("C1".to_string(), "C2".to_string(), BondOrder::Aromatic),
560            ("C2".to_string(), "C3".to_string(), BondOrder::Aromatic),
561            ("C3".to_string(), "C1".to_string(), BondOrder::Aromatic),
562        ];
563
564        let template = Template::new("BENZENE_RING", atom_names, bonds);
565
566        assert_eq!(template.bond_count(), 3);
567        assert!(template.has_bond("C1", "C2"));
568        assert!(template.has_bond("C2", "C3"));
569        assert!(template.has_bond("C3", "C1"));
570    }
571
572    #[test]
573    fn template_with_triple_bond() {
574        let atom_names = vec!["N1".to_string(), "N2".to_string()];
575        let bonds = vec![("N1".to_string(), "N2".to_string(), BondOrder::Triple)];
576
577        let template = Template::new("N2", atom_names, bonds);
578
579        assert_eq!(template.bond_count(), 1);
580        assert!(template.has_bond("N1", "N2"));
581        assert!(!template.has_bond("N1", "N3"));
582    }
583}