Skip to main content

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/// Cosine Linear potential for bond angles with linear equilibrium geometry.
76///
77/// # Physics
78///
79/// Models the angle bending energy for atoms with linear equilibrium geometry ($\theta_0 = 180°$),
80/// using a simple linear function of the cosine of the angle.
81///
82/// - **Formula**: $$ E = K (1 + \cos\theta) $$
83/// - **Derivative Factor (`diff`)**: $$ \Gamma = \frac{dE}{d(\cos\theta)} = K $$
84///
85/// # Parameters
86///
87/// - `k`: Force constant $K$.
88///
89/// # Inputs
90///
91/// - `cos_theta`: Cosine of the bond angle $\theta_{ijk}$.
92///
93/// # Implementation Notes
94///
95/// - Single multiply-add for energy; zero arithmetic for derivative.
96/// - Pure polynomial evaluation; no trigonometric functions required.
97/// - Branchless and panic-free.
98#[derive(Clone, Copy, Debug, Default)]
99pub struct CosineLinear;
100
101impl<T: Real> AngleKernel<T> for CosineLinear {
102    type Params = T;
103
104    /// Computes only the potential energy.
105    ///
106    /// # Formula
107    ///
108    /// $$ E = K (1 + \cos\theta) $$
109    #[inline(always)]
110    fn energy(cos_theta: T, k: Self::Params) -> T {
111        let one = T::from(1.0f32);
112        k * (one + cos_theta)
113    }
114
115    /// Computes only the derivative factor $\Gamma$.
116    ///
117    /// # Formula
118    ///
119    /// $$ \Gamma = K $$
120    ///
121    /// This factor allows computing forces via the chain rule:
122    /// $$ \vec{F} = -\Gamma \cdot \nabla (\cos\theta) $$
123    #[inline(always)]
124    fn diff(_cos_theta: T, k: Self::Params) -> T {
125        k
126    }
127
128    /// Computes both energy and derivative factor efficiently.
129    ///
130    /// This method reuses intermediate calculations to minimize operations.
131    #[inline(always)]
132    fn compute(cos_theta: T, k: Self::Params) -> EnergyDiff<T> {
133        let one = T::from(1.0f32);
134        let energy = k * (one + cos_theta);
135        let diff = k;
136
137        EnergyDiff { energy, diff }
138    }
139}
140
141/// Theta Harmonic potential for bond angles.
142///
143/// # Physics
144///
145/// Models the angle bending energy using a harmonic approximation directly on the angle $\theta$ (in radians).
146///
147/// - **Formula**: $$ E = \frac{1}{2} K (\theta - \theta_0)^2 $$
148/// - **Derivative Factor (`diff`)**: $$ \Gamma = \frac{dE}{d(\cos\theta)} = -K \frac{\theta - \theta_0}{\sin\theta} $$
149///
150/// # Parameters
151///
152/// - `k_half`: Half force constant $K_{half} = K/2$.
153/// - `theta0`: The equilibrium angle $\theta_0$ in radians.
154///
155/// # Inputs
156///
157/// - `cos_theta`: Cosine of the bond angle $\theta_{ijk}$.
158///
159/// # Implementation Notes
160///
161/// - Uses `k_half` to save one multiplication in the energy step.
162/// - Handles $\theta=0$ and $\theta=\pi$ analytically using L'Hopital's rule.
163/// - Needs a single `acos` call for angle calculation.
164#[derive(Clone, Copy, Debug, Default)]
165pub struct ThetaHarmonic;
166
167impl<T: Real> AngleKernel<T> for ThetaHarmonic {
168    type Params = (T, T);
169
170    /// Computes only the potential energy.
171    ///
172    /// # Formula
173    ///
174    /// $$ E = K_{half} (\theta - \theta_0)^2 $$
175    #[inline(always)]
176    fn energy(cos_theta: T, (k_half, theta0): Self::Params) -> T {
177        let one = T::from(1.0f32);
178        let minus_one = T::from(-1.0f32);
179
180        let c = cos_theta.max(minus_one).min(one);
181        let theta = c.acos();
182
183        let delta = theta - theta0;
184
185        k_half * delta * delta
186    }
187
188    /// Computes only the derivative factor $\Gamma$.
189    ///
190    /// # Formula
191    ///
192    /// $$ \Gamma = -2 K_{half} \frac{\theta - \theta_0}{\sin\theta} $$
193    ///
194    /// This factor allows computing forces via the chain rule:
195    /// $$ \vec{F} = -\Gamma \cdot \nabla (\cos\theta) $$
196    #[inline(always)]
197    fn diff(cos_theta: T, (k_half, theta0): Self::Params) -> T {
198        let one = T::from(1.0f32);
199        let minus_one = T::from(-1.0f32);
200        let zero = T::from(0.0f32);
201        let singularity_thresh = T::from(1.0e-4f32);
202        let epsilon = T::from(1.0e-20f32);
203
204        let c = cos_theta.max(minus_one).min(one);
205
206        let theta = c.acos();
207        let sin_theta = (one - c * c).max(zero).sqrt();
208
209        let factor = if sin_theta > singularity_thresh {
210            (theta - theta0) / sin_theta
211        } else {
212            let s_safe = sin_theta.max(epsilon);
213
214            if c > zero {
215                one - theta0 / s_safe
216            } else {
217                let pi = T::pi();
218                minus_one + (pi - theta0) / s_safe
219            }
220        };
221
222        let k = k_half + k_half;
223
224        -k * factor
225    }
226
227    /// Computes both energy and derivative factor efficiently.
228    ///
229    /// This method reuses intermediate calculations to minimize operations.
230    #[inline(always)]
231    fn compute(cos_theta: T, (k_half, theta0): Self::Params) -> EnergyDiff<T> {
232        let one = T::from(1.0f32);
233        let minus_one = T::from(-1.0f32);
234        let zero = T::from(0.0f32);
235        let singularity_thresh = T::from(1.0e-4f32);
236        let epsilon = T::from(1.0e-20f32);
237
238        let c = cos_theta.max(minus_one).min(one);
239        let theta = c.acos();
240        let sin_theta = (one - c * c).max(zero).sqrt();
241
242        let delta = theta - theta0;
243        let energy = k_half * delta * delta;
244
245        let factor = if sin_theta > singularity_thresh {
246            delta / sin_theta
247        } else {
248            let s_safe = sin_theta.max(epsilon);
249            if c > zero {
250                one - theta0 / s_safe
251            } else {
252                let pi = T::pi();
253                minus_one + (pi - theta0) / s_safe
254            }
255        };
256
257        let k = k_half + k_half;
258        let diff = -k * factor;
259
260        EnergyDiff { energy, diff }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use approx::assert_relative_eq;
268    use std::f64::consts::PI;
269
270    // ------------------------------------------------------------------------
271    // Test Constants
272    // ------------------------------------------------------------------------
273
274    const H: f64 = 1e-6;
275    const TOL_DIFF: f64 = 1e-4;
276
277    // ========================================================================
278    // CosineHarmonic Tests
279    // ========================================================================
280
281    mod cosine_harmonic {
282        use super::*;
283
284        const K_HALF: f64 = 50.0; // C/2 = 50
285        const COS0: f64 = 0.5; // cos(60°) = 0.5
286
287        fn params() -> (f64, f64) {
288            (K_HALF, COS0)
289        }
290
291        // --------------------------------------------------------------------
292        // 1. Sanity Checks
293        // --------------------------------------------------------------------
294
295        #[test]
296        fn sanity_compute_equals_separate() {
297            let cos_theta = 0.7_f64;
298            let p = params();
299
300            let result = CosineHarmonic::compute(cos_theta, p);
301            let energy_only = CosineHarmonic::energy(cos_theta, p);
302            let diff_only = CosineHarmonic::diff(cos_theta, p);
303
304            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
305            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
306        }
307
308        #[test]
309        fn sanity_f32_f64_consistency() {
310            let cos_theta = 0.6;
311            let p64 = params();
312            let p32 = (K_HALF as f32, COS0 as f32);
313
314            let e64 = CosineHarmonic::energy(cos_theta, p64);
315            let e32 = CosineHarmonic::energy(cos_theta as f32, p32);
316
317            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
318        }
319
320        #[test]
321        fn sanity_equilibrium() {
322            let result = CosineHarmonic::compute(COS0, params());
323
324            assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
325            assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
326        }
327
328        // --------------------------------------------------------------------
329        // 2. Numerical Stability
330        // --------------------------------------------------------------------
331
332        #[test]
333        fn stability_cos_one() {
334            let result = CosineHarmonic::compute(1.0, params());
335
336            assert!(result.energy.is_finite());
337            assert!(result.diff.is_finite());
338        }
339
340        #[test]
341        fn stability_cos_minus_one() {
342            let result = CosineHarmonic::compute(-1.0, params());
343
344            assert!(result.energy.is_finite());
345            assert!(result.diff.is_finite());
346        }
347
348        // --------------------------------------------------------------------
349        // 3. Finite Difference Verification
350        // --------------------------------------------------------------------
351
352        fn finite_diff_check(cos_theta: f64) {
353            let p = params();
354
355            let c_plus = cos_theta + H;
356            let c_minus = cos_theta - H;
357            let e_plus = CosineHarmonic::energy(c_plus, p);
358            let e_minus = CosineHarmonic::energy(c_minus, p);
359            let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
360
361            let gamma = CosineHarmonic::diff(cos_theta, p);
362
363            assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
364        }
365
366        #[test]
367        fn finite_diff_bent() {
368            finite_diff_check(0.3);
369        }
370
371        #[test]
372        fn finite_diff_wide() {
373            finite_diff_check(0.8);
374        }
375
376        #[test]
377        fn finite_diff_near_equilibrium() {
378            finite_diff_check(COS0 + 0.01);
379        }
380
381        // --------------------------------------------------------------------
382        // 4. CosineHarmonic Specific
383        // --------------------------------------------------------------------
384
385        #[test]
386        fn specific_quadratic_scaling() {
387            let delta1 = 0.1;
388            let delta2 = 0.2;
389            let e1 = CosineHarmonic::energy(COS0 + delta1, params());
390            let e2 = CosineHarmonic::energy(COS0 + delta2, params());
391
392            assert_relative_eq!(e2 / e1, 4.0, epsilon = 1e-10);
393        }
394
395        #[test]
396        fn specific_no_trig_needed() {
397            let cos_theta = 0.7;
398            let delta = cos_theta - COS0;
399            let expected = K_HALF * delta * delta;
400
401            assert_relative_eq!(
402                CosineHarmonic::energy(cos_theta, params()),
403                expected,
404                epsilon = 1e-14
405            );
406        }
407    }
408
409    // ========================================================================
410    // CosineLinear Tests
411    // ========================================================================
412
413    mod cosine_linear {
414        use super::*;
415
416        const K: f64 = 100.0; // K = 100
417
418        fn params() -> f64 {
419            K
420        }
421
422        // --------------------------------------------------------------------
423        // 1. Sanity Checks
424        // --------------------------------------------------------------------
425
426        #[test]
427        fn sanity_compute_equals_separate() {
428            let cos_theta = 0.3_f64;
429            let p = params();
430
431            let result = CosineLinear::compute(cos_theta, p);
432            let energy_only = CosineLinear::energy(cos_theta, p);
433            let diff_only = CosineLinear::diff(cos_theta, p);
434
435            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
436            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
437        }
438
439        #[test]
440        fn sanity_f32_f64_consistency() {
441            let cos_theta = -0.3;
442            let p64 = params();
443            let p32 = K as f32;
444
445            let e64 = CosineLinear::energy(cos_theta, p64);
446            let e32 = CosineLinear::energy(cos_theta as f32, p32);
447
448            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
449        }
450
451        #[test]
452        fn sanity_equilibrium() {
453            let result = CosineLinear::compute(-1.0, params());
454
455            assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
456            assert_relative_eq!(result.diff, K, epsilon = 1e-14);
457        }
458
459        // --------------------------------------------------------------------
460        // 2. Numerical Stability
461        // --------------------------------------------------------------------
462
463        #[test]
464        fn stability_cos_one() {
465            let result = CosineLinear::compute(1.0, params());
466
467            assert!(result.energy.is_finite());
468            assert!(result.diff.is_finite());
469        }
470
471        #[test]
472        fn stability_cos_minus_one() {
473            let result = CosineLinear::compute(-1.0, params());
474
475            assert!(result.energy.is_finite());
476            assert!(result.diff.is_finite());
477        }
478
479        // --------------------------------------------------------------------
480        // 3. Finite Difference Verification
481        // --------------------------------------------------------------------
482
483        fn finite_diff_check(cos_theta: f64) {
484            let p = params();
485
486            let c_plus = cos_theta + H;
487            let c_minus = cos_theta - H;
488            let e_plus = CosineLinear::energy(c_plus, p);
489            let e_minus = CosineLinear::energy(c_minus, p);
490            let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
491
492            let gamma = CosineLinear::diff(cos_theta, p);
493
494            assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
495        }
496
497        #[test]
498        fn finite_diff_acute() {
499            finite_diff_check(0.3);
500        }
501
502        #[test]
503        fn finite_diff_obtuse() {
504            finite_diff_check(-0.5);
505        }
506
507        #[test]
508        fn finite_diff_near_equilibrium() {
509            finite_diff_check(-0.99);
510        }
511
512        // --------------------------------------------------------------------
513        // 4. CosineLinear Specific
514        // --------------------------------------------------------------------
515
516        #[test]
517        fn specific_linear_scaling() {
518            let e1 = CosineLinear::energy(0.0, params());
519            let e2 = CosineLinear::energy(-0.5, params());
520
521            assert_relative_eq!(e1 / e2, 2.0, epsilon = 1e-10);
522        }
523
524        #[test]
525        fn specific_constant_derivative() {
526            let p = params();
527
528            assert_relative_eq!(CosineLinear::diff(1.0, p), K, epsilon = 1e-14);
529            assert_relative_eq!(CosineLinear::diff(0.0, p), K, epsilon = 1e-14);
530            assert_relative_eq!(CosineLinear::diff(-0.5, p), K, epsilon = 1e-14);
531            assert_relative_eq!(CosineLinear::diff(-1.0, p), K, epsilon = 1e-14);
532        }
533    }
534
535    // ========================================================================
536    // ThetaHarmonic Tests
537    // ========================================================================
538
539    mod theta_harmonic {
540        use super::*;
541
542        const K_HALF: f64 = 50.0; // K/2 = 50
543        const THETA0: f64 = PI / 3.0; // 60° equilibrium
544        const COS0: f64 = 0.5; // cos(60°)
545
546        fn params() -> (f64, f64) {
547            (K_HALF, THETA0)
548        }
549
550        // --------------------------------------------------------------------
551        // 1. Sanity Checks
552        // --------------------------------------------------------------------
553
554        #[test]
555        fn sanity_compute_equals_separate() {
556            let cos_theta = 0.7_f64;
557            let p = params();
558
559            let result = ThetaHarmonic::compute(cos_theta, p);
560            let energy_only = ThetaHarmonic::energy(cos_theta, p);
561            let diff_only = ThetaHarmonic::diff(cos_theta, p);
562
563            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
564            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
565        }
566
567        #[test]
568        fn sanity_f32_f64_consistency() {
569            let cos_theta = 0.6;
570            let p64 = params();
571            let p32 = (K_HALF as f32, THETA0 as f32);
572
573            let e64 = ThetaHarmonic::energy(cos_theta, p64);
574            let e32 = ThetaHarmonic::energy(cos_theta as f32, p32);
575
576            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-4);
577        }
578
579        #[test]
580        fn sanity_equilibrium() {
581            let result = ThetaHarmonic::compute(COS0, params());
582
583            assert_relative_eq!(result.energy, 0.0, epsilon = 1e-10);
584            assert!(result.diff.abs() < 1e-8);
585        }
586
587        // --------------------------------------------------------------------
588        // 2. Numerical Stability
589        // --------------------------------------------------------------------
590
591        #[test]
592        fn stability_theta_zero() {
593            let result = ThetaHarmonic::compute(1.0, params());
594
595            assert!(result.energy.is_finite());
596            assert!(result.diff.is_finite());
597        }
598
599        #[test]
600        fn stability_theta_pi() {
601            let result = ThetaHarmonic::compute(-1.0, params());
602
603            assert!(result.energy.is_finite());
604            assert!(result.diff.is_finite());
605        }
606
607        #[test]
608        fn stability_cos_slightly_outside() {
609            let result = ThetaHarmonic::compute(1.0001, params());
610
611            assert!(result.energy.is_finite());
612            assert!(result.diff.is_finite());
613        }
614
615        // --------------------------------------------------------------------
616        // 3. Finite Difference Verification
617        // --------------------------------------------------------------------
618
619        fn finite_diff_check(cos_theta: f64) {
620            if cos_theta.abs() > 0.999 {
621                return;
622            }
623
624            let p = params();
625
626            let c_plus = cos_theta + H;
627            let c_minus = cos_theta - H;
628            let e_plus = ThetaHarmonic::energy(c_plus, p);
629            let e_minus = ThetaHarmonic::energy(c_minus, p);
630            let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
631
632            let gamma = ThetaHarmonic::diff(cos_theta, p);
633
634            assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
635        }
636
637        #[test]
638        fn finite_diff_acute() {
639            finite_diff_check(0.7);
640        }
641
642        #[test]
643        fn finite_diff_right() {
644            finite_diff_check(0.0);
645        }
646
647        #[test]
648        fn finite_diff_obtuse() {
649            finite_diff_check(-0.5);
650        }
651
652        #[test]
653        fn finite_diff_near_equilibrium() {
654            finite_diff_check(COS0 + 0.02);
655        }
656
657        // --------------------------------------------------------------------
658        // 4. ThetaHarmonic Specific
659        // --------------------------------------------------------------------
660
661        #[test]
662        fn specific_energy_formula() {
663            let cos_theta = 0.0;
664            let theta = PI / 2.0;
665            let delta = theta - THETA0;
666            let expected = K_HALF * delta * delta;
667
668            assert_relative_eq!(
669                ThetaHarmonic::energy(cos_theta, params()),
670                expected,
671                epsilon = 1e-10
672            );
673        }
674
675        #[test]
676        fn specific_diff_chain_rule() {
677            let cos_theta = 0.0;
678            let theta = PI / 2.0;
679            let sin_theta = 1.0;
680
681            let k = 2.0 * K_HALF;
682            let expected_gamma = -k * (theta - THETA0) / sin_theta;
683
684            assert_relative_eq!(
685                ThetaHarmonic::diff(cos_theta, params()),
686                expected_gamma,
687                epsilon = 1e-10
688            );
689        }
690    }
691}