dreid_kernel/potentials/bonded/
angle.rs1use crate::math::Real;
2use crate::traits::AngleKernel;
3use crate::types::EnergyDiff;
4
5#[derive(Clone, Copy, Debug, Default)]
34pub struct CosineHarmonic;
35
36impl CosineHarmonic {
37 #[inline(always)]
54 pub fn precompute<T: Real>(c: T, theta0_deg: T) -> (T, T) {
55 let deg_to_rad = T::pi() / T::from(180.0);
56 let theta0 = theta0_deg * deg_to_rad;
57 let c_half = c * T::from(0.5);
58 let cos0 = theta0.cos();
59 (c_half, cos0)
60 }
61}
62
63impl<T: Real> AngleKernel<T> for CosineHarmonic {
64 type Params = (T, T);
65
66 #[inline(always)]
72 fn energy(cos_theta: T, (c_half, cos0): Self::Params) -> T {
73 let delta = cos_theta - cos0;
74 c_half * delta * delta
75 }
76
77 #[inline(always)]
86 fn diff(cos_theta: T, (c_half, cos0): Self::Params) -> T {
87 let c = c_half + c_half;
88 c * (cos_theta - cos0)
89 }
90
91 #[inline(always)]
95 fn compute(cos_theta: T, (c_half, cos0): Self::Params) -> EnergyDiff<T> {
96 let delta = cos_theta - cos0;
97
98 let energy = c_half * delta * delta;
99
100 let c = c_half + c_half;
101 let diff = c * delta;
102
103 EnergyDiff { energy, diff }
104 }
105}
106
107#[derive(Clone, Copy, Debug, Default)]
131pub struct CosineLinear;
132
133impl<T: Real> AngleKernel<T> for CosineLinear {
134 type Params = T;
135
136 #[inline(always)]
142 fn energy(cos_theta: T, c: Self::Params) -> T {
143 let one = T::from(1.0f32);
144 c * (one + cos_theta)
145 }
146
147 #[inline(always)]
156 fn diff(_cos_theta: T, c: Self::Params) -> T {
157 c
158 }
159
160 #[inline(always)]
164 fn compute(cos_theta: T, c: Self::Params) -> EnergyDiff<T> {
165 let one = T::from(1.0f32);
166 let energy = c * (one + cos_theta);
167 let diff = c;
168
169 EnergyDiff { energy, diff }
170 }
171}
172
173#[derive(Clone, Copy, Debug, Default)]
202pub struct ThetaHarmonic;
203
204impl ThetaHarmonic {
205 #[inline(always)]
222 pub fn precompute<T: Real>(k: T, theta0_deg: T) -> (T, T) {
223 let deg_to_rad = T::pi() / T::from(180.0);
224 let k_half = k * T::from(0.5);
225 let theta0 = theta0_deg * deg_to_rad;
226 (k_half, theta0)
227 }
228}
229
230impl<T: Real> AngleKernel<T> for ThetaHarmonic {
231 type Params = (T, T);
232
233 #[inline(always)]
239 fn energy(cos_theta: T, (k_half, theta0): Self::Params) -> T {
240 let one = T::from(1.0f32);
241 let minus_one = T::from(-1.0f32);
242
243 let c = cos_theta.max(minus_one).min(one);
244 let theta = c.acos();
245
246 let delta = theta - theta0;
247
248 k_half * delta * delta
249 }
250
251 #[inline(always)]
260 fn diff(cos_theta: T, (k_half, theta0): Self::Params) -> T {
261 let one = T::from(1.0f32);
262 let minus_one = T::from(-1.0f32);
263 let zero = T::from(0.0f32);
264 let singularity_thresh = T::from(1.0e-4f32);
265 let epsilon = T::from(1.0e-20f32);
266
267 let c = cos_theta.max(minus_one).min(one);
268
269 let theta = c.acos();
270 let sin_theta = (one - c * c).max(zero).sqrt();
271
272 let factor = if sin_theta > singularity_thresh {
273 (theta - theta0) / sin_theta
274 } else {
275 let s_safe = sin_theta.max(epsilon);
276
277 if c > zero {
278 one - theta0 / s_safe
279 } else {
280 let pi = T::pi();
281 minus_one + (pi - theta0) / s_safe
282 }
283 };
284
285 let k = k_half + k_half;
286
287 -k * factor
288 }
289
290 #[inline(always)]
294 fn compute(cos_theta: T, (k_half, theta0): Self::Params) -> EnergyDiff<T> {
295 let one = T::from(1.0f32);
296 let minus_one = T::from(-1.0f32);
297 let zero = T::from(0.0f32);
298 let singularity_thresh = T::from(1.0e-4f32);
299 let epsilon = T::from(1.0e-20f32);
300
301 let c = cos_theta.max(minus_one).min(one);
302 let theta = c.acos();
303 let sin_theta = (one - c * c).max(zero).sqrt();
304
305 let delta = theta - theta0;
306 let energy = k_half * delta * delta;
307
308 let factor = if sin_theta > singularity_thresh {
309 delta / sin_theta
310 } else {
311 let s_safe = sin_theta.max(epsilon);
312 if c > zero {
313 one - theta0 / s_safe
314 } else {
315 let pi = T::pi();
316 minus_one + (pi - theta0) / s_safe
317 }
318 };
319
320 let k = k_half + k_half;
321 let diff = -k * factor;
322
323 EnergyDiff { energy, diff }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use approx::assert_relative_eq;
331 use std::f64::consts::PI;
332
333 const H: f64 = 1e-6;
338 const TOL_DIFF: f64 = 1e-4;
339
340 mod cosine_harmonic {
345 use super::*;
346
347 const C: f64 = 100.0;
348 const THETA0_DEG: f64 = 60.0;
349 const C_HALF: f64 = C / 2.0;
350 const COS0: f64 = 0.5;
351
352 fn params() -> (f64, f64) {
353 CosineHarmonic::precompute(C, THETA0_DEG)
354 }
355
356 #[test]
361 fn sanity_compute_equals_separate() {
362 let cos_theta = 0.7_f64;
363 let p = params();
364
365 let result = CosineHarmonic::compute(cos_theta, p);
366 let energy_only = CosineHarmonic::energy(cos_theta, p);
367 let diff_only = CosineHarmonic::diff(cos_theta, p);
368
369 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
370 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
371 }
372
373 #[test]
374 fn sanity_f32_f64_consistency() {
375 let cos_theta = 0.6;
376 let p64 = params();
377 let p32 = CosineHarmonic::precompute(C as f32, THETA0_DEG as f32);
378
379 let e64 = CosineHarmonic::energy(cos_theta, p64);
380 let e32 = CosineHarmonic::energy(cos_theta as f32, p32);
381
382 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
383 }
384
385 #[test]
386 fn sanity_equilibrium() {
387 let result = CosineHarmonic::compute(COS0, params());
388
389 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-12);
390 assert_relative_eq!(result.diff, 0.0, epsilon = 1e-12);
391 }
392
393 #[test]
398 fn stability_cos_one() {
399 let result = CosineHarmonic::compute(1.0, params());
400
401 assert!(result.energy.is_finite());
402 assert!(result.diff.is_finite());
403 }
404
405 #[test]
406 fn stability_cos_minus_one() {
407 let result = CosineHarmonic::compute(-1.0, params());
408
409 assert!(result.energy.is_finite());
410 assert!(result.diff.is_finite());
411 }
412
413 fn finite_diff_check(cos_theta: f64) {
418 let p = params();
419
420 let c_plus = cos_theta + H;
421 let c_minus = cos_theta - H;
422 let e_plus = CosineHarmonic::energy(c_plus, p);
423 let e_minus = CosineHarmonic::energy(c_minus, p);
424 let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
425
426 let gamma = CosineHarmonic::diff(cos_theta, p);
427
428 assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
429 }
430
431 #[test]
432 fn finite_diff_bent() {
433 finite_diff_check(0.3);
434 }
435
436 #[test]
437 fn finite_diff_wide() {
438 finite_diff_check(0.8);
439 }
440
441 #[test]
442 fn finite_diff_near_equilibrium() {
443 finite_diff_check(COS0 + 0.01);
444 }
445
446 #[test]
451 fn specific_quadratic_scaling() {
452 let delta1 = 0.1;
453 let delta2 = 0.2;
454 let e1 = CosineHarmonic::energy(COS0 + delta1, params());
455 let e2 = CosineHarmonic::energy(COS0 + delta2, params());
456
457 assert_relative_eq!(e2 / e1, 4.0, epsilon = 1e-10);
458 }
459
460 #[test]
461 fn specific_no_trig_needed() {
462 let cos_theta = 0.7;
463 let delta = cos_theta - COS0;
464 let expected = C_HALF * delta * delta;
465
466 assert_relative_eq!(
467 CosineHarmonic::energy(cos_theta, params()),
468 expected,
469 epsilon = 1e-14
470 );
471 }
472
473 #[test]
478 fn precompute_values() {
479 let (c_half, cos0) = CosineHarmonic::precompute(C, THETA0_DEG);
480 assert_relative_eq!(c_half, C_HALF, epsilon = 1e-14);
481 assert_relative_eq!(cos0, COS0, epsilon = 1e-10);
482 }
483
484 #[test]
485 fn precompute_round_trip() {
486 let p = CosineHarmonic::precompute(C, THETA0_DEG);
487 let e = CosineHarmonic::energy(COS0, p);
488 assert_relative_eq!(e, 0.0, epsilon = 1e-10);
489 }
490 }
491
492 mod cosine_linear {
497 use super::*;
498
499 const C: f64 = 100.0;
500
501 fn params() -> f64 {
502 C
503 }
504
505 #[test]
510 fn sanity_compute_equals_separate() {
511 let cos_theta = 0.3_f64;
512 let p = params();
513
514 let result = CosineLinear::compute(cos_theta, p);
515 let energy_only = CosineLinear::energy(cos_theta, p);
516 let diff_only = CosineLinear::diff(cos_theta, p);
517
518 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
519 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
520 }
521
522 #[test]
523 fn sanity_f32_f64_consistency() {
524 let cos_theta = -0.3;
525 let p64 = params();
526 let p32 = C as f32;
527
528 let e64 = CosineLinear::energy(cos_theta, p64);
529 let e32 = CosineLinear::energy(cos_theta as f32, p32);
530
531 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
532 }
533
534 #[test]
535 fn sanity_equilibrium() {
536 let result = CosineLinear::compute(-1.0, params());
537
538 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
539 assert_relative_eq!(result.diff, C, epsilon = 1e-14);
540 }
541
542 #[test]
547 fn stability_cos_one() {
548 let result = CosineLinear::compute(1.0, params());
549
550 assert!(result.energy.is_finite());
551 assert!(result.diff.is_finite());
552 }
553
554 #[test]
555 fn stability_cos_minus_one() {
556 let result = CosineLinear::compute(-1.0, params());
557
558 assert!(result.energy.is_finite());
559 assert!(result.diff.is_finite());
560 }
561
562 fn finite_diff_check(cos_theta: f64) {
567 let p = params();
568
569 let c_plus = cos_theta + H;
570 let c_minus = cos_theta - H;
571 let e_plus = CosineLinear::energy(c_plus, p);
572 let e_minus = CosineLinear::energy(c_minus, p);
573 let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
574
575 let gamma = CosineLinear::diff(cos_theta, p);
576
577 assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
578 }
579
580 #[test]
581 fn finite_diff_acute() {
582 finite_diff_check(0.3);
583 }
584
585 #[test]
586 fn finite_diff_obtuse() {
587 finite_diff_check(-0.5);
588 }
589
590 #[test]
591 fn finite_diff_near_equilibrium() {
592 finite_diff_check(-0.99);
593 }
594
595 #[test]
600 fn specific_linear_scaling() {
601 let e1 = CosineLinear::energy(0.0, params());
602 let e2 = CosineLinear::energy(-0.5, params());
603
604 assert_relative_eq!(e1 / e2, 2.0, epsilon = 1e-10);
605 }
606
607 #[test]
608 fn specific_constant_derivative() {
609 let p = params();
610
611 assert_relative_eq!(CosineLinear::diff(1.0, p), C, epsilon = 1e-14);
612 assert_relative_eq!(CosineLinear::diff(0.0, p), C, epsilon = 1e-14);
613 assert_relative_eq!(CosineLinear::diff(-0.5, p), C, epsilon = 1e-14);
614 assert_relative_eq!(CosineLinear::diff(-1.0, p), C, epsilon = 1e-14);
615 }
616 }
617
618 mod theta_harmonic {
623 use super::*;
624
625 const K: f64 = 100.0;
626 const THETA0_DEG: f64 = 60.0;
627 const K_HALF: f64 = K / 2.0;
628 const THETA0: f64 = PI / 3.0;
629 const COS0: f64 = 0.5;
630
631 fn params() -> (f64, f64) {
632 ThetaHarmonic::precompute(K, THETA0_DEG)
633 }
634
635 #[test]
640 fn sanity_compute_equals_separate() {
641 let cos_theta = 0.7_f64;
642 let p = params();
643
644 let result = ThetaHarmonic::compute(cos_theta, p);
645 let energy_only = ThetaHarmonic::energy(cos_theta, p);
646 let diff_only = ThetaHarmonic::diff(cos_theta, p);
647
648 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
649 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
650 }
651
652 #[test]
653 fn sanity_f32_f64_consistency() {
654 let cos_theta = 0.6;
655 let p64 = params();
656 let p32 = ThetaHarmonic::precompute(K as f32, THETA0_DEG as f32);
657
658 let e64 = ThetaHarmonic::energy(cos_theta, p64);
659 let e32 = ThetaHarmonic::energy(cos_theta as f32, p32);
660
661 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-4);
662 }
663
664 #[test]
665 fn sanity_equilibrium() {
666 let result = ThetaHarmonic::compute(COS0, params());
667
668 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-10);
669 assert!(result.diff.abs() < 1e-8);
670 }
671
672 #[test]
677 fn stability_theta_zero() {
678 let result = ThetaHarmonic::compute(1.0, params());
679
680 assert!(result.energy.is_finite());
681 assert!(result.diff.is_finite());
682 }
683
684 #[test]
685 fn stability_theta_pi() {
686 let result = ThetaHarmonic::compute(-1.0, params());
687
688 assert!(result.energy.is_finite());
689 assert!(result.diff.is_finite());
690 }
691
692 #[test]
693 fn stability_cos_slightly_outside() {
694 let result = ThetaHarmonic::compute(1.0001, params());
695
696 assert!(result.energy.is_finite());
697 assert!(result.diff.is_finite());
698 }
699
700 fn finite_diff_check(cos_theta: f64) {
705 if cos_theta.abs() > 0.999 {
706 return;
707 }
708
709 let p = params();
710
711 let c_plus = cos_theta + H;
712 let c_minus = cos_theta - H;
713 let e_plus = ThetaHarmonic::energy(c_plus, p);
714 let e_minus = ThetaHarmonic::energy(c_minus, p);
715 let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
716
717 let gamma = ThetaHarmonic::diff(cos_theta, p);
718
719 assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
720 }
721
722 #[test]
723 fn finite_diff_acute() {
724 finite_diff_check(0.7);
725 }
726
727 #[test]
728 fn finite_diff_right() {
729 finite_diff_check(0.0);
730 }
731
732 #[test]
733 fn finite_diff_obtuse() {
734 finite_diff_check(-0.5);
735 }
736
737 #[test]
738 fn finite_diff_near_equilibrium() {
739 finite_diff_check(COS0 + 0.02);
740 }
741
742 #[test]
747 fn specific_energy_formula() {
748 let cos_theta = 0.0;
749 let theta = PI / 2.0;
750 let delta = theta - THETA0;
751 let expected = K_HALF * delta * delta;
752
753 assert_relative_eq!(
754 ThetaHarmonic::energy(cos_theta, params()),
755 expected,
756 epsilon = 1e-10
757 );
758 }
759
760 #[test]
761 fn specific_diff_chain_rule() {
762 let cos_theta = 0.0;
763 let theta = PI / 2.0;
764 let sin_theta = 1.0;
765
766 let k = 2.0 * K_HALF;
767 let expected_gamma = -k * (theta - THETA0) / sin_theta;
768
769 assert_relative_eq!(
770 ThetaHarmonic::diff(cos_theta, params()),
771 expected_gamma,
772 epsilon = 1e-10
773 );
774 }
775
776 #[test]
781 fn precompute_values() {
782 let (k_half, theta0) = ThetaHarmonic::precompute(K, THETA0_DEG);
783 assert_relative_eq!(k_half, K_HALF, epsilon = 1e-14);
784 assert_relative_eq!(theta0, THETA0, epsilon = 1e-10);
785 }
786
787 #[test]
788 fn precompute_round_trip() {
789 let p = ThetaHarmonic::precompute(K, THETA0_DEG);
790 let e = ThetaHarmonic::energy(COS0, p);
791 assert_relative_eq!(e, 0.0, epsilon = 1e-10);
792 }
793 }
794}