1use crate::math::Real;
2use crate::traits::TorsionKernel;
3use crate::types::EnergyDiff;
4
5#[derive(Clone, Copy, Debug, Default)]
38pub struct Torsion;
39
40impl Torsion {
41 #[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 #[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 #[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 #[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#[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#[inline(always)]
143fn multiple_angle_0<T: Real>() -> (T, T) {
144 (T::from(1.0f32), T::from(0.0f32))
145}
146
147#[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#[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#[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 const H: f64 = 1e-6;
212 const TOL_DIFF: f64 = 1e-4;
213
214 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 #[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 #[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 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 #[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 #[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 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}