dreid_kernel/potentials/bonded/
angle.rs

1use crate::math::Real;
2use crate::traits::AngleKernel;
3use crate::types::EnergyDiff;
4
5/// Cosine Harmonic potential for bond angles.
6///
7/// # Physics
8///
9/// Models the angle bending energy using a simple harmonic approximation on the cosine of the angle.
10///
11/// - **Formula**: $$ E = \frac{1}{2} C (\cos\theta - \cos\theta_0)^2 $$
12/// - **Derivative Factor (`diff`)**: $$ \Gamma = \frac{dE}{d(\cos\theta)} = C (\cos\theta - \cos\theta_0) $$
13///
14/// # Parameters
15///
16/// - `k_half`: Half force constant $C_{half} = C/2$.
17/// - `cos0`: The cosine of the equilibrium angle $\cos\theta_0$.
18///
19/// # Inputs
20///
21/// - `cos_theta`: Cosine of the bond angle $\theta_{ijk}$.
22///
23/// # Implementation Notes
24///
25/// - Pure polynomial evaluation; no `acos`, `sin`, or `sqrt` required.
26/// - All intermediate calculations are shared between energy and force computations.
27/// - Branchless and panic-free.
28#[derive(Clone, Copy, Debug, Default)]
29pub struct CosineHarmonic;
30
31impl<T: Real> AngleKernel<T> for CosineHarmonic {
32    type Params = (T, T);
33
34    /// Computes only the potential energy.
35    ///
36    /// # Formula
37    ///
38    /// $$ E = C_{half} (\Delta)^2, \quad \text{where } \Delta = \cos\theta - \cos\theta_0 $$
39    #[inline(always)]
40    fn energy(cos_theta: T, (k_half, cos0): Self::Params) -> T {
41        let delta = cos_theta - cos0;
42        k_half * delta * delta
43    }
44
45    /// Computes only the derivative factor $\Gamma$.
46    ///
47    /// # Formula
48    ///
49    /// $$ \Gamma = 2 C_{half} (\cos\theta - \cos\theta_0) $$
50    ///
51    /// This factor allows computing forces via the chain rule:
52    /// $$ \vec{F} = -\Gamma \cdot \nabla (\cos\theta) $$
53    #[inline(always)]
54    fn diff(cos_theta: T, (k_half, cos0): Self::Params) -> T {
55        let c = k_half + k_half;
56        c * (cos_theta - cos0)
57    }
58
59    /// Computes both energy and derivative factor efficiently.
60    ///
61    /// This method reuses intermediate calculations to minimize operations.
62    #[inline(always)]
63    fn compute(cos_theta: T, (k_half, cos0): Self::Params) -> EnergyDiff<T> {
64        let delta = cos_theta - cos0;
65
66        let energy = k_half * delta * delta;
67
68        let c = k_half + k_half;
69        let diff = c * delta;
70
71        EnergyDiff { energy, diff }
72    }
73}
74
75/// Theta Harmonic potential for bond angles.
76///
77/// # Physics
78///
79/// Models the angle bending energy using a harmonic approximation directly on the angle $\theta$ (in radians).
80///
81/// - **Formula**: $$ E = \frac{1}{2} K (\theta - \theta_0)^2 $$
82/// - **Derivative Factor (`diff`)**: $$ \Gamma = \frac{dE}{d(\cos\theta)} = -K \frac{\theta - \theta_0}{\sin\theta} $$
83///
84/// # Parameters
85///
86/// - `k_half`: Half force constant $K_{half} = K/2$.
87/// - `theta0`: The equilibrium angle $\theta_0$ in radians.
88///
89/// # Inputs
90///
91/// - `cos_theta`: Cosine of the bond angle $\theta_{ijk}$.
92///
93/// # Implementation Notes
94///
95/// - Uses `k_half` to save one multiplication in the energy step.
96/// - Handles $\theta=0$ and $\theta=\pi$ analytically using L'Hopital's rule.
97/// - Needs a single `acos` call for angle calculation.
98#[derive(Clone, Copy, Debug, Default)]
99pub struct ThetaHarmonic;
100
101impl<T: Real> AngleKernel<T> for ThetaHarmonic {
102    type Params = (T, T);
103
104    /// Computes only the potential energy.
105    ///
106    /// # Formula
107    ///
108    /// $$ E = K_{half} (\theta - \theta_0)^2 $$
109    #[inline(always)]
110    fn energy(cos_theta: T, (k_half, theta0): Self::Params) -> T {
111        let one = T::from(1.0f32);
112        let minus_one = T::from(-1.0f32);
113
114        let c = cos_theta.max(minus_one).min(one);
115        let theta = c.acos();
116
117        let delta = theta - theta0;
118
119        k_half * delta * delta
120    }
121
122    /// Computes only the derivative factor $\Gamma$.
123    ///
124    /// # Formula
125    ///
126    /// $$ \Gamma = -2 K_{half} \frac{\theta - \theta_0}{\sin\theta} $$
127    ///
128    /// This factor allows computing forces via the chain rule:
129    /// $$ \vec{F} = -\Gamma \cdot \nabla (\cos\theta) $$
130    #[inline(always)]
131    fn diff(cos_theta: T, (k_half, theta0): Self::Params) -> T {
132        let one = T::from(1.0f32);
133        let minus_one = T::from(-1.0f32);
134        let zero = T::from(0.0f32);
135        let singularity_thresh = T::from(1.0e-4f32);
136        let epsilon = T::from(1.0e-20f32);
137
138        let c = cos_theta.max(minus_one).min(one);
139
140        let theta = c.acos();
141        let sin_theta = (one - c * c).max(zero).sqrt();
142
143        let factor = if sin_theta > singularity_thresh {
144            (theta - theta0) / sin_theta
145        } else {
146            let s_safe = sin_theta.max(epsilon);
147
148            if c > zero {
149                one - theta0 / s_safe
150            } else {
151                let pi = T::pi();
152                minus_one + (pi - theta0) / s_safe
153            }
154        };
155
156        let k = k_half + k_half;
157
158        -k * factor
159    }
160
161    /// Computes both energy and derivative factor efficiently.
162    ///
163    /// This method reuses intermediate calculations to minimize operations.
164    #[inline(always)]
165    fn compute(cos_theta: T, (k_half, theta0): Self::Params) -> EnergyDiff<T> {
166        let one = T::from(1.0f32);
167        let minus_one = T::from(-1.0f32);
168        let zero = T::from(0.0f32);
169        let singularity_thresh = T::from(1.0e-4f32);
170        let epsilon = T::from(1.0e-20f32);
171
172        let c = cos_theta.max(minus_one).min(one);
173        let theta = c.acos();
174        let sin_theta = (one - c * c).max(zero).sqrt();
175
176        let delta = theta - theta0;
177        let energy = k_half * delta * delta;
178
179        let factor = if sin_theta > singularity_thresh {
180            delta / sin_theta
181        } else {
182            let s_safe = sin_theta.max(epsilon);
183            if c > zero {
184                one - theta0 / s_safe
185            } else {
186                let pi = T::pi();
187                minus_one + (pi - theta0) / s_safe
188            }
189        };
190
191        let k = k_half + k_half;
192        let diff = -k * factor;
193
194        EnergyDiff { energy, diff }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use approx::assert_relative_eq;
202    use std::f64::consts::PI;
203
204    // ------------------------------------------------------------------------
205    // Test Constants
206    // ------------------------------------------------------------------------
207
208    const H: f64 = 1e-6;
209    const TOL_DIFF: f64 = 1e-4;
210
211    // ========================================================================
212    // CosineHarmonic Tests
213    // ========================================================================
214
215    mod cosine_harmonic {
216        use super::*;
217
218        const K_HALF: f64 = 50.0; // C/2 = 50
219        const COS0: f64 = 0.5; // cos(60°) = 0.5
220
221        fn params() -> (f64, f64) {
222            (K_HALF, COS0)
223        }
224
225        // --------------------------------------------------------------------
226        // 1. Sanity Checks
227        // --------------------------------------------------------------------
228
229        #[test]
230        fn sanity_compute_equals_separate() {
231            let cos_theta = 0.7_f64;
232            let p = params();
233
234            let result = CosineHarmonic::compute(cos_theta, p);
235            let energy_only = CosineHarmonic::energy(cos_theta, p);
236            let diff_only = CosineHarmonic::diff(cos_theta, p);
237
238            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
239            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
240        }
241
242        #[test]
243        fn sanity_f32_f64_consistency() {
244            let cos_theta = 0.6;
245            let p64 = params();
246            let p32 = (K_HALF as f32, COS0 as f32);
247
248            let e64 = CosineHarmonic::energy(cos_theta, p64);
249            let e32 = CosineHarmonic::energy(cos_theta as f32, p32);
250
251            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
252        }
253
254        #[test]
255        fn sanity_equilibrium() {
256            let result = CosineHarmonic::compute(COS0, params());
257
258            assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
259            assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
260        }
261
262        // --------------------------------------------------------------------
263        // 2. Numerical Stability
264        // --------------------------------------------------------------------
265
266        #[test]
267        fn stability_cos_one() {
268            let result = CosineHarmonic::compute(1.0, params());
269
270            assert!(result.energy.is_finite());
271            assert!(result.diff.is_finite());
272        }
273
274        #[test]
275        fn stability_cos_minus_one() {
276            let result = CosineHarmonic::compute(-1.0, params());
277
278            assert!(result.energy.is_finite());
279            assert!(result.diff.is_finite());
280        }
281
282        // --------------------------------------------------------------------
283        // 3. Finite Difference Verification
284        // --------------------------------------------------------------------
285
286        fn finite_diff_check(cos_theta: f64) {
287            let p = params();
288
289            let c_plus = cos_theta + H;
290            let c_minus = cos_theta - H;
291            let e_plus = CosineHarmonic::energy(c_plus, p);
292            let e_minus = CosineHarmonic::energy(c_minus, p);
293            let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
294
295            let gamma = CosineHarmonic::diff(cos_theta, p);
296
297            assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
298        }
299
300        #[test]
301        fn finite_diff_bent() {
302            finite_diff_check(0.3);
303        }
304
305        #[test]
306        fn finite_diff_wide() {
307            finite_diff_check(0.8);
308        }
309
310        #[test]
311        fn finite_diff_near_equilibrium() {
312            finite_diff_check(COS0 + 0.01);
313        }
314
315        // --------------------------------------------------------------------
316        // 4. CosineHarmonic Specific
317        // --------------------------------------------------------------------
318
319        #[test]
320        fn specific_quadratic_scaling() {
321            let delta1 = 0.1;
322            let delta2 = 0.2;
323            let e1 = CosineHarmonic::energy(COS0 + delta1, params());
324            let e2 = CosineHarmonic::energy(COS0 + delta2, params());
325
326            assert_relative_eq!(e2 / e1, 4.0, epsilon = 1e-10);
327        }
328
329        #[test]
330        fn specific_no_trig_needed() {
331            let cos_theta = 0.7;
332            let delta = cos_theta - COS0;
333            let expected = K_HALF * delta * delta;
334
335            assert_relative_eq!(
336                CosineHarmonic::energy(cos_theta, params()),
337                expected,
338                epsilon = 1e-14
339            );
340        }
341    }
342
343    // ========================================================================
344    // ThetaHarmonic Tests
345    // ========================================================================
346
347    mod theta_harmonic {
348        use super::*;
349
350        const K_HALF: f64 = 50.0; // K/2 = 50
351        const THETA0: f64 = PI / 3.0; // 60° equilibrium
352        const COS0: f64 = 0.5; // cos(60°)
353
354        fn params() -> (f64, f64) {
355            (K_HALF, THETA0)
356        }
357
358        // --------------------------------------------------------------------
359        // 1. Sanity Checks
360        // --------------------------------------------------------------------
361
362        #[test]
363        fn sanity_compute_equals_separate() {
364            let cos_theta = 0.7_f64;
365            let p = params();
366
367            let result = ThetaHarmonic::compute(cos_theta, p);
368            let energy_only = ThetaHarmonic::energy(cos_theta, p);
369            let diff_only = ThetaHarmonic::diff(cos_theta, p);
370
371            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
372            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
373        }
374
375        #[test]
376        fn sanity_f32_f64_consistency() {
377            let cos_theta = 0.6;
378            let p64 = params();
379            let p32 = (K_HALF as f32, THETA0 as f32);
380
381            let e64 = ThetaHarmonic::energy(cos_theta, p64);
382            let e32 = ThetaHarmonic::energy(cos_theta as f32, p32);
383
384            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-4);
385        }
386
387        #[test]
388        fn sanity_equilibrium() {
389            let result = ThetaHarmonic::compute(COS0, params());
390
391            assert_relative_eq!(result.energy, 0.0, epsilon = 1e-10);
392            assert!(result.diff.abs() < 1e-8);
393        }
394
395        // --------------------------------------------------------------------
396        // 2. Numerical Stability
397        // --------------------------------------------------------------------
398
399        #[test]
400        fn stability_theta_zero() {
401            let result = ThetaHarmonic::compute(1.0, params());
402
403            assert!(result.energy.is_finite());
404            assert!(result.diff.is_finite());
405        }
406
407        #[test]
408        fn stability_theta_pi() {
409            let result = ThetaHarmonic::compute(-1.0, params());
410
411            assert!(result.energy.is_finite());
412            assert!(result.diff.is_finite());
413        }
414
415        #[test]
416        fn stability_cos_slightly_outside() {
417            let result = ThetaHarmonic::compute(1.0001, params());
418
419            assert!(result.energy.is_finite());
420            assert!(result.diff.is_finite());
421        }
422
423        // --------------------------------------------------------------------
424        // 3. Finite Difference Verification
425        // --------------------------------------------------------------------
426
427        fn finite_diff_check(cos_theta: f64) {
428            if cos_theta.abs() > 0.999 {
429                return;
430            }
431
432            let p = params();
433
434            let c_plus = cos_theta + H;
435            let c_minus = cos_theta - H;
436            let e_plus = ThetaHarmonic::energy(c_plus, p);
437            let e_minus = ThetaHarmonic::energy(c_minus, p);
438            let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
439
440            let gamma = ThetaHarmonic::diff(cos_theta, p);
441
442            assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
443        }
444
445        #[test]
446        fn finite_diff_acute() {
447            finite_diff_check(0.7);
448        }
449
450        #[test]
451        fn finite_diff_right() {
452            finite_diff_check(0.0);
453        }
454
455        #[test]
456        fn finite_diff_obtuse() {
457            finite_diff_check(-0.5);
458        }
459
460        #[test]
461        fn finite_diff_near_equilibrium() {
462            finite_diff_check(COS0 + 0.02);
463        }
464
465        // --------------------------------------------------------------------
466        // 4. ThetaHarmonic Specific
467        // --------------------------------------------------------------------
468
469        #[test]
470        fn specific_energy_formula() {
471            let cos_theta = 0.0;
472            let theta = PI / 2.0;
473            let delta = theta - THETA0;
474            let expected = K_HALF * delta * delta;
475
476            assert_relative_eq!(
477                ThetaHarmonic::energy(cos_theta, params()),
478                expected,
479                epsilon = 1e-10
480            );
481        }
482
483        #[test]
484        fn specific_diff_chain_rule() {
485            let cos_theta = 0.0;
486            let theta = PI / 2.0;
487            let sin_theta = 1.0;
488
489            let k = 2.0 * K_HALF;
490            let expected_gamma = -k * (theta - THETA0) / sin_theta;
491
492            assert_relative_eq!(
493                ThetaHarmonic::diff(cos_theta, params()),
494                expected_gamma,
495                epsilon = 1e-10
496            );
497        }
498    }
499}