1use crate::base::Potential2;
29use crate::math::Vector;
30
31#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct Cubic<T> {
40 k: T,
42 k_cubic: T,
44 r0: T,
46}
47
48impl<T: Vector> Cubic<T> {
49 #[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 #[inline]
76 pub fn k(&self) -> T {
77 self.k
78 }
79
80 #[inline]
82 pub fn k_cubic(&self) -> T {
83 self.k_cubic
84 }
85
86 #[inline]
88 pub fn r0(&self) -> T {
89 self.r0
90 }
91}
92
93impl<T: Vector> Potential2<T> for Cubic<T> {
94 #[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 #[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 #[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}