1use crate::math::Real;
2use crate::traits::TorsionKernel;
3use crate::types::EnergyDiff;
4
5#[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 #[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 #[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 #[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#[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#[inline(always)]
106fn multiple_angle_0<T: Real>() -> (T, T) {
107 (T::from(1.0f32), T::from(0.0f32))
108}
109
110#[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#[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#[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 const H: f64 = 1e-6;
175 const TOL_DIFF: f64 = 1e-4;
176
177 mod torsion {
182 use super::*;
183
184 const V_HALF: f64 = 2.5; fn params_n1() -> (f64, u8, f64, f64) {
187 (V_HALF, 1, 1.0, 0.0)
189 }
190
191 fn params_n2() -> (f64, u8, f64, f64) {
192 (V_HALF, 2, 1.0, 0.0)
194 }
195
196 fn params_n3() -> (f64, u8, f64, f64) {
197 (V_HALF, 3, 1.0, 0.0)
199 }
200
201 fn params_n4() -> (f64, u8, f64, f64) {
202 (V_HALF, 4, 1.0, 0.0)
204 }
205
206 #[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 #[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 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 #[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 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}