Skip to main content

dreid_kernel/potentials/nonbonded/
vdw.rs

1use crate::math::Real;
2use crate::traits::PairKernel;
3use crate::types::EnergyDiff;
4
5/// Lennard-Jones 12-6 potential for Van der Waals interactions.
6///
7/// # Physics
8///
9/// Models the short-range repulsion and long-range attraction between non-bonded atoms.
10///
11/// - **Formula**: $$ E = D_0 \left[ \left( \frac{R_0}{r} \right)^{12} - 2 \left( \frac{R_0}{r} \right)^6 \right] $$
12/// - **Derivative Factor (`diff`)**: $$ D = -\frac{1}{r} \frac{dE}{dr} = \frac{12 D_0}{r^2} \left[ \left( \frac{R_0}{r} \right)^{12} - \left( \frac{R_0}{r} \right)^6 \right] $$
13///
14/// # Parameters
15///
16/// - `d0`: The energy well depth $D_0$.
17/// - `r0_sq`: The squared equilibrium distance $R_0^2$.
18///
19/// # Inputs
20///
21/// - `r_sq`: Squared distance $r^2$ between two atoms.
22///
23/// # Implementation Notes
24///
25/// - This implementation avoids `sqrt` entirely by operating on squared distances.
26/// - The power chain `s -> s3 -> s6` is used for efficient calculation of $r^{-6}$ and $r^{-12}$ terms.
27/// - All intermediate calculations are shared between energy and force computations.
28/// - Branchless and panic-free.
29#[derive(Clone, Copy, Debug, Default)]
30pub struct LennardJones;
31
32impl<T: Real> PairKernel<T> for LennardJones {
33    type Params = (T, T);
34
35    /// Computes only the potential energy.
36    ///
37    /// # Formula
38    ///
39    /// $$ E = D_0 (s^6 - 2s^3), \quad \text{where } s = (R_0/r)^2 $$
40    #[inline(always)]
41    fn energy(r_sq: T, (d0, r0_sq): Self::Params) -> T {
42        let s = r0_sq * r_sq.recip();
43        let s3 = s * s * s;
44        let s6 = s3 * s3;
45
46        d0 * (s6 - T::from(2.0) * s3)
47    }
48
49    /// Computes only the force pre-factor $D$.
50    ///
51    /// # Formula
52    ///
53    /// $$ D = \frac{12 D_0}{r^2} (s^6 - s^3), \quad \text{where } s = (R_0/r)^2 $$
54    ///
55    /// This factor is defined such that the force vector can be computed
56    /// by a single vector multiplication: $\vec{F} = -D \cdot \vec{r}$.
57    #[inline(always)]
58    fn diff(r_sq: T, (d0, r0_sq): Self::Params) -> T {
59        let inv_r2 = r_sq.recip();
60        let s = r0_sq * inv_r2;
61        let s3 = s * s * s;
62        let s6 = s3 * s3;
63
64        T::from(12.0) * d0 * inv_r2 * (s6 - s3)
65    }
66
67    /// Computes both energy and force pre-factor efficiently.
68    ///
69    /// This method reuses intermediate calculations to minimize operations.
70    #[inline(always)]
71    fn compute(r_sq: T, (d0, r0_sq): Self::Params) -> EnergyDiff<T> {
72        let inv_r2 = r_sq.recip();
73        let s = r0_sq * inv_r2;
74        let s3 = s * s * s;
75        let s6 = s3 * s3;
76
77        let energy = d0 * (s6 - T::from(2.0) * s3);
78
79        let diff = T::from(12.0) * d0 * inv_r2 * (s6 - s3);
80
81        EnergyDiff { energy, diff }
82    }
83}
84
85/// Buckingham (Exponential-6) potential for Van der Waals interactions.
86///
87/// # Physics
88///
89/// Models short-range repulsion exponentially and long-range attraction with an $r^{-6}$ term,
90/// providing a more physically accurate repulsion wall than Lennard-Jones.
91///
92/// - **Formula**: $$ E = D_0 \left[ \frac{6}{\zeta-6} \exp\left(\zeta(1 - \frac{r}{R_0})\right) - \frac{\zeta}{\zeta-6} \left(\frac{R_0}{r}\right)^6 \right] $$
93/// - **Derivative Factor (`diff`)**: $$ D = -\frac{1}{r} \frac{dE}{dr} = \frac{6\zeta D_0}{r(\zeta-6)R_0} \left[ \exp\left(\zeta(1 - \frac{r}{R_0})\right) - \left(\frac{R_0}{r}\right)^7 \right] $$
94///
95/// # Parameters
96///
97/// For computational efficiency, the physical parameters ($D_0, R_0, \zeta$) are pre-computed
98/// into the standard Buckingham form ($A, B, C$):
99/// - `a`: The repulsion pre-factor $A = \frac{6 D_0}{\zeta-6} e^{\zeta}$.
100/// - `b`: The repulsion decay constant $B = \zeta / R_0$.
101/// - `c`: The attraction pre-factor $C = \frac{\zeta D_0 R_0^6}{\zeta-6}$.
102/// - `r_max_sq`: The squared distance of the local energy maximum $r_{\text{max}}^2$.
103/// - `two_e_max`: Twice the energy at the local maximum, $2 E(r_{\text{max}})$.
104///
105/// # Inputs
106///
107/// - `r_sq`: Squared distance $r^2$ between two atoms.
108///
109/// # Implementation Notes
110///
111/// - The kernel operates on the computationally efficient $A, B, C$ form.
112/// - For $r < r_{\text{max}}$, the energy is reflected about the local maximum:
113///   $E_{\text{ref}}(r) = 2 E_{\text{max}} - E(r)$. This produces a repulsive wall
114///   that diverges to $+\infty$ as $r \to 0$ via the $C/r^6$ attraction term,
115///   while maintaining $C^1$ continuity at $r_{\text{max}}$ (where $E'(r_{\text{max}}) = 0$).
116/// - A branchless sign-flip replaces the traditional constant penalty, providing
117///   physically correct short-range behavior at zero additional runtime cost.
118/// - Requires one `sqrt` and one `exp` call, making it computationally more demanding than LJ.
119/// - Power chain `inv_r2 -> inv_r6 -> inv_r8` is used for the attractive term.
120#[derive(Clone, Copy, Debug, Default)]
121pub struct Buckingham;
122
123impl<T: Real> PairKernel<T> for Buckingham {
124    type Params = (T, T, T, T, T);
125
126    /// Computes only the potential energy.
127    ///
128    /// # Formula
129    ///
130    /// $$ E(r) = \begin{cases} A e^{-Br} - C/r^6 & r \ge r_{\text{max}} \\ 2 E_{\text{max}} - (A e^{-Br} - C/r^6) & r < r_{\text{max}} \end{cases} $$
131    #[inline(always)]
132    fn energy(r_sq: T, (a, b, c, r_max_sq, two_e_max): Self::Params) -> T {
133        let mask = T::from((r_sq >= r_max_sq) as u8 as f32);
134        let sign = mask + mask - T::from(1.0);
135
136        let r = r_sq.sqrt();
137        let inv_r2 = r_sq.recip();
138        let inv_r6 = inv_r2 * inv_r2 * inv_r2;
139
140        let e_buck = a * T::exp(-b * r) - c * inv_r6;
141
142        sign * e_buck + (T::from(1.0) - mask) * two_e_max
143    }
144
145    /// Computes only the force pre-factor $D$.
146    ///
147    /// # Formula
148    ///
149    /// $$ D(r) = \text{sign}(r) \left( \frac{A B e^{-Br}}{r} - \frac{6C}{r^8} \right) $$
150    ///
151    /// where $\text{sign}(r) = +1$ for $r \ge r_{\text{max}}$ and $-1$ otherwise.
152    /// At the maximum, $D(r_{\text{max}}) = 0$ from both sides, ensuring $C^1$ continuity.
153    ///
154    /// This factor is defined such that the force vector can be computed
155    /// by a single vector multiplication: $\vec{F} = -D \cdot \vec{r}$.
156    #[inline(always)]
157    fn diff(r_sq: T, (a, b, c, r_max_sq, _two_e_max): Self::Params) -> T {
158        let mask = T::from((r_sq >= r_max_sq) as u8 as f32);
159        let sign = mask + mask - T::from(1.0);
160
161        let inv_r = r_sq.rsqrt();
162        let r = r_sq * inv_r;
163        let inv_r2 = inv_r * inv_r;
164        let inv_r4 = inv_r2 * inv_r2;
165        let inv_r8 = inv_r4 * inv_r4;
166
167        let exp_term = T::exp(-b * r);
168        let d_buck = a * b * exp_term * inv_r - T::from(6.0) * c * inv_r8;
169
170        sign * d_buck
171    }
172
173    /// Computes both energy and force pre-factor efficiently.
174    ///
175    /// This method reuses intermediate calculations to minimize operations.
176    #[inline(always)]
177    fn compute(r_sq: T, (a, b, c, r_max_sq, two_e_max): Self::Params) -> EnergyDiff<T> {
178        let mask = T::from((r_sq >= r_max_sq) as u8 as f32);
179        let sign = mask + mask - T::from(1.0);
180
181        let inv_r = r_sq.rsqrt();
182        let r = r_sq * inv_r;
183        let inv_r2 = inv_r * inv_r;
184        let inv_r4 = inv_r2 * inv_r2;
185        let inv_r6 = inv_r4 * inv_r2;
186        let inv_r8 = inv_r6 * inv_r2;
187
188        let exp_term = T::exp(-b * r);
189
190        let e_buck = a * exp_term - c * inv_r6;
191        let d_buck = a * b * exp_term * inv_r - T::from(6.0) * c * inv_r8;
192
193        EnergyDiff {
194            energy: sign * e_buck + (T::from(1.0) - mask) * two_e_max,
195            diff: sign * d_buck,
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use approx::assert_relative_eq;
204
205    // ------------------------------------------------------------------------
206    // Test Constants
207    // ------------------------------------------------------------------------
208
209    const H: f64 = 1e-6;
210    const TOL_DIFF: f64 = 1e-4;
211
212    // Typical LJ parameters: D0 = 0.1 kcal/mol, R0 = 3.5 Å
213    const D0: f64 = 0.1;
214    const R0: f64 = 3.5;
215    const R0_SQ: f64 = R0 * R0;
216
217    // Buckingham parameters: A, B, C
218    const BUCK_A: f64 = 1000.0;
219    const BUCK_B: f64 = 3.0;
220    const BUCK_C: f64 = 50.0;
221
222    // ========================================================================
223    // Lennard-Jones Tests
224    // ========================================================================
225
226    mod lennard_jones {
227        use super::*;
228
229        // --------------------------------------------------------------------
230        // 1. Sanity Checks
231        // --------------------------------------------------------------------
232
233        #[test]
234        fn sanity_compute_equals_separate() {
235            let r_sq = 9.0_f64;
236            let params = (D0, R0_SQ);
237
238            let result = LennardJones::compute(r_sq, params);
239            let energy_only = LennardJones::energy(r_sq, params);
240            let diff_only = LennardJones::diff(r_sq, params);
241
242            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-14);
243            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-14);
244        }
245
246        #[test]
247        fn sanity_f32_f64_consistency() {
248            let r_sq_64 = 12.25_f64;
249            let r_sq_32 = 12.25_f32;
250            let params_64 = (D0, R0_SQ);
251            let params_32 = (D0 as f32, R0_SQ as f32);
252
253            let e64 = LennardJones::energy(r_sq_64, params_64);
254            let e32 = LennardJones::energy(r_sq_32, params_32);
255
256            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
257        }
258
259        #[test]
260        fn sanity_equilibrium_energy_minimum() {
261            let e = LennardJones::energy(R0_SQ, (D0, R0_SQ));
262            assert_relative_eq!(e, -D0, epsilon = 1e-10);
263        }
264
265        #[test]
266        fn sanity_equilibrium_zero_force() {
267            let d = LennardJones::diff(R0_SQ, (D0, R0_SQ));
268            assert_relative_eq!(d, 0.0, epsilon = 1e-10);
269        }
270
271        // --------------------------------------------------------------------
272        // 2. Numerical Stability
273        // --------------------------------------------------------------------
274
275        #[test]
276        fn stability_large_distance() {
277            let r_sq = 1e6_f64;
278            let result = LennardJones::compute(r_sq, (D0, R0_SQ));
279
280            assert!(result.energy.is_finite());
281            assert!(result.diff.is_finite());
282            assert!(result.energy.abs() < 1e-10);
283        }
284
285        #[test]
286        fn stability_small_distance() {
287            let r_sq = 1.0_f64;
288            let result = LennardJones::compute(r_sq, (D0, R0_SQ));
289
290            assert!(result.energy.is_finite());
291            assert!(result.diff.is_finite());
292            assert!(result.energy > 0.0);
293        }
294
295        // --------------------------------------------------------------------
296        // 3. Finite Difference Verification
297        // --------------------------------------------------------------------
298
299        fn finite_diff_check(r: f64, params: (f64, f64)) {
300            let r_sq = r * r;
301
302            let r_plus = r + H;
303            let r_minus = r - H;
304            let e_plus = LennardJones::energy(r_plus * r_plus, params);
305            let e_minus = LennardJones::energy(r_minus * r_minus, params);
306            let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
307
308            let d_analytic = LennardJones::diff(r_sq, params);
309            let de_dr_analytic = -d_analytic * r;
310
311            assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
312        }
313
314        #[test]
315        fn finite_diff_repulsion_region() {
316            finite_diff_check(2.5, (D0, R0_SQ));
317        }
318
319        #[test]
320        fn finite_diff_equilibrium_region() {
321            finite_diff_check(R0, (D0, R0_SQ));
322        }
323
324        #[test]
325        fn finite_diff_attraction_region() {
326            finite_diff_check(5.0, (D0, R0_SQ));
327        }
328
329        #[test]
330        fn finite_diff_long_range() {
331            finite_diff_check(10.0, (D0, R0_SQ));
332        }
333
334        // --------------------------------------------------------------------
335        // 4. LJ-Specific: Physical Behavior
336        // --------------------------------------------------------------------
337
338        #[test]
339        fn specific_repulsion_positive_energy() {
340            let e = LennardJones::energy(4.0, (D0, R0_SQ));
341            assert!(e > 0.0);
342        }
343
344        #[test]
345        fn specific_attraction_negative_energy() {
346            let e = LennardJones::energy(25.0, (D0, R0_SQ));
347            assert!(e < 0.0);
348        }
349
350        #[test]
351        fn specific_diff_sign_repulsion() {
352            let d = LennardJones::diff(4.0, (D0, R0_SQ));
353            assert!(d > 0.0);
354        }
355
356        #[test]
357        fn specific_diff_sign_attraction() {
358            let d = LennardJones::diff(25.0, (D0, R0_SQ));
359            assert!(d < 0.0);
360        }
361    }
362
363    // ========================================================================
364    // Buckingham Tests
365    // ========================================================================
366
367    mod buckingham {
368        use super::*;
369
370        /// Computes the local maximum of the Buckingham potential via Newton's method.
371        fn reflection_params(a: f64, b: f64, c: f64) -> (f64, f64) {
372            let mut r = 1.0_f64;
373            for _ in 0..100 {
374                let exp_term = (-b * r).exp();
375                let r7 = r.powi(7);
376                let g = a * b * exp_term * r7 - 6.0 * c;
377                let gp = a * b * exp_term * r.powi(6) * (7.0 - b * r);
378                r -= g / gp;
379            }
380            let e_max = a * (-b * r).exp() - c / r.powi(6);
381            (r * r, 2.0 * e_max)
382        }
383
384        fn params() -> (f64, f64, f64, f64, f64) {
385            let (r_max_sq, two_e_max) = reflection_params(BUCK_A, BUCK_B, BUCK_C);
386            (BUCK_A, BUCK_B, BUCK_C, r_max_sq, two_e_max)
387        }
388
389        // --------------------------------------------------------------------
390        // 1. Sanity Checks
391        // --------------------------------------------------------------------
392
393        #[test]
394        fn sanity_compute_equals_separate() {
395            let r_sq = 4.0_f64;
396            let p = params();
397
398            let result = Buckingham::compute(r_sq, p);
399            let energy_only = Buckingham::energy(r_sq, p);
400            let diff_only = Buckingham::diff(r_sq, p);
401
402            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
403            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
404        }
405
406        #[test]
407        fn sanity_compute_equals_separate_reflected() {
408            let p = params();
409            let r_sq = 0.25_f64;
410
411            let result = Buckingham::compute(r_sq, p);
412            let energy_only = Buckingham::energy(r_sq, p);
413            let diff_only = Buckingham::diff(r_sq, p);
414
415            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
416            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
417        }
418
419        #[test]
420        fn sanity_f32_f64_consistency() {
421            let r_sq = 4.0;
422            let p64 = params();
423            let (r_max_sq_32, two_e_max_32) = reflection_params(BUCK_A, BUCK_B, BUCK_C);
424            let p32 = (
425                BUCK_A as f32,
426                BUCK_B as f32,
427                BUCK_C as f32,
428                r_max_sq_32 as f32,
429                two_e_max_32 as f32,
430            );
431
432            let e64 = Buckingham::energy(r_sq, p64);
433            let e32 = Buckingham::energy(r_sq as f32, p32);
434
435            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-3);
436        }
437
438        // --------------------------------------------------------------------
439        // 2. Numerical Stability
440        // --------------------------------------------------------------------
441
442        #[test]
443        fn stability_reflected_region() {
444            let r_sq = 0.1_f64;
445            let result = Buckingham::compute(r_sq, params());
446
447            assert!(result.energy.is_finite());
448            assert!(result.diff.is_finite());
449            assert!(result.energy > 0.0);
450            assert!(result.diff > 0.0);
451        }
452
453        #[test]
454        fn stability_large_distance() {
455            let r_sq = 1e4_f64;
456            let result = Buckingham::compute(r_sq, params());
457
458            assert!(result.energy.is_finite());
459            assert!(result.diff.is_finite());
460        }
461
462        #[test]
463        fn stability_near_zero() {
464            let r_sq = 1e-20_f64;
465            let p = params();
466            let e = Buckingham::energy(r_sq, p);
467
468            assert!(e.is_finite());
469            assert!(e > 0.0);
470        }
471
472        // --------------------------------------------------------------------
473        // 3. Finite Difference Verification
474        // --------------------------------------------------------------------
475
476        fn finite_diff_check(r: f64) {
477            let p = params();
478            let r_sq = r * r;
479
480            let r_plus = r + H;
481            let r_minus = r - H;
482            let e_plus = Buckingham::energy(r_plus * r_plus, p);
483            let e_minus = Buckingham::energy(r_minus * r_minus, p);
484            let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
485
486            let d_analytic = Buckingham::diff(r_sq, p);
487            let de_dr_analytic = -d_analytic * r;
488
489            assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
490        }
491
492        #[test]
493        fn finite_diff_reflected_region() {
494            finite_diff_check(0.8);
495        }
496
497        #[test]
498        fn finite_diff_normal_short_range() {
499            finite_diff_check(1.5);
500        }
501
502        #[test]
503        fn finite_diff_medium_range() {
504            finite_diff_check(3.0);
505        }
506
507        #[test]
508        fn finite_diff_long_range() {
509            finite_diff_check(8.0);
510        }
511
512        // --------------------------------------------------------------------
513        // 4. Buckingham-Specific: Reflection Properties
514        // --------------------------------------------------------------------
515
516        #[test]
517        fn specific_reflection_diverges() {
518            let p = params();
519            let e_close = Buckingham::energy(0.01, p);
520            let e_far = Buckingham::energy(0.25, p);
521            assert!(e_close > e_far);
522        }
523
524        #[test]
525        fn specific_diff_at_maximum_is_zero() {
526            let p = params();
527            let r_max_sq = p.3;
528            let d = Buckingham::diff(r_max_sq, p);
529            assert_relative_eq!(d, 0.0, epsilon = 1e-6);
530        }
531
532        #[test]
533        fn specific_c1_continuity_at_maximum() {
534            let p = params();
535            let r_max = p.3.sqrt();
536            let eps = 1e-8;
537
538            let r_inside = r_max - eps;
539            let r_outside = r_max + eps;
540
541            let d_inside = Buckingham::diff(r_inside * r_inside, p);
542            let d_outside = Buckingham::diff(r_outside * r_outside, p);
543
544            let de_dr_inside = -d_inside * r_inside;
545            let de_dr_outside = -d_outside * r_outside;
546
547            assert_relative_eq!(de_dr_inside, de_dr_outside, epsilon = 1e-3);
548        }
549
550        #[test]
551        fn specific_energy_continuity_at_maximum() {
552            let p = params();
553            let r_max = p.3.sqrt();
554            let eps = 1e-8;
555
556            let e_inside = Buckingham::energy((r_max - eps).powi(2), p);
557            let e_outside = Buckingham::energy((r_max + eps).powi(2), p);
558
559            assert_relative_eq!(e_inside, e_outside, epsilon = 1e-4);
560        }
561
562        #[test]
563        fn specific_finite_diff_across_boundary() {
564            let p = params();
565            let r_max = p.3.sqrt();
566
567            let h = 1e-6;
568
569            let r_out = r_max + 0.01;
570            let e_p = Buckingham::energy((r_out + h).powi(2), p);
571            let e_m = Buckingham::energy((r_out - h).powi(2), p);
572            let de_dr_num_out = (e_p - e_m) / (2.0 * h);
573            let de_dr_ana_out = -Buckingham::diff(r_out * r_out, p) * r_out;
574            assert_relative_eq!(de_dr_num_out, de_dr_ana_out, epsilon = TOL_DIFF);
575
576            let r_in = r_max - 0.01;
577            let e_p = Buckingham::energy((r_in + h).powi(2), p);
578            let e_m = Buckingham::energy((r_in - h).powi(2), p);
579            let de_dr_num_in = (e_p - e_m) / (2.0 * h);
580            let de_dr_ana_in = -Buckingham::diff(r_in * r_in, p) * r_in;
581            assert_relative_eq!(de_dr_num_in, de_dr_ana_in, epsilon = TOL_DIFF);
582        }
583    }
584}