use ndarray::{Array1, Array2, Array3};
use crate::compute::error::ComputeError;
use molrs::units::constants::COULOMB_REAL as KAPPA;
pub use molrs::units::constants::BOLTZMANN_REAL as K_B;
const FOUR_PI_OVER_3: f64 = 4.1887902047863905;
pub fn compute_dipole_moment(
charges: &Array1<f64>,
positions: &Array2<f64>,
) -> Result<Array1<f64>, ComputeError> {
let n = charges.len();
if positions.shape() != [n, 3] {
return Err(ComputeError::DimensionMismatch {
expected: n * 3,
got: positions.len(),
what: "positions (expected (n_atoms, 3))",
});
}
let mut m = Array1::zeros(3);
for i in 0..n {
let q = charges[i];
if !q.is_finite() {
return Err(ComputeError::NonFinite {
where_: "charges",
index: i,
});
}
m[0] += q * positions[[i, 0]];
m[1] += q * positions[[i, 1]];
m[2] += q * positions[[i, 2]];
}
Ok(m)
}
pub fn compute_current_density(
dipole_moments: &Array2<f64>,
dt: f64,
volume: f64,
) -> Result<Array2<f64>, ComputeError> {
let shape = dipole_moments.shape();
if shape[1] != 3 {
return Err(ComputeError::DimensionMismatch {
expected: 3,
got: shape[1],
what: "dipole_moments (expected (n_frames, 3))",
});
}
if dt <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "dt",
value: dt.to_string(),
});
}
if volume <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "volume",
value: volume.to_string(),
});
}
let n_frames = shape[0];
let mut j = Array2::from_elem((n_frames, 3), f64::NAN);
if n_frames < 2 {
return Ok(j);
}
let scale = 1.0 / (volume * dt);
for t in 1..n_frames {
for d in 0..3 {
j[[t, d]] = (dipole_moments[[t, d]] - dipole_moments[[t - 1, d]]) * scale;
}
}
Ok(j)
}
pub fn static_dielectric_constant(
dipole_moments: &Array2<f64>,
volume: f64,
temperature: f64,
epsilon_inf: f64,
) -> Result<f64, ComputeError> {
let shape = dipole_moments.shape();
if shape[1] != 3 {
return Err(ComputeError::DimensionMismatch {
expected: 3,
got: shape[1],
what: "dipole_moments (expected (n_frames, 3))",
});
}
if shape[0] < 2 {
return Err(ComputeError::EmptyInput);
}
if volume <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "volume",
value: volume.to_string(),
});
}
if temperature <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "temperature",
value: temperature.to_string(),
});
}
let n = shape[0] as f64;
let mut mean_m = Array1::<f64>::zeros(3);
for t in 0..shape[0] {
for d in 0..3 {
mean_m[d] += dipole_moments[[t, d]];
}
}
for d in 0..3 {
mean_m[d] /= n;
}
let mut variance = 0.0;
for t in 0..shape[0] {
for d in 0..3 {
let dev = dipole_moments[[t, d]] - mean_m[d];
variance += dev * dev;
}
}
variance /= n;
let prefactor = FOUR_PI_OVER_3 * KAPPA / (volume * K_B * temperature);
Ok(epsilon_inf + prefactor * variance)
}
pub fn static_dielectric_constant_components(
dipole_moments: &Array2<f64>,
volume: f64,
temperature: f64,
epsilon_inf: f64,
) -> Result<StaticDielectricResult, ComputeError> {
let shape = dipole_moments.shape();
if shape[1] != 3 {
return Err(ComputeError::DimensionMismatch {
expected: 3,
got: shape[1],
what: "dipole_moments (expected (n_frames, 3))",
});
}
if shape[0] < 2 {
return Err(ComputeError::EmptyInput);
}
if volume <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "volume",
value: volume.to_string(),
});
}
if temperature <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "temperature",
value: temperature.to_string(),
});
}
let n = shape[0] as f64;
let n_frames = shape[0];
let mut mean_m = Array1::<f64>::zeros(3);
let mut mean_sq = Array1::<f64>::zeros(3);
for t in 0..n_frames {
for d in 0..3 {
let m = dipole_moments[[t, d]];
mean_m[d] += m;
mean_sq[d] += m * m;
}
}
for d in 0..3 {
mean_m[d] /= n;
mean_sq[d] /= n;
}
let mut fluctuation = Array1::<f64>::zeros(3);
let mut eps = Array1::<f64>::zeros(3);
let per_axis_prefactor = 3.0 * FOUR_PI_OVER_3 * KAPPA / (volume * K_B * temperature);
for d in 0..3 {
fluctuation[d] = mean_sq[d] - mean_m[d] * mean_m[d];
eps[d] = epsilon_inf + per_axis_prefactor * fluctuation[d];
}
let eps_mean = (eps[0] + eps[1] + eps[2]) / 3.0;
Ok(StaticDielectricResult {
dipole_mean: mean_m,
dipole_sq_mean: mean_sq,
fluctuation,
eps,
eps_mean,
epsilon_inf,
n_frames,
})
}
pub fn decompose_current(
per_particle_current: &Array3<f64>,
water_mask: &Array1<bool>,
) -> Result<(Array2<f64>, Array2<f64>), ComputeError> {
let shape = per_particle_current.shape();
let n_particles = shape[0];
let n_frames = shape[1];
if shape[2] != 3 {
return Err(ComputeError::DimensionMismatch {
expected: 3,
got: shape[2],
what: "per_particle_current (expected (n_particles, n_frames, 3))",
});
}
if water_mask.len() != n_particles {
return Err(ComputeError::DimensionMismatch {
expected: n_particles,
got: water_mask.len(),
what: "water_mask",
});
}
let mut j_water = Array2::zeros((n_frames, 3));
let mut j_ion = Array2::zeros((n_frames, 3));
for p in 0..n_particles {
let target = if water_mask[p] {
&mut j_water
} else {
&mut j_ion
};
for t in 0..n_frames {
for d in 0..3 {
target[[t, d]] += per_particle_current[[p, t, d]];
}
}
}
Ok((j_water, j_ion))
}
#[derive(Debug, Clone)]
pub struct StaticDielectricResult {
pub dipole_mean: Array1<f64>,
pub dipole_sq_mean: Array1<f64>,
pub fluctuation: Array1<f64>,
pub eps: Array1<f64>,
pub eps_mean: f64,
pub epsilon_inf: f64,
pub n_frames: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{Axis, arr1};
#[test]
fn test_dipole_moment_two_charges() {
let charges = arr1(&[1.0, -1.0]);
let positions = ndarray::arr2(&[[2.0, 0.0, 0.0], [0.0, 0.0, 0.0]]);
let m = compute_dipole_moment(&charges, &positions).unwrap();
assert!((m[0] - 2.0).abs() < 1e-10);
assert!((m[1] - 0.0).abs() < 1e-10);
assert!((m[2] - 0.0).abs() < 1e-10);
}
#[test]
fn test_dipole_moment_zero_charge() {
let charges = arr1(&[0.0, 0.0, 0.0]);
let positions = ndarray::Array2::zeros((3, 3));
let m = compute_dipole_moment(&charges, &positions).unwrap();
assert!((m[0].abs() + m[1].abs() + m[2].abs()) < 1e-10);
}
#[test]
fn test_dipole_moment_wrong_shape() {
let charges = arr1(&[1.0, 2.0]);
let positions = ndarray::Array2::zeros((3, 3));
assert!(compute_dipole_moment(&charges, &positions).is_err());
}
#[test]
fn test_current_density_constant_dipole() {
let dm = ndarray::Array2::from_elem((3, 3), 1.0);
let j = compute_current_density(&dm, 1.0, 1.0).unwrap();
assert_eq!(j.shape(), &[3, 3]);
assert!(j[[0, 0]].is_nan());
assert!((j[[1, 0]]).abs() < 1e-10);
assert!((j[[2, 0]]).abs() < 1e-10);
}
#[test]
fn test_current_density_linear() {
let dm = ndarray::arr2(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
let j = compute_current_density(&dm, 1.0, 1.0).unwrap();
assert!(j[[0, 0]].is_nan());
assert!((j[[1, 0]] - 1.0).abs() < 1e-10);
assert!((j[[2, 0]] - 1.0).abs() < 1e-10);
}
#[test]
fn test_current_density_dt_scaling() {
let dm = ndarray::arr2(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
let j1 = compute_current_density(&dm, 1.0, 1.0).unwrap();
let j2 = compute_current_density(&dm, 2.0, 1.0).unwrap();
assert!((j2[[1, 0]] * 2.0 - j1[[1, 0]]).abs() < 1e-10);
}
#[test]
fn test_static_dielectric_zero_fluctuation() {
let dm = ndarray::Array2::from_elem((10, 3), 0.0);
let eps = static_dielectric_constant(&dm, 1000.0, 300.0, 1.0).unwrap();
assert!((eps - 1.0).abs() < 1e-10);
}
#[test]
fn test_static_dielectric_known_fluctuation() {
let dm = ndarray::arr2(&[[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]]);
let eps = static_dielectric_constant(&dm, 1000.0, 300.0, 1.0).unwrap();
let expected = 1.0 + FOUR_PI_OVER_3 * KAPPA * 1.0 / (1000.0 * K_B * 300.0);
assert!((eps - expected).abs() < 1e-10);
}
#[test]
fn test_static_dielectric_single_frame_rejected() {
let dm = ndarray::Array2::zeros((1, 3));
assert!(static_dielectric_constant(&dm, 1000.0, 300.0, 1.0).is_err());
}
#[test]
fn test_decompose_current_conservation() {
let n_particles = 4;
let n_frames = 5;
let mut current = Array3::zeros((n_particles, n_frames, 3));
for p in 0..n_particles {
for t in 0..n_frames {
current[[p, t, 0]] = p as f64 + t as f64;
current[[p, t, 1]] = (p as f64) * 2.0;
current[[p, t, 2]] = t as f64 * 0.5;
}
}
let mask = arr1(&[true, true, false, false]);
let (j_w, j_i) = decompose_current(¤t, &mask).unwrap();
assert_eq!(j_w.shape(), &[n_frames, 3]);
assert_eq!(j_i.shape(), &[n_frames, 3]);
let total: Array2<f64> = current.sum_axis(Axis(0));
for t in 0..n_frames {
for d in 0..3 {
assert!(((j_w[[t, d]] + j_i[[t, d]]) - total[[t, d]]).abs() < 1e-12);
}
}
}
#[test]
fn test_decompose_current_mask_mismatch() {
let current = Array3::zeros((2, 3, 3));
let mask = arr1(&[true, false, true]);
assert!(decompose_current(¤t, &mask).is_err());
}
#[test]
fn test_immutability_dipole_moment() {
let charges = arr1(&[1.0, -1.0]);
let positions = ndarray::arr2(&[[2.0, 0.0, 0.0], [0.0, 0.0, 0.0]]);
let pos_copy = positions.clone();
compute_dipole_moment(&charges, &positions).unwrap();
assert_eq!(positions, pos_copy);
}
#[test]
fn test_immutability_current_density() {
let dm = ndarray::arr2(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
let dm_copy = dm.clone();
compute_current_density(&dm, 1.0, 1.0).unwrap();
assert_eq!(dm, dm_copy);
}
#[test]
fn test_static_dielectric_components() {
let dm = ndarray::arr2(&[[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]]);
let result = static_dielectric_constant_components(&dm, 1000.0, 300.0, 1.0).unwrap();
assert_eq!(result.dipole_mean.len(), 3);
assert_eq!(result.eps.len(), 3);
assert_eq!(result.n_frames, 2);
assert!(result.eps[0] > result.eps[1]);
assert!((result.eps[1] - 1.0).abs() < 1e-10);
assert!((result.eps[2] - 1.0).abs() < 1e-10);
let scalar = static_dielectric_constant(&dm, 1000.0, 300.0, 1.0).unwrap();
assert!((result.eps_mean - scalar).abs() < 1e-10);
}
#[test]
fn test_static_dielectric_components_isotropic() {
let dm = ndarray::arr2(&[[1.0, 1.0, 1.0], [-1.0, -1.0, -1.0]]);
let result = static_dielectric_constant_components(&dm, 1000.0, 300.0, 1.0).unwrap();
let eps_x = result.eps[0];
assert!((result.eps[1] - eps_x).abs() < 1e-12);
assert!((result.eps[2] - eps_x).abs() < 1e-12);
assert!((result.eps_mean - eps_x).abs() < 1e-12);
}
}