use std::collections::VecDeque;
use molrs::types::F;
use crate::compute::error::ComputeError;
#[derive(Debug, Clone)]
pub struct VACFAccumulator {
resolution: usize,
n_dof: usize, acc: Vec<F>, dof_sum: Vec<F>, head: Vec<Vec<F>>, ring: VecDeque<Vec<F>>, n_frames: usize,
}
impl VACFAccumulator {
pub fn new(resolution: usize) -> Result<Self, ComputeError> {
if resolution == 0 {
return Err(ComputeError::OutOfRange {
field: "VACFAccumulator::resolution",
value: resolution.to_string(),
});
}
Ok(Self {
resolution,
n_dof: 0,
acc: vec![0.0; resolution + 1],
dof_sum: Vec::new(),
head: Vec::new(),
ring: VecDeque::new(),
n_frames: 0,
})
}
pub fn resolution(&self) -> usize {
self.resolution
}
pub fn n_frames(&self) -> usize {
self.n_frames
}
pub fn accumulate(&mut self, velocities: &[F]) -> Result<(), ComputeError> {
if velocities.is_empty() {
return Err(ComputeError::BadShape {
expected: "a non-empty velocity frame".to_string(),
got: "0".to_string(),
});
}
if self.n_dof == 0 {
self.n_dof = velocities.len();
self.dof_sum = vec![0.0; self.n_dof];
} else if velocities.len() != self.n_dof {
return Err(ComputeError::DimensionMismatch {
expected: self.n_dof,
got: velocities.len(),
what: "VACF DOF count",
});
}
self.acc[0] += velocities.iter().map(|v| v * v).sum::<F>();
for k in 1..=self.resolution.min(self.ring.len()) {
let past = &self.ring[self.ring.len() - k];
self.acc[k] += velocities
.iter()
.zip(past.iter())
.map(|(a, b)| a * b)
.sum::<F>();
}
for (s, v) in self.dof_sum.iter_mut().zip(velocities.iter()) {
*s += v;
}
if self.head.len() < self.resolution {
self.head.push(velocities.to_vec());
}
self.ring.push_back(velocities.to_vec());
if self.ring.len() > self.resolution {
self.ring.pop_front();
}
self.n_frames += 1;
Ok(())
}
pub fn finalize(&self) -> Result<Vec<F>, ComputeError> {
let t = self.n_frames;
if t < 2 {
return Err(ComputeError::EmptyInput);
}
let max_lag = self.resolution.min(t - 1);
let tf = t as F;
let inv_n_dof = 1.0 / self.n_dof as F;
let mu: Vec<F> = self.dof_sum.iter().map(|s| s / tf).collect();
let sum_mu_sq: F = mu.iter().map(|m| m * m).sum();
let sum_mu_s: F = mu.iter().zip(self.dof_sum.iter()).map(|(m, s)| m * s).sum();
let mut mu_dot_tail: F = 0.0; let mut mu_dot_head: F = 0.0;
let mut out = Vec::with_capacity(max_lag + 1);
out.push((self.acc[0] - 2.0 * sum_mu_s + tf * sum_mu_sq) * inv_n_dof);
for k in 1..=max_lag {
let newest = &self.ring[self.ring.len() - k]; let oldest = &self.head[k - 1]; mu_dot_tail += mu.iter().zip(newest.iter()).map(|(m, v)| m * v).sum::<F>();
mu_dot_head += mu.iter().zip(oldest.iter()).map(|(m, v)| m * v).sum::<F>();
let corr = 2.0 * sum_mu_s - mu_dot_tail - mu_dot_head;
out.push((self.acc[k] - corr + (tf - k as F) * sum_mu_sq) * inv_n_dof);
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compute::VACF;
use crate::compute::traits::Compute;
use molrs::Frame;
use ndarray::Array2;
use rand::{RngExt, SeedableRng};
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]] = 0.3 + rng.random_range(-1.0..1.0);
}
}
s
}
#[test]
fn streaming_matches_batch_fft_vacf() {
let (n, dof, res) = (128, 9, 20);
let v = rng_series(n, dof, 11);
let batch = VACF.compute(&no_frames(), (&v, 1.0, res)).unwrap();
let mut acc = VACFAccumulator::new(res).unwrap();
for t in 0..n {
let frame: Vec<f64> = (0..dof).map(|d| v[[t, d]]).collect();
acc.accumulate(&frame).unwrap();
}
let streamed = acc.finalize().unwrap();
assert_eq!(streamed.len(), batch.acf.len());
let scale = batch.acf[0].abs().max(1.0);
for (k, &s) in streamed.iter().enumerate() {
assert!(
(s - batch.acf[k]).abs() / scale < 1e-12,
"lag {k}: streamed {} vs batch {}",
s,
batch.acf[k]
);
}
}
#[test]
fn short_trajectory_caps_lag_at_t_minus_one() {
let (n, dof, res) = (6, 3, 20); let v = rng_series(n, dof, 5);
let batch = VACF.compute(&no_frames(), (&v, 1.0, res)).unwrap();
let mut acc = VACFAccumulator::new(res).unwrap();
for t in 0..n {
let frame: Vec<f64> = (0..dof).map(|d| v[[t, d]]).collect();
acc.accumulate(&frame).unwrap();
}
let streamed = acc.finalize().unwrap();
assert_eq!(streamed.len(), n); let scale = batch.acf[0].abs().max(1.0);
for (k, &s) in streamed.iter().enumerate() {
assert!((s - batch.acf[k]).abs() / scale < 1e-12, "lag {k}");
}
}
#[test]
fn zero_resolution_and_dof_mismatch_are_errors() {
assert!(matches!(
VACFAccumulator::new(0).unwrap_err(),
ComputeError::OutOfRange { .. }
));
let mut acc = VACFAccumulator::new(4).unwrap();
acc.accumulate(&[1.0, 2.0]).unwrap();
assert!(matches!(
acc.accumulate(&[1.0]).unwrap_err(),
ComputeError::DimensionMismatch { .. }
));
assert!(matches!(
acc.finalize().unwrap_err(),
ComputeError::EmptyInput
));
}
}