use crate::matrix::Matrix;
pub struct DeltaParams {
pub width: usize,
pub order: usize,
}
impl Default for DeltaParams {
fn default() -> Self {
Self { width: 9, order: 1 }
}
}
pub fn delta(data: &Matrix, params: &DeltaParams) -> Matrix {
let width = params.width;
let order = params.order;
assert!(width >= 3 && width % 2 == 1, "width must be odd and >= 3");
assert!(order >= 1, "order must be >= 1");
let n_rows = data.rows();
let t = data.cols();
let half = (width - 1) / 2;
let interior_coeffs = sg_coeffs(width, order, half as f64);
let left_coeffs: Vec<Vec<f64>> = (0..half)
.map(|i| sg_coeffs(width, order, i as f64))
.collect();
let right_coeffs: Vec<Vec<f64>> = (0..half)
.map(|k| {
let eval_pos = (half + 1 + k) as f64;
sg_coeffs(width, order, eval_pos)
})
.collect();
let mut result = Matrix::zeros(n_rows, t);
for r in 0..n_rows {
for c in 0..t {
let val = if c < half {
let coeffs = &left_coeffs[c];
(0..width).map(|j| coeffs[j] * data.get(r, j)).sum()
} else if c >= t.saturating_sub(half) {
let k = c - (t - half);
let coeffs = &right_coeffs[k];
let win_start = t - width;
(0..width)
.map(|j| coeffs[j] * data.get(r, win_start + j))
.sum()
} else {
let win_start = c - half;
(0..width)
.map(|j| interior_coeffs[j] * data.get(r, win_start + j))
.sum()
};
*result.get_mut(r, c) = val;
}
}
result
}
fn sg_coeffs(window_length: usize, deriv: usize, eval_pos: f64) -> Vec<f64> {
let polyorder = deriv;
let p = polyorder + 1; let n = window_length;
let mut a = vec![0.0f64; n * p];
for j in 0..n {
let x = j as f64 - eval_pos;
let mut xpow = 1.0f64;
for k in 0..p {
a[j * p + k] = xpow;
xpow *= x;
}
}
let mut ata = vec![0.0f64; p * p];
for k1 in 0..p {
for k2 in 0..p {
let sum: f64 = (0..n).map(|j| a[j * p + k1] * a[j * p + k2]).sum();
ata[k1 * p + k2] = sum;
}
}
let mut v = vec![0.0f64; p];
v[deriv] = 1.0;
gaussian_solve(&ata, &mut v, p);
let fact = factorial(deriv) as f64;
(0..n)
.map(|j| fact * (0..p).map(|k| v[k] * a[j * p + k]).sum::<f64>())
.collect()
}
fn gaussian_solve(a: &[f64], b: &mut [f64], n: usize) {
let mut mat = a.to_vec(); let mut rhs = b.to_vec();
for col in 0..n {
let pivot_row = (col..n).max_by(|&r1, &r2| {
mat[r1 * n + col]
.abs()
.partial_cmp(&mat[r2 * n + col].abs())
.unwrap()
});
let pivot_row = pivot_row.unwrap();
if pivot_row != col {
for j in 0..n {
mat.swap(col * n + j, pivot_row * n + j);
}
rhs.swap(col, pivot_row);
}
let pivot = mat[col * n + col];
if pivot.abs() < 1e-14 {
continue; }
for row in (col + 1)..n {
let factor = mat[row * n + col] / pivot;
for j in col..n {
let v = mat[col * n + j];
mat[row * n + j] -= factor * v;
}
rhs[row] -= factor * rhs[col];
}
}
for i in (0..n).rev() {
let mut sum = rhs[i];
for j in (i + 1)..n {
sum -= mat[i * n + j] * b[j];
}
b[i] = sum / mat[i * n + i];
}
}
fn factorial(n: usize) -> usize {
(1..=n).product()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_delta_shape() {
let data = Matrix::from_fn(20, 100, |r, c| (r + c) as f64);
let result = delta(&data, &DeltaParams::default());
assert_eq!(result.shape(), (20, 100));
}
#[test]
fn test_delta_linear_signal() {
let slope = 2.0;
let data = Matrix::from_fn(1, 50, |_r, c| slope * c as f64);
let result = delta(&data, &DeltaParams::default());
for c in 0..50 {
let diff = (result.get(0, c) - slope).abs();
assert!(
diff < 1e-9,
"col {c}: got {}, expected {slope}",
result.get(0, c)
);
}
}
#[test]
fn test_delta_interior_coefficients_sum_to_zero() {
let coeffs = sg_coeffs(9, 1, 4.0);
let sum: f64 = coeffs.iter().sum();
assert!(sum.abs() < 1e-12, "sum = {sum}");
}
}