potentials/pair/
gauss.rs

1//! # Gaussian Potential
2//!
3//! A soft Gaussian potential used in coarse-grained models and GEM
4//! (Gaussian Electrostatic Model) force fields.
5//!
6//! ## Formula
7//!
8//! ```text
9//! V(r) = A * exp(-B * r^2)
10//! ```
11//!
12//! where:
13//! - `A`: Amplitude (energy units)
14//! - `B`: Gaussian width parameter (1/length^2 units)
15//!
16//! ## Force Factor
17//!
18//! ```text
19//! S = -(dV/dr) / r = 2 * A * B * exp(-B * r^2)
20//! ```
21//!
22//! ## Implementation Notes
23//!
24//! - Stores `-B` internally for efficient `exp(-B*r^2)` computation
25//! - Bounded: V(0) = A, V(∞) = 0
26//! - Smooth: Infinitely differentiable
27//! - Purely repulsive (if A > 0) or attractive (if A < 0)
28
29use crate::base::Potential2;
30use crate::math::Vector;
31
32/// Gaussian potential.
33///
34/// ## Parameters
35///
36/// - `a`: Amplitude (energy units)
37/// - `b`: Width parameter (1/length^2 units)
38///
39/// ## Precomputed Values
40///
41/// - `neg_b`: Stores `-b` internally for efficient `exp(-b*r^2)` computation
42#[derive(Clone, Copy, Debug, PartialEq)]
43pub struct Gauss<T> {
44    /// Amplitude
45    a: T,
46    /// Width parameter (stored negative for efficiency)
47    neg_b: T,
48}
49
50impl<T: Vector> Gauss<T> {
51    /// Creates a new Gaussian potential.
52    ///
53    /// ## Arguments
54    ///
55    /// - `a`: Amplitude (energy units)
56    /// - `b`: Width parameter (1/length^2 units)
57    ///
58    /// ## Example
59    ///
60    /// ```
61    /// use potentials::pair::Gauss;
62    ///
63    /// // Repulsive Gaussian
64    /// let gauss = Gauss::<f64>::new(1.0, 0.5);
65    /// ```
66    #[inline]
67    pub fn new(a: f64, b: f64) -> Self {
68        Self {
69            a: T::splat(a),
70            neg_b: T::splat(-b),
71        }
72    }
73
74    /// Creates a Gaussian potential from sigma parameterization.
75    ///
76    /// ```text
77    /// V(r) = A * exp(-r^2 / (2 * sigma^2))
78    /// ```
79    ///
80    /// ## Arguments
81    ///
82    /// - `a`: Amplitude
83    /// - `sigma`: Gaussian width
84    #[inline]
85    pub fn from_sigma(a: f64, sigma: f64) -> Self {
86        let b = 1.0 / (2.0 * sigma * sigma);
87        Self::new(a, b)
88    }
89
90    /// Returns the amplitude.
91    #[inline]
92    pub fn amplitude(&self) -> T {
93        self.a
94    }
95}
96
97impl<T: Vector> Potential2<T> for Gauss<T> {
98    /// Computes the potential energy.
99    ///
100    /// ```text
101    /// V(r) = A * exp(-B * r^2)
102    /// ```
103    #[inline(always)]
104    fn energy(&self, r_sq: T) -> T {
105        // exp(-B * r^2) = exp(neg_b * r_sq)
106        let exp_term = (self.neg_b * r_sq).exp();
107        self.a * exp_term
108    }
109
110    /// Computes the force factor.
111    ///
112    /// ```text
113    /// dV/dr = A * exp(-B*r^2) * (-2*B*r)
114    /// S = -(dV/dr)/r = 2*A*B * exp(-B*r^2)
115    /// ```
116    #[inline(always)]
117    fn force_factor(&self, r_sq: T) -> T {
118        let exp_term = (self.neg_b * r_sq).exp();
119
120        // S = -2 * neg_b * A * exp_term (since neg_b = -B)
121        let two = T::splat(2.0);
122        T::zero() - two * self.neg_b * self.a * exp_term
123    }
124
125    /// Computes energy and force factor together (optimized).
126    ///
127    /// Shares the computation of `exp_term` and `a_exp`.
128    #[inline(always)]
129    fn energy_force(&self, r_sq: T) -> (T, T) {
130        let exp_term = (self.neg_b * r_sq).exp();
131        let a_exp = self.a * exp_term;
132
133        let energy = a_exp;
134
135        let two = T::splat(2.0);
136        let force = T::zero() - two * self.neg_b * a_exp;
137
138        (energy, force)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use approx::assert_relative_eq;
146
147    #[test]
148    fn test_gauss_at_zero() {
149        let gauss: Gauss<f64> = Gauss::new(5.0, 1.0);
150        let energy = gauss.energy(0.0);
151        assert_relative_eq!(energy, 5.0, epsilon = 1e-10);
152    }
153
154    #[test]
155    fn test_gauss_decay() {
156        let a = 1.0;
157        let b = 0.5;
158        let gauss: Gauss<f64> = Gauss::new(a, b);
159
160        let r = 2.0;
161        let r_sq = r * r;
162        let energy = gauss.energy(r_sq);
163        let expected = a * (-b * r_sq).exp();
164
165        assert_relative_eq!(energy, expected, epsilon = 1e-10);
166    }
167
168    #[test]
169    fn test_gauss_force_at_zero() {
170        let a = 3.0;
171        let b = 0.5;
172        let gauss: Gauss<f64> = Gauss::new(a, b);
173
174        let force = gauss.force_factor(1e-20);
175        let expected = 2.0 * a * b;
176
177        assert_relative_eq!(force, expected, epsilon = 1e-10);
178    }
179
180    #[test]
181    fn test_gauss_from_sigma() {
182        let a = 2.0;
183        let sigma = 1.5;
184        let g1: Gauss<f64> = Gauss::from_sigma(a, sigma);
185        let g2: Gauss<f64> = Gauss::new(a, 1.0 / (2.0 * sigma * sigma));
186
187        let r_sq = 3.0;
188        assert_relative_eq!(g1.energy(r_sq), g2.energy(r_sq), epsilon = 1e-10);
189    }
190
191    #[test]
192    fn test_gauss_energy_force_consistency() {
193        let gauss: Gauss<f64> = Gauss::new(10.0, 0.25);
194        let r_sq = 2.25;
195
196        let (e1, f1) = gauss.energy_force(r_sq);
197        let e2 = gauss.energy(r_sq);
198        let f2 = gauss.force_factor(r_sq);
199
200        assert_relative_eq!(e1, e2, epsilon = 1e-10);
201        assert_relative_eq!(f1, f2, epsilon = 1e-10);
202    }
203
204    #[test]
205    fn test_gauss_numerical_derivative() {
206        let gauss: Gauss<f64> = Gauss::new(10.0, 0.25);
207        let r = 1.5;
208        let r_sq = r * r;
209
210        let h = 1e-6;
211        let v_plus = gauss.energy((r + h) * (r + h));
212        let v_minus = gauss.energy((r - h) * (r - h));
213        let dv_dr_numerical = (v_plus - v_minus) / (2.0 * h);
214
215        let s_numerical = -dv_dr_numerical / r;
216        let s_analytical = gauss.force_factor(r_sq);
217
218        assert_relative_eq!(s_analytical, s_numerical, epsilon = 1e-6);
219    }
220}