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