dreid_kernel/potentials/nonbonded/
coulomb.rs

1use crate::math::Real;
2use crate::traits::PairKernel;
3use crate::types::EnergyDiff;
4
5/// Standard Coulomb potential implementation 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`: The 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    // Typical charge product: Q_eff = 332.0637 * q_i * q_j / epsilon
89    // For testing: Q_eff = 1.0 (simplified)
90    const Q_EFF: f64 = 1.0;
91
92    // ------------------------------------------------------------------------
93    // 1. Sanity Checks
94    // ------------------------------------------------------------------------
95
96    #[test]
97    fn sanity_compute_equals_separate() {
98        let r_sq = 4.0_f64;
99        let params = Q_EFF;
100
101        let result = Coulomb::compute(r_sq, params);
102        let energy_only = Coulomb::energy(r_sq, params);
103        let diff_only = Coulomb::diff(r_sq, params);
104
105        assert_relative_eq!(result.energy, energy_only, epsilon = 1e-14);
106        assert_relative_eq!(result.diff, diff_only, epsilon = 1e-14);
107    }
108
109    #[test]
110    fn sanity_f32_f64_consistency() {
111        let r_sq_64 = 4.0_f64;
112        let r_sq_32 = 4.0_f32;
113
114        let e64 = Coulomb::energy(r_sq_64, Q_EFF);
115        let e32 = Coulomb::energy(r_sq_32, Q_EFF as f32);
116
117        assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
118    }
119
120    #[test]
121    fn sanity_energy_positive_for_like_charges() {
122        let r_sq = 4.0_f64;
123        let e = Coulomb::energy(r_sq, 1.0);
124        assert!(e > 0.0);
125    }
126
127    #[test]
128    fn sanity_energy_negative_for_unlike_charges() {
129        let r_sq = 4.0_f64;
130        let e: f64 = Coulomb::energy(r_sq, -1.0);
131        assert!(e < 0.0);
132    }
133
134    // ------------------------------------------------------------------------
135    // 2. Numerical Stability
136    // ------------------------------------------------------------------------
137
138    #[test]
139    fn stability_large_distance() {
140        let r_sq = 1e10_f64;
141        let result = Coulomb::compute(r_sq, Q_EFF);
142
143        assert!(result.energy.is_finite());
144        assert!(result.diff.is_finite());
145        assert!(result.energy.abs() < 1e-4);
146        assert!(result.diff.abs() < 1e-14);
147    }
148
149    #[test]
150    fn stability_small_distance() {
151        let r_sq = 1e-6_f64;
152        let result = Coulomb::compute(r_sq, Q_EFF);
153
154        assert!(result.energy.is_finite());
155        assert!(result.diff.is_finite());
156        assert!(result.energy > 0.0);
157    }
158
159    // ------------------------------------------------------------------------
160    // 3. Finite Difference Verification
161    // ------------------------------------------------------------------------
162
163    fn finite_diff_check(r: f64, params: f64) {
164        let r_sq = r * r;
165
166        let r_plus = r + H;
167        let r_minus = r - H;
168        let e_plus = Coulomb::energy(r_plus * r_plus, params);
169        let e_minus = Coulomb::energy(r_minus * r_minus, params);
170        let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
171
172        let d_analytic = Coulomb::diff(r_sq, params);
173        let de_dr_analytic = -d_analytic * r;
174
175        assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
176    }
177
178    #[test]
179    fn finite_diff_short_range() {
180        finite_diff_check(1.0, Q_EFF);
181    }
182
183    #[test]
184    fn finite_diff_medium_range() {
185        finite_diff_check(3.0, Q_EFF);
186    }
187
188    #[test]
189    fn finite_diff_long_range() {
190        finite_diff_check(10.0, Q_EFF);
191    }
192
193    #[test]
194    fn finite_diff_negative_charge() {
195        finite_diff_check(2.5, -Q_EFF);
196    }
197
198    // ------------------------------------------------------------------------
199    // 4. Coulomb-Specific: Scaling Laws
200    // ------------------------------------------------------------------------
201
202    #[test]
203    fn specific_inverse_r_scaling() {
204        let e1 = Coulomb::energy(1.0, Q_EFF);
205        let e2 = Coulomb::energy(4.0, Q_EFF);
206
207        assert_relative_eq!(e1 / e2, 2.0, epsilon = 1e-10);
208    }
209
210    #[test]
211    fn specific_inverse_r3_scaling_for_diff() {
212        let d1 = Coulomb::diff(1.0, Q_EFF);
213        let d2 = Coulomb::diff(4.0, Q_EFF);
214
215        assert_relative_eq!(d1 / d2, 8.0, epsilon = 1e-10);
216    }
217}