use super::reductions::{sum, sum_squares};
pub fn rolling_mean(values: &[f64], window: usize) -> Vec<f64> {
let mut out = vec![0.0; values.len()];
rolling_mean_into(values, window, &mut out);
out
}
pub fn rolling_mean_into(values: &[f64], window: usize, out: &mut [f64]) {
assert_eq!(
values.len(),
out.len(),
"rolling_mean_into: out.len() must equal values.len()"
);
assert!(window >= 1, "rolling_mean_into: window must be >= 1");
let n = values.len();
if n == 0 {
return;
}
let mut running = 0.0_f64;
let prefix_end = (window - 1).min(n);
for i in 0..prefix_end {
running += values[i];
out[i] = running / (i as f64 + 1.0);
}
if n < window {
return;
}
let first_sum = sum(&values[..window]);
let inv_w = 1.0 / window as f64;
out[window - 1] = first_sum * inv_w;
let mut s = first_sum;
for i in window..n {
s += values[i] - values[i - window];
out[i] = s * inv_w;
}
}
pub fn rolling_std(values: &[f64], window: usize) -> Vec<f64> {
let mut out = vec![0.0; values.len()];
rolling_std_into(values, window, &mut out);
out
}
pub fn rolling_std_into(values: &[f64], window: usize, out: &mut [f64]) {
assert_eq!(
values.len(),
out.len(),
"rolling_std_into: out.len() must equal values.len()"
);
assert!(window >= 1, "rolling_std_into: window must be >= 1");
let n = values.len();
if n == 0 {
return;
}
let mut s = 0.0_f64;
let mut q = 0.0_f64;
let prefix_end = (window - 1).min(n);
for i in 0..prefix_end {
let v = values[i];
s += v;
q += v * v;
let len = (i + 1) as f64;
let mean = s / len;
let var = (q - s * mean) / len;
out[i] = var.max(0.0).sqrt();
}
if n < window {
return;
}
let first_sum = sum(&values[..window]);
let first_sq = sum_squares(&values[..window]);
let w_f = window as f64;
let mean = first_sum / w_f;
let var = (first_sq - first_sum * mean) / w_f;
out[window - 1] = var.max(0.0).sqrt();
s = first_sum;
q = first_sq;
for i in window..n {
let inc = values[i];
let dec = values[i - window];
s += inc - dec;
q += inc * inc - dec * dec;
let mean = s / w_f;
let var = (q - s * mean) / w_f;
out[i] = var.max(0.0).sqrt();
}
}
#[cfg(test)]
pub(crate) fn rolling_mean_scalar(values: &[f64], window: usize) -> Vec<f64> {
let n = values.len();
let mut out = vec![0.0; n];
if n == 0 {
return out;
}
let mut running = 0.0_f64;
let prefix_end = (window - 1).min(n);
for i in 0..prefix_end {
running += values[i];
out[i] = running / (i as f64 + 1.0);
}
if n < window {
return out;
}
let inv_w = 1.0 / window as f64;
let mut s = 0.0_f64;
for &v in &values[..window] {
s += v;
}
out[window - 1] = s * inv_w;
for i in window..n {
s += values[i] - values[i - window];
out[i] = s * inv_w;
}
out
}
#[cfg(test)]
pub(crate) fn rolling_std_scalar(values: &[f64], window: usize) -> Vec<f64> {
let n = values.len();
let mut out = vec![0.0; n];
if n == 0 {
return out;
}
let mut s = 0.0_f64;
let mut q = 0.0_f64;
let prefix_end = (window - 1).min(n);
for i in 0..prefix_end {
let v = values[i];
s += v;
q += v * v;
let len = (i + 1) as f64;
let mean = s / len;
let var = (q - s * mean) / len;
out[i] = var.max(0.0).sqrt();
}
if n < window {
return out;
}
let w_f = window as f64;
s = 0.0;
q = 0.0;
for &v in &values[..window] {
s += v;
q += v * v;
}
let mean = s / w_f;
let var = (q - s * mean) / w_f;
out[window - 1] = var.max(0.0).sqrt();
for i in window..n {
let inc = values[i];
let dec = values[i - window];
s += inc - dec;
q += inc * inc - dec * dec;
let mean = s / w_f;
let var = (q - s * mean) / w_f;
out[i] = var.max(0.0).sqrt();
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_slice_eq(a: &[f64], b: &[f64], rel: f64, abs: f64) {
assert_eq!(a.len(), b.len());
for (i, (&x, &y)) in a.iter().zip(b.iter()).enumerate() {
let diff = (x - y).abs();
let tol = abs.max(rel * x.abs().max(y.abs()));
assert!(
diff <= tol,
"mismatch at index {}: simd={} scalar={} diff={}",
i,
x,
y,
diff
);
}
}
#[test]
fn rolling_mean_known() {
let v = [1.0, 2.0, 3.0, 4.0, 5.0];
let out = rolling_mean(&v, 3);
let expected = [1.0, 1.5, 2.0, 3.0, 4.0];
for (a, b) in out.iter().zip(expected.iter()) {
assert!((a - b).abs() < 1e-12);
}
}
#[test]
fn rolling_mean_parity() {
for n in [0, 1, 3, 4, 5, 8, 16, 100, 1000, 5000] {
let values: Vec<f64> = (0..n)
.map(|i| ((i as f64) * 0.7).sin() * 100.0 + 50.0)
.collect();
for w in [1, 2, 3, 4, 5, 8, 32] {
let s = rolling_mean_scalar(&values, w);
let v = rolling_mean(&values, w);
approx_slice_eq(&v, &s, 1e-10, 1e-9);
}
}
}
#[test]
fn rolling_std_parity() {
for n in [0, 1, 3, 4, 5, 8, 16, 100, 1000, 5000] {
let values: Vec<f64> = (0..n)
.map(|i| ((i as f64) * 0.31).cos() * 20.0 + 100.0)
.collect();
for w in [1, 2, 3, 4, 5, 8, 32] {
let s = rolling_std_scalar(&values, w);
let v = rolling_std(&values, w);
approx_slice_eq(&v, &s, 1e-8, 1e-8);
}
}
}
#[test]
fn rolling_std_window_one_is_zero() {
let v = [5.0, 9.0, 1.0, 100.0];
let out = rolling_std(&v, 1);
for x in out {
assert!(x.abs() < 1e-12);
}
}
#[test]
#[should_panic(expected = "window must be >= 1")]
fn rolling_mean_window_zero_panics() {
let v = [1.0, 2.0];
let mut out = [0.0; 2];
rolling_mean_into(&v, 0, &mut out);
}
}