use super::ThreebodyAngleEnergy;
use crate::twobody::{compute_uniform_hermite_coeffs, SplineCoeffs};
use std::f64::consts::PI;
#[derive(Clone, Debug)]
pub struct SplinedAngle {
coeffs: Vec<SplineCoeffs>,
inv_delta: f64,
}
impl SplinedAngle {
const ANGLE_MIN: f64 = 0.0;
const ANGLE_MAX: f64 = PI;
pub fn new(potential: &dyn ThreebodyAngleEnergy, n_points: usize) -> Self {
assert!(n_points >= 4, "Need at least 4 grid points");
let delta = (Self::ANGLE_MAX - Self::ANGLE_MIN) / (n_points - 1) as f64;
let mut u_vals = Vec::with_capacity(n_points);
let mut f_vals = Vec::with_capacity(n_points);
for i in 0..n_points {
let angle = Self::ANGLE_MIN + i as f64 * delta;
u_vals.push(potential.threebody_angle_energy(angle));
f_vals.push(potential.threebody_angle_force(angle));
}
let coeffs = compute_uniform_hermite_coeffs(&u_vals, &f_vals, delta);
Self {
coeffs,
inv_delta: 1.0 / delta,
}
}
pub fn coefficients(&self) -> &[SplineCoeffs] {
&self.coeffs
}
pub const fn angle_min(&self) -> f64 {
Self::ANGLE_MIN
}
pub const fn angle_max(&self) -> f64 {
Self::ANGLE_MAX
}
pub const fn inv_delta(&self) -> f64 {
self.inv_delta
}
#[inline(always)]
pub fn energy_and_force(&self, angle: f64) -> (f64, f64) {
let (i, eps) = self.index_eps(angle);
let c = &self.coeffs[i];
let energy = c.u[0] + eps * (c.u[1] + eps * (c.u[2] + eps * c.u[3]));
let force = c.f[0] + eps * (c.f[1] + eps * c.f[2]);
(energy, force)
}
#[inline(always)]
fn index_eps(&self, angle: f64) -> (usize, f64) {
let clamped = angle.clamp(Self::ANGLE_MIN, Self::ANGLE_MAX);
let t = clamped * self.inv_delta; let i = (t as usize).min(self.coeffs.len() - 2);
let eps = t - i as f64;
(i, eps)
}
}
impl ThreebodyAngleEnergy for SplinedAngle {
#[inline(always)]
fn threebody_angle_energy(&self, angle: f64) -> f64 {
let (i, eps) = self.index_eps(angle);
let c = &self.coeffs[i];
c.u[0] + eps * (c.u[1] + eps * (c.u[2] + eps * c.u[3]))
}
#[inline(always)]
fn threebody_angle_force(&self, angle: f64) -> f64 {
let (i, eps) = self.index_eps(angle);
let c = &self.coeffs[i];
c.f[0] + eps * (c.f[1] + eps * c.f[2])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::threebody::HarmonicTorsion;
use approx::assert_relative_eq;
#[test]
fn test_splined_angle_energy_accuracy() {
let eq_angle = std::f64::consts::FRAC_PI_2;
let pot = HarmonicTorsion::new(eq_angle, 100.0);
let splined = SplinedAngle::new(&pot, 500);
let test_angles = [0.1, 0.5, 1.0, 1.2, 1.5, 2.0, 2.5, 3.0];
for &angle in &test_angles {
let exact = pot.threebody_angle_energy(angle);
let approx = splined.threebody_angle_energy(angle);
let rel_err = if exact.abs() > 1e-10 {
((approx - exact) / exact).abs()
} else {
(approx - exact).abs()
};
assert!(
rel_err < 1e-6,
"Energy error too large at angle={angle}: exact={exact}, approx={approx}, rel_err={rel_err}",
);
}
}
#[test]
fn test_splined_angle_force_accuracy() {
let eq_angle = std::f64::consts::FRAC_PI_4;
let pot = HarmonicTorsion::new(eq_angle, 50.0);
let splined = SplinedAngle::new(&pot, 500);
let test_angles = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0];
for &angle in &test_angles {
let exact = pot.threebody_angle_force(angle);
let approx = splined.threebody_angle_force(angle);
let rel_err = if exact.abs() > 1e-10 {
((approx - exact) / exact).abs()
} else {
(approx - exact).abs()
};
assert!(
rel_err < 1e-6,
"Force error too large at angle={angle}: exact={exact}, approx={approx}, rel_err={rel_err}",
);
}
}
#[test]
fn test_energy_and_force() {
let pot = HarmonicTorsion::new(1.0, 50.0);
let splined = SplinedAngle::new(&pot, 500);
let angle = 1.5;
let (energy, force) = splined.energy_and_force(angle);
assert_relative_eq!(
energy,
splined.threebody_angle_energy(angle),
epsilon = 1e-15
);
assert_relative_eq!(force, splined.threebody_angle_force(angle), epsilon = 1e-15);
}
#[test]
fn test_boundary_behavior() {
let pot = HarmonicTorsion::new(1.0, 10.0);
let splined = SplinedAngle::new(&pot, 200);
assert_relative_eq!(
splined.threebody_angle_energy(0.0),
pot.threebody_angle_energy(0.0),
epsilon = 1e-10
);
assert_relative_eq!(
splined.threebody_angle_energy(PI),
pot.threebody_angle_energy(PI),
epsilon = 1e-10
);
let e_at_zero = splined.threebody_angle_energy(0.0);
let e_below = splined.threebody_angle_energy(-0.1);
assert_relative_eq!(e_at_zero, e_below, epsilon = 1e-10);
let e_at_pi = splined.threebody_angle_energy(PI);
let e_above = splined.threebody_angle_energy(PI + 0.1);
assert_relative_eq!(e_at_pi, e_above, epsilon = 1e-10);
}
#[test]
fn test_implements_threebody_trait() {
let pot = HarmonicTorsion::new(1.0, 10.0);
let splined = SplinedAngle::new(&pot, 200);
let boxed: Box<dyn ThreebodyAngleEnergy> = Box::new(splined);
let energy = boxed.threebody_angle_energy(1.5);
let expected = pot.threebody_angle_energy(1.5);
assert_relative_eq!(energy, expected, epsilon = 1e-6);
}
}