use scirs2_core::linalg::solve_ndarray;
use scirs2_core::ndarray::{Array1, Array2};
pub(crate) fn smooth_savgol(y: &[f64], window: usize, poly_order: usize) -> Vec<f64> {
let n = y.len();
if n == 0 {
return Vec::new();
}
let window = if window.is_multiple_of(2) {
window + 1
} else {
window
};
let window = window.max(3).min(n);
let poly_order = poly_order.min(window - 1);
let half = window / 2;
let y_filled = pre_interpolate_nans(y);
let center_coeffs = compute_sg_kernel(window, poly_order, half);
let mut result = vec![0.0_f64; n];
for i in 0..n {
if i >= half && i + half < n {
result[i] = center_coeffs
.iter()
.zip(&y_filled[i - half..=i + half])
.map(|(&c, &v)| c * v)
.sum();
} else {
let left = i.min(half);
let right = (n - 1 - i).min(half);
let actual_window = left + right + 1;
let actual_poly = poly_order.min(actual_window - 1);
let edge_kernel = compute_sg_kernel(actual_window, actual_poly, left);
let start = i - left;
result[i] = edge_kernel
.iter()
.zip(&y_filled[start..start + actual_window])
.map(|(&c, &v)| c * v)
.sum();
}
}
result
}
fn compute_sg_kernel(window: usize, poly_order: usize, eval_pos: usize) -> Vec<f64> {
if window == 0 {
return Vec::new();
}
let half = window / 2;
let mut a = Array2::<f64>::zeros((window, poly_order + 1));
for i in 0..window {
let x = i as f64 - half as f64;
for j in 0..=poly_order {
a[[i, j]] = x.powi(j as i32);
}
}
let eval_x = eval_pos as f64 - half as f64;
let e: Array1<f64> = Array1::from_iter((0..=poly_order).map(|j| eval_x.powi(j as i32)));
let at = a.t().to_owned(); let ata: Array2<f64> = at.dot(&a);
let beta_tilde = match solve_ndarray(&ata, &e) {
Ok(beta) => beta,
Err(_) => return vec![1.0 / window as f64; window],
};
let h = a.dot(&beta_tilde);
h.to_vec()
}
fn pre_interpolate_nans(y: &[f64]) -> Vec<f64> {
let n = y.len();
let mut result = y.to_vec();
let valid: Vec<usize> = (0..n).filter(|&i| !y[i].is_nan()).collect();
if valid.is_empty() {
return vec![0.0; n];
}
let first_valid = valid[0];
let fill_leading = y[first_valid];
result[..first_valid]
.iter_mut()
.for_each(|v| *v = fill_leading);
let last_valid = valid[valid.len() - 1]; let fill_trailing = y[last_valid];
result[(last_valid + 1)..]
.iter_mut()
.for_each(|v| *v = fill_trailing);
for w in valid.windows(2) {
let (i0, i1) = (w[0], w[1]);
if i1 > i0 + 1 {
let v0 = y[i0];
let v1 = y[i1];
for (offset, slot) in result[(i0 + 1)..i1].iter_mut().enumerate() {
let t = (offset + 1) as f64 / (i1 - i0) as f64;
*slot = v0 + t * (v1 - v0);
}
}
}
result
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn empty_input_returns_empty() {
assert!(smooth_savgol(&[], 7, 2).is_empty());
}
#[test]
fn constant_series_reproduced() {
let y = vec![3.0_f64; 30];
let z = smooth_savgol(&y, 7, 2);
for (&zi, &yi) in z.iter().zip(y.iter()) {
assert!(
(zi - yi).abs() < 1e-6,
"constant not reproduced: got {zi}, expected {yi}"
);
}
}
#[test]
fn even_window_bumped_to_odd() {
let y: Vec<f64> = (0..20).map(|i| i as f64).collect();
let z = smooth_savgol(&y, 6, 2); assert_eq!(z.len(), y.len());
assert!(z.iter().all(|v| v.is_finite()));
}
#[test]
fn pre_interpolate_all_nan_returns_zeros() {
let y = vec![f64::NAN; 5];
let result = pre_interpolate_nans(&y);
assert_eq!(result, vec![0.0; 5]);
}
#[test]
fn pre_interpolate_leading_and_trailing() {
let y = vec![f64::NAN, f64::NAN, 4.0, 8.0, f64::NAN];
let result = pre_interpolate_nans(&y);
assert_eq!(result[0], 4.0); assert_eq!(result[1], 4.0);
assert_eq!(result[2], 4.0);
assert_eq!(result[3], 8.0);
assert_eq!(result[4], 8.0); }
#[test]
fn kernel_length_matches_window() {
let kernel = compute_sg_kernel(7, 2, 3);
assert_eq!(kernel.len(), 7);
}
#[test]
fn kernel_sums_to_one_for_constant_polynomial() {
let kernel = compute_sg_kernel(7, 2, 3);
let sum: f64 = kernel.iter().sum();
assert!((sum - 1.0).abs() < 1e-10, "kernel sum = {sum}");
}
}