use crate::twobody::IsotropicTwobodyEnergy;
use coulomb::Cutoff;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(
feature = "serde",
derive(Deserialize, Serialize),
serde(deny_unknown_fields)
)]
pub struct KimHummer {
#[cfg_attr(feature = "serde", serde(alias = "eps", alias = "ε"))]
epsilon: f64,
#[cfg_attr(feature = "serde", serde(alias = "σ"))]
sigma: f64,
}
impl KimHummer {
const TWO_TO_TWO_SIXTH: f64 = 1.2599210498948732;
pub const fn new(epsilon: f64, sigma: f64) -> Self {
Self { epsilon, sigma }
}
#[inline(always)]
pub fn r0(&self) -> f64 {
f64::powf(2.0, 1.0 / 6.0) * self.sigma
}
#[inline(always)]
pub const fn epsilon(&self) -> f64 {
self.epsilon
}
#[inline(always)]
pub const fn sigma(&self) -> f64 {
self.sigma
}
}
impl IsotropicTwobodyEnergy for KimHummer {
#[inline]
fn isotropic_twobody_energy(&self, distance_squared: f64) -> f64 {
let sigma_squared = self.sigma * self.sigma;
let sr6 = (sigma_squared / distance_squared).powi(3); let sr12 = sr6 * sr6;
if self.epsilon < 0.0 {
4.0 * self.epsilon.abs() * (sr12 - sr6)
} else if self.epsilon > 0.0 {
let r0_squared = Self::TWO_TO_TWO_SIXTH * sigma_squared;
if distance_squared < r0_squared {
(4.0 * self.epsilon).mul_add(sr12 - sr6, 2.0 * self.epsilon)
} else {
-4.0 * self.epsilon * (sr12 - sr6)
}
} else {
0.01 * sr12
}
}
#[inline]
fn isotropic_twobody_force(&self, distance_squared: f64) -> f64 {
let sigma_squared = self.sigma * self.sigma;
let x = (sigma_squared / distance_squared).powi(3);
if self.epsilon < 0.0 {
3.0 * 4.0 * self.epsilon.abs() * x * (2.0 * x - 1.0) / distance_squared
} else if self.epsilon > 0.0 {
let r0_squared = Self::TWO_TO_TWO_SIXTH * sigma_squared;
let lj_force = 3.0 * 4.0 * self.epsilon * x * (2.0 * x - 1.0) / distance_squared;
if distance_squared < r0_squared {
lj_force
} else {
-lj_force
}
} else {
0.06 * x * x / distance_squared
}
}
}
impl Cutoff for KimHummer {
fn cutoff(&self) -> f64 {
f64::INFINITY
}
fn cutoff_squared(&self) -> f64 {
f64::INFINITY
}
fn lower_cutoff(&self) -> f64 {
self.sigma * 0.6
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const SIGMA: f64 = 6.0;
fn r0() -> f64 {
f64::powf(2.0, 1.0 / 6.0) * SIGMA
}
#[test]
fn test_attractive_minimum_value() {
let epsilon = -0.5;
let kh = KimHummer::new(epsilon, SIGMA);
let r = r0();
let u = kh.isotropic_twobody_energy(r * r);
assert_relative_eq!(u, epsilon, epsilon = 1e-8);
}
#[test]
fn test_attractive_at_sigma() {
let epsilon = -0.5;
let kh = KimHummer::new(epsilon, SIGMA);
let r_squared = SIGMA * SIGMA;
let u = kh.isotropic_twobody_energy(r_squared);
assert_relative_eq!(u, 0.0, epsilon = 1e-8);
}
#[test]
fn test_attractive_repulsive_core() {
let epsilon = -0.5;
let kh = KimHummer::new(epsilon, SIGMA);
let r = 0.9 * SIGMA;
let u = kh.isotropic_twobody_energy(r * r);
assert!(u > 0.0);
}
#[test]
fn test_attractive_well_shape() {
let epsilon = -0.5;
let kh = KimHummer::new(epsilon, SIGMA);
let r = 1.5 * SIGMA;
let u = kh.isotropic_twobody_energy(r * r);
assert!(u < 0.0);
assert!(u > epsilon); }
#[test]
fn test_repulsive_cusp_value() {
let epsilon = 0.3;
let kh = KimHummer::new(epsilon, SIGMA);
let r = r0();
let u = kh.isotropic_twobody_energy(r * r);
assert_relative_eq!(u, epsilon, epsilon = 1e-8);
}
#[test]
fn test_repulsive_always_positive() {
let epsilon = 0.3;
let kh = KimHummer::new(epsilon, SIGMA);
for i in 0..100 {
let r = 0.8 * SIGMA + (i as f64) * 0.022 * SIGMA; let u = kh.isotropic_twobody_energy(r * r);
assert!(u > 0.0, "U({}) = {} should be > 0", r, u);
}
}
#[test]
fn test_repulsive_inner_branch() {
let epsilon = 0.3;
let kh = KimHummer::new(epsilon, SIGMA);
let r = 0.95 * r0(); let sr6 = (SIGMA / r).powi(6);
let sr12 = sr6 * sr6;
let expected = 4.0 * epsilon * (sr12 - sr6) + 2.0 * epsilon;
let u = kh.isotropic_twobody_energy(r * r);
assert_relative_eq!(u, expected, epsilon = 1e-8);
}
#[test]
fn test_repulsive_outer_branch() {
let epsilon = 0.3;
let kh = KimHummer::new(epsilon, SIGMA);
let r = 1.5 * SIGMA; let sr6 = (SIGMA / r).powi(6);
let sr12 = sr6 * sr6;
let expected = -4.0 * epsilon * (sr12 - sr6);
let u = kh.isotropic_twobody_energy(r * r);
assert_relative_eq!(u, expected, epsilon = 1e-8);
}
#[test]
fn test_repulsive_continuity_at_r0() {
let epsilon = 0.3;
let kh = KimHummer::new(epsilon, SIGMA);
let delta = 1e-8;
let r0_val = r0();
let r_below = r0_val - delta;
let r_above = r0_val + delta;
let u_below = kh.isotropic_twobody_energy(r_below * r_below);
let u_above = kh.isotropic_twobody_energy(r_above * r_above);
assert_relative_eq!(u_below, u_above, epsilon = 1e-4);
}
#[test]
fn test_neutral_soft_wall() {
let epsilon = 0.0;
let kh = KimHummer::new(epsilon, SIGMA);
let r = SIGMA;
let expected = 0.01 * (SIGMA / r).powi(12);
let u = kh.isotropic_twobody_energy(r * r);
assert_relative_eq!(u, expected, epsilon = 1e-8);
}
#[test]
fn test_neutral_always_positive() {
let epsilon = 0.0;
let kh = KimHummer::new(epsilon, SIGMA);
for i in 0..100 {
let r = 0.8 * SIGMA + (i as f64) * 0.022 * SIGMA;
let u = kh.isotropic_twobody_energy(r * r);
assert!(u > 0.0, "U({}) = {} should be > 0", r, u);
}
}
#[test]
fn test_neutral_monotonic_decrease() {
let epsilon = 0.0;
let kh = KimHummer::new(epsilon, SIGMA);
let mut prev_u = f64::INFINITY;
for i in 1..100 {
let r = 0.8 * SIGMA + (i as f64) * 0.022 * SIGMA;
let u = kh.isotropic_twobody_energy(r * r);
assert!(u < prev_u, "U({}) should be < U(prev)", r);
prev_u = u;
}
}
#[test]
fn test_large_r_approaches_zero() {
for epsilon in [-0.5, 0.0, 0.3] {
let kh = KimHummer::new(epsilon, SIGMA);
let r = 100.0 * SIGMA;
let u = kh.isotropic_twobody_energy(r * r);
assert_relative_eq!(u, 0.0, epsilon = 1e-6);
}
}
#[test]
fn test_r_zero_is_nan() {
for epsilon in [-0.5, 0.0, 0.3] {
let kh = KimHummer::new(epsilon, SIGMA);
let u = kh.isotropic_twobody_energy(0.0);
assert!(u.is_nan() || u.is_infinite());
}
}
#[test]
fn test_kimhummer_force() {
use crate::twobody::LennardJones;
let sigma = 6.0;
let kh_att = KimHummer::new(-0.5, sigma);
let lj = LennardJones::new(0.5, sigma);
let r2 = (1.5 * sigma).powi(2);
assert_relative_eq!(
kh_att.isotropic_twobody_force(r2),
lj.isotropic_twobody_force(r2)
);
let kh_rep = KimHummer::new(0.3, sigma);
let r0_sq = 1.2599210498948732 * sigma * sigma;
let f_below = kh_rep.isotropic_twobody_force(r0_sq - 1e-8);
let f_above = kh_rep.isotropic_twobody_force(r0_sq + 1e-8);
assert_relative_eq!(f_below, f_above, epsilon = 1e-3);
let kh_neut = KimHummer::new(0.0, sigma);
assert!(kh_neut.isotropic_twobody_force(r2) > 0.0);
}
}