dreid_kernel/potentials/bonded/
torsion.rs

1use crate::math::Real;
2use crate::traits::TorsionKernel;
3use crate::types::EnergyDiff;
4
5/// Periodic torsion potential for dihedral angles.
6///
7/// # Physics
8///
9/// Models the rotational barrier around a bond axis using a periodic cosine function.
10///
11/// - **Formula**: $$ E = \frac{1}{2} V [1 - \cos(n(\phi - \phi_0))] $$
12/// - **Derivative (`diff`)**: $$ T = \frac{dE}{d\phi} = \frac{1}{2} V \cdot n \cdot \sin(n(\phi - \phi_0)) $$
13///
14/// # Parameters
15///
16/// - `v_half`: Half barrier height $V_{half} = V/2$.
17/// - `n`: Periodicity/multiplicity.
18/// - `cos_n_phi0`: $\cos(n\phi_0)$, pre-computed phase cosine.
19/// - `sin_n_phi0`: $\sin(n\phi_0)$, pre-computed phase sine.
20///
21/// # Inputs
22///
23/// - `cos_phi`: Cosine of the dihedral angle $\cos\phi$.
24/// - `sin_phi`: Sine of the dihedral angle $\sin\phi$.
25///
26/// # Implementation Notes
27///
28/// - Uses optimized closed-form formulas for common periodicities ($n = 1, 2, 3$).
29/// - Falls back to Chebyshev recurrence for higher periodicities.
30/// - All intermediate calculations are shared between energy and torque computations.
31/// - Branchless and panic-free.
32#[derive(Clone, Copy, Debug, Default)]
33pub struct Torsion;
34
35impl<T: Real> TorsionKernel<T> for Torsion {
36    type Params = (T, u8, T, T);
37
38    /// Computes only the potential energy.
39    ///
40    /// # Formula
41    ///
42    /// $$ E = V_{half} [1 - \cos(n(\phi - \phi_0))] $$
43    #[inline(always)]
44    fn energy(cos_phi: T, sin_phi: T, (v_half, n, cos_n_phi0, sin_n_phi0): Self::Params) -> T {
45        let one = T::from(1.0f32);
46        let (cos_n_phi, sin_n_phi) = multiple_angle(cos_phi, sin_phi, n);
47        let cos_n_delta = cos_n_phi * cos_n_phi0 + sin_n_phi * sin_n_phi0;
48        v_half * (one - cos_n_delta)
49    }
50
51    /// Computes only the torque $T$.
52    ///
53    /// # Formula
54    ///
55    /// $$ T = V_{half} \cdot n \cdot \sin(n(\phi - \phi_0)) $$
56    ///
57    /// This factor allows computing forces via the chain rule:
58    /// $$ \vec{F} = -T \cdot \nabla \phi $$
59    #[inline(always)]
60    fn diff(cos_phi: T, sin_phi: T, (v_half, n, cos_n_phi0, sin_n_phi0): Self::Params) -> T {
61        let (cos_n_phi, sin_n_phi) = multiple_angle(cos_phi, sin_phi, n);
62        let sin_n_delta = sin_n_phi * cos_n_phi0 - cos_n_phi * sin_n_phi0;
63        let n_t = T::from(n as f32);
64        v_half * n_t * sin_n_delta
65    }
66
67    /// Computes both energy and torque efficiently.
68    ///
69    /// This method reuses intermediate calculations to minimize operations.
70    #[inline(always)]
71    fn compute(
72        cos_phi: T,
73        sin_phi: T,
74        (v_half, n, cos_n_phi0, sin_n_phi0): Self::Params,
75    ) -> EnergyDiff<T> {
76        let one = T::from(1.0f32);
77
78        let (cos_n_phi, sin_n_phi) = multiple_angle(cos_phi, sin_phi, n);
79
80        let cos_n_delta = cos_n_phi * cos_n_phi0 + sin_n_phi * sin_n_phi0;
81        let sin_n_delta = sin_n_phi * cos_n_phi0 - cos_n_phi * sin_n_phi0;
82
83        let energy = v_half * (one - cos_n_delta);
84
85        let n_t = T::from(n as f32);
86        let diff = v_half * n_t * sin_n_delta;
87
88        EnergyDiff { energy, diff }
89    }
90}
91
92/// Computes $(\cos(n\phi), \sin(n\phi))$ using optimized paths for common $n$.
93#[inline(always)]
94fn multiple_angle<T: Real>(cos_phi: T, sin_phi: T, n: u8) -> (T, T) {
95    match n {
96        0 => multiple_angle_0(),
97        1 => (cos_phi, sin_phi),
98        2 => multiple_angle_2(cos_phi, sin_phi),
99        3 => multiple_angle_3(cos_phi, sin_phi),
100        _ => multiple_angle_chebyshev(cos_phi, sin_phi, n),
101    }
102}
103
104/// $n = 0$: $(\cos(0), \sin(0)) = (1, 0)$.
105#[inline(always)]
106fn multiple_angle_0<T: Real>() -> (T, T) {
107    (T::from(1.0f32), T::from(0.0f32))
108}
109
110/// $n = 2$: Double-angle formulas.
111#[inline(always)]
112fn multiple_angle_2<T: Real>(cos_phi: T, sin_phi: T) -> (T, T) {
113    let one = T::from(1.0f32);
114    let two = T::from(2.0f32);
115
116    let cos_2phi = two * cos_phi * cos_phi - one;
117    let sin_2phi = two * sin_phi * cos_phi;
118
119    (cos_2phi, sin_2phi)
120}
121
122/// $n = 3$: Triple-angle formulas.
123#[inline(always)]
124fn multiple_angle_3<T: Real>(cos_phi: T, sin_phi: T) -> (T, T) {
125    let three = T::from(3.0f32);
126    let four = T::from(4.0f32);
127
128    let cos2 = cos_phi * cos_phi;
129    let sin2 = sin_phi * sin_phi;
130
131    let cos_3phi = four * cos2 * cos_phi - three * cos_phi;
132    let sin_3phi = three * sin_phi - four * sin2 * sin_phi;
133
134    (cos_3phi, sin_3phi)
135}
136
137/// General case: Chebyshev recurrence for $n \geq 4$.
138#[inline(always)]
139fn multiple_angle_chebyshev<T: Real>(cos_phi: T, sin_phi: T, n: u8) -> (T, T) {
140    let zero = T::from(0.0f32);
141    let one = T::from(1.0f32);
142    let two = T::from(2.0f32);
143
144    let mut cos_prev2 = one;
145    let mut sin_prev2 = zero;
146    let mut cos_prev1 = cos_phi;
147    let mut sin_prev1 = sin_phi;
148
149    let two_cos = two * cos_phi;
150
151    for _ in 2..=n {
152        let cos_curr = two_cos * cos_prev1 - cos_prev2;
153        let sin_curr = two_cos * sin_prev1 - sin_prev2;
154
155        cos_prev2 = cos_prev1;
156        sin_prev2 = sin_prev1;
157        cos_prev1 = cos_curr;
158        sin_prev1 = sin_curr;
159    }
160
161    (cos_prev1, sin_prev1)
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use approx::assert_relative_eq;
168    use std::f64::consts::PI;
169
170    // ------------------------------------------------------------------------
171    // Test Constants
172    // ------------------------------------------------------------------------
173
174    const H: f64 = 1e-6;
175    const TOL_DIFF: f64 = 1e-4;
176
177    // ========================================================================
178    // Torsion Tests
179    // ========================================================================
180
181    mod torsion {
182        use super::*;
183
184        const V_HALF: f64 = 2.5; // V/2 = 2.5 kcal/mol
185
186        fn params_n1() -> (f64, u8, f64, f64) {
187            // n=1, phi0=0 => cos(0)=1, sin(0)=0
188            (V_HALF, 1, 1.0, 0.0)
189        }
190
191        fn params_n2() -> (f64, u8, f64, f64) {
192            // n=2, phi0=π => cos(2π)=1, sin(2π)=0
193            (V_HALF, 2, 1.0, 0.0)
194        }
195
196        fn params_n3() -> (f64, u8, f64, f64) {
197            // n=3, phi0=0 => cos(0)=1, sin(0)=0
198            (V_HALF, 3, 1.0, 0.0)
199        }
200
201        fn params_n4() -> (f64, u8, f64, f64) {
202            // n=4, phi0=0 => cos(0)=1, sin(0)=0
203            (V_HALF, 4, 1.0, 0.0)
204        }
205
206        // --------------------------------------------------------------------
207        // 1. Sanity Checks
208        // --------------------------------------------------------------------
209
210        #[test]
211        fn sanity_compute_equals_separate() {
212            let phi = PI / 4.0;
213            let (cos_phi, sin_phi) = (phi.cos(), phi.sin());
214            let p = params_n2();
215
216            let result = Torsion::compute(cos_phi, sin_phi, p);
217            let energy_only = Torsion::energy(cos_phi, sin_phi, p);
218            let diff_only = Torsion::diff(cos_phi, sin_phi, p);
219
220            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
221            assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
222        }
223
224        #[test]
225        fn sanity_f32_f64_consistency() {
226            let phi = PI / 3.0;
227            let (cos_phi, sin_phi) = (phi.cos(), phi.sin());
228            let p64 = params_n2();
229            let p32 = (V_HALF as f32, 2u8, 1.0f32, 0.0f32);
230
231            let e64 = Torsion::energy(cos_phi, sin_phi, p64);
232            let e32 = Torsion::energy(cos_phi as f32, sin_phi as f32, p32);
233
234            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
235        }
236
237        #[test]
238        fn sanity_equilibrium_n2() {
239            let (cos_phi, sin_phi) = (1.0_f64, 0.0_f64);
240            let result = Torsion::compute(cos_phi, sin_phi, params_n2());
241
242            assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
243            assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
244        }
245
246        // --------------------------------------------------------------------
247        // 2. Numerical Stability
248        // --------------------------------------------------------------------
249
250        #[test]
251        fn stability_phi_zero() {
252            let result = Torsion::compute(1.0, 0.0, params_n3());
253
254            assert!(result.energy.is_finite());
255            assert!(result.diff.is_finite());
256        }
257
258        #[test]
259        fn stability_phi_pi() {
260            let result = Torsion::compute(-1.0, 0.0, params_n3());
261
262            assert!(result.energy.is_finite());
263            assert!(result.diff.is_finite());
264        }
265
266        #[test]
267        fn stability_high_periodicity() {
268            let p = (V_HALF, 6u8, 1.0, 0.0);
269            let phi = PI / 5.0;
270            let result = Torsion::compute(phi.cos(), phi.sin(), p);
271
272            assert!(result.energy.is_finite());
273            assert!(result.diff.is_finite());
274        }
275
276        // --------------------------------------------------------------------
277        // 3. Finite Difference Verification
278        // --------------------------------------------------------------------
279
280        fn finite_diff_check(phi: f64, params: (f64, u8, f64, f64)) {
281            let (cos_phi, sin_phi) = (phi.cos(), phi.sin());
282
283            let phi_plus = phi + H;
284            let phi_minus = phi - H;
285            let e_plus = Torsion::energy(phi_plus.cos(), phi_plus.sin(), params);
286            let e_minus = Torsion::energy(phi_minus.cos(), phi_minus.sin(), params);
287            let de_dphi_numerical = (e_plus - e_minus) / (2.0 * H);
288
289            let torque = Torsion::diff(cos_phi, sin_phi, params);
290
291            assert_relative_eq!(de_dphi_numerical, torque, epsilon = TOL_DIFF);
292        }
293
294        #[test]
295        fn finite_diff_n1() {
296            finite_diff_check(PI / 4.0, params_n1());
297        }
298
299        #[test]
300        fn finite_diff_n2() {
301            finite_diff_check(PI / 3.0, params_n2());
302        }
303
304        #[test]
305        fn finite_diff_n3() {
306            finite_diff_check(PI / 6.0, params_n3());
307        }
308
309        #[test]
310        fn finite_diff_n4() {
311            finite_diff_check(PI / 5.0, params_n4());
312        }
313
314        #[test]
315        fn finite_diff_various_phi() {
316            for phi in [0.1, 0.5, 1.0, 2.0, 3.0, 5.0] {
317                finite_diff_check(phi, params_n2());
318            }
319        }
320
321        // --------------------------------------------------------------------
322        // 4. Torsion Specific
323        // --------------------------------------------------------------------
324
325        #[test]
326        fn specific_barrier_height() {
327            let phi = PI;
328            let e = Torsion::energy(phi.cos(), phi.sin(), params_n1());
329
330            assert_relative_eq!(e, 2.0 * V_HALF, epsilon = 1e-10);
331        }
332
333        #[test]
334        fn specific_n_minima() {
335            let p = params_n3();
336            let mut min_count = 0;
337            for i in 0..36 {
338                let phi = i as f64 * PI / 18.0;
339                let e = Torsion::energy(phi.cos(), phi.sin(), p);
340                if e < 0.01 {
341                    min_count += 1;
342                }
343            }
344            assert_eq!(min_count, 3);
345        }
346
347        #[test]
348        fn specific_periodicity() {
349            let phi = 0.5;
350            let p = params_n3();
351            let e1 = Torsion::energy(phi.cos(), phi.sin(), p);
352            let phi2 = phi + 2.0 * PI / 3.0;
353            let e2 = Torsion::energy(phi2.cos(), phi2.sin(), p);
354
355            assert_relative_eq!(e1, e2, epsilon = 1e-10);
356        }
357    }
358
359    // ========================================================================
360    // Multiple Angle Helper Tests
361    // ========================================================================
362
363    mod multiple_angle_tests {
364        use super::*;
365
366        fn check_multiple_angle(phi: f64, n: u8) {
367            let (cos_phi, sin_phi) = (phi.cos(), phi.sin());
368            let (cos_n, sin_n) = multiple_angle(cos_phi, sin_phi, n);
369
370            let expected_cos = (n as f64 * phi).cos();
371            let expected_sin = (n as f64 * phi).sin();
372
373            assert_relative_eq!(cos_n, expected_cos, epsilon = 1e-10);
374            assert_relative_eq!(sin_n, expected_sin, epsilon = 1e-10);
375        }
376
377        #[test]
378        fn multiple_angle_n0() {
379            let (cos_n, sin_n) = multiple_angle(0.5_f64, 0.866, 0);
380            assert_relative_eq!(cos_n, 1.0, epsilon = 1e-14);
381            assert_relative_eq!(sin_n, 0.0, epsilon = 1e-14);
382        }
383
384        #[test]
385        fn multiple_angle_n1() {
386            let phi = PI / 4.0;
387            check_multiple_angle(phi, 1);
388        }
389
390        #[test]
391        fn multiple_angle_n2() {
392            let phi = PI / 3.0;
393            check_multiple_angle(phi, 2);
394        }
395
396        #[test]
397        fn multiple_angle_n3() {
398            let phi = PI / 5.0;
399            check_multiple_angle(phi, 3);
400        }
401
402        #[test]
403        fn multiple_angle_n4_chebyshev() {
404            let phi = PI / 6.0;
405            check_multiple_angle(phi, 4);
406        }
407
408        #[test]
409        fn multiple_angle_n6_chebyshev() {
410            let phi = PI / 7.0;
411            check_multiple_angle(phi, 6);
412        }
413    }
414}