Skip to main content

dreid_kernel/potentials/nonbonded/
coulomb.rs

1use crate::math::Real;
2use crate::traits::PairKernel;
3use crate::types::EnergyDiff;
4
5/// Standard Coulomb potential for electrostatics.
6///
7/// # Physics
8///
9/// Models the electrostatic interaction between two point charges.
10///
11/// - **Formula**: $$ E = \frac{C \cdot q_i q_j}{\epsilon \cdot r} $$
12/// - **Derivative Factor (`diff`)**: $$ D = -\frac{1}{r} \frac{dE}{dr} = \frac{E}{r^2} = \frac{C \cdot q_i q_j}{\epsilon \cdot r^3} $$
13///
14/// # Parameters
15///
16/// - `q_product`: Effective charge product $Q_{eff} = \frac{C \cdot q_i q_j}{\epsilon}$.
17///
18/// # Inputs
19///
20/// - `r_sq`: Squared distance $r^2$ between two atoms.
21///
22/// # Implementation Notes
23///
24/// - Optimized to minimize arithmetic operations by reusing intermediate `inv_r` and `inv_r2`.
25/// - Utilizes `rsqrt` for potential hardware acceleration.
26/// - Branchless and panic-free.
27#[derive(Clone, Copy, Debug, Default)]
28pub struct Coulomb;
29
30impl<T: Real> PairKernel<T> for Coulomb {
31    type Params = T;
32
33    /// Computes only the potential energy.
34    ///
35    /// # Formula
36    ///
37    /// $$ E = \frac{Q_{eff}}{r} $$
38    #[inline(always)]
39    fn energy(r_sq: T, q_product: Self::Params) -> T {
40        q_product * r_sq.rsqrt()
41    }
42
43    /// Computes only the force pre-factor $D$.
44    ///
45    /// # Formula
46    ///
47    /// $$ D = \frac{Q_{eff}}{r^3} $$
48    ///
49    /// This factor is defined such that the force vector can be computed
50    /// by a single vector multiplication: $\vec{F} = -D \cdot \vec{r}$.
51    #[inline(always)]
52    fn diff(r_sq: T, q_product: Self::Params) -> T {
53        let inv_r = r_sq.rsqrt();
54        let energy = q_product * inv_r;
55
56        let inv_r2 = inv_r * inv_r;
57        energy * inv_r2
58    }
59
60    /// Computes both energy and force pre-factor efficiently.
61    ///
62    /// This method reuses intermediate calculations to minimize operations.
63    #[inline(always)]
64    fn compute(r_sq: T, q_product: Self::Params) -> EnergyDiff<T> {
65        let inv_r = r_sq.rsqrt();
66        let inv_r2 = inv_r * inv_r;
67
68        let energy = q_product * inv_r;
69
70        let diff = energy * inv_r2;
71
72        EnergyDiff { energy, diff }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use approx::assert_relative_eq;
80
81    // ------------------------------------------------------------------------
82    // Test Constants
83    // ------------------------------------------------------------------------
84
85    const H: f64 = 1e-6;
86    const TOL_DIFF: f64 = 1e-5;
87
88    const Q_EFF: f64 = 1.0;
89
90    fn params() -> f64 {
91        Q_EFF
92    }
93
94    // ------------------------------------------------------------------------
95    // 1. Sanity Checks
96    // ------------------------------------------------------------------------
97
98    #[test]
99    fn sanity_compute_equals_separate() {
100        let r_sq = 4.0_f64;
101        let p = params();
102
103        let result = Coulomb::compute(r_sq, p);
104        let energy_only = Coulomb::energy(r_sq, p);
105        let diff_only = Coulomb::diff(r_sq, p);
106
107        assert_relative_eq!(result.energy, energy_only, epsilon = 1e-14);
108        assert_relative_eq!(result.diff, diff_only, epsilon = 1e-14);
109    }
110
111    #[test]
112    fn sanity_f32_f64_consistency() {
113        let r_sq = 4.0;
114        let p64 = params();
115        let p32 = Q_EFF as f32;
116
117        let e64 = Coulomb::energy(r_sq, p64);
118        let e32 = Coulomb::energy(r_sq as f32, p32);
119
120        assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
121    }
122
123    #[test]
124    fn sanity_energy_positive_for_like_charges() {
125        let r_sq = 4.0_f64;
126        let e = Coulomb::energy(r_sq, 1.0);
127        assert!(e > 0.0);
128    }
129
130    #[test]
131    fn sanity_energy_negative_for_unlike_charges() {
132        let r_sq = 4.0_f64;
133        let e: f64 = Coulomb::energy(r_sq, -1.0);
134        assert!(e < 0.0);
135    }
136
137    // ------------------------------------------------------------------------
138    // 2. Numerical Stability
139    // ------------------------------------------------------------------------
140
141    #[test]
142    fn stability_large_distance() {
143        let r_sq = 1e10_f64;
144        let result = Coulomb::compute(r_sq, params());
145
146        assert!(result.energy.is_finite());
147        assert!(result.diff.is_finite());
148        assert!(result.energy.abs() < 1e-4);
149        assert!(result.diff.abs() < 1e-14);
150    }
151
152    #[test]
153    fn stability_small_distance() {
154        let r_sq = 1e-6_f64;
155        let result = Coulomb::compute(r_sq, params());
156
157        assert!(result.energy.is_finite());
158        assert!(result.diff.is_finite());
159        assert!(result.energy > 0.0);
160    }
161
162    // ------------------------------------------------------------------------
163    // 3. Finite Difference Verification
164    // ------------------------------------------------------------------------
165
166    fn finite_diff_check(r: f64, p: f64) {
167        let r_sq = r * r;
168
169        let r_plus = r + H;
170        let r_minus = r - H;
171        let e_plus = Coulomb::energy(r_plus * r_plus, p);
172        let e_minus = Coulomb::energy(r_minus * r_minus, p);
173        let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
174
175        let d_analytic = Coulomb::diff(r_sq, p);
176        let de_dr_analytic = -d_analytic * r;
177
178        assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
179    }
180
181    #[test]
182    fn finite_diff_short_range() {
183        finite_diff_check(1.0, params());
184    }
185
186    #[test]
187    fn finite_diff_medium_range() {
188        finite_diff_check(3.0, params());
189    }
190
191    #[test]
192    fn finite_diff_long_range() {
193        finite_diff_check(10.0, params());
194    }
195
196    #[test]
197    fn finite_diff_negative_charge() {
198        finite_diff_check(2.5, -Q_EFF);
199    }
200
201    // ------------------------------------------------------------------------
202    // 4. Coulomb-Specific: Scaling Laws
203    // ------------------------------------------------------------------------
204
205    #[test]
206    fn specific_inverse_r_scaling() {
207        let e1 = Coulomb::energy(1.0, params());
208        let e2 = Coulomb::energy(4.0, params());
209
210        assert_relative_eq!(e1 / e2, 2.0, epsilon = 1e-10);
211    }
212
213    #[test]
214    fn specific_inverse_r3_scaling_for_diff() {
215        let d1 = Coulomb::diff(1.0, params());
216        let d2 = Coulomb::diff(4.0, params());
217
218        assert_relative_eq!(d1 / d2, 8.0, epsilon = 1e-10);
219    }
220}