Skip to main content

dreid_forge/model/
topology.rs

1//! Force field topology and potential function definitions.
2//!
3//! This module defines the data structures for DREIDING force field
4//! parameters and potential energy functions. After parameterization,
5//! a [`ForgedSystem`] contains all information needed to run molecular
6//! dynamics or energy minimization simulations.
7//!
8//! # Potential Functions
9//!
10//! The DREIDING force field supports several potential function types:
11//!
12//! - **Bonds**: [`BondPotential`] — Harmonic or Morse stretching
13//! - **Angles**: [`AnglePotential`] — Cosine-harmonic, cosine-linear, or theta-harmonic bending
14//! - **Torsions**: [`TorsionPotential`] — Periodic torsion potentials
15//! - **Inversions**: [`InversionPotential`] — Out-of-plane (planar/umbrella)
16//! - **Van der Waals**: [`VdwPairPotential`] — LJ 12-6 or Buckingham Exp-6
17//! - **Hydrogen bonds**: [`HBondPotential`] — Directional H-bond terms
18//!
19//! # Output Structure
20//!
21//! The [`ForgedSystem`] struct combines the original molecular system
22//! with computed atom types, partial charges, and all bonded/non-bonded
23//! potential parameters needed for simulation.
24
25use super::system::System;
26
27/// Per-atom force field parameters.
28///
29/// Contains the computed properties for a single atom after
30/// DREIDING parameterization.
31///
32/// # Fields
33///
34/// * `charge` — Partial atomic charge in elementary charge units
35/// * `mass` — Atomic mass in atomic mass units (amu)
36/// * `type_idx` — Index into the atom type table
37#[derive(Debug, Clone, PartialEq)]
38pub struct AtomParam {
39    /// Partial atomic charge (e).
40    pub charge: f64,
41    /// Atomic mass (amu).
42    pub mass: f64,
43    /// Index into the atom type name table.
44    pub type_idx: usize,
45}
46
47/// Bond stretching potential functions.
48#[derive(Debug, Clone, PartialEq)]
49pub enum BondPotential {
50    /// Harmonic bond stretching potential.
51    Harmonic {
52        /// Atom indices (i, j).
53        atoms: (usize, usize),
54        /// Half force constant (kcal/mol/Ų).
55        k_half: f64,
56        /// Equilibrium bond length (Å).
57        r0: f64,
58    },
59    /// Morse anharmonic bond potential.
60    Morse {
61        /// Atom indices (i, j).
62        atoms: (usize, usize),
63        /// Dissociation energy (kcal/mol).
64        de: f64,
65        /// Equilibrium bond length (Å).
66        r0: f64,
67        /// Stiffness parameter alpha (Å⁻¹).
68        alpha: f64,
69    },
70}
71
72/// Angle bending potential functions.
73#[derive(Debug, Clone, PartialEq)]
74pub enum AnglePotential {
75    /// Cosine-harmonic angle potential (for θ₀ ≠ 180°).
76    CosineHarmonic {
77        /// Atom indices (i, j, k) where j is the central atom.
78        atoms: (usize, usize, usize),
79        /// Half cosine-harmonic constant (kcal/mol).
80        c_half: f64,
81        /// Cosine of equilibrium angle.
82        cos0: f64,
83    },
84    /// Cosine-linear angle potential for linear geometries (θ₀ = 180°).
85    CosineLinear {
86        /// Atom indices (i, j, k) where j is the central atom.
87        atoms: (usize, usize, usize),
88        /// Cosine-linear constant (kcal/mol).
89        c: f64,
90    },
91    /// Simple theta-harmonic angle potential.
92    ThetaHarmonic {
93        /// Atom indices (i, j, k) where j is the central atom.
94        atoms: (usize, usize, usize),
95        /// Half force constant (kcal/mol/rad²).
96        k_half: f64,
97        /// Equilibrium angle (radians).
98        theta0: f64,
99    },
100}
101
102/// Torsion (proper dihedral) potential.
103#[derive(Debug, Clone, PartialEq)]
104pub struct TorsionPotential {
105    /// Atom indices (i, j, k, l) where j-k is the central bond.
106    pub atoms: (usize, usize, usize, usize),
107    /// Half barrier height (kcal/mol).
108    pub v_half: f64,
109    /// Periodicity (number of minima in 360°).
110    pub n: u8,
111    /// Precomputed cos(n·φ₀).
112    pub cos_n_phi0: f64,
113    /// Precomputed sin(n·φ₀).
114    pub sin_n_phi0: f64,
115}
116
117/// Inversion (out-of-plane) potential functions.
118#[derive(Debug, Clone, PartialEq)]
119pub enum InversionPotential {
120    /// Planar inversion for sp² centers.
121    Planar {
122        /// Atom indices (center, axis, plane1, plane2) where center is the sp² atom,
123        /// axis is the out-of-plane neighbor, and plane1/plane2 are in-plane neighbors.
124        atoms: (usize, usize, usize, usize),
125        /// Half inversion constant (kcal/mol).
126        c_half: f64,
127    },
128    /// Umbrella inversion for pyramidal centers.
129    Umbrella {
130        /// Atom indices (center, axis, plane1, plane2) where center is the pyramidal atom,
131        /// axis is one neighbor defining the inversion axis, and plane1/plane2 are the other neighbors.
132        atoms: (usize, usize, usize, usize),
133        /// Half inversion constant (kcal/mol).
134        c_half: f64,
135        /// Cosine of equilibrium inversion angle.
136        cos_psi0: f64,
137    },
138}
139
140/// Van der Waals non-bonded pair potential functions.
141#[derive(Debug, Clone, PartialEq)]
142pub enum VdwPairPotential {
143    /// Lennard-Jones 12-6 potential.
144    LennardJones {
145        /// First atom type index.
146        type1_idx: usize,
147        /// Second atom type index.
148        type2_idx: usize,
149        /// Energy well depth (kcal/mol).
150        d0: f64,
151        /// Squared equilibrium distance (Ų).
152        r0_sq: f64,
153    },
154    /// Buckingham (Exponential-6) potential with energy reflection.
155    Buckingham {
156        /// First atom type index.
157        type1_idx: usize,
158        /// Second atom type index.
159        type2_idx: usize,
160        /// Repulsion prefactor (kcal/mol).
161        a: f64,
162        /// Repulsion decay (Å⁻¹).
163        b: f64,
164        /// Attraction coefficient (kcal·Å⁶/mol).
165        c: f64,
166        /// Squared distance of energy maximum (Ų).
167        r_max_sq: f64,
168        /// Twice the energy at maximum (kcal/mol).
169        two_e_max: f64,
170    },
171}
172
173/// Hydrogen bond directional potential.
174#[derive(Debug, Clone, PartialEq)]
175pub struct HBondPotential {
176    /// Donor atom type index (D in D-H···A).
177    pub donor_type_idx: usize,
178    /// Hydrogen atom type index (H in D-H···A).
179    pub hydrogen_type_idx: usize,
180    /// Acceptor atom type index (A in D-H···A).
181    pub acceptor_type_idx: usize,
182    /// Energy well depth (kcal/mol).
183    pub d_hb: f64,
184    /// Squared equilibrium distance (Ų).
185    pub r_hb_sq: f64,
186}
187
188/// Collection of all potential energy functions for a system.
189///
190/// Groups all bonded and non-bonded interaction parameters
191/// computed during DREIDING parameterization.
192#[derive(Debug, Clone, Default)]
193pub struct Potentials {
194    /// Bond stretching potentials.
195    pub bonds: Vec<BondPotential>,
196    /// Angle bending potentials.
197    pub angles: Vec<AnglePotential>,
198    /// Torsion (proper dihedral) potentials.
199    pub torsions: Vec<TorsionPotential>,
200    /// Inversion (out-of-plane) potentials.
201    pub inversions: Vec<InversionPotential>,
202    /// Van der Waals pair potentials between atom types.
203    pub vdw_pairs: Vec<VdwPairPotential>,
204    /// Hydrogen bond potentials.
205    pub h_bonds: Vec<HBondPotential>,
206}
207
208/// A fully parameterized molecular system.
209///
210/// Contains the original [`System`] along with computed DREIDING
211/// force field parameters including atom types, partial charges,
212/// and all potential energy function parameters.
213///
214/// This is the primary output of the [`forge`](crate::forge) function
215/// and contains everything needed to write simulation input files
216/// for molecular dynamics packages.
217///
218/// # Fields
219///
220/// * `system` — Original molecular structure
221/// * `atom_types` — DREIDING atom type names (e.g., "C_3", "O_2")
222/// * `atom_properties` — Per-atom charges, masses, and type indices
223/// * `potentials` — All bonded and non-bonded potential parameters
224#[derive(Debug, Clone)]
225pub struct ForgedSystem {
226    /// The original molecular system with atoms and bonds.
227    pub system: System,
228    /// DREIDING atom type names, indexed by type_idx.
229    pub atom_types: Vec<String>,
230    /// Per-atom force field parameters.
231    pub atom_properties: Vec<AtomParam>,
232    /// All potential energy function parameters.
233    pub potentials: Potentials,
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn bond_harmonic_construction() {
242        let bond = BondPotential::Harmonic {
243            atoms: (0, 1),
244            k_half: 350.0,
245            r0: 1.54,
246        };
247
248        match bond {
249            BondPotential::Harmonic { atoms, k_half, r0 } => {
250                assert_eq!(atoms.0, 0);
251                assert_eq!(atoms.1, 1);
252                assert_eq!(k_half, 350.0);
253                assert_eq!(r0, 1.54);
254            }
255            _ => panic!("expected Harmonic variant"),
256        }
257    }
258
259    #[test]
260    fn bond_morse_construction() {
261        let bond = BondPotential::Morse {
262            atoms: (2, 3),
263            de: 70.0,
264            r0: 1.10,
265            alpha: 2.0,
266        };
267
268        match bond {
269            BondPotential::Morse {
270                atoms,
271                de,
272                r0,
273                alpha,
274            } => {
275                assert_eq!(atoms.0, 2);
276                assert_eq!(atoms.1, 3);
277                assert_eq!(de, 70.0);
278                assert_eq!(r0, 1.10);
279                assert_eq!(alpha, 2.0);
280            }
281            _ => panic!("expected Morse variant"),
282        }
283    }
284
285    #[test]
286    fn bond_partial_eq() {
287        let b1 = BondPotential::Harmonic {
288            atoms: (0, 1),
289            k_half: 350.0,
290            r0: 1.54,
291        };
292        let b2 = BondPotential::Harmonic {
293            atoms: (0, 1),
294            k_half: 350.0,
295            r0: 1.54,
296        };
297        let b3 = BondPotential::Harmonic {
298            atoms: (0, 1),
299            k_half: 400.0,
300            r0: 1.54,
301        };
302
303        assert_eq!(b1, b2);
304        assert_ne!(b1, b3);
305    }
306
307    #[test]
308    fn bond_clone() {
309        let bond = BondPotential::Harmonic {
310            atoms: (5, 6),
311            k_half: 350.0,
312            r0: 1.09,
313        };
314        let cloned = bond.clone();
315        assert_eq!(bond, cloned);
316    }
317
318    #[test]
319    fn angle_cosine_harmonic_construction() {
320        let angle = AnglePotential::CosineHarmonic {
321            atoms: (0, 1, 2),
322            c_half: 50.0,
323            cos0: 0.5,
324        };
325
326        match angle {
327            AnglePotential::CosineHarmonic {
328                atoms,
329                c_half,
330                cos0,
331            } => {
332                assert_eq!(atoms.0, 0);
333                assert_eq!(atoms.1, 1);
334                assert_eq!(atoms.2, 2);
335                assert_eq!(c_half, 50.0);
336                assert_eq!(cos0, 0.5);
337            }
338            _ => panic!("expected CosineHarmonic variant"),
339        }
340    }
341
342    #[test]
343    fn angle_cosine_linear_construction() {
344        let angle = AnglePotential::CosineLinear {
345            atoms: (6, 7, 8),
346            c: 100.0,
347        };
348
349        match angle {
350            AnglePotential::CosineLinear { atoms, c } => {
351                assert_eq!(atoms.0, 6);
352                assert_eq!(atoms.1, 7);
353                assert_eq!(atoms.2, 8);
354                assert_eq!(c, 100.0);
355            }
356            _ => panic!("expected CosineLinear variant"),
357        }
358    }
359
360    #[test]
361    fn angle_theta_harmonic_construction() {
362        let angle = AnglePotential::ThetaHarmonic {
363            atoms: (3, 4, 5),
364            k_half: 50.0,
365            theta0: 1.911,
366        };
367
368        match angle {
369            AnglePotential::ThetaHarmonic {
370                atoms,
371                k_half,
372                theta0,
373            } => {
374                assert_eq!(atoms.0, 3);
375                assert_eq!(atoms.1, 4);
376                assert_eq!(atoms.2, 5);
377                assert_eq!(k_half, 50.0);
378                assert_eq!(theta0, 1.911);
379            }
380            _ => panic!("expected ThetaHarmonic variant"),
381        }
382    }
383
384    #[test]
385    fn angle_partial_eq() {
386        let a1 = AnglePotential::CosineHarmonic {
387            atoms: (0, 1, 2),
388            c_half: 50.0,
389            cos0: 0.5,
390        };
391        let a2 = AnglePotential::CosineHarmonic {
392            atoms: (0, 1, 2),
393            c_half: 50.0,
394            cos0: 0.5,
395        };
396        assert_eq!(a1, a2);
397    }
398
399    #[test]
400    fn torsion_construction() {
401        let torsion = TorsionPotential {
402            atoms: (0, 1, 2, 3),
403            v_half: 1.0,
404            n: 3,
405            cos_n_phi0: -1.0,
406            sin_n_phi0: 0.0,
407        };
408
409        assert_eq!(torsion.atoms.0, 0);
410        assert_eq!(torsion.atoms.1, 1);
411        assert_eq!(torsion.atoms.2, 2);
412        assert_eq!(torsion.atoms.3, 3);
413        assert_eq!(torsion.v_half, 1.0);
414        assert_eq!(torsion.n, 3);
415        assert_eq!(torsion.cos_n_phi0, -1.0);
416        assert_eq!(torsion.sin_n_phi0, 0.0);
417    }
418
419    #[test]
420    fn torsion_partial_eq() {
421        let t1 = TorsionPotential {
422            atoms: (0, 1, 2, 3),
423            v_half: 1.0,
424            n: 3,
425            cos_n_phi0: -1.0,
426            sin_n_phi0: 0.0,
427        };
428        let t2 = TorsionPotential {
429            atoms: (0, 1, 2, 3),
430            v_half: 1.0,
431            n: 3,
432            cos_n_phi0: -1.0,
433            sin_n_phi0: 0.0,
434        };
435        let t3 = TorsionPotential {
436            atoms: (0, 1, 2, 3),
437            v_half: 2.0,
438            n: 3,
439            cos_n_phi0: -1.0,
440            sin_n_phi0: 0.0,
441        };
442
443        assert_eq!(t1, t2);
444        assert_ne!(t1, t3);
445    }
446
447    #[test]
448    fn torsion_clone() {
449        let torsion = TorsionPotential {
450            atoms: (5, 6, 7, 8),
451            v_half: 22.5,
452            n: 2,
453            cos_n_phi0: 1.0,
454            sin_n_phi0: 0.0,
455        };
456        let cloned = torsion.clone();
457        assert_eq!(torsion, cloned);
458    }
459
460    #[test]
461    fn inversion_planar_construction() {
462        let inversion = InversionPotential::Planar {
463            atoms: (1, 0, 2, 3),
464            c_half: 20.0,
465        };
466
467        match inversion {
468            InversionPotential::Planar { atoms, c_half } => {
469                assert_eq!(atoms.0, 1);
470                assert_eq!(atoms.1, 0);
471                assert_eq!(atoms.2, 2);
472                assert_eq!(atoms.3, 3);
473                assert_eq!(c_half, 20.0);
474            }
475            _ => panic!("expected Planar variant"),
476        }
477    }
478
479    #[test]
480    fn inversion_umbrella_construction() {
481        let inversion = InversionPotential::Umbrella {
482            atoms: (5, 6, 7, 8),
483            c_half: 13.33,
484            cos_psi0: 0.5774,
485        };
486
487        match inversion {
488            InversionPotential::Umbrella {
489                atoms,
490                c_half,
491                cos_psi0,
492            } => {
493                assert_eq!(atoms.0, 5);
494                assert_eq!(atoms.1, 6);
495                assert_eq!(atoms.2, 7);
496                assert_eq!(atoms.3, 8);
497                assert_eq!(c_half, 13.33);
498                assert_eq!(cos_psi0, 0.5774);
499            }
500            _ => panic!("expected Umbrella variant"),
501        }
502    }
503
504    #[test]
505    fn inversion_partial_eq() {
506        let i1 = InversionPotential::Planar {
507            atoms: (1, 0, 2, 3),
508            c_half: 20.0,
509        };
510        let i2 = InversionPotential::Planar {
511            atoms: (1, 0, 2, 3),
512            c_half: 20.0,
513        };
514        assert_eq!(i1, i2);
515    }
516
517    #[test]
518    fn vdw_lennard_jones_construction() {
519        let vdw = VdwPairPotential::LennardJones {
520            type1_idx: 0,
521            type2_idx: 1,
522            d0: 0.0951,
523            r0_sq: 15.195,
524        };
525
526        match vdw {
527            VdwPairPotential::LennardJones {
528                type1_idx,
529                type2_idx,
530                d0,
531                r0_sq,
532            } => {
533                assert_eq!(type1_idx, 0);
534                assert_eq!(type2_idx, 1);
535                assert_eq!(d0, 0.0951);
536                assert_eq!(r0_sq, 15.195);
537            }
538            _ => panic!("expected LennardJones variant"),
539        }
540    }
541
542    #[test]
543    fn vdw_buckingham_construction() {
544        let vdw = VdwPairPotential::Buckingham {
545            type1_idx: 2,
546            type2_idx: 3,
547            a: 1000.0,
548            b: 3.08,
549            c: 500.0,
550            r_max_sq: 16.0,
551            two_e_max: 10.5,
552        };
553
554        match vdw {
555            VdwPairPotential::Buckingham {
556                type1_idx,
557                type2_idx,
558                a,
559                b,
560                c,
561                r_max_sq,
562                two_e_max,
563            } => {
564                assert_eq!(type1_idx, 2);
565                assert_eq!(type2_idx, 3);
566                assert_eq!(a, 1000.0);
567                assert_eq!(b, 3.08);
568                assert_eq!(c, 500.0);
569                assert_eq!(r_max_sq, 16.0);
570                assert_eq!(two_e_max, 10.5);
571            }
572            _ => panic!("expected Buckingham variant"),
573        }
574    }
575
576    #[test]
577    fn vdw_partial_eq() {
578        let v1 = VdwPairPotential::LennardJones {
579            type1_idx: 0,
580            type2_idx: 1,
581            d0: 0.0951,
582            r0_sq: 15.195,
583        };
584        let v2 = VdwPairPotential::LennardJones {
585            type1_idx: 0,
586            type2_idx: 1,
587            d0: 0.0951,
588            r0_sq: 15.195,
589        };
590        let v3 = VdwPairPotential::LennardJones {
591            type1_idx: 0,
592            type2_idx: 1,
593            d0: 0.1000,
594            r0_sq: 15.195,
595        };
596
597        assert_eq!(v1, v2);
598        assert_ne!(v1, v3);
599    }
600
601    #[test]
602    fn hbond_construction() {
603        let hbond = HBondPotential {
604            donor_type_idx: 0,
605            hydrogen_type_idx: 1,
606            acceptor_type_idx: 2,
607            d_hb: 4.0,
608            r_hb_sq: 7.5625,
609        };
610
611        assert_eq!(hbond.donor_type_idx, 0);
612        assert_eq!(hbond.hydrogen_type_idx, 1);
613        assert_eq!(hbond.acceptor_type_idx, 2);
614        assert_eq!(hbond.d_hb, 4.0);
615        assert_eq!(hbond.r_hb_sq, 7.5625);
616    }
617
618    #[test]
619    fn hbond_partial_eq() {
620        let h1 = HBondPotential {
621            donor_type_idx: 0,
622            hydrogen_type_idx: 1,
623            acceptor_type_idx: 2,
624            d_hb: 4.0,
625            r_hb_sq: 7.5625,
626        };
627        let h2 = HBondPotential {
628            donor_type_idx: 0,
629            hydrogen_type_idx: 1,
630            acceptor_type_idx: 2,
631            d_hb: 4.0,
632            r_hb_sq: 7.5625,
633        };
634        let h3 = HBondPotential {
635            donor_type_idx: 0,
636            hydrogen_type_idx: 1,
637            acceptor_type_idx: 2,
638            d_hb: 5.0,
639            r_hb_sq: 7.5625,
640        };
641
642        assert_eq!(h1, h2);
643        assert_ne!(h1, h3);
644    }
645
646    #[test]
647    fn hbond_clone() {
648        let hbond = HBondPotential {
649            donor_type_idx: 3,
650            hydrogen_type_idx: 4,
651            acceptor_type_idx: 5,
652            d_hb: 7.0,
653            r_hb_sq: 7.5625,
654        };
655        let cloned = hbond.clone();
656        assert_eq!(hbond, cloned);
657    }
658
659    #[test]
660    fn atom_param_construction() {
661        let param = AtomParam {
662            charge: -0.5,
663            mass: 12.011,
664            type_idx: 2,
665        };
666
667        assert_eq!(param.charge, -0.5);
668        assert_eq!(param.mass, 12.011);
669        assert_eq!(param.type_idx, 2);
670    }
671
672    #[test]
673    fn atom_param_partial_eq() {
674        let p1 = AtomParam {
675            charge: 0.0,
676            mass: 1.008,
677            type_idx: 0,
678        };
679        let p2 = AtomParam {
680            charge: 0.0,
681            mass: 1.008,
682            type_idx: 0,
683        };
684        let p3 = AtomParam {
685            charge: 0.1,
686            mass: 1.008,
687            type_idx: 0,
688        };
689
690        assert_eq!(p1, p2);
691        assert_ne!(p1, p3);
692    }
693
694    #[test]
695    fn atom_param_clone() {
696        let param = AtomParam {
697            charge: -0.3,
698            mass: 15.999,
699            type_idx: 5,
700        };
701        let cloned = param.clone();
702        assert_eq!(param, cloned);
703    }
704
705    #[test]
706    fn potentials_default() {
707        let pots = Potentials::default();
708
709        assert_eq!(pots.bonds.len(), 0);
710        assert_eq!(pots.angles.len(), 0);
711        assert_eq!(pots.torsions.len(), 0);
712        assert_eq!(pots.inversions.len(), 0);
713        assert_eq!(pots.vdw_pairs.len(), 0);
714        assert_eq!(pots.h_bonds.len(), 0);
715    }
716
717    #[test]
718    fn potentials_add_elements() {
719        let mut pots = Potentials::default();
720
721        pots.bonds.push(BondPotential::Harmonic {
722            atoms: (0, 1),
723            k_half: 350.0,
724            r0: 1.54,
725        });
726
727        pots.angles.push(AnglePotential::CosineHarmonic {
728            atoms: (0, 1, 2),
729            c_half: 50.0,
730            cos0: -0.333,
731        });
732
733        pots.torsions.push(TorsionPotential {
734            atoms: (0, 1, 2, 3),
735            v_half: 1.0,
736            n: 3,
737            cos_n_phi0: -1.0,
738            sin_n_phi0: 0.0,
739        });
740
741        assert_eq!(pots.bonds.len(), 1);
742        assert_eq!(pots.angles.len(), 1);
743        assert_eq!(pots.torsions.len(), 1);
744    }
745
746    #[test]
747    fn potentials_clone() {
748        let mut pots = Potentials::default();
749        pots.bonds.push(BondPotential::Harmonic {
750            atoms: (0, 1),
751            k_half: 350.0,
752            r0: 1.54,
753        });
754
755        let cloned = pots.clone();
756        assert_eq!(pots.bonds.len(), cloned.bonds.len());
757    }
758}