Skip to main content

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