dreid_kernel/potentials/bonded/
stretch.rs1use crate::math::Real;
2use crate::traits::PairKernel;
3use crate::types::EnergyDiff;
4
5#[derive(Clone, Copy, Debug, Default)]
34pub struct Harmonic;
35
36impl Harmonic {
37 #[inline(always)]
54 pub fn precompute<T: Real>(k: T, r0: T) -> (T, T) {
55 let k_half = k * T::from(0.5);
56 (k_half, r0)
57 }
58}
59
60impl<T: Real> PairKernel<T> for Harmonic {
61 type Params = (T, T);
62
63 #[inline(always)]
69 fn energy(r_sq: T, (k_half, r0): Self::Params) -> T {
70 let r = r_sq.sqrt();
71 let delta = r - r0;
72 k_half * delta * delta
73 }
74
75 #[inline(always)]
84 fn diff(r_sq: T, (k_half, r0): Self::Params) -> T {
85 let inv_r = r_sq.rsqrt();
86 let r = r_sq * inv_r;
87 let delta = r - r0;
88
89 let k = k_half + k_half;
90
91 -k * delta * inv_r
92 }
93
94 #[inline(always)]
98 fn compute(r_sq: T, (k_half, r0): Self::Params) -> EnergyDiff<T> {
99 let inv_r = r_sq.rsqrt();
100 let r = r_sq * inv_r;
101 let delta = r - r0;
102
103 let energy = k_half * delta * delta;
104
105 let k = k_half + k_half;
106 let diff = -k * delta * inv_r;
107
108 EnergyDiff { energy, diff }
109 }
110}
111
112#[derive(Clone, Copy, Debug, Default)]
136pub struct Morse;
137
138impl<T: Real> PairKernel<T> for Morse {
139 type Params = (T, T, T);
140
141 #[inline(always)]
147 fn energy(r_sq: T, (de, r0, alpha): Self::Params) -> T {
148 let r = r_sq.sqrt();
149 let t_val = T::exp(-alpha * (r - r0));
150 let term = t_val - T::from(1.0);
151
152 de * term * term
153 }
154
155 #[inline(always)]
164 fn diff(r_sq: T, (de, r0, alpha): Self::Params) -> T {
165 let inv_r = r_sq.rsqrt();
166 let r = r_sq * inv_r;
167
168 let t_val = T::exp(-alpha * (r - r0));
169 let term_minus_one = t_val - T::from(1.0);
170
171 let f_mag = T::from(2.0) * alpha * de * t_val * term_minus_one;
172
173 f_mag * inv_r
174 }
175
176 #[inline(always)]
180 fn compute(r_sq: T, (de, r0, alpha): Self::Params) -> EnergyDiff<T> {
181 let inv_r = r_sq.rsqrt();
182 let r = r_sq * inv_r;
183
184 let t_val = T::exp(-alpha * (r - r0));
185 let term_minus_one = t_val - T::from(1.0);
186
187 let energy = de * term_minus_one * term_minus_one;
188
189 let f_mag = T::from(2.0) * alpha * de * t_val * term_minus_one;
190 let diff = f_mag * inv_r;
191
192 EnergyDiff { energy, diff }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use approx::assert_relative_eq;
200
201 const H: f64 = 1e-6;
206 const TOL_DIFF: f64 = 1e-4;
207
208 mod harmonic {
213 use super::*;
214
215 const K: f64 = 700.0;
216 const R0: f64 = 1.5;
217 const K_HALF: f64 = K / 2.0;
218
219 fn params() -> (f64, f64) {
220 Harmonic::precompute(K, R0)
221 }
222
223 #[test]
228 fn sanity_compute_equals_separate() {
229 let r_sq = 2.25_f64;
230 let p = params();
231
232 let result = Harmonic::compute(r_sq, p);
233 let energy_only = Harmonic::energy(r_sq, p);
234 let diff_only = Harmonic::diff(r_sq, p);
235
236 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
237 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
238 }
239
240 #[test]
241 fn sanity_f32_f64_consistency() {
242 let r_sq = 3.0;
243 let p64 = params();
244 let p32 = Harmonic::precompute(K as f32, R0 as f32);
245
246 let e64 = Harmonic::energy(r_sq, p64);
247 let e32 = Harmonic::energy(r_sq as f32, p32);
248
249 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-4);
250 }
251
252 #[test]
253 fn sanity_equilibrium() {
254 let r0_sq = R0 * R0;
255 let result = Harmonic::compute(r0_sq, params());
256
257 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
258 assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
259 }
260
261 #[test]
266 fn stability_stretched() {
267 let r = 3.0;
268 let result = Harmonic::compute(r * r, params());
269
270 assert!(result.energy.is_finite());
271 assert!(result.diff.is_finite());
272 assert!(result.energy > 0.0);
273 }
274
275 #[test]
276 fn stability_compressed() {
277 let r = 0.5;
278 let result = Harmonic::compute(r * r, params());
279
280 assert!(result.energy.is_finite());
281 assert!(result.diff.is_finite());
282 assert!(result.energy > 0.0);
283 }
284
285 fn finite_diff_check(r: f64) {
290 let p = params();
291 let r_sq = r * r;
292
293 let r_plus = r + H;
294 let r_minus = r - H;
295 let e_plus = Harmonic::energy(r_plus * r_plus, p);
296 let e_minus = Harmonic::energy(r_minus * r_minus, p);
297 let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
298
299 let d = Harmonic::diff(r_sq, p);
300 let de_dr_analytic = -d * r;
301
302 assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
303 }
304
305 #[test]
306 fn finite_diff_stretched() {
307 finite_diff_check(2.0);
308 }
309
310 #[test]
311 fn finite_diff_compressed() {
312 finite_diff_check(1.0);
313 }
314
315 #[test]
316 fn finite_diff_near_equilibrium() {
317 finite_diff_check(R0 + 0.01);
318 }
319
320 #[test]
325 fn specific_quadratic_scaling() {
326 let delta1 = 0.1;
327 let delta2 = 0.2;
328 let e1 = Harmonic::energy((R0 + delta1).powi(2), params());
329 let e2 = Harmonic::energy((R0 + delta2).powi(2), params());
330
331 assert_relative_eq!(e2 / e1, 4.0, epsilon = 1e-10);
332 }
333
334 #[test]
335 fn specific_force_restoring() {
336 let d_stretched = Harmonic::diff((R0 + 0.5).powi(2), params());
337 let d_compressed = Harmonic::diff((R0 - 0.5).powi(2), params());
338
339 assert!(d_stretched < 0.0);
340 assert!(d_compressed > 0.0);
341 }
342
343 #[test]
348 fn precompute_values() {
349 let (k_half, r0) = Harmonic::precompute(K, R0);
350 assert_relative_eq!(k_half, K_HALF, epsilon = 1e-14);
351 assert_relative_eq!(r0, R0, epsilon = 1e-14);
352 }
353
354 #[test]
355 fn precompute_round_trip() {
356 let p = Harmonic::precompute(K, R0);
357 let e = Harmonic::energy(R0 * R0, p);
358 assert_relative_eq!(e, 0.0, epsilon = 1e-14);
359 }
360 }
361
362 mod morse {
367 use super::*;
368
369 const DE: f64 = 70.0;
370 const R0: f64 = 1.5;
371 const ALPHA: f64 = 2.0;
372
373 fn params() -> (f64, f64, f64) {
374 (DE, R0, ALPHA)
375 }
376
377 #[test]
382 fn sanity_compute_equals_separate() {
383 let r_sq = 2.0_f64;
384 let p = params();
385
386 let result = Morse::compute(r_sq, p);
387 let energy_only = Morse::energy(r_sq, p);
388 let diff_only = Morse::diff(r_sq, p);
389
390 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
391 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
392 }
393
394 #[test]
395 fn sanity_f32_f64_consistency() {
396 let r_sq = 3.0;
397 let p64 = params();
398 let p32 = (DE as f32, R0 as f32, ALPHA as f32);
399
400 let e64 = Morse::energy(r_sq, p64);
401 let e32 = Morse::energy(r_sq as f32, p32);
402
403 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-4);
404 }
405
406 #[test]
407 fn sanity_equilibrium() {
408 let r0_sq = R0 * R0;
409 let result = Morse::compute(r0_sq, params());
410
411 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
412 assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
413 }
414
415 #[test]
420 fn stability_large_stretch() {
421 let r = 10.0;
422 let result = Morse::compute(r * r, params());
423
424 assert!(result.energy.is_finite());
425 assert!(result.diff.is_finite());
426 assert_relative_eq!(result.energy, DE, epsilon = 1e-3);
427 }
428
429 #[test]
430 fn stability_compressed() {
431 let r = 0.5;
432 let result = Morse::compute(r * r, params());
433
434 assert!(result.energy.is_finite());
435 assert!(result.diff.is_finite());
436 assert!(result.energy > DE);
437 }
438
439 fn finite_diff_check(r: f64) {
444 let p = params();
445 let r_sq = r * r;
446
447 let r_plus = r + H;
448 let r_minus = r - H;
449 let e_plus = Morse::energy(r_plus * r_plus, p);
450 let e_minus = Morse::energy(r_minus * r_minus, p);
451 let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
452
453 let d = Morse::diff(r_sq, p);
454 let de_dr_analytic = -d * r;
455
456 assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
457 }
458
459 #[test]
460 fn finite_diff_stretched() {
461 finite_diff_check(2.5);
462 }
463
464 #[test]
465 fn finite_diff_compressed() {
466 finite_diff_check(1.0);
467 }
468
469 #[test]
470 fn finite_diff_near_equilibrium() {
471 finite_diff_check(R0 + 0.01);
472 }
473
474 #[test]
479 fn specific_dissociation_limit() {
480 let e_far = Morse::energy(100.0_f64.powi(2), params());
481 assert_relative_eq!(e_far, DE, epsilon = 1e-6);
482 }
483
484 #[test]
485 fn specific_anharmonic_asymmetry() {
486 let delta = 0.3;
487 let e_stretch = Morse::energy((R0 + delta).powi(2), params());
488 let e_compress = Morse::energy((R0 - delta).powi(2), params());
489
490 assert!(e_compress > e_stretch);
491 }
492
493 #[test]
494 fn specific_force_restoring() {
495 let d_stretched = Morse::diff((R0 + 0.5).powi(2), params());
496 let d_compressed = Morse::diff((R0 - 0.3).powi(2), params());
497
498 assert!(d_stretched < 0.0);
499 assert!(d_compressed > 0.0);
500 }
501 }
502}