use sim_lib_numbers_tensor_linalg::{DenseSolveOptions, DenseSolveReport, solve_dense_f64};
use crate::{
BoundaryMode, SignalError, convolution::validate_real_signal, linalg_support::dense_error,
wavelet::extended,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SavitzkyGolaySpec {
pub window_length: usize,
pub polynomial_order: usize,
pub derivative_order: usize,
pub sample_spacing: f64,
pub singularity_threshold: f64,
}
impl Default for SavitzkyGolaySpec {
fn default() -> Self {
Self {
window_length: 5,
polynomial_order: 2,
derivative_order: 0,
sample_spacing: 1.0,
singularity_threshold: 1e-12,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FiniteImpulseResponse {
pub coefficients: Vec<f64>,
pub spec: SavitzkyGolaySpec,
pub design_report: DenseSolveReport,
}
pub fn savitzky_golay(spec: SavitzkyGolaySpec) -> Result<FiniteImpulseResponse, SignalError> {
validate_spec(spec)?;
let terms = spec.polynomial_order + 1;
let radius = spec.window_length / 2;
let offsets = (0..spec.window_length)
.map(|index| index as f64 - radius as f64)
.collect::<Vec<_>>();
let mut gram = vec![0.0; terms * terms];
for row in 0..terms {
for column in 0..terms {
gram[row * terms + column] = offsets
.iter()
.map(|offset| offset.powi((row + column) as i32))
.sum();
}
}
let mut target = vec![0.0; terms];
target[spec.derivative_order] =
factorial(spec.derivative_order) / spec.sample_spacing.powi(spec.derivative_order as i32);
let solution = solve_dense_f64(
&gram,
&target,
DenseSolveOptions {
singularity_threshold: spec.singularity_threshold,
},
)
.map_err(|error| dense_error(error, "Savitzky-Golay design"))?;
let coefficients = offsets
.iter()
.map(|offset| {
solution
.values
.iter()
.enumerate()
.map(|(power, coefficient)| coefficient * offset.powi(power as i32))
.sum::<f64>()
})
.collect::<Vec<_>>();
validate_real_signal(&coefficients)?;
Ok(FiniteImpulseResponse {
coefficients,
spec,
design_report: solution.report,
})
}
pub fn apply_savitzky_golay(
signal: &[f64],
filter: &FiniteImpulseResponse,
boundary: BoundaryMode,
) -> Result<Vec<f64>, SignalError> {
if signal.is_empty() {
return Err(SignalError::InvalidLength {
len: 0,
reason: "Savitzky-Golay application requires at least one sample",
});
}
validate_real_signal(signal)?;
if filter.coefficients.len() != filter.spec.window_length {
return Err(SignalError::LengthMismatch {
expected: filter.spec.window_length,
actual: filter.coefficients.len(),
});
}
validate_real_signal(&filter.coefficients)?;
let radius = (filter.coefficients.len() / 2) as isize;
let output = (0..signal.len())
.map(|center| {
filter
.coefficients
.iter()
.enumerate()
.map(|(tap, coefficient)| {
let sample =
extended(signal, center as isize + tap as isize - radius, boundary);
coefficient * sample
})
.sum::<f64>()
})
.collect::<Vec<_>>();
validate_real_signal(&output)?;
Ok(output)
}
fn validate_spec(spec: SavitzkyGolaySpec) -> Result<(), SignalError> {
if spec.window_length == 0 || spec.window_length.is_multiple_of(2) {
return Err(SignalError::InvalidPolicy {
policy: "Savitzky-Golay window length",
reason: "the centered window length must be positive and odd",
});
}
if spec.polynomial_order >= spec.window_length {
return Err(SignalError::InvalidPolicy {
policy: "Savitzky-Golay polynomial order",
reason: "the polynomial order must be smaller than the window length",
});
}
if spec.derivative_order > spec.polynomial_order {
return Err(SignalError::InvalidPolicy {
policy: "Savitzky-Golay derivative order",
reason: "the derivative order must not exceed the fitted polynomial order",
});
}
if !spec.sample_spacing.is_finite() || spec.sample_spacing <= 0.0 {
return Err(SignalError::InvalidPolicy {
policy: "Savitzky-Golay sample spacing",
reason: "sample spacing must be finite and strictly positive",
});
}
Ok(())
}
fn factorial(value: usize) -> f64 {
(1..=value).fold(1.0, |product, factor| product * factor as f64)
}