use crate::base::Potential3;
use crate::math::Vector;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Harm<T> {
k: T,
theta0: T,
}
impl<T: Vector> Harm<T> {
#[inline]
pub fn new(k: f64, theta0: f64) -> Self {
Self {
k: T::splat(k),
theta0: T::splat(theta0),
}
}
#[inline]
pub fn from_degrees(k: f64, theta0_deg: f64) -> Self {
let theta0_rad = theta0_deg * core::f64::consts::PI / 180.0;
Self::new(k, theta0_rad)
}
#[inline]
pub fn k(&self) -> T {
self.k
}
#[inline]
pub fn theta0(&self) -> T {
self.theta0
}
}
impl<T: Vector> Potential3<T> for Harm<T> {
#[inline(always)]
fn energy(&self, _r_ij_sq: T, _r_jk_sq: T, cos_theta: T) -> T {
let theta = cos_theta.acos();
let dtheta = theta - self.theta0;
self.k * dtheta * dtheta
}
#[inline(always)]
fn derivative(&self, _r_ij_sq: T, _r_jk_sq: T, cos_theta: T) -> T {
let theta = cos_theta.acos();
let dtheta = theta - self.theta0;
let one = T::one();
let sin_sq = one - cos_theta * cos_theta;
let sin_theta = sin_sq.max(T::splat(1e-10)).sqrt();
let two = T::splat(2.0);
T::zero() - two * self.k * dtheta / sin_theta
}
#[inline(always)]
fn energy_derivative(&self, r_ij_sq: T, r_jk_sq: T, cos_theta: T) -> (T, T) {
let theta = cos_theta.acos();
let dtheta = theta - self.theta0;
let energy = self.k * dtheta * dtheta;
let one = T::one();
let sin_sq = one - cos_theta * cos_theta;
let sin_theta = sin_sq.max(T::splat(1e-10)).sqrt();
let two = T::splat(2.0);
let derivative = T::zero() - two * self.k * dtheta / sin_theta;
let _ = (r_ij_sq, r_jk_sq);
(energy, derivative)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use core::f64::consts::PI;
#[test]
fn test_harm_at_equilibrium() {
let theta0 = PI / 3.0;
let harm: Harm<f64> = Harm::new(100.0, theta0);
let cos_theta = theta0.cos();
let energy = harm.energy(1.0, 1.0, cos_theta);
assert_relative_eq!(energy, 0.0, epsilon = 1e-10);
}
#[test]
fn test_harm_from_degrees() {
let h1: Harm<f64> = Harm::from_degrees(100.0, 109.5);
let h2: Harm<f64> = Harm::new(100.0, 109.5 * PI / 180.0);
let cos_theta = 0.5;
assert_relative_eq!(
h1.energy(1.0, 1.0, cos_theta),
h2.energy(1.0, 1.0, cos_theta),
epsilon = 1e-10
);
}
#[test]
fn test_harm_displaced() {
let k = 100.0;
let theta0 = PI / 2.0;
let harm: Harm<f64> = Harm::new(k, theta0);
let theta = PI / 3.0;
let cos_theta = theta.cos();
let energy = harm.energy(1.0, 1.0, cos_theta);
let dtheta = theta - theta0;
let expected = k * dtheta * dtheta;
assert_relative_eq!(energy, expected, epsilon = 1e-10);
}
#[test]
fn test_harm_numerical_derivative() {
let harm: Harm<f64> = Harm::new(100.0, PI / 3.0);
let cos_theta = 0.3;
let h = 1e-6;
let e_plus = harm.energy(1.0, 1.0, cos_theta + h);
let e_minus = harm.energy(1.0, 1.0, cos_theta - h);
let deriv_numerical = (e_plus - e_minus) / (2.0 * h);
let deriv_analytical = harm.derivative(1.0, 1.0, cos_theta);
assert_relative_eq!(deriv_analytical, deriv_numerical, epsilon = 1e-6);
}
}