pub fn measure_diffusion(series: &[f64], dt: f64, window_size: usize) -> f64 {
if series.len() < 4 {
return 0.0;
}
let smoothed = uniform_filter(series, window_size);
let n = smoothed.len();
let max_lag = (n / 4).max(1);
let mut sum_msd_tau = 0.0;
let mut sum_tau_sq = 0.0;
for lag in 1..=max_lag {
let tau = lag as f64 * dt;
let mut acc = 0.0;
let mut count = 0usize;
for t in 0..(n - lag) {
let dx = smoothed[t + lag] - smoothed[t];
acc += dx * dx;
count += 1;
}
let msd = acc / count as f64;
sum_msd_tau += msd * tau;
sum_tau_sq += tau * tau;
}
if sum_tau_sq <= 0.0 {
return 0.0;
}
sum_msd_tau / sum_tau_sq
}
pub fn uniform_filter(data: &[f64], window: usize) -> Vec<f64> {
if window == 0 || data.len() < window {
return data.to_vec();
}
let half_w = window / 2;
let n = data.len();
let mut filtered = Vec::with_capacity(n);
for i in 0..n {
let lo = i.saturating_sub(half_w);
let hi = (i + half_w + 1).min(n);
let sum: f64 = data[lo..hi].iter().sum();
filtered.push(sum / (hi - lo) as f64);
}
filtered
}
fn median(sorted: &[f64]) -> f64 {
let n = sorted.len();
if n % 2 == 1 {
sorted[n / 2]
} else {
0.5 * (sorted[n / 2 - 1] + sorted[n / 2])
}
}
pub fn median_absolute_deviation(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let med = median(&sorted);
let mut devs: Vec<f64> = values.iter().map(|&v| (v - med).abs()).collect();
devs.sort_by(|a, b| a.partial_cmp(b).unwrap());
1.4826 * median(&devs)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{Rng, SeedableRng};
fn normal_draws(rng: &mut impl Rng, n: usize, mean: f64, sigma: f64) -> Vec<f64> {
(0..n)
.map(|_| {
let u1: f64 = rng.random::<f64>().max(f64::MIN_POSITIVE);
let u2: f64 = rng.random();
let z = (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos();
mean + sigma * z
})
.collect()
}
#[test]
fn test_measure_diffusion_constant() {
let series = vec![100.0; 100];
let d = measure_diffusion(&series, 1e6, 5);
assert!(d.abs() < 1e-10);
}
#[test]
fn test_measure_diffusion_recovers_random_walk() {
let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
let sigma = 0.05_f64;
let dt = 1000.0;
let steps = normal_draws(&mut rng, 4000, 0.0, sigma);
let mut x = 150.0;
let series: Vec<f64> = steps
.iter()
.map(|s| {
x += s;
x
})
.collect();
let d_true = sigma * sigma / dt;
let d = measure_diffusion(&series, dt, 1);
assert!(
d > 0.4 * d_true && d < 2.5 * d_true,
"D = {d:.3e}, true = {d_true:.3e}"
);
}
#[test]
fn test_uniform_filter_identity() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let filtered = uniform_filter(&data, 1);
for (a, b) in data.iter().zip(filtered.iter()) {
assert!((a - b).abs() < 1e-10);
}
}
#[test]
fn test_median_absolute_deviation() {
let values = [1.0, 2.0, 3.0, 4.0, 5.0];
let mad = median_absolute_deviation(&values);
assert!((mad - 1.4826).abs() < 1e-12, "MAD = {mad}");
let mut rng = rand::rngs::StdRng::seed_from_u64(5);
let draws = normal_draws(&mut rng, 20_000, 10.0, 2.0);
let mad_g = median_absolute_deviation(&draws);
assert!((mad_g - 2.0).abs() < 0.1, "Gaussian MAD = {mad_g}");
}
#[test]
fn test_median_absolute_deviation_empty() {
assert_eq!(median_absolute_deviation(&[]), 0.0);
}
}