use molrs::store::frame_access::FrameAccess;
use ndarray::{Array1, Array2};
use rustfft::FftPlanner;
use crate::compute::error::ComputeError;
use crate::compute::result::ComputeResult;
use crate::compute::traits::Compute;
use molrs::signal as sig;
#[derive(Debug, Clone)]
pub struct EinsteinConductivityResult {
pub lag_times: Array1<f64>,
pub msd: Array1<f64>,
}
impl ComputeResult for EinsteinConductivityResult {}
#[derive(Debug, Clone, Copy, Default)]
pub struct EinsteinConductivity;
pub type EinsteinConductivityArgs<'a> = (&'a Array2<f64>, f64, usize);
impl Compute for EinsteinConductivity {
type Args<'a> = EinsteinConductivityArgs<'a>;
type Output = EinsteinConductivityResult;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
_frames: &[&'a FA],
args: Self::Args<'a>,
) -> Result<Self::Output, ComputeError> {
let (dipole, dt, max_correlation_time) = args;
let shape = dipole.shape();
if shape[1] != 3 {
return Err(ComputeError::DimensionMismatch {
expected: 3,
got: shape[1],
what: "translational_dipole (expected (n_frames, 3))",
});
}
let n_frames = shape[0];
if n_frames < 2 {
return Err(ComputeError::EmptyInput);
}
if dt <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "dt",
value: dt.to_string(),
});
}
let max_lag = max_correlation_time.min(n_frames - 1);
let n = n_frames;
let sq: Vec<f64> = (0..n)
.map(|t| (0..3).map(|d| dipole[[t, d]] * dipole[[t, d]]).sum::<f64>())
.collect();
let mut planner = FftPlanner::new();
let mut s2 = Array1::<f64>::zeros(max_lag + 1);
for d in 0..3 {
let col: Array1<f64> = (0..n).map(|t| dipole[[t, d]]).collect();
let acf = sig::acf_fft_with_planner(&mut planner, &col, max_lag).map_err(|e| {
ComputeError::OutOfRange {
field: "acf_fft",
value: e.to_string(),
}
})?;
for k in 0..=max_lag {
s2[k] += acf[k];
}
}
let mut msd = Array1::<f64>::zeros(max_lag + 1);
let mut q = 2.0 * sq.iter().sum::<f64>();
for tau in 0..=max_lag {
if tau >= 1 {
q -= sq[tau - 1] + sq[n - tau];
}
let count = (n - tau) as f64;
msd[tau] = q / count - 2.0 * s2[tau] / count;
}
msd[0] = 0.0;
let lag_times = Array1::from_iter((0..=max_lag).map(|i| i as f64 * dt));
Ok(EinsteinConductivityResult { lag_times, msd })
}
}
#[cfg(test)]
mod tests {
use super::super::green_kubo_conductivity::GreenKuboConductivity;
use super::*;
use molrs::Frame;
use ndarray::{Array1 as A1, Array2};
use rand::{RngExt, SeedableRng};
fn einstein_helfand_prefactor() -> f64 {
use molrs::units::constants::{
ANGSTROM_M, BOLTZMANN as K_B_SI, ELEMENTARY_CHARGE as E_C, PICOSECOND_S,
};
(E_C * E_C * ANGSTROM_M * ANGSTROM_M / PICOSECOND_S) / (6.0 * ANGSTROM_M.powi(3) * K_B_SI)
}
fn no_frames() -> Vec<&'static Frame> {
Vec::new()
}
fn rng_series(n: usize, cols: usize, seed: u64) -> Array2<f64> {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let mut s = Array2::zeros((n, cols));
for t in 0..n {
for c in 0..cols {
s[[t, c]] = rng.random_range(-1.0..1.0);
}
}
s
}
#[test]
fn einstein_conductivity_msd_matches_direct_time_origin_average() {
let n = 256;
let dt = 0.5;
let mct = 80;
let dipole = rng_series(n, 3, 3);
let max_lag = mct.min(n - 1);
let mut expected = A1::<f64>::zeros(max_lag + 1);
for tau in 1..=max_lag {
let count = n - tau;
let mut acc = 0.0;
for t in 0..count {
let mut s = 0.0;
for d in 0..3 {
let dx = dipole[[t + tau, d]] - dipole[[t, d]];
s += dx * dx;
}
acc += s;
}
expected[tau] = acc / count as f64;
}
let raw = EinsteinConductivity
.compute(&no_frames(), (&dipole, dt, mct))
.unwrap();
assert_eq!(raw.msd.len(), expected.len());
for k in 0..raw.msd.len() {
assert!((raw.msd[k] - expected[k]).abs() < 1e-12, "k={k}");
assert!((raw.lag_times[k] - k as f64 * dt).abs() < 1e-12);
}
}
#[test]
fn einstein_conductivity_msd_exact_small() {
let dipole = ndarray::arr2(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [3.0, 0.0, 0.0]]);
let raw = EinsteinConductivity
.compute(&no_frames(), (&dipole, 1.0, 2))
.unwrap();
assert!((raw.msd[0] - 0.0).abs() < 1e-12);
assert!((raw.msd[1] - 2.5).abs() < 1e-12);
assert!((raw.msd[2] - 9.0).abs() < 1e-12);
assert_eq!(raw.lag_times.len(), 3);
let prefactor = einstein_helfand_prefactor();
assert!((prefactor - 3.0988e6).abs() / 3.0988e6 < 1e-3);
}
#[test]
fn series_computes_reject_bad_shape() {
let bad = rng_series(10, 2, 1);
assert!(matches!(
EinsteinConductivity.compute(&no_frames(), (&bad, 1.0, 5)),
Err(ComputeError::DimensionMismatch { .. })
));
assert!(matches!(
GreenKuboConductivity.compute(&no_frames(), (&bad, 1.0, 5)),
Err(ComputeError::DimensionMismatch { .. })
));
}
#[test]
fn einstein_conductivity_plus_linear_fit_matches_manual_ols() {
use crate::compute::fit::LinearFit;
use crate::compute::traits::Fit;
let n = 256;
let dt = 0.5;
let mct = 80;
let (volume, temperature) = (1000.0, 300.0);
let (start_frac, end_frac) = (0.2, 0.8);
let dipole = rng_series(n, 3, 17);
let raw = EinsteinConductivity
.compute(&no_frames(), (&dipole, dt, mct))
.unwrap();
let fit = LinearFit {
window: (start_frac, end_frac),
}
.fit((&raw.lag_times, &raw.msd))
.unwrap();
let (fs, fe) = (fit.fit_start, fit.fit_end);
let np = (fe - fs + 1) as f64;
let (mut sx, mut sy, mut sxx, mut sxy) = (0.0, 0.0, 0.0, 0.0);
for i in fs..=fe {
let x = raw.lag_times[i];
let y = raw.msd[i];
sx += x;
sy += y;
sxx += x * x;
sxy += x * y;
}
let manual_slope = (np * sxy - sx * sy) / (np * sxx - sx * sx);
assert!((fit.slope - manual_slope).abs() < 1e-12);
let prefactor = einstein_helfand_prefactor();
let sigma = prefactor * fit.slope / (volume * temperature);
assert!(sigma.is_finite());
}
#[test]
fn einstein_conductivity_plus_fit_recovers_nernst_einstein() {
use crate::compute::fit::LinearFit;
use crate::compute::traits::Fit;
use molrs::units::constants::{
ANGSTROM_M, BOLTZMANN as K_B_SI, ELEMENTARY_CHARGE as E_C, PICOSECOND_S,
};
let n_realisations = 48usize;
let n_ions = 50usize;
let n_frames = 1500usize;
let dt = 1.0_f64; let q = 1.0_f64; let volume = 1.0e5_f64; let temperature = 300.0_f64; let step = 0.5_f64; let ne_prefactor =
(E_C * E_C * ANGSTROM_M * ANGSTROM_M / PICOSECOND_S) / (ANGSTROM_M.powi(3) * K_B_SI);
let eh_prefactor = einstein_helfand_prefactor();
let mut rng = rand::rngs::StdRng::seed_from_u64(20260601);
let mut sigma_eh_sum = 0.0_f64;
let mut sigma_ne_sum = 0.0_f64;
for _ in 0..n_realisations {
let mut pos = vec![[0.0_f64; 3]; n_ions];
let mut dipole = Array2::<f64>::zeros((n_frames, 3));
let mut step_sq_sum = 0.0_f64;
let mut step_count = 0.0_f64;
for f in 0..n_frames {
for ion in &mut pos {
if f > 0 {
for c in ion.iter_mut() {
let s = rng.random_range(-step..step);
*c += s;
step_sq_sum += s * s;
step_count += 1.0;
}
}
}
let mut m = [0.0_f64; 3];
for p in &pos {
for d in 0..3 {
m[d] += q * p[d];
}
}
for d in 0..3 {
dipole[[f, d]] = m[d];
}
}
let max_corr = n_frames / 5;
let raw = EinsteinConductivity
.compute(&no_frames(), (&dipole, dt, max_corr))
.unwrap();
let fit = LinearFit { window: (0.1, 0.5) }
.fit((&raw.lag_times, &raw.msd))
.unwrap();
sigma_eh_sum += eh_prefactor * fit.slope / (volume * temperature);
let var_axis = step_sq_sum / step_count; let d_diff = var_axis / (2.0 * dt); let number_density = n_ions as f64 / volume; sigma_ne_sum += ne_prefactor * number_density * q * q * d_diff / temperature;
}
let sigma_eh = sigma_eh_sum / n_realisations as f64;
let sigma_ne = sigma_ne_sum / n_realisations as f64;
let rel_err = (sigma_eh - sigma_ne).abs() / sigma_ne.abs();
assert!(
rel_err < 0.13,
"ensemble EH σ = {sigma_eh} S/m vs Nernst–Einstein {sigma_ne} S/m (rel err {rel_err:.3})"
);
assert!(sigma_eh > 0.0);
}
}