pub(super) fn central_differences(x: &[f64], dt: f64) -> Vec<f64> {
let n = x.len();
if n < 2 {
return vec![0.0; n];
}
let mut dx = vec![0.0; n];
dx[0] = (x[1] - x[0]) / dt;
dx[n - 1] = (x[n - 1] - x[n - 2]) / dt;
for i in 1..n - 1 {
dx[i] = (x[i + 1] - x[i - 1]) / (2.0 * dt);
}
dx
}
pub(super) fn savitzky_golay_derivative(x: &[f64], dt: f64) -> Vec<f64> {
let n = x.len();
if n < 5 {
return central_differences(x, dt);
}
let coeffs = [-2.0_f64, -1.0, 0.0, 1.0, 2.0];
let norm = 10.0 * dt;
let mut dx = central_differences(x, dt);
for i in 2..n - 2 {
dx[i] = coeffs
.iter()
.enumerate()
.map(|(k, &c)| c * x[i + k - 2])
.sum::<f64>()
/ norm;
}
dx
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn central_differences_linear() {
let dt = 0.1_f64;
let n = 10;
let x: Vec<f64> = (0..n).map(|i| 2.0 * i as f64 * dt).collect();
let dx = central_differences(&x, dt);
for &d in &dx {
assert!((d - 2.0).abs() < 1e-10, "expected 2.0, got {d}");
}
}
#[test]
fn central_differences_short_slice() {
let dx = central_differences(&[42.0], 0.1);
assert_eq!(dx, vec![0.0]);
}
#[test]
fn savitzky_golay_falls_back_for_small_n() {
let dt = 0.1_f64;
let x = vec![1.0, 2.0, 3.0, 4.0];
let sg = savitzky_golay_derivative(&x, dt);
let cd = central_differences(&x, dt);
for (a, b) in sg.iter().zip(cd.iter()) {
assert!((a - b).abs() < 1e-12, "mismatch: sg={a}, cd={b}");
}
}
#[test]
fn savitzky_golay_linear_recovers_exact_slope() {
let dt = 0.1_f64;
let n = 20;
let x: Vec<f64> = (0..n).map(|i| 3.0 * i as f64 * dt).collect();
let dx = savitzky_golay_derivative(&x, dt);
for &d in &dx {
assert!((d - 3.0).abs() < 1e-9, "expected 3.0, got {d}");
}
}
}