#[derive(Debug, Clone, Copy)]
pub struct Kalman {
estimate: f64,
error: f64,
measurement_variance: f64,
process_noise: f64,
noise_gain: f64,
min_measurement_variance: f64,
}
impl Default for Kalman {
fn default() -> Self {
Self {
estimate: 0.0,
error: 0.1,
measurement_variance: 0.0,
process_noise: 1e-3,
noise_gain: 3e-4,
min_measurement_variance: 1.0,
}
}
}
impl Kalman {
pub fn new() -> Self {
Self::default()
}
pub fn with_process_noise(mut self, process_noise: f64) -> Self {
self.process_noise = process_noise;
self
}
pub fn estimate(&self) -> f64 {
self.estimate
}
pub fn update(&mut self, measurement: f64) -> f64 {
let residual = measurement - self.estimate;
self.measurement_variance = ((1.0 - self.noise_gain) * self.measurement_variance
+ self.noise_gain * residual * residual)
.max(self.min_measurement_variance);
let predicted_error = self.error + self.process_noise;
let gain = predicted_error / (predicted_error + self.measurement_variance);
self.estimate += gain * residual;
self.error = (1.0 - gain) * predicted_error;
self.estimate
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_zero_signal_keeps_a_zero_estimate() {
let mut kalman = Kalman::new();
for _ in 0..100 {
kalman.update(0.0);
}
assert!(
kalman.estimate().abs() < 1e-9,
"estimate drifted to {}",
kalman.estimate()
);
}
#[test]
fn a_sustained_gradient_is_tracked() {
let mut kalman = Kalman::new();
for _ in 0..200 {
kalman.update(10.0);
}
assert!(
(kalman.estimate() - 10.0).abs() < 1.0,
"a steady 10 ms gradient should be tracked, got {}",
kalman.estimate()
);
}
#[test]
fn symmetric_noise_does_not_move_the_estimate() {
let mut kalman = Kalman::new();
for step in 0..400 {
kalman.update(if step % 2 == 0 { 8.0 } else { -8.0 });
}
assert!(
kalman.estimate().abs() < 2.0,
"alternating noise should average out, got {}",
kalman.estimate()
);
}
#[test]
fn a_negative_gradient_is_tracked_too() {
let mut kalman = Kalman::new();
for _ in 0..200 {
kalman.update(-6.0);
}
assert!(
(kalman.estimate() + 6.0).abs() < 1.0,
"a draining queue should read negative, got {}",
kalman.estimate()
);
}
#[test]
fn process_noise_controls_how_fast_a_change_is_tracked() {
let mut slow = Kalman::new().with_process_noise(1e-5);
let mut fast = Kalman::new().with_process_noise(1e-1);
for _ in 0..30 {
slow.update(20.0);
fast.update(20.0);
}
assert!(
fast.estimate() > slow.estimate(),
"more process noise should track faster: slow {}, fast {}",
slow.estimate(),
fast.estimate()
);
}
}