#[derive(Debug, Clone)]
pub struct ScalarKalman {
estimate: f32,
variance: f32,
process_noise: f32,
measurement_noise: f32,
initialized: bool,
last_gain: f32,
}
impl ScalarKalman {
pub fn new(process_noise: f32, measurement_noise: f32) -> Self {
Self {
estimate: 0.0,
variance: 0.0,
process_noise: process_noise.max(0.0),
measurement_noise: measurement_noise.max(1e-9),
initialized: false,
last_gain: 0.0,
}
}
pub fn with_initial(mut self, estimate: f32, variance: f32) -> Self {
self.estimate = estimate;
self.variance = variance.max(0.0);
self.initialized = true;
self
}
pub fn observe(&mut self, measurement: f32) -> f32 {
if !self.initialized {
self.estimate = measurement;
self.variance = self.measurement_noise;
self.initialized = true;
self.last_gain = 1.0;
return self.estimate;
}
self.variance += self.process_noise;
let gain = self.variance / (self.variance + self.measurement_noise);
self.estimate += gain * (measurement - self.estimate);
self.variance *= 1.0 - gain;
self.last_gain = gain;
self.estimate
}
pub fn estimate(&self) -> f32 {
self.estimate
}
pub fn variance(&self) -> f32 {
self.variance
}
pub fn last_gain(&self) -> f32 {
self.last_gain
}
pub fn is_initialized(&self) -> bool {
self.initialized
}
pub fn set_process_noise(&mut self, q: f32) {
self.process_noise = q.max(0.0);
}
pub fn set_measurement_noise(&mut self, r: f32) {
self.measurement_noise = r.max(1e-9);
}
pub fn reset(&mut self) {
self.initialized = false;
self.variance = 0.0;
self.last_gain = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
mod retuning {
use super::*;
fn settled_gain(q: f32, r: f32) -> f32 {
let mut f = ScalarKalman::new(q, r);
for _ in 0..200 {
f.observe(10.0);
}
f.last_gain()
}
#[test]
fn more_process_noise_trusts_the_measurement_more() {
let sluggish = settled_gain(0.001, 1.0);
let eager = settled_gain(1.0, 1.0);
assert!(eager > sluggish, "higher Q means a higher gain: {eager} against {sluggish}");
}
#[test]
fn more_measurement_noise_trusts_the_measurement_less() {
let trusting = settled_gain(0.1, 0.01);
let sceptical = settled_gain(0.1, 100.0);
assert!(
sceptical < trusting,
"higher R means a lower gain: {sceptical} against {trusting}"
);
}
#[test]
fn retuning_a_live_filter_changes_where_it_goes_next() {
let mut f = ScalarKalman::new(0.001, 1.0);
for _ in 0..200 {
f.observe(10.0);
}
let before = f.last_gain();
f.set_process_noise(10.0);
f.observe(10.0);
assert!(
f.last_gain() > before,
"the new Q applies to the next measurement: {} against {before}",
f.last_gain()
);
}
#[test]
fn neither_knob_can_be_set_to_something_that_breaks_the_arithmetic() {
let mut f = ScalarKalman::new(0.1, 1.0);
f.set_process_noise(-5.0);
f.set_measurement_noise(0.0);
for _ in 0..50 {
let out = f.observe(3.0);
assert!(out.is_finite(), "an estimate stays a number: {out}");
}
assert!(f.variance().is_finite());
assert!(f.last_gain().is_finite());
}
}
#[test]
fn the_first_measurement_seeds_the_estimate() {
let mut k = ScalarKalman::new(0.01, 1.0);
assert!(!k.is_initialized());
assert_eq!(k.observe(42.0), 42.0);
assert!(k.is_initialized());
}
#[test]
fn it_converges_toward_a_constant_signal() {
let mut k = ScalarKalman::new(0.001, 1.0);
for i in 0..200 {
let noise = if i % 2 == 0 { 8.0 } else { -8.0 };
k.observe(100.0 + noise);
}
assert!((k.estimate() - 100.0).abs() < 1.0, "settled on the true value, got {}", k.estimate());
}
#[test]
fn the_gain_falls_as_the_estimate_settles() {
let mut k = ScalarKalman::new(0.001, 1.0);
k.observe(50.0); let early = {
k.observe(50.0);
k.last_gain()
};
for _ in 0..100 {
k.observe(50.0);
}
let late = k.last_gain();
assert!(late < early, "gain shrinks as confidence grows: early {early}, late {late}");
assert!(k.variance() < 1.0, "variance shrank below the measurement noise");
}
#[test]
fn more_measurement_noise_smooths_harder() {
let step = |r: f32| {
let mut k = ScalarKalman::new(0.001, r);
for _ in 0..20 {
k.observe(0.0);
}
k.observe(100.0); k.estimate()
};
let responsive = step(0.5);
let smooth = step(50.0);
assert!(smooth < responsive, "higher R moves less on the jump: smooth {smooth} vs responsive {responsive}");
}
#[test]
fn it_tracks_a_moving_signal_when_process_noise_allows() {
let mut k = ScalarKalman::new(1.0, 1.0);
for t in 0..100 {
k.observe(t as f32);
}
assert!((k.estimate() - 99.0).abs() < 5.0, "followed the ramp, got {}", k.estimate());
}
#[test]
fn with_initial_seeds_without_a_measurement() {
let k = ScalarKalman::new(0.01, 1.0).with_initial(7.0, 0.5);
assert!(k.is_initialized());
assert_eq!(k.estimate(), 7.0);
assert_eq!(k.variance(), 0.5);
}
}