Skip to main content

dreid_kernel/potentials/bonded/
stretch.rs

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