use crate::compute::result::ComputeResult;
use ndarray::Array1;
use molrs::store::frame_access::FrameAccess;
use crate::compute::error::ComputeError;
use crate::compute::traits::Compute;
fn validate_series(p: &ndarray::Array2<f64>, name: &'static str) -> Result<usize, ComputeError> {
let shape = p.shape();
if shape[1] != 3 {
return Err(ComputeError::DimensionMismatch {
expected: 3,
got: shape[1],
what: name,
});
}
let n_frames = shape[0];
if n_frames < 2 {
return Err(ComputeError::EmptyInput);
}
for (idx, &v) in p.iter().enumerate() {
if !v.is_finite() {
return Err(ComputeError::NonFinite {
where_: name,
index: idx,
});
}
}
Ok(n_frames)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct OnsagerCorrelation;
pub type OnsagerCorrelationArgs<'a> = (
&'a ndarray::Array2<f64>,
&'a ndarray::Array2<f64>,
f64,
usize,
);
fn collective_cross_correlation(
p_i: &ndarray::Array2<f64>,
p_j: &ndarray::Array2<f64>,
dt: f64,
max_correlation_time: usize,
) -> Result<OnsagerResult, ComputeError> {
let n_i = validate_series(p_i, "onsager p_i (expected (n_frames, 3))")?;
let n_j = validate_series(p_j, "onsager p_j (expected (n_frames, 3))")?;
if n_i != n_j {
return Err(ComputeError::DimensionMismatch {
expected: n_i,
got: n_j,
what: "onsager p_i / p_j frame count",
});
}
if dt <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "dt",
value: dt.to_string(),
});
}
let n_frames = n_i;
let max_lag = max_correlation_time.min(n_frames - 1);
let mut correlation = Array1::<f64>::zeros(max_lag + 1);
for tau in 1..=max_lag {
let count = n_frames - tau;
let mut acc = 0.0;
for t in 0..count {
let mut s = 0.0;
for d in 0..3 {
let di = p_i[[t + tau, d]] - p_i[[t, d]];
let dj = p_j[[t + tau, d]] - p_j[[t, d]];
s += di * dj;
}
acc += s;
}
correlation[tau] = acc / count as f64;
}
let lag_times = Array1::from_iter((0..=max_lag).map(|i| i as f64 * dt));
Ok(OnsagerResult {
lag_times,
correlation,
})
}
impl Compute for OnsagerCorrelation {
type Args<'a> = OnsagerCorrelationArgs<'a>;
type Output = OnsagerResult;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
_frames: &[&'a FA],
args: Self::Args<'a>,
) -> Result<Self::Output, ComputeError> {
let (p_i, p_j, dt, max_correlation_time) = args;
collective_cross_correlation(p_i, p_j, dt, max_correlation_time)
}
}
#[derive(Debug, Clone)]
pub struct OnsagerResult {
pub lag_times: Array1<f64>,
pub correlation: Array1<f64>,
}
impl ComputeResult for OnsagerResult {}
#[cfg(test)]
mod tests {
use super::*;
use molrs::Frame;
use ndarray::array;
fn no_frames() -> Vec<&'static Frame> {
Vec::new()
}
#[test]
fn self_correlation_is_collective_msd() {
let p = array![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
[4.0, 0.0, 0.0],
];
let r = OnsagerCorrelation
.compute(&no_frames(), (&p, &p, 1.0, 4))
.unwrap();
for tau in 0..=4 {
let expected = (tau as f64) * (tau as f64);
assert!(
(r.correlation[tau] - expected).abs() < 1e-12,
"lag {tau}: got {}, expected {expected}",
r.correlation[tau]
);
}
assert!((r.lag_times[2] - 2.0).abs() < 1e-12);
}
#[test]
fn anticorrelated_species_give_negative_offdiagonal() {
let pi = array![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let pj = array![[0.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [-2.0, 0.0, 0.0]];
let r = OnsagerCorrelation
.compute(&no_frames(), (&pi, &pj, 0.5, 2))
.unwrap();
assert!((r.correlation[1] + 1.0).abs() < 1e-12); assert!((r.correlation[2] + 4.0).abs() < 1e-12);
assert!((r.lag_times[1] - 0.5).abs() < 1e-12);
}
#[test]
fn time_origin_average_matches_manual() {
let p = array![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 2.0, 0.0]];
let r = OnsagerCorrelation
.compute(&no_frames(), (&p, &p, 1.0, 2))
.unwrap();
assert!((r.correlation[1] - 2.5).abs() < 1e-12);
}
#[test]
fn rejects_bad_shape_and_mismatch() {
let p3 = array![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let p_short = array![[0.0, 0.0, 0.0]];
assert!(
OnsagerCorrelation
.compute(&no_frames(), (&p3, &p_short, 1.0, 1))
.is_err()
);
let bad = array![[0.0, 0.0], [1.0, 0.0]];
assert!(
OnsagerCorrelation
.compute(&no_frames(), (&bad, &bad, 1.0, 1))
.is_err()
);
assert!(
OnsagerCorrelation
.compute(&no_frames(), (&p3, &p3, 0.0, 1))
.is_err()
);
}
}