use crate::core::scalar::PidScalar;
#[derive(Debug, Clone, Copy)]
pub struct DerivativeFilter<S: PidScalar> {
tau_f: S,
prev_output: S,
initialized: bool,
}
impl<S: PidScalar> DerivativeFilter<S> {
pub fn new(tau_f: S) -> Self {
Self {
tau_f,
prev_output: S::ZERO,
initialized: false,
}
}
pub fn from_gains(kd: S, kp: S, n: S) -> Self {
let denominator = n * kp;
let tau_f = if denominator.abs() > S::EPSILON {
kd / denominator
} else {
kd / n
};
Self::new(tau_f)
}
pub fn apply(&mut self, raw_derivative: S, dt: S) -> S {
if !self.initialized {
self.prev_output = raw_derivative;
self.initialized = true;
return raw_derivative;
}
if dt <= S::ZERO || self.tau_f <= S::ZERO {
self.prev_output = raw_derivative;
return raw_derivative;
}
let alpha = self.tau_f / (self.tau_f + dt);
let filtered = alpha * self.prev_output + (S::ONE - alpha) * raw_derivative;
self.prev_output = filtered;
filtered
}
pub fn reset(&mut self) {
self.prev_output = S::ZERO;
self.initialized = false;
}
pub fn tau_f(&self) -> S {
self.tau_f
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_value_passthrough() {
let mut filt = DerivativeFilter::<f64>::new(0.1);
let out = filt.apply(5.0, 0.01);
assert_eq!(out, 5.0);
}
#[test]
fn filter_attenuates_step_change() {
let mut filt = DerivativeFilter::<f64>::new(0.1);
filt.apply(0.0, 0.01);
let out = filt.apply(10.0, 0.01);
assert!(out < 2.0, "Filter should attenuate: got {}", out);
assert!(out > 0.0, "Filter should be positive: got {}", out);
}
#[test]
fn filter_converges_to_constant() {
let mut filt = DerivativeFilter::<f64>::new(0.01);
filt.apply(0.0, 0.001);
for _ in 0..1000 {
filt.apply(5.0, 0.001);
}
let out = filt.apply(5.0, 0.001);
assert!(
(out - 5.0).abs() < 0.01,
"Should converge to 5.0, got {}",
out
);
}
#[test]
fn reset_clears_state() {
let mut filt = DerivativeFilter::<f64>::new(0.1);
filt.apply(100.0, 0.01);
filt.reset();
let out = filt.apply(0.0, 0.01);
assert_eq!(out, 0.0);
}
#[test]
fn from_gains_computes_tau() {
let filt = DerivativeFilter::<f64>::from_gains(1.0, 2.0, 10.0);
assert!((filt.tau_f() - 0.05).abs() < 1e-10);
}
#[test]
fn noise_attenuation() {
let mut filt = DerivativeFilter::<f64>::new(0.05);
let dt = 0.001;
filt.apply(0.0, dt);
let mut max_output = 0.0_f64;
for i in 0..100 {
let noisy = if i % 2 == 0 { 10.0 } else { -10.0 };
let out = filt.apply(noisy, dt);
max_output = max_output.max(out.abs());
}
assert!(
max_output < 10.0,
"Filter should attenuate noise: max={}",
max_output
);
}
}