dreid_kernel/potentials/bonded/
stretch.rs

1use crate::math::Real;
2use crate::traits::PairKernel;
3use crate::types::EnergyDiff;
4
5/// Harmonic potential implementation for 1-2 bond stretching.
6///
7/// # Physics
8///
9/// Models the bond stretching as a harmonic oscillator.
10///
11/// - **Formula**: $$ E = \frac{1}{2} K (R - R_0)^2 $$
12/// - **Derivative Factor (`diff`)**: $$ D = -\frac{K (R - R_0)}{R} $$
13///
14/// # Parameters
15///
16/// - `k_half`: Half force constant $K_{half} = K/2$.
17/// - `r0`: Equilibrium distance $R_0$.
18///
19/// # Inputs
20///
21/// - `r_sq`: Squared distance $r^2$ between two atoms.
22///
23/// # Implementation Notes
24///
25/// - Requires square root to obtain $R$.
26/// - All intermediate calculations are shared between energy and force computations.
27/// - Branchless and panic-free.
28#[derive(Clone, Copy, Debug, Default)]
29pub struct Harmonic;
30
31impl<T: Real> PairKernel<T> for Harmonic {
32    type Params = (T, T);
33
34    /// Computes only the potential energy.
35    ///
36    /// # Formula
37    ///
38    /// $$ E = K_{half} (R - R_0)^2 $$
39    #[inline(always)]
40    fn energy(r_sq: T, (k_half, r0): Self::Params) -> T {
41        let r = r_sq.sqrt();
42        let delta = r - r0;
43        k_half * delta * delta
44    }
45
46    /// Computes only the force pre-factor $D$.
47    ///
48    /// # Formula
49    ///
50    /// $$ D = -\frac{2 K_{half} (R - R_0)}{R} $$
51    ///
52    /// This factor is defined such that the force vector can be computed
53    /// by a single vector multiplication: $\vec{F} = -D \cdot \vec{r}$.
54    #[inline(always)]
55    fn diff(r_sq: T, (k_half, r0): Self::Params) -> T {
56        let inv_r = r_sq.rsqrt();
57        let r = r_sq * inv_r;
58        let delta = r - r0;
59
60        let k = k_half + k_half;
61
62        -k * delta * inv_r
63    }
64
65    /// Computes both energy and force pre-factor efficiently.
66    ///
67    /// This method reuses intermediate calculations to minimize operations.
68    #[inline(always)]
69    fn compute(r_sq: T, (k_half, r0): Self::Params) -> EnergyDiff<T> {
70        let inv_r = r_sq.rsqrt();
71        let r = r_sq * inv_r;
72        let delta = r - r0;
73
74        let energy = k_half * delta * delta;
75
76        let k = k_half + k_half;
77        let diff = -k * delta * inv_r;
78
79        EnergyDiff { energy, diff }
80    }
81}
82
83/// Morse potential implementation for 1-2 bond stretching.
84///
85/// # Physics
86///
87/// Models bond stretching with anharmonicity, allowing for bond dissociation.
88///
89/// - **Formula**: $$ E = D_e [ e^{-\alpha(R - R_0)} - 1 ]^2 $$
90/// - **Derivative Factor (`diff`)**: $$ D = \frac{2 \alpha D_e e^{-\alpha(R - R_0)} \left( e^{-\alpha(R - R_0)} - 1 \right)}{R} $$
91///
92/// # Parameters
93///
94/// - `de`: Dissociation energy $D_e$.
95/// - `r0`: Equilibrium distance $R_0$.
96/// - `alpha`: Stiffness parameter $\alpha$.
97///
98/// # Inputs
99///
100/// - `r_sq`: Squared distance $r^2$ between two atoms.
101///
102/// # Implementation Notes
103///
104/// - Requires `sqrt` and `exp`.
105/// - More computationally expensive than Harmonic.
106#[derive(Clone, Copy, Debug, Default)]
107pub struct Morse;
108
109impl<T: Real> PairKernel<T> for Morse {
110    type Params = (T, T, T);
111
112    /// Computes only the potential energy.
113    ///
114    /// # Formula
115    ///
116    /// $$ E = D_e [ e^{-\alpha(R - R_0)} - 1 ]^2 $$
117    #[inline(always)]
118    fn energy(r_sq: T, (de, r0, alpha): Self::Params) -> T {
119        let r = r_sq.sqrt();
120        let t_val = T::exp(-alpha * (r - r0));
121        let term = t_val - T::from(1.0);
122
123        de * term * term
124    }
125
126    /// Computes only the force pre-factor $D$.
127    ///
128    /// # Formula
129    ///
130    /// $$ D = \frac{2 \alpha D_e e^{-\alpha(R - R_0)} \left( e^{-\alpha(R - R_0)} - 1 \right)}{R} $$
131    ///
132    /// This factor is defined such that the force vector can be computed
133    /// by a single vector multiplication: $\vec{F} = -D \cdot \vec{r}$.
134    #[inline(always)]
135    fn diff(r_sq: T, (de, r0, alpha): Self::Params) -> T {
136        let inv_r = r_sq.rsqrt();
137        let r = r_sq * inv_r;
138
139        let t_val = T::exp(-alpha * (r - r0));
140        let term_minus_one = t_val - T::from(1.0);
141
142        let f_mag = T::from(2.0) * alpha * de * t_val * term_minus_one;
143
144        f_mag * inv_r
145    }
146
147    /// Computes both energy and force pre-factor efficiently.
148    ///
149    /// This method reuses intermediate calculations to minimize operations.
150    #[inline(always)]
151    fn compute(r_sq: T, (de, r0, alpha): Self::Params) -> EnergyDiff<T> {
152        let inv_r = r_sq.rsqrt();
153        let r = r_sq * inv_r;
154
155        let t_val = T::exp(-alpha * (r - r0));
156        let term_minus_one = t_val - T::from(1.0);
157
158        let energy = de * term_minus_one * term_minus_one;
159
160        let f_mag = T::from(2.0) * alpha * de * t_val * term_minus_one;
161        let diff = f_mag * inv_r;
162
163        EnergyDiff { energy, diff }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use approx::assert_relative_eq;
171
172    // ------------------------------------------------------------------------
173    // Test Constants
174    // ------------------------------------------------------------------------
175
176    const H: f64 = 1e-6;
177    const TOL_DIFF: f64 = 1e-4;
178
179    // ========================================================================
180    // Harmonic Tests
181    // ========================================================================
182
183    mod harmonic {
184        use super::*;
185
186        const K_HALF: f64 = 350.0; // K/2 = 350, so K = 700 kcal/(mol·Å²)
187        const R0: f64 = 1.5; // Equilibrium distance Å
188
189        fn params() -> (f64, f64) {
190            (K_HALF, R0)
191        }
192
193        // --------------------------------------------------------------------
194        // 1. Sanity Checks
195        // --------------------------------------------------------------------
196
197        #[test]
198        fn sanity_compute_equals_separate() {
199            let r_sq = 2.25_f64;
200            let p = params();
201
202            let result = Harmonic::compute(r_sq, p);
203            let energy_only = Harmonic::energy(r_sq, p);
204            let diff_only = Harmonic::diff(r_sq, p);
205
206            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
207            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
208        }
209
210        #[test]
211        fn sanity_f32_f64_consistency() {
212            let r_sq = 3.0;
213            let p64 = params();
214            let p32 = (K_HALF as f32, R0 as f32);
215
216            let e64 = Harmonic::energy(r_sq, p64);
217            let e32 = Harmonic::energy(r_sq as f32, p32);
218
219            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-4);
220        }
221
222        #[test]
223        fn sanity_equilibrium() {
224            let r0_sq = R0 * R0;
225            let result = Harmonic::compute(r0_sq, params());
226
227            assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
228            assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
229        }
230
231        // --------------------------------------------------------------------
232        // 2. Numerical Stability
233        // --------------------------------------------------------------------
234
235        #[test]
236        fn stability_stretched() {
237            let r = 3.0;
238            let result = Harmonic::compute(r * r, params());
239
240            assert!(result.energy.is_finite());
241            assert!(result.diff.is_finite());
242            assert!(result.energy > 0.0);
243        }
244
245        #[test]
246        fn stability_compressed() {
247            let r = 0.5;
248            let result = Harmonic::compute(r * r, params());
249
250            assert!(result.energy.is_finite());
251            assert!(result.diff.is_finite());
252            assert!(result.energy > 0.0);
253        }
254
255        // --------------------------------------------------------------------
256        // 3. Finite Difference Verification
257        // --------------------------------------------------------------------
258
259        fn finite_diff_check(r: f64) {
260            let p = params();
261            let r_sq = r * r;
262
263            let r_plus = r + H;
264            let r_minus = r - H;
265            let e_plus = Harmonic::energy(r_plus * r_plus, p);
266            let e_minus = Harmonic::energy(r_minus * r_minus, p);
267            let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
268
269            let d = Harmonic::diff(r_sq, p);
270            let de_dr_analytic = -d * r;
271
272            assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
273        }
274
275        #[test]
276        fn finite_diff_stretched() {
277            finite_diff_check(2.0);
278        }
279
280        #[test]
281        fn finite_diff_compressed() {
282            finite_diff_check(1.0);
283        }
284
285        #[test]
286        fn finite_diff_near_equilibrium() {
287            finite_diff_check(R0 + 0.01);
288        }
289
290        // --------------------------------------------------------------------
291        // 4. Harmonic Specific
292        // --------------------------------------------------------------------
293
294        #[test]
295        fn specific_quadratic_scaling() {
296            let delta1 = 0.1;
297            let delta2 = 0.2;
298            let e1 = Harmonic::energy((R0 + delta1).powi(2), params());
299            let e2 = Harmonic::energy((R0 + delta2).powi(2), params());
300
301            assert_relative_eq!(e2 / e1, 4.0, epsilon = 1e-10);
302        }
303
304        #[test]
305        fn specific_force_restoring() {
306            let d_stretched = Harmonic::diff((R0 + 0.5).powi(2), params());
307            let d_compressed = Harmonic::diff((R0 - 0.5).powi(2), params());
308
309            assert!(d_stretched < 0.0);
310            assert!(d_compressed > 0.0);
311        }
312    }
313
314    // ========================================================================
315    // Morse Tests
316    // ========================================================================
317
318    mod morse {
319        use super::*;
320
321        const DE: f64 = 70.0; // Dissociation energy kcal/mol
322        const R0: f64 = 1.5; // Equilibrium distance Å
323        const ALPHA: f64 = 2.0; // Stiffness parameter Å⁻¹
324
325        fn params() -> (f64, f64, f64) {
326            (DE, R0, ALPHA)
327        }
328
329        // --------------------------------------------------------------------
330        // 1. Sanity Checks
331        // --------------------------------------------------------------------
332
333        #[test]
334        fn sanity_compute_equals_separate() {
335            let r_sq = 2.0_f64;
336            let p = params();
337
338            let result = Morse::compute(r_sq, p);
339            let energy_only = Morse::energy(r_sq, p);
340            let diff_only = Morse::diff(r_sq, p);
341
342            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
343            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
344        }
345
346        #[test]
347        fn sanity_f32_f64_consistency() {
348            let r_sq = 3.0;
349            let p64 = params();
350            let p32 = (DE as f32, R0 as f32, ALPHA as f32);
351
352            let e64 = Morse::energy(r_sq, p64);
353            let e32 = Morse::energy(r_sq as f32, p32);
354
355            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-4);
356        }
357
358        #[test]
359        fn sanity_equilibrium() {
360            let r0_sq = R0 * R0;
361            let result = Morse::compute(r0_sq, params());
362
363            assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
364            assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
365        }
366
367        // --------------------------------------------------------------------
368        // 2. Numerical Stability
369        // --------------------------------------------------------------------
370
371        #[test]
372        fn stability_large_stretch() {
373            let r = 10.0;
374            let result = Morse::compute(r * r, params());
375
376            assert!(result.energy.is_finite());
377            assert!(result.diff.is_finite());
378            assert_relative_eq!(result.energy, DE, epsilon = 1e-3);
379        }
380
381        #[test]
382        fn stability_compressed() {
383            let r = 0.5;
384            let result = Morse::compute(r * r, params());
385
386            assert!(result.energy.is_finite());
387            assert!(result.diff.is_finite());
388            assert!(result.energy > DE);
389        }
390
391        // --------------------------------------------------------------------
392        // 3. Finite Difference Verification
393        // --------------------------------------------------------------------
394
395        fn finite_diff_check(r: f64) {
396            let p = params();
397            let r_sq = r * r;
398
399            let r_plus = r + H;
400            let r_minus = r - H;
401            let e_plus = Morse::energy(r_plus * r_plus, p);
402            let e_minus = Morse::energy(r_minus * r_minus, p);
403            let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
404
405            let d = Morse::diff(r_sq, p);
406            let de_dr_analytic = -d * r;
407
408            assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
409        }
410
411        #[test]
412        fn finite_diff_stretched() {
413            finite_diff_check(2.5);
414        }
415
416        #[test]
417        fn finite_diff_compressed() {
418            finite_diff_check(1.0);
419        }
420
421        #[test]
422        fn finite_diff_near_equilibrium() {
423            finite_diff_check(R0 + 0.01);
424        }
425
426        // --------------------------------------------------------------------
427        // 4. Morse Specific
428        // --------------------------------------------------------------------
429
430        #[test]
431        fn specific_dissociation_limit() {
432            let e_far = Morse::energy(100.0_f64.powi(2), params());
433            assert_relative_eq!(e_far, DE, epsilon = 1e-6);
434        }
435
436        #[test]
437        fn specific_anharmonic_asymmetry() {
438            let delta = 0.3;
439            let e_stretch = Morse::energy((R0 + delta).powi(2), params());
440            let e_compress = Morse::energy((R0 - delta).powi(2), params());
441
442            assert!(e_compress > e_stretch);
443        }
444
445        #[test]
446        fn specific_force_restoring() {
447            let d_stretched = Morse::diff((R0 + 0.5).powi(2), params());
448            let d_compressed = Morse::diff((R0 - 0.3).powi(2), params());
449
450            assert!(d_stretched < 0.0);
451            assert!(d_compressed > 0.0);
452        }
453    }
454}