potentials/bond/g96.rs
1//! # GROMOS96 Bond Potential
2//!
3//! The GROMOS96 quartic bond potential, which avoids expensive
4//! square root operations by working directly with r^2.
5//!
6//! ## Formula
7//!
8//! ```text
9//! V(r) = (k/4) * (r^2 - r0^2)^2
10//! ```
11//!
12//! where:
13//! - `k`: Force constant (energy/length^4 units)
14//! - `r0`: Equilibrium bond length (length units)
15//!
16//! ## Force Factor
17//!
18//! ```text
19//! S = -(dV/dr) / r = -k * (r^2 - r0^2)
20//! ```
21//!
22//! ## Implementation Notes
23//!
24//! - Operates entirely on r^2, avoiding sqrt and division by r
25//! - Near equilibrium: V_G96 ≈ k * r0^2 * (r - r0)^2
26//! - Relation to harmonic: k_G96 = k_harm / r0^2
27
28use crate::base::Potential2;
29use crate::math::Vector;
30
31/// GROMOS96 bond potential.
32///
33/// ## Parameters
34///
35/// - `k`: Force constant (energy/length^4 units)
36/// - `r0`: Equilibrium distance (length units)
37///
38/// ## Precomputed Values
39///
40/// - `k_quarter`: Stores `k/4` for efficient computation
41/// - `r0_sq`: Stores `r0^2` to avoid sqrt
42#[derive(Clone, Copy, Debug, PartialEq)]
43pub struct G96<T> {
44 /// Force constant divided by 4
45 k_quarter: T,
46 /// Equilibrium distance squared
47 r0_sq: T,
48}
49
50impl<T: Vector> G96<T> {
51 /// Creates a new GROMOS96 bond potential.
52 ///
53 /// ## Arguments
54 ///
55 /// - `k`: Force constant (energy/length^4 units)
56 /// - `r0`: Equilibrium bond length (length units)
57 ///
58 /// ## Example
59 ///
60 /// ```
61 /// use potentials::bond::G96;
62 ///
63 /// // Convert from harmonic: k_g96 = k_harm / r0^2
64 /// let k_harm = 300.0;
65 /// let r0 = 1.54;
66 /// let bond = G96::<f64>::new(k_harm / (r0 * r0), r0);
67 /// ```
68 #[inline]
69 pub fn new(k: f64, r0: f64) -> Self {
70 Self {
71 k_quarter: T::splat(k / 4.0),
72 r0_sq: T::splat(r0 * r0),
73 }
74 }
75
76 /// Creates from r0 squared directly (avoids a multiplication).
77 #[inline]
78 pub fn from_r0_sq(k: f64, r0_sq: f64) -> Self {
79 Self {
80 k_quarter: T::splat(k / 4.0),
81 r0_sq: T::splat(r0_sq),
82 }
83 }
84
85 /// Returns the equilibrium distance squared.
86 #[inline]
87 pub fn r0_sq(&self) -> T {
88 self.r0_sq
89 }
90}
91
92impl<T: Vector> Potential2<T> for G96<T> {
93 /// Computes the potential energy.
94 ///
95 /// ```text
96 /// V(r) = (k/4) * (r^2 - r0^2)^2
97 /// ```
98 ///
99 /// Note: No sqrt required!
100 #[inline(always)]
101 fn energy(&self, r_sq: T) -> T {
102 let delta = r_sq - self.r0_sq;
103 self.k_quarter * delta * delta
104 }
105
106 /// Computes the force factor.
107 ///
108 /// ```text
109 /// dV/dr = (k/4) * 2 * (r^2 - r0^2) * 2r = k * r * (r^2 - r0^2)
110 /// S = -(dV/dr)/r = -k * (r^2 - r0^2)
111 /// ```
112 ///
113 /// Note: No sqrt or division required!
114 #[inline(always)]
115 fn force_factor(&self, r_sq: T) -> T {
116 let delta = r_sq - self.r0_sq;
117 let four = T::splat(4.0);
118 T::zero() - four * self.k_quarter * delta
119 }
120
121 /// Computes energy and force factor together (optimized).
122 ///
123 /// Shares the computation of `delta`.
124 #[inline(always)]
125 fn energy_force(&self, r_sq: T) -> (T, T) {
126 let delta = r_sq - self.r0_sq;
127 let four = T::splat(4.0);
128
129 let energy = self.k_quarter * delta * delta;
130 let force = T::zero() - four * self.k_quarter * delta;
131
132 (energy, force)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use approx::assert_relative_eq;
140
141 #[test]
142 fn test_g96_at_equilibrium() {
143 let g96: G96<f64> = G96::new(100.0, 1.5);
144
145 let r0 = 1.5;
146 let energy = g96.energy(r0 * r0);
147 assert_relative_eq!(energy, 0.0, epsilon = 1e-10);
148 }
149
150 #[test]
151 fn test_g96_force_at_equilibrium() {
152 let g96: G96<f64> = G96::new(100.0, 1.5);
153 let r0 = 1.5;
154
155 let force = g96.force_factor(r0 * r0);
156 assert_relative_eq!(force, 0.0, epsilon = 1e-10);
157 }
158
159 #[test]
160 fn test_g96_displaced() {
161 let k = 4.0;
162 let r0 = 1.0;
163 let g96: G96<f64> = G96::new(k, r0);
164
165 let r_sq = 2.0;
166 let energy = g96.energy(r_sq);
167
168 assert_relative_eq!(energy, 1.0, epsilon = 1e-10);
169 }
170
171 #[test]
172 fn test_g96_numerical_derivative() {
173 let g96: G96<f64> = G96::new(100.0, 1.5);
174 let r = 1.6;
175 let r_sq = r * r;
176
177 let h = 1e-6;
178 let v_plus = g96.energy((r + h) * (r + h));
179 let v_minus = g96.energy((r - h) * (r - h));
180 let dv_dr_numerical = (v_plus - v_minus) / (2.0 * h);
181
182 let s_numerical = -dv_dr_numerical / r;
183 let s_analytical = g96.force_factor(r_sq);
184
185 assert_relative_eq!(s_analytical, s_numerical, epsilon = 1e-6);
186 }
187
188 #[test]
189 fn test_g96_from_r0_sq() {
190 let g96_a: G96<f64> = G96::new(100.0, 1.5);
191 let g96_b: G96<f64> = G96::from_r0_sq(100.0, 1.5 * 1.5);
192
193 let r_sq = 2.0;
194 assert_relative_eq!(g96_a.energy(r_sq), g96_b.energy(r_sq), epsilon = 1e-10);
195 }
196}