potentials/bond/
cubic.rs

1//! # Cubic Bond Potential
2//!
3//! Harmonic potential with a cubic anharmonic correction term.
4//!
5//! ## Formula
6//!
7//! ```text
8//! V(r) = k * (r - r0)^2 + k_cubic * (r - r0)^3
9//! ```
10//!
11//! where:
12//! - `k`: Quadratic force constant (energy/length^2 units)
13//! - `k_cubic`: Cubic correction (energy/length^3 units)
14//! - `r0`: Equilibrium bond length (length units)
15//!
16//! ## Force Factor
17//!
18//! ```text
19//! S = -(dV/dr) / r = -(2*k*(r-r0) + 3*k_cubic*(r-r0)^2) / r
20//! ```
21//!
22//! ## Implementation Notes
23//!
24//! - Asymmetric: stretching and compression have different energies
25//! - `k_cubic < 0`: softer for stretching, stiffer for compression (typical)
26//! - Unbound for large deformations; use only for small displacements
27
28use crate::base::Potential2;
29use crate::math::Vector;
30
31/// Cubic anharmonic bond potential.
32///
33/// ## Parameters
34///
35/// - `k`: Quadratic force constant (energy/length^2 units)
36/// - `k_cubic`: Cubic correction (energy/length^3 units)
37/// - `r0`: Equilibrium distance (length units)
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct Cubic<T> {
40    /// Quadratic force constant
41    k: T,
42    /// Cubic correction
43    k_cubic: T,
44    /// Equilibrium distance
45    r0: T,
46}
47
48impl<T: Vector> Cubic<T> {
49    /// Creates a new cubic bond potential.
50    ///
51    /// ## Arguments
52    ///
53    /// - `k`: Quadratic force constant (energy/length^2)
54    /// - `k_cubic`: Cubic correction (energy/length^3)
55    /// - `r0`: Equilibrium bond length (length units)
56    ///
57    /// ## Example
58    ///
59    /// ```
60    /// use potentials::bond::Cubic;
61    ///
62    /// // Bond with slight anharmonic correction
63    /// let bond = Cubic::<f64>::new(300.0, -50.0, 1.54);
64    /// ```
65    #[inline]
66    pub fn new(k: f64, k_cubic: f64, r0: f64) -> Self {
67        Self {
68            k: T::splat(k),
69            k_cubic: T::splat(k_cubic),
70            r0: T::splat(r0),
71        }
72    }
73
74    /// Returns the quadratic force constant.
75    #[inline]
76    pub fn k(&self) -> T {
77        self.k
78    }
79
80    /// Returns the cubic correction.
81    #[inline]
82    pub fn k_cubic(&self) -> T {
83        self.k_cubic
84    }
85
86    /// Returns the equilibrium distance.
87    #[inline]
88    pub fn r0(&self) -> T {
89        self.r0
90    }
91}
92
93impl<T: Vector> Potential2<T> for Cubic<T> {
94    /// Computes the potential energy.
95    ///
96    /// ```text
97    /// V(r) = k * (r - r0)^2 + k_cubic * (r - r0)^3
98    /// ```
99    #[inline(always)]
100    fn energy(&self, r_sq: T) -> T {
101        let r = r_sq.sqrt();
102        let dr = r - self.r0;
103        let dr_sq = dr * dr;
104        let dr_cube = dr_sq * dr;
105
106        self.k * dr_sq + self.k_cubic * dr_cube
107    }
108
109    /// Computes the force factor.
110    ///
111    /// ```text
112    /// dV/dr = 2*k*(r-r0) + 3*k_cubic*(r-r0)^2
113    /// S = -(dV/dr)/r = -(2*k*(r-r0) + 3*k_cubic*(r-r0)^2) / r
114    /// ```
115    #[inline(always)]
116    fn force_factor(&self, r_sq: T) -> T {
117        let r = r_sq.sqrt();
118        let r_inv = r.recip();
119        let dr = r - self.r0;
120
121        let two = T::splat(2.0);
122        let three = T::splat(3.0);
123
124        let dv_dr = two * self.k * dr + three * self.k_cubic * dr * dr;
125        T::zero() - dv_dr * r_inv
126    }
127
128    /// Computes energy and force factor together (optimized).
129    ///
130    /// Shares the computation of `r`, `r_inv`, `dr`, and `dr_sq`.
131    #[inline(always)]
132    fn energy_force(&self, r_sq: T) -> (T, T) {
133        let r = r_sq.sqrt();
134        let r_inv = r.recip();
135        let dr = r - self.r0;
136        let dr_sq = dr * dr;
137
138        let two = T::splat(2.0);
139        let three = T::splat(3.0);
140
141        let energy = self.k * dr_sq + self.k_cubic * dr_sq * dr;
142        let dv_dr = two * self.k * dr + three * self.k_cubic * dr_sq;
143        let force = T::zero() - dv_dr * r_inv;
144
145        (energy, force)
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use approx::assert_relative_eq;
153
154    #[test]
155    fn test_cubic_at_equilibrium() {
156        let cubic: Cubic<f64> = Cubic::new(300.0, -50.0, 1.5);
157        let r0 = 1.5;
158
159        let energy = cubic.energy(r0 * r0);
160        assert_relative_eq!(energy, 0.0, epsilon = 1e-10);
161    }
162
163    #[test]
164    fn test_cubic_force_at_equilibrium() {
165        let cubic: Cubic<f64> = Cubic::new(300.0, -50.0, 1.5);
166        let r0 = 1.5;
167
168        let force = cubic.force_factor(r0 * r0);
169        assert_relative_eq!(force, 0.0, epsilon = 1e-10);
170    }
171
172    #[test]
173    fn test_cubic_reduces_to_harmonic() {
174        let k = 300.0;
175        let r0 = 1.5;
176        let cubic: Cubic<f64> = Cubic::new(k, 0.0, r0);
177        let harm = crate::bond::Harm::<f64>::new(k, r0);
178
179        let r_sq = 1.6 * 1.6;
180        assert_relative_eq!(cubic.energy(r_sq), harm.energy(r_sq), epsilon = 1e-10);
181        assert_relative_eq!(
182            cubic.force_factor(r_sq),
183            harm.force_factor(r_sq),
184            epsilon = 1e-10
185        );
186    }
187
188    #[test]
189    fn test_cubic_asymmetry() {
190        let cubic: Cubic<f64> = Cubic::new(100.0, -20.0, 1.0);
191
192        let dr = 0.2;
193        let e_stretch = cubic.energy((1.0 + dr).powi(2));
194        let e_compress = cubic.energy((1.0 - dr).powi(2));
195
196        assert!(
197            e_stretch < e_compress,
198            "With k_cubic < 0, stretching {} should be lower than compression {}",
199            e_stretch,
200            e_compress
201        );
202    }
203
204    #[test]
205    fn test_cubic_numerical_derivative() {
206        let cubic: Cubic<f64> = Cubic::new(300.0, -50.0, 1.5);
207        let r = 1.6;
208        let r_sq = r * r;
209
210        let h = 1e-6;
211        let v_plus = cubic.energy((r + h) * (r + h));
212        let v_minus = cubic.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 = cubic.force_factor(r_sq);
217
218        assert_relative_eq!(s_analytical, s_numerical, epsilon = 1e-6);
219    }
220}