use crate::error::SignalError;
use crate::linear_algebra::Matrix;
use crate::scalar::Numeric;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SavitzkyGolay<const WINDOW: usize, const POLYNOMIAL_TERMS: usize, T: Numeric = f64> {
samples: [T; WINDOW],
next: usize,
initialized: bool,
smoothing_weights: [T; WINDOW],
first_derivative_weights: [T; WINDOW],
second_derivative_weights: [T; WINDOW],
delay_seconds: T,
}
impl<const WINDOW: usize, const POLYNOMIAL_TERMS: usize, T: Numeric>
SavitzkyGolay<WINDOW, POLYNOMIAL_TERMS, T>
{
pub fn centered(dt: T) -> Result<Self, SignalError> {
if WINDOW % 2 == 0 {
return Err(SignalError::WindowEvenLength);
}
Self::build(dt, (WINDOW - 1) / 2)
}
pub fn latest(dt: T) -> Result<Self, SignalError> {
Self::build(dt, WINDOW - 1)
}
#[inline]
#[must_use]
pub fn filter(&mut self, input: T) -> T {
if self.initialized {
self.samples[self.next] = input;
self.next = (self.next + 1) % WINDOW;
} else {
self.samples = [input; WINDOW];
self.next = 0;
self.initialized = true;
}
self.value()
}
#[inline]
pub fn reset(&mut self) {
self.samples = [T::ZERO; WINDOW];
self.next = 0;
self.initialized = false;
}
#[inline]
#[must_use]
pub fn value(&self) -> T {
self.weighted_sum(&self.smoothing_weights)
}
#[inline]
#[must_use]
pub fn first_derivative(&self) -> T {
self.weighted_sum(&self.first_derivative_weights)
}
#[inline]
#[must_use]
pub fn second_derivative(&self) -> T {
self.weighted_sum(&self.second_derivative_weights)
}
#[inline]
#[must_use]
pub fn delay(&self) -> T {
self.delay_seconds
}
fn build(dt: T, read_position: usize) -> Result<Self, SignalError> {
if !dt.is_finite() {
return Err(SignalError::NonFinite);
}
if dt <= T::ZERO {
return Err(SignalError::NonPositiveTimestep);
}
if WINDOW == 0 || POLYNOMIAL_TERMS == 0 {
return Err(SignalError::WindowTooShort);
}
if POLYNOMIAL_TERMS > WINDOW {
return Err(SignalError::PolynomialOrderTooHigh);
}
let fit = Matrix::<WINDOW, POLYNOMIAL_TERMS, T>::from_fn(|sample, term| {
let offset = T::from_usize(sample) - T::from_usize(read_position);
offset.powi(term as i32)
});
let inverse = fit.pseudo_inverse()?;
let smoothing_weights = core::array::from_fn(|sample| inverse[(0, sample)]);
let first_derivative_weights = core::array::from_fn(|sample| {
if POLYNOMIAL_TERMS >= 2 {
inverse[(1, sample)] / dt
} else {
T::ZERO
}
});
let second_derivative_weights = core::array::from_fn(|sample| {
if POLYNOMIAL_TERMS >= 3 {
T::TWO * inverse[(2, sample)] / (dt * dt)
} else {
T::ZERO
}
});
Ok(Self {
samples: [T::ZERO; WINDOW],
next: 0,
initialized: false,
smoothing_weights,
first_derivative_weights,
second_derivative_weights,
delay_seconds: T::from_usize(WINDOW - 1 - read_position) * dt,
})
}
#[inline]
#[must_use]
fn weighted_sum(&self, weights: &[T; WINDOW]) -> T {
let mut total = T::ZERO;
#[allow(clippy::needless_range_loop)]
for position in 0..WINDOW {
total += weights[position] * self.samples[(self.next + position) % WINDOW];
}
total
}
}