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 ThetaHarmonic;
100
101impl<T: Real> AngleKernel<T> for ThetaHarmonic {
102 type Params = (T, T);
103
104 #[inline(always)]
110 fn energy(cos_theta: T, (k_half, theta0): Self::Params) -> T {
111 let one = T::from(1.0f32);
112 let minus_one = T::from(-1.0f32);
113
114 let c = cos_theta.max(minus_one).min(one);
115 let theta = c.acos();
116
117 let delta = theta - theta0;
118
119 k_half * delta * delta
120 }
121
122 #[inline(always)]
131 fn diff(cos_theta: T, (k_half, theta0): Self::Params) -> T {
132 let one = T::from(1.0f32);
133 let minus_one = T::from(-1.0f32);
134 let zero = T::from(0.0f32);
135 let singularity_thresh = T::from(1.0e-4f32);
136 let epsilon = T::from(1.0e-20f32);
137
138 let c = cos_theta.max(minus_one).min(one);
139
140 let theta = c.acos();
141 let sin_theta = (one - c * c).max(zero).sqrt();
142
143 let factor = if sin_theta > singularity_thresh {
144 (theta - theta0) / sin_theta
145 } else {
146 let s_safe = sin_theta.max(epsilon);
147
148 if c > zero {
149 one - theta0 / s_safe
150 } else {
151 let pi = T::pi();
152 minus_one + (pi - theta0) / s_safe
153 }
154 };
155
156 let k = k_half + k_half;
157
158 -k * factor
159 }
160
161 #[inline(always)]
165 fn compute(cos_theta: T, (k_half, theta0): Self::Params) -> EnergyDiff<T> {
166 let one = T::from(1.0f32);
167 let minus_one = T::from(-1.0f32);
168 let zero = T::from(0.0f32);
169 let singularity_thresh = T::from(1.0e-4f32);
170 let epsilon = T::from(1.0e-20f32);
171
172 let c = cos_theta.max(minus_one).min(one);
173 let theta = c.acos();
174 let sin_theta = (one - c * c).max(zero).sqrt();
175
176 let delta = theta - theta0;
177 let energy = k_half * delta * delta;
178
179 let factor = if sin_theta > singularity_thresh {
180 delta / sin_theta
181 } else {
182 let s_safe = sin_theta.max(epsilon);
183 if c > zero {
184 one - theta0 / s_safe
185 } else {
186 let pi = T::pi();
187 minus_one + (pi - theta0) / s_safe
188 }
189 };
190
191 let k = k_half + k_half;
192 let diff = -k * factor;
193
194 EnergyDiff { energy, diff }
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use approx::assert_relative_eq;
202 use std::f64::consts::PI;
203
204 const H: f64 = 1e-6;
209 const TOL_DIFF: f64 = 1e-4;
210
211 mod cosine_harmonic {
216 use super::*;
217
218 const K_HALF: f64 = 50.0; const COS0: f64 = 0.5; fn params() -> (f64, f64) {
222 (K_HALF, COS0)
223 }
224
225 #[test]
230 fn sanity_compute_equals_separate() {
231 let cos_theta = 0.7_f64;
232 let p = params();
233
234 let result = CosineHarmonic::compute(cos_theta, p);
235 let energy_only = CosineHarmonic::energy(cos_theta, p);
236 let diff_only = CosineHarmonic::diff(cos_theta, p);
237
238 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
239 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
240 }
241
242 #[test]
243 fn sanity_f32_f64_consistency() {
244 let cos_theta = 0.6;
245 let p64 = params();
246 let p32 = (K_HALF as f32, COS0 as f32);
247
248 let e64 = CosineHarmonic::energy(cos_theta, p64);
249 let e32 = CosineHarmonic::energy(cos_theta as f32, p32);
250
251 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
252 }
253
254 #[test]
255 fn sanity_equilibrium() {
256 let result = CosineHarmonic::compute(COS0, params());
257
258 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
259 assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
260 }
261
262 #[test]
267 fn stability_cos_one() {
268 let result = CosineHarmonic::compute(1.0, params());
269
270 assert!(result.energy.is_finite());
271 assert!(result.diff.is_finite());
272 }
273
274 #[test]
275 fn stability_cos_minus_one() {
276 let result = CosineHarmonic::compute(-1.0, params());
277
278 assert!(result.energy.is_finite());
279 assert!(result.diff.is_finite());
280 }
281
282 fn finite_diff_check(cos_theta: f64) {
287 let p = params();
288
289 let c_plus = cos_theta + H;
290 let c_minus = cos_theta - H;
291 let e_plus = CosineHarmonic::energy(c_plus, p);
292 let e_minus = CosineHarmonic::energy(c_minus, p);
293 let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
294
295 let gamma = CosineHarmonic::diff(cos_theta, p);
296
297 assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
298 }
299
300 #[test]
301 fn finite_diff_bent() {
302 finite_diff_check(0.3);
303 }
304
305 #[test]
306 fn finite_diff_wide() {
307 finite_diff_check(0.8);
308 }
309
310 #[test]
311 fn finite_diff_near_equilibrium() {
312 finite_diff_check(COS0 + 0.01);
313 }
314
315 #[test]
320 fn specific_quadratic_scaling() {
321 let delta1 = 0.1;
322 let delta2 = 0.2;
323 let e1 = CosineHarmonic::energy(COS0 + delta1, params());
324 let e2 = CosineHarmonic::energy(COS0 + delta2, params());
325
326 assert_relative_eq!(e2 / e1, 4.0, epsilon = 1e-10);
327 }
328
329 #[test]
330 fn specific_no_trig_needed() {
331 let cos_theta = 0.7;
332 let delta = cos_theta - COS0;
333 let expected = K_HALF * delta * delta;
334
335 assert_relative_eq!(
336 CosineHarmonic::energy(cos_theta, params()),
337 expected,
338 epsilon = 1e-14
339 );
340 }
341 }
342
343 mod theta_harmonic {
348 use super::*;
349
350 const K_HALF: f64 = 50.0; const THETA0: f64 = PI / 3.0; const COS0: f64 = 0.5; fn params() -> (f64, f64) {
355 (K_HALF, THETA0)
356 }
357
358 #[test]
363 fn sanity_compute_equals_separate() {
364 let cos_theta = 0.7_f64;
365 let p = params();
366
367 let result = ThetaHarmonic::compute(cos_theta, p);
368 let energy_only = ThetaHarmonic::energy(cos_theta, p);
369 let diff_only = ThetaHarmonic::diff(cos_theta, p);
370
371 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
372 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
373 }
374
375 #[test]
376 fn sanity_f32_f64_consistency() {
377 let cos_theta = 0.6;
378 let p64 = params();
379 let p32 = (K_HALF as f32, THETA0 as f32);
380
381 let e64 = ThetaHarmonic::energy(cos_theta, p64);
382 let e32 = ThetaHarmonic::energy(cos_theta as f32, p32);
383
384 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-4);
385 }
386
387 #[test]
388 fn sanity_equilibrium() {
389 let result = ThetaHarmonic::compute(COS0, params());
390
391 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-10);
392 assert!(result.diff.abs() < 1e-8);
393 }
394
395 #[test]
400 fn stability_theta_zero() {
401 let result = ThetaHarmonic::compute(1.0, params());
402
403 assert!(result.energy.is_finite());
404 assert!(result.diff.is_finite());
405 }
406
407 #[test]
408 fn stability_theta_pi() {
409 let result = ThetaHarmonic::compute(-1.0, params());
410
411 assert!(result.energy.is_finite());
412 assert!(result.diff.is_finite());
413 }
414
415 #[test]
416 fn stability_cos_slightly_outside() {
417 let result = ThetaHarmonic::compute(1.0001, params());
418
419 assert!(result.energy.is_finite());
420 assert!(result.diff.is_finite());
421 }
422
423 fn finite_diff_check(cos_theta: f64) {
428 if cos_theta.abs() > 0.999 {
429 return;
430 }
431
432 let p = params();
433
434 let c_plus = cos_theta + H;
435 let c_minus = cos_theta - H;
436 let e_plus = ThetaHarmonic::energy(c_plus, p);
437 let e_minus = ThetaHarmonic::energy(c_minus, p);
438 let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
439
440 let gamma = ThetaHarmonic::diff(cos_theta, p);
441
442 assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
443 }
444
445 #[test]
446 fn finite_diff_acute() {
447 finite_diff_check(0.7);
448 }
449
450 #[test]
451 fn finite_diff_right() {
452 finite_diff_check(0.0);
453 }
454
455 #[test]
456 fn finite_diff_obtuse() {
457 finite_diff_check(-0.5);
458 }
459
460 #[test]
461 fn finite_diff_near_equilibrium() {
462 finite_diff_check(COS0 + 0.02);
463 }
464
465 #[test]
470 fn specific_energy_formula() {
471 let cos_theta = 0.0;
472 let theta = PI / 2.0;
473 let delta = theta - THETA0;
474 let expected = K_HALF * delta * delta;
475
476 assert_relative_eq!(
477 ThetaHarmonic::energy(cos_theta, params()),
478 expected,
479 epsilon = 1e-10
480 );
481 }
482
483 #[test]
484 fn specific_diff_chain_rule() {
485 let cos_theta = 0.0;
486 let theta = PI / 2.0;
487 let sin_theta = 1.0;
488
489 let k = 2.0 * K_HALF;
490 let expected_gamma = -k * (theta - THETA0) / sin_theta;
491
492 assert_relative_eq!(
493 ThetaHarmonic::diff(cos_theta, params()),
494 expected_gamma,
495 epsilon = 1e-10
496 );
497 }
498 }
499}