#![forbid(unsafe_code)]
use std::fmt;
pub mod pme;
pub mod real;
pub use pme::reciprocal_space_energy;
pub use real::{K_COULOMB, direct_coulomb, direct_coulomb_cutoff, direct_coulomb_damped};
#[derive(Debug, Clone, PartialEq)]
pub enum EwaldError {
SingularBoxMatrix,
}
impl fmt::Display for EwaldError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EwaldError::SingularBoxMatrix => {
write!(f, "singular or near-singular box matrix (determinant < 1e-10)")
}
}
}
}
impl std::error::Error for EwaldError {}
#[derive(Clone, Debug)]
pub struct PmeConfig {
pub alpha: f64,
pub kmax: [usize; 3],
pub r_cut: f64,
pub mesh: [usize; 3],
pub spline_order: u8,
}
impl Default for PmeConfig {
fn default() -> Self {
Self {
alpha: 0.0, kmax: [5, 5, 5],
r_cut: 8.0,
mesh: [16, 16, 16],
spline_order: 4,
}
}
}
#[derive(Clone, Debug)]
pub struct BoxVectors(pub [[f64; 3]; 3]);
impl BoxVectors {
pub fn cubic(a: f64) -> Self {
BoxVectors([[a, 0.0, 0.0], [0.0, a, 0.0], [0.0, 0.0, a]])
}
pub fn volume(&self) -> f64 {
let a = self.0[0];
let b = self.0[1];
let c = self.0[2];
(a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
+ a[2] * (b[0] * c[1] - b[1] * c[0]))
.abs()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct EwaldResult {
pub real_energy: f64,
pub reciprocal_energy: f64,
pub self_energy: f64,
pub total_energy: f64,
}
pub fn spme_energy(
coords: &[[f64; 3]],
charges: &[f64],
box_vecs: &BoxVectors,
config: &PmeConfig,
) -> Result<EwaldResult, EwaldError> {
if box_vecs.volume().abs() < 1e-10 {
return Err(EwaldError::SingularBoxMatrix);
}
let alpha = if config.alpha > 0.0 {
config.alpha
} else {
3.5 / config.r_cut
};
let real = real::direct_coulomb_damped(coords, charges, alpha);
let reciprocal = pme::reciprocal_space_energy(coords, charges, box_vecs, config)?;
let self_corr = compute_self_energy(charges, alpha);
Ok(EwaldResult {
real_energy: real,
reciprocal_energy: reciprocal,
self_energy: self_corr,
total_energy: real + reciprocal + self_corr,
})
}
#[allow(dead_code)]
fn direct_coulomb_cutoff_coords(coords: &[[f64; 3]], charges: &[f64], r_cut: f64) -> f64 {
let mut energy = 0.0;
let n = coords.len();
for i in 0..n {
for j in (i + 1)..n {
let dx = coords[i][0] - coords[j][0];
let dy = coords[i][1] - coords[j][1];
let dz = coords[i][2] - coords[j][2];
let r = (dx * dx + dy * dy + dz * dz).sqrt();
if r > 1e-6 && r < r_cut {
energy += K_COULOMB * charges[i] * charges[j] / r;
}
}
}
energy
}
fn compute_self_energy(charges: &[f64], alpha: f64) -> f64 {
let alpha = if alpha > 0.0 { alpha } else { 3.5 / 8.0 };
let sqrt_pi = std::f64::consts::PI.sqrt();
let mut self_e = 0.0;
for &q in charges {
self_e -= K_COULOMB * alpha / sqrt_pi * q * q;
}
self_e
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cubic_box_volume() {
let box_vecs = BoxVectors::cubic(10.0);
assert!((box_vecs.volume() - 1000.0).abs() < 1e-6);
}
#[test]
fn test_direct_coulomb_flat_coords() {
let coords = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let charges = vec![1.0, -1.0];
let energy = direct_coulomb(&coords, &charges);
let expected = -K_COULOMB;
assert!(
(energy - expected).abs() < 1.0,
"energy={}, expected={}",
energy,
expected
);
}
#[test]
fn test_spme_singular_box_returns_error() {
let degenerate_box = BoxVectors([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]);
let coords = [[0.0, 0.0, 0.0]];
let charges = [1.0];
let result = spme_energy(&coords, &charges, °enerate_box, &PmeConfig::default());
assert_eq!(result, Err(EwaldError::SingularBoxMatrix));
}
#[test]
fn test_spme_valid_box() {
let box_vecs = BoxVectors::cubic(10.0);
let coords = [[5.0, 5.0, 5.0]];
let charges = [1.0];
let result = spme_energy(&coords, &charges, &box_vecs, &PmeConfig::default());
assert!(result.is_ok());
}
}