dreid_kernel/potentials/bonded/
angle.rs1use crate::math::Real;
2use crate::traits::AngleKernel;
3use crate::types::EnergyDiff;
4
5#[derive(Clone, Copy, Debug, Default)]
29pub struct CosineHarmonic;
30
31impl<T: Real> AngleKernel<T> for CosineHarmonic {
32 type Params = (T, T);
33
34 #[inline(always)]
40 fn energy(cos_theta: T, (k_half, cos0): Self::Params) -> T {
41 let delta = cos_theta - cos0;
42 k_half * delta * delta
43 }
44
45 #[inline(always)]
54 fn diff(cos_theta: T, (k_half, cos0): Self::Params) -> T {
55 let c = k_half + k_half;
56 c * (cos_theta - cos0)
57 }
58
59 #[inline(always)]
63 fn compute(cos_theta: T, (k_half, cos0): Self::Params) -> EnergyDiff<T> {
64 let delta = cos_theta - cos0;
65
66 let energy = k_half * delta * delta;
67
68 let c = k_half + k_half;
69 let diff = c * delta;
70
71 EnergyDiff { energy, diff }
72 }
73}
74
75#[derive(Clone, Copy, Debug, Default)]
99pub struct CosineLinear;
100
101impl<T: Real> AngleKernel<T> for CosineLinear {
102 type Params = T;
103
104 #[inline(always)]
110 fn energy(cos_theta: T, k: Self::Params) -> T {
111 let one = T::from(1.0f32);
112 k * (one + cos_theta)
113 }
114
115 #[inline(always)]
124 fn diff(_cos_theta: T, k: Self::Params) -> T {
125 k
126 }
127
128 #[inline(always)]
132 fn compute(cos_theta: T, k: Self::Params) -> EnergyDiff<T> {
133 let one = T::from(1.0f32);
134 let energy = k * (one + cos_theta);
135 let diff = k;
136
137 EnergyDiff { energy, diff }
138 }
139}
140
141#[derive(Clone, Copy, Debug, Default)]
165pub struct ThetaHarmonic;
166
167impl<T: Real> AngleKernel<T> for ThetaHarmonic {
168 type Params = (T, T);
169
170 #[inline(always)]
176 fn energy(cos_theta: T, (k_half, theta0): Self::Params) -> T {
177 let one = T::from(1.0f32);
178 let minus_one = T::from(-1.0f32);
179
180 let c = cos_theta.max(minus_one).min(one);
181 let theta = c.acos();
182
183 let delta = theta - theta0;
184
185 k_half * delta * delta
186 }
187
188 #[inline(always)]
197 fn diff(cos_theta: T, (k_half, theta0): Self::Params) -> T {
198 let one = T::from(1.0f32);
199 let minus_one = T::from(-1.0f32);
200 let zero = T::from(0.0f32);
201 let singularity_thresh = T::from(1.0e-4f32);
202 let epsilon = T::from(1.0e-20f32);
203
204 let c = cos_theta.max(minus_one).min(one);
205
206 let theta = c.acos();
207 let sin_theta = (one - c * c).max(zero).sqrt();
208
209 let factor = if sin_theta > singularity_thresh {
210 (theta - theta0) / sin_theta
211 } else {
212 let s_safe = sin_theta.max(epsilon);
213
214 if c > zero {
215 one - theta0 / s_safe
216 } else {
217 let pi = T::pi();
218 minus_one + (pi - theta0) / s_safe
219 }
220 };
221
222 let k = k_half + k_half;
223
224 -k * factor
225 }
226
227 #[inline(always)]
231 fn compute(cos_theta: T, (k_half, theta0): Self::Params) -> EnergyDiff<T> {
232 let one = T::from(1.0f32);
233 let minus_one = T::from(-1.0f32);
234 let zero = T::from(0.0f32);
235 let singularity_thresh = T::from(1.0e-4f32);
236 let epsilon = T::from(1.0e-20f32);
237
238 let c = cos_theta.max(minus_one).min(one);
239 let theta = c.acos();
240 let sin_theta = (one - c * c).max(zero).sqrt();
241
242 let delta = theta - theta0;
243 let energy = k_half * delta * delta;
244
245 let factor = if sin_theta > singularity_thresh {
246 delta / sin_theta
247 } else {
248 let s_safe = sin_theta.max(epsilon);
249 if c > zero {
250 one - theta0 / s_safe
251 } else {
252 let pi = T::pi();
253 minus_one + (pi - theta0) / s_safe
254 }
255 };
256
257 let k = k_half + k_half;
258 let diff = -k * factor;
259
260 EnergyDiff { energy, diff }
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use approx::assert_relative_eq;
268 use std::f64::consts::PI;
269
270 const H: f64 = 1e-6;
275 const TOL_DIFF: f64 = 1e-4;
276
277 mod cosine_harmonic {
282 use super::*;
283
284 const K_HALF: f64 = 50.0; const COS0: f64 = 0.5; fn params() -> (f64, f64) {
288 (K_HALF, COS0)
289 }
290
291 #[test]
296 fn sanity_compute_equals_separate() {
297 let cos_theta = 0.7_f64;
298 let p = params();
299
300 let result = CosineHarmonic::compute(cos_theta, p);
301 let energy_only = CosineHarmonic::energy(cos_theta, p);
302 let diff_only = CosineHarmonic::diff(cos_theta, p);
303
304 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
305 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
306 }
307
308 #[test]
309 fn sanity_f32_f64_consistency() {
310 let cos_theta = 0.6;
311 let p64 = params();
312 let p32 = (K_HALF as f32, COS0 as f32);
313
314 let e64 = CosineHarmonic::energy(cos_theta, p64);
315 let e32 = CosineHarmonic::energy(cos_theta as f32, p32);
316
317 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
318 }
319
320 #[test]
321 fn sanity_equilibrium() {
322 let result = CosineHarmonic::compute(COS0, params());
323
324 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
325 assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
326 }
327
328 #[test]
333 fn stability_cos_one() {
334 let result = CosineHarmonic::compute(1.0, params());
335
336 assert!(result.energy.is_finite());
337 assert!(result.diff.is_finite());
338 }
339
340 #[test]
341 fn stability_cos_minus_one() {
342 let result = CosineHarmonic::compute(-1.0, params());
343
344 assert!(result.energy.is_finite());
345 assert!(result.diff.is_finite());
346 }
347
348 fn finite_diff_check(cos_theta: f64) {
353 let p = params();
354
355 let c_plus = cos_theta + H;
356 let c_minus = cos_theta - H;
357 let e_plus = CosineHarmonic::energy(c_plus, p);
358 let e_minus = CosineHarmonic::energy(c_minus, p);
359 let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
360
361 let gamma = CosineHarmonic::diff(cos_theta, p);
362
363 assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
364 }
365
366 #[test]
367 fn finite_diff_bent() {
368 finite_diff_check(0.3);
369 }
370
371 #[test]
372 fn finite_diff_wide() {
373 finite_diff_check(0.8);
374 }
375
376 #[test]
377 fn finite_diff_near_equilibrium() {
378 finite_diff_check(COS0 + 0.01);
379 }
380
381 #[test]
386 fn specific_quadratic_scaling() {
387 let delta1 = 0.1;
388 let delta2 = 0.2;
389 let e1 = CosineHarmonic::energy(COS0 + delta1, params());
390 let e2 = CosineHarmonic::energy(COS0 + delta2, params());
391
392 assert_relative_eq!(e2 / e1, 4.0, epsilon = 1e-10);
393 }
394
395 #[test]
396 fn specific_no_trig_needed() {
397 let cos_theta = 0.7;
398 let delta = cos_theta - COS0;
399 let expected = K_HALF * delta * delta;
400
401 assert_relative_eq!(
402 CosineHarmonic::energy(cos_theta, params()),
403 expected,
404 epsilon = 1e-14
405 );
406 }
407 }
408
409 mod cosine_linear {
414 use super::*;
415
416 const K: f64 = 100.0; fn params() -> f64 {
419 K
420 }
421
422 #[test]
427 fn sanity_compute_equals_separate() {
428 let cos_theta = 0.3_f64;
429 let p = params();
430
431 let result = CosineLinear::compute(cos_theta, p);
432 let energy_only = CosineLinear::energy(cos_theta, p);
433 let diff_only = CosineLinear::diff(cos_theta, p);
434
435 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
436 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
437 }
438
439 #[test]
440 fn sanity_f32_f64_consistency() {
441 let cos_theta = -0.3;
442 let p64 = params();
443 let p32 = K as f32;
444
445 let e64 = CosineLinear::energy(cos_theta, p64);
446 let e32 = CosineLinear::energy(cos_theta as f32, p32);
447
448 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
449 }
450
451 #[test]
452 fn sanity_equilibrium() {
453 let result = CosineLinear::compute(-1.0, params());
454
455 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
456 assert_relative_eq!(result.diff, K, epsilon = 1e-14);
457 }
458
459 #[test]
464 fn stability_cos_one() {
465 let result = CosineLinear::compute(1.0, params());
466
467 assert!(result.energy.is_finite());
468 assert!(result.diff.is_finite());
469 }
470
471 #[test]
472 fn stability_cos_minus_one() {
473 let result = CosineLinear::compute(-1.0, params());
474
475 assert!(result.energy.is_finite());
476 assert!(result.diff.is_finite());
477 }
478
479 fn finite_diff_check(cos_theta: f64) {
484 let p = params();
485
486 let c_plus = cos_theta + H;
487 let c_minus = cos_theta - H;
488 let e_plus = CosineLinear::energy(c_plus, p);
489 let e_minus = CosineLinear::energy(c_minus, p);
490 let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
491
492 let gamma = CosineLinear::diff(cos_theta, p);
493
494 assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
495 }
496
497 #[test]
498 fn finite_diff_acute() {
499 finite_diff_check(0.3);
500 }
501
502 #[test]
503 fn finite_diff_obtuse() {
504 finite_diff_check(-0.5);
505 }
506
507 #[test]
508 fn finite_diff_near_equilibrium() {
509 finite_diff_check(-0.99);
510 }
511
512 #[test]
517 fn specific_linear_scaling() {
518 let e1 = CosineLinear::energy(0.0, params());
519 let e2 = CosineLinear::energy(-0.5, params());
520
521 assert_relative_eq!(e1 / e2, 2.0, epsilon = 1e-10);
522 }
523
524 #[test]
525 fn specific_constant_derivative() {
526 let p = params();
527
528 assert_relative_eq!(CosineLinear::diff(1.0, p), K, epsilon = 1e-14);
529 assert_relative_eq!(CosineLinear::diff(0.0, p), K, epsilon = 1e-14);
530 assert_relative_eq!(CosineLinear::diff(-0.5, p), K, epsilon = 1e-14);
531 assert_relative_eq!(CosineLinear::diff(-1.0, p), K, epsilon = 1e-14);
532 }
533 }
534
535 mod theta_harmonic {
540 use super::*;
541
542 const K_HALF: f64 = 50.0; const THETA0: f64 = PI / 3.0; const COS0: f64 = 0.5; fn params() -> (f64, f64) {
547 (K_HALF, THETA0)
548 }
549
550 #[test]
555 fn sanity_compute_equals_separate() {
556 let cos_theta = 0.7_f64;
557 let p = params();
558
559 let result = ThetaHarmonic::compute(cos_theta, p);
560 let energy_only = ThetaHarmonic::energy(cos_theta, p);
561 let diff_only = ThetaHarmonic::diff(cos_theta, p);
562
563 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
564 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
565 }
566
567 #[test]
568 fn sanity_f32_f64_consistency() {
569 let cos_theta = 0.6;
570 let p64 = params();
571 let p32 = (K_HALF as f32, THETA0 as f32);
572
573 let e64 = ThetaHarmonic::energy(cos_theta, p64);
574 let e32 = ThetaHarmonic::energy(cos_theta as f32, p32);
575
576 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-4);
577 }
578
579 #[test]
580 fn sanity_equilibrium() {
581 let result = ThetaHarmonic::compute(COS0, params());
582
583 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-10);
584 assert!(result.diff.abs() < 1e-8);
585 }
586
587 #[test]
592 fn stability_theta_zero() {
593 let result = ThetaHarmonic::compute(1.0, params());
594
595 assert!(result.energy.is_finite());
596 assert!(result.diff.is_finite());
597 }
598
599 #[test]
600 fn stability_theta_pi() {
601 let result = ThetaHarmonic::compute(-1.0, params());
602
603 assert!(result.energy.is_finite());
604 assert!(result.diff.is_finite());
605 }
606
607 #[test]
608 fn stability_cos_slightly_outside() {
609 let result = ThetaHarmonic::compute(1.0001, params());
610
611 assert!(result.energy.is_finite());
612 assert!(result.diff.is_finite());
613 }
614
615 fn finite_diff_check(cos_theta: f64) {
620 if cos_theta.abs() > 0.999 {
621 return;
622 }
623
624 let p = params();
625
626 let c_plus = cos_theta + H;
627 let c_minus = cos_theta - H;
628 let e_plus = ThetaHarmonic::energy(c_plus, p);
629 let e_minus = ThetaHarmonic::energy(c_minus, p);
630 let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
631
632 let gamma = ThetaHarmonic::diff(cos_theta, p);
633
634 assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
635 }
636
637 #[test]
638 fn finite_diff_acute() {
639 finite_diff_check(0.7);
640 }
641
642 #[test]
643 fn finite_diff_right() {
644 finite_diff_check(0.0);
645 }
646
647 #[test]
648 fn finite_diff_obtuse() {
649 finite_diff_check(-0.5);
650 }
651
652 #[test]
653 fn finite_diff_near_equilibrium() {
654 finite_diff_check(COS0 + 0.02);
655 }
656
657 #[test]
662 fn specific_energy_formula() {
663 let cos_theta = 0.0;
664 let theta = PI / 2.0;
665 let delta = theta - THETA0;
666 let expected = K_HALF * delta * delta;
667
668 assert_relative_eq!(
669 ThetaHarmonic::energy(cos_theta, params()),
670 expected,
671 epsilon = 1e-10
672 );
673 }
674
675 #[test]
676 fn specific_diff_chain_rule() {
677 let cos_theta = 0.0;
678 let theta = PI / 2.0;
679 let sin_theta = 1.0;
680
681 let k = 2.0 * K_HALF;
682 let expected_gamma = -k * (theta - THETA0) / sin_theta;
683
684 assert_relative_eq!(
685 ThetaHarmonic::diff(cos_theta, params()),
686 expected_gamma,
687 epsilon = 1e-10
688 );
689 }
690 }
691}