use wide::{f64x4, CmpEq};
const LANES: usize = 4;
pub fn rate_of_change(values: &[f64], lag: usize) -> Vec<f64> {
let mut out = vec![0.0; values.len()];
rate_of_change_into(values, lag, &mut out);
out
}
pub fn rate_of_change_into(values: &[f64], lag: usize, out: &mut [f64]) {
assert_eq!(
values.len(),
out.len(),
"rate_of_change_into: out.len() must equal values.len()"
);
let n = values.len();
if lag == 0 || n <= lag {
for slot in out.iter_mut() {
*slot = 0.0;
}
return;
}
for slot in out[..lag].iter_mut() {
*slot = 0.0;
}
let one_hundred = f64x4::splat(100.0);
let zero = f64x4::ZERO;
let mut i = lag;
while i + LANES <= n {
let cur = f64x4::new([values[i], values[i + 1], values[i + 2], values[i + 3]]);
let prev = f64x4::new([
values[i - lag],
values[i + 1 - lag],
values[i + 2 - lag],
values[i + 3 - lag],
]);
let diff = cur - prev;
let ratio = diff / prev * one_hundred;
let safe = prev.cmp_eq(zero).blend(zero, ratio);
let arr = safe.to_array();
out[i] = arr[0];
out[i + 1] = arr[1];
out[i + 2] = arr[2];
out[i + 3] = arr[3];
i += LANES;
}
while i < n {
let prev = values[i - lag];
out[i] = if prev == 0.0 {
0.0
} else {
(values[i] - prev) / prev * 100.0
};
i += 1;
}
}
#[cfg(test)]
pub(crate) fn rate_of_change_scalar(values: &[f64], lag: usize) -> Vec<f64> {
let n = values.len();
let mut out = vec![0.0; n];
if lag == 0 || n <= lag {
return out;
}
for i in lag..n {
let prev = values[i - lag];
out[i] = if prev == 0.0 {
0.0
} else {
(values[i] - prev) / prev * 100.0
};
}
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(), "length mismatch");
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 known_values() {
let v = vec![100.0, 110.0, 121.0, 133.1];
let out = rate_of_change(&v, 1);
assert_eq!(out[0], 0.0);
for i in 1..v.len() {
assert!((out[i] - 10.0).abs() < 1e-9, "out[{}] = {}", i, out[i]);
}
}
#[test]
fn lag_larger_than_input() {
let v = vec![1.0, 2.0, 3.0];
assert_eq!(rate_of_change(&v, 5), vec![0.0, 0.0, 0.0]);
assert_eq!(rate_of_change(&v, 3), vec![0.0, 0.0, 0.0]);
}
#[test]
fn zero_predecessor_yields_zero() {
let v = vec![0.0, 10.0, 0.0, 5.0, 0.0, 7.0];
let out = rate_of_change(&v, 2);
assert_eq!(out[0], 0.0);
assert_eq!(out[1], 0.0);
assert_eq!(out[2], 0.0);
assert!((out[3] - -50.0).abs() < 1e-9);
assert_eq!(out[4], 0.0);
assert!((out[5] - 40.0).abs() < 1e-9);
}
#[test]
fn parity_random_sizes_and_lags() {
for n in [0, 1, 4, 5, 7, 8, 16, 31, 100, 1000, 5000] {
let values: Vec<f64> = (0..n)
.map(|i| 100.0 + ((i as f64) * 0.13).sin() * 5.0)
.collect();
for lag in [1, 2, 3, 4, 5, 7, 16, 32] {
let s = rate_of_change_scalar(&values, lag);
let v = rate_of_change(&values, lag);
approx_slice_eq(&v, &s, 1e-12, 1e-12);
}
}
}
#[test]
#[should_panic(expected = "out.len()")]
fn into_length_mismatch_panics() {
let v = [1.0, 2.0, 3.0];
let mut out = [0.0; 2];
rate_of_change_into(&v, 1, &mut out);
}
}