1use crate::math::Real;
2use crate::traits::PairKernel;
3use crate::types::EnergyDiff;
4
5#[derive(Clone, Copy, Debug, Default)]
35pub struct LennardJones;
36
37impl LennardJones {
38 #[inline(always)]
55 pub fn precompute<T: Real>(d0: T, r0: T) -> (T, T) {
56 (d0, r0 * r0)
57 }
58}
59
60impl<T: Real> PairKernel<T> for LennardJones {
61 type Params = (T, T);
62
63 #[inline(always)]
69 fn energy(r_sq: T, (d0, r0_sq): Self::Params) -> T {
70 let s = r0_sq * r_sq.recip();
71 let s3 = s * s * s;
72 let s6 = s3 * s3;
73
74 d0 * (s6 - T::from(2.0) * s3)
75 }
76
77 #[inline(always)]
86 fn diff(r_sq: T, (d0, r0_sq): Self::Params) -> T {
87 let inv_r2 = r_sq.recip();
88 let s = r0_sq * inv_r2;
89 let s3 = s * s * s;
90 let s6 = s3 * s3;
91
92 T::from(12.0) * d0 * inv_r2 * (s6 - s3)
93 }
94
95 #[inline(always)]
99 fn compute(r_sq: T, (d0, r0_sq): Self::Params) -> EnergyDiff<T> {
100 let inv_r2 = r_sq.recip();
101 let s = r0_sq * inv_r2;
102 let s3 = s * s * s;
103 let s6 = s3 * s3;
104
105 let energy = d0 * (s6 - T::from(2.0) * s3);
106
107 let diff = T::from(12.0) * d0 * inv_r2 * (s6 - s3);
108
109 EnergyDiff { energy, diff }
110 }
111}
112
113#[derive(Clone, Copy, Debug, Default)]
156pub struct Buckingham;
157
158impl Buckingham {
159 #[inline(always)]
182 pub fn precompute<T: Real>(d0: T, r0: T, zeta: T) -> (T, T, T, T, T) {
183 let six = T::from(6.0);
184 let zeta_minus_6 = zeta - six;
185
186 let r0_2 = r0 * r0;
187 let r0_3 = r0_2 * r0;
188 let r0_6 = r0_3 * r0_3;
189
190 let a = six * d0 * T::exp(zeta) / zeta_minus_6;
191 let b = zeta / r0;
192 let c = zeta * d0 * r0_6 / zeta_minus_6;
193
194 let mut lo = T::from(0.0);
195 let mut hi = T::from(7.0) / b;
196 for _ in 0..52 {
197 let mid = (lo + hi) * T::from(0.5);
198 let r2 = mid * mid;
199 let r3 = r2 * mid;
200 let r6 = r3 * r3;
201 let r7 = r6 * mid;
202 let g = a * b * T::exp(-b * mid) * r7 - six * c;
203 if g < T::from(0.0) {
204 lo = mid;
205 } else {
206 hi = mid;
207 }
208 }
209 let r = (lo + hi) * T::from(0.5);
210
211 let r_max_sq = r * r;
212
213 let inv_r = r.recip();
214 let inv_r2 = inv_r * inv_r;
215 let inv_r3 = inv_r2 * inv_r;
216 let inv_r6 = inv_r3 * inv_r3;
217 let e_max = a * T::exp(-b * r) - c * inv_r6;
218 let two_e_max = e_max + e_max;
219
220 (a, b, c, r_max_sq, two_e_max)
221 }
222}
223
224impl<T: Real> PairKernel<T> for Buckingham {
225 type Params = (T, T, T, T, T);
226
227 #[inline(always)]
233 fn energy(r_sq: T, (a, b, c, r_max_sq, two_e_max): Self::Params) -> T {
234 let mask = T::from((r_sq >= r_max_sq) as u8 as f32);
235 let sign = mask + mask - T::from(1.0);
236
237 let r = r_sq.sqrt();
238 let inv_r2 = r_sq.recip();
239 let inv_r6 = inv_r2 * inv_r2 * inv_r2;
240
241 let e_buck = a * T::exp(-b * r) - c * inv_r6;
242
243 sign * e_buck + (T::from(1.0) - mask) * two_e_max
244 }
245
246 #[inline(always)]
258 fn diff(r_sq: T, (a, b, c, r_max_sq, _two_e_max): Self::Params) -> T {
259 let mask = T::from((r_sq >= r_max_sq) as u8 as f32);
260 let sign = mask + mask - T::from(1.0);
261
262 let inv_r = r_sq.rsqrt();
263 let r = r_sq * inv_r;
264 let inv_r2 = inv_r * inv_r;
265 let inv_r4 = inv_r2 * inv_r2;
266 let inv_r8 = inv_r4 * inv_r4;
267
268 let exp_term = T::exp(-b * r);
269 let d_buck = a * b * exp_term * inv_r - T::from(6.0) * c * inv_r8;
270
271 sign * d_buck
272 }
273
274 #[inline(always)]
278 fn compute(r_sq: T, (a, b, c, r_max_sq, two_e_max): Self::Params) -> EnergyDiff<T> {
279 let mask = T::from((r_sq >= r_max_sq) as u8 as f32);
280 let sign = mask + mask - T::from(1.0);
281
282 let inv_r = r_sq.rsqrt();
283 let r = r_sq * inv_r;
284 let inv_r2 = inv_r * inv_r;
285 let inv_r4 = inv_r2 * inv_r2;
286 let inv_r6 = inv_r4 * inv_r2;
287 let inv_r8 = inv_r6 * inv_r2;
288
289 let exp_term = T::exp(-b * r);
290
291 let e_buck = a * exp_term - c * inv_r6;
292 let d_buck = a * b * exp_term * inv_r - T::from(6.0) * c * inv_r8;
293
294 EnergyDiff {
295 energy: sign * e_buck + (T::from(1.0) - mask) * two_e_max,
296 diff: sign * d_buck,
297 }
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use approx::assert_relative_eq;
305
306 const H: f64 = 1e-6;
311 const TOL_DIFF: f64 = 1e-4;
312
313 mod lennard_jones {
318 use super::*;
319
320 const D0: f64 = 0.1;
321 const R0: f64 = 3.5;
322 const R0_SQ: f64 = R0 * R0;
323
324 fn params() -> (f64, f64) {
325 LennardJones::precompute(D0, R0)
326 }
327
328 #[test]
333 fn sanity_compute_equals_separate() {
334 let r_sq = 9.0_f64;
335 let p = params();
336
337 let result = LennardJones::compute(r_sq, p);
338 let energy_only = LennardJones::energy(r_sq, p);
339 let diff_only = LennardJones::diff(r_sq, p);
340
341 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-14);
342 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-14);
343 }
344
345 #[test]
346 fn sanity_f32_f64_consistency() {
347 let r_sq = 12.25;
348 let p64 = params();
349 let p32 = LennardJones::precompute(D0 as f32, R0 as f32);
350
351 let e64 = LennardJones::energy(r_sq, p64);
352 let e32 = LennardJones::energy(r_sq as f32, p32);
353
354 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
355 }
356
357 #[test]
358 fn sanity_equilibrium_energy_minimum() {
359 let e = LennardJones::energy(R0_SQ, params());
360 assert_relative_eq!(e, -D0, epsilon = 1e-10);
361 }
362
363 #[test]
364 fn sanity_equilibrium_zero_force() {
365 let d = LennardJones::diff(R0_SQ, params());
366 assert_relative_eq!(d, 0.0, epsilon = 1e-10);
367 }
368
369 #[test]
374 fn stability_large_distance() {
375 let r_sq = 1e6_f64;
376 let result = LennardJones::compute(r_sq, params());
377
378 assert!(result.energy.is_finite());
379 assert!(result.diff.is_finite());
380 assert!(result.energy.abs() < 1e-10);
381 }
382
383 #[test]
384 fn stability_small_distance() {
385 let r_sq = 1.0_f64;
386 let result = LennardJones::compute(r_sq, params());
387
388 assert!(result.energy.is_finite());
389 assert!(result.diff.is_finite());
390 assert!(result.energy > 0.0);
391 }
392
393 fn finite_diff_check(r: f64) {
398 let p = params();
399 let r_sq = r * r;
400
401 let r_plus = r + H;
402 let r_minus = r - H;
403 let e_plus = LennardJones::energy(r_plus * r_plus, p);
404 let e_minus = LennardJones::energy(r_minus * r_minus, p);
405 let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
406
407 let d_analytic = LennardJones::diff(r_sq, p);
408 let de_dr_analytic = -d_analytic * r;
409
410 assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
411 }
412
413 #[test]
414 fn finite_diff_repulsion_region() {
415 finite_diff_check(2.5);
416 }
417
418 #[test]
419 fn finite_diff_equilibrium_region() {
420 finite_diff_check(R0);
421 }
422
423 #[test]
424 fn finite_diff_attraction_region() {
425 finite_diff_check(5.0);
426 }
427
428 #[test]
429 fn finite_diff_long_range() {
430 finite_diff_check(10.0);
431 }
432
433 #[test]
438 fn specific_repulsion_positive_energy() {
439 let e = LennardJones::energy(4.0, params());
440 assert!(e > 0.0);
441 }
442
443 #[test]
444 fn specific_attraction_negative_energy() {
445 let e = LennardJones::energy(25.0, params());
446 assert!(e < 0.0);
447 }
448
449 #[test]
450 fn specific_diff_sign_repulsion() {
451 let d = LennardJones::diff(4.0, params());
452 assert!(d > 0.0);
453 }
454
455 #[test]
456 fn specific_diff_sign_attraction() {
457 let d = LennardJones::diff(25.0, params());
458 assert!(d < 0.0);
459 }
460
461 #[test]
466 fn precompute_values() {
467 let (d0, r0_sq) = LennardJones::precompute(D0, R0);
468 assert_relative_eq!(d0, D0, epsilon = 1e-14);
469 assert_relative_eq!(r0_sq, R0_SQ, epsilon = 1e-14);
470 }
471
472 #[test]
473 fn precompute_round_trip() {
474 let p = LennardJones::precompute(D0, R0);
475 let e = LennardJones::energy(R0_SQ, p);
476 assert_relative_eq!(e, -D0, epsilon = 1e-10);
477 }
478 }
479
480 mod buckingham {
485 use super::*;
486
487 const D0: f64 = 1.0;
488 const R0: f64 = 2.0;
489 const ZETA: f64 = 12.0;
490
491 fn params() -> (f64, f64, f64, f64, f64) {
492 Buckingham::precompute(D0, R0, ZETA)
493 }
494
495 #[test]
500 fn sanity_compute_equals_separate() {
501 let r_sq = 4.0_f64;
502 let p = params();
503
504 let result = Buckingham::compute(r_sq, p);
505 let energy_only = Buckingham::energy(r_sq, p);
506 let diff_only = Buckingham::diff(r_sq, p);
507
508 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
509 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
510 }
511
512 #[test]
513 fn sanity_compute_equals_separate_reflected() {
514 let p = params();
515 let r_sq = 0.25_f64;
516
517 let result = Buckingham::compute(r_sq, p);
518 let energy_only = Buckingham::energy(r_sq, p);
519 let diff_only = Buckingham::diff(r_sq, p);
520
521 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
522 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
523 }
524
525 #[test]
526 fn sanity_f32_f64_consistency() {
527 let r_sq = 4.0;
528 let p64 = params();
529 let p32 = Buckingham::precompute(D0 as f32, R0 as f32, ZETA as f32);
530
531 let e64 = Buckingham::energy(r_sq, p64);
532 let e32 = Buckingham::energy(r_sq as f32, p32);
533
534 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-3);
535 }
536
537 #[test]
538 fn sanity_equilibrium_zero_force() {
539 let d = Buckingham::diff(R0 * R0, params());
540 assert_relative_eq!(d, 0.0, epsilon = 1e-10);
541 }
542
543 #[test]
548 fn stability_reflected_region() {
549 let r_sq = 0.1_f64;
550 let result = Buckingham::compute(r_sq, params());
551
552 assert!(result.energy.is_finite());
553 assert!(result.diff.is_finite());
554 assert!(result.energy > 0.0);
555 assert!(result.diff > 0.0);
556 }
557
558 #[test]
559 fn stability_large_distance() {
560 let r_sq = 1e4_f64;
561 let result = Buckingham::compute(r_sq, params());
562
563 assert!(result.energy.is_finite());
564 assert!(result.diff.is_finite());
565 }
566
567 #[test]
568 fn stability_near_zero() {
569 let r_sq = 1e-20_f64;
570 let p = params();
571 let e = Buckingham::energy(r_sq, p);
572
573 assert!(e.is_finite());
574 assert!(e > 0.0);
575 }
576
577 fn finite_diff_check(r: f64) {
582 let p = params();
583 let r_sq = r * r;
584
585 let r_plus = r + H;
586 let r_minus = r - H;
587 let e_plus = Buckingham::energy(r_plus * r_plus, p);
588 let e_minus = Buckingham::energy(r_minus * r_minus, p);
589 let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
590
591 let d_analytic = Buckingham::diff(r_sq, p);
592 let de_dr_analytic = -d_analytic * r;
593
594 assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
595 }
596
597 #[test]
598 fn finite_diff_reflected_region() {
599 finite_diff_check(0.4);
600 }
601
602 #[test]
603 fn finite_diff_normal_short_range() {
604 finite_diff_check(1.5);
605 }
606
607 #[test]
608 fn finite_diff_medium_range() {
609 finite_diff_check(3.0);
610 }
611
612 #[test]
613 fn finite_diff_long_range() {
614 finite_diff_check(8.0);
615 }
616
617 #[test]
622 fn specific_reflection_diverges() {
623 let p = params();
624 let e_close = Buckingham::energy(0.01, p);
625 let e_far = Buckingham::energy(0.25, p);
626 assert!(e_close > e_far);
627 }
628
629 #[test]
630 fn specific_diff_at_maximum_is_zero() {
631 let p = params();
632 let r_max_sq = p.3;
633 let d = Buckingham::diff(r_max_sq, p);
634 assert_relative_eq!(d, 0.0, epsilon = 1e-6);
635 }
636
637 #[test]
638 fn specific_c1_continuity_at_maximum() {
639 let p = params();
640 let r_max = p.3.sqrt();
641 let eps = 1e-8;
642
643 let r_inside = r_max - eps;
644 let r_outside = r_max + eps;
645
646 let d_inside = Buckingham::diff(r_inside * r_inside, p);
647 let d_outside = Buckingham::diff(r_outside * r_outside, p);
648
649 let de_dr_inside = -d_inside * r_inside;
650 let de_dr_outside = -d_outside * r_outside;
651
652 assert_relative_eq!(de_dr_inside, de_dr_outside, epsilon = 1e-3);
653 }
654
655 #[test]
656 fn specific_energy_continuity_at_maximum() {
657 let p = params();
658 let r_max = p.3.sqrt();
659 let eps = 1e-8;
660
661 let e_inside = Buckingham::energy((r_max - eps).powi(2), p);
662 let e_outside = Buckingham::energy((r_max + eps).powi(2), p);
663
664 assert_relative_eq!(e_inside, e_outside, epsilon = 1e-4);
665 }
666
667 #[test]
668 fn specific_finite_diff_across_boundary() {
669 let p = params();
670 let r_max = p.3.sqrt();
671
672 let r_out = r_max + 0.01;
673 let e_p = Buckingham::energy((r_out + H).powi(2), p);
674 let e_m = Buckingham::energy((r_out - H).powi(2), p);
675 let de_dr_num_out = (e_p - e_m) / (2.0 * H);
676 let de_dr_ana_out = -Buckingham::diff(r_out * r_out, p) * r_out;
677 assert_relative_eq!(de_dr_num_out, de_dr_ana_out, epsilon = TOL_DIFF);
678
679 let r_in = r_max - 0.01;
680 let e_p = Buckingham::energy((r_in + H).powi(2), p);
681 let e_m = Buckingham::energy((r_in - H).powi(2), p);
682 let de_dr_num_in = (e_p - e_m) / (2.0 * H);
683 let de_dr_ana_in = -Buckingham::diff(r_in * r_in, p) * r_in;
684 assert_relative_eq!(de_dr_num_in, de_dr_ana_in, epsilon = TOL_DIFF);
685 }
686
687 #[test]
692 fn precompute_values() {
693 let (a, b, c, r_max_sq, two_e_max) = Buckingham::precompute(D0, R0, ZETA);
694 assert_relative_eq!(a, 12.0_f64.exp(), epsilon = 1e-12);
695 assert_relative_eq!(b, 6.0, epsilon = 1e-14);
696 assert_relative_eq!(c, 128.0, epsilon = 1e-10);
697 assert!(r_max_sq > 0.0);
698 assert!(two_e_max.is_finite());
699 }
700
701 #[test]
702 fn precompute_r_max_is_critical_point() {
703 let (a, b, c, r_max_sq, _) = params();
704 let r = r_max_sq.sqrt();
705 let g = a * b * (-b * r).exp() * r.powi(7) - 6.0 * c;
706 assert!(g.abs() < 1e-9);
707 }
708
709 #[test]
710 fn precompute_r_max_less_than_r0() {
711 let (_, _, _, r_max_sq, _) = params();
712 assert!(r_max_sq < R0 * R0);
713 }
714
715 #[test]
716 fn precompute_e_max_positive() {
717 let (_, _, _, _, two_e_max) = params();
718 assert!(two_e_max > 0.0);
719 }
720
721 #[test]
722 fn precompute_round_trip() {
723 let p = Buckingham::precompute(D0, R0, ZETA);
724 let e = Buckingham::energy(R0 * R0, p);
725 assert_relative_eq!(e, -D0, epsilon = 1e-6);
726 }
727 }
728}