Skip to main content

pamoja_kit/
kalman.rs

1//! Getting a steady value from a jittery sensor.
2
3/// Estimates a steady value from noisy readings with a one-dimensional Kalman filter.
4///
5/// Where [`Smoother`](crate::Smoother) blends with a fixed weight, a Kalman filter sets the
6/// blend from how much it trusts its estimate versus each reading, so it settles quickly and
7/// then holds steady. It tracks an estimate and its uncertainty: each step it grows the
8/// uncertainty by the process noise (how much the true value may drift between readings),
9/// then pulls the estimate toward the new reading by a gain derived from that uncertainty
10/// and the measurement noise (how noisy the sensor is). It suits a slowly changing quantity
11/// read by a noisy sensor: a battery voltage, a tank level, a temperature.
12///
13/// # Examples
14///
15/// ```
16/// use pamoja_kit::Kalman;
17///
18/// // Low process noise, higher measurement noise: trust history, smooth hard.
19/// let mut level = Kalman::new(0.01, 1.0, 0.0);
20/// let mut value = 0.0;
21/// for reading in [10.0, 9.0, 11.0, 10.0, 10.0] {
22///     value = level.update(reading);
23/// }
24/// assert!((value - 10.0).abs() < 1.0); // settles near the true 10
25/// ```
26#[derive(Clone, Copy, Debug)]
27pub struct Kalman {
28    estimate: f32,
29    error: f32,
30    process: f32,
31    measurement: f32,
32    started: bool,
33}
34
35impl Kalman {
36    /// Creates a filter.
37    ///
38    /// # Arguments
39    ///
40    /// * `process_noise` - how much the true value may change between readings; larger
41    ///   tracks faster, smaller smooths harder. Its magnitude is used, and one that is not a
42    ///   finite number is taken as zero.
43    /// * `measurement_noise` - how noisy each reading is; larger trusts readings less. Its
44    ///   magnitude is used, and one that is not a finite number is taken as zero, which
45    ///   trusts every reading.
46    /// * `initial` - the starting estimate, used until the first reading replaces it.
47    ///
48    /// # Returns
49    ///
50    /// A filter awaiting its first reading.
51    pub fn new(process_noise: f32, measurement_noise: f32, initial: f32) -> Self {
52        Self {
53            estimate: initial,
54            error: 1.0,
55            process: noise(process_noise),
56            measurement: noise(measurement_noise),
57            started: false,
58        }
59    }
60
61    /// Folds in a reading and returns the updated estimate.
62    ///
63    /// The first reading seeds the estimate and is returned unchanged. A reading that is not
64    /// a finite number, such as the NaN a failed sensor reports, is ignored and the estimate
65    /// returned as it stood.
66    ///
67    /// # Arguments
68    ///
69    /// * `reading` - the latest measurement.
70    ///
71    /// # Returns
72    ///
73    /// The filtered estimate after this reading.
74    pub fn update(&mut self, reading: f32) -> f32 {
75        if !reading.is_finite() {
76            return self.estimate;
77        }
78        if !self.started {
79            self.estimate = reading;
80            self.started = true;
81            return self.estimate;
82        }
83        let predicted_error = self.error + self.process;
84        let gain = if self.measurement == 0.0 {
85            1.0
86        } else {
87            predicted_error / (predicted_error + self.measurement)
88        };
89        self.estimate += gain * (reading - self.estimate);
90        self.error = (1.0 - gain) * predicted_error;
91        self.estimate
92    }
93
94    /// Returns the current estimate.
95    pub fn estimate(&self) -> f32 {
96        self.estimate
97    }
98}
99
100// The magnitude of a noise, taken by hand because `f32::abs` lives in `std`, with one that
101// is not a finite number read as zero.
102fn noise(value: f32) -> f32 {
103    if !value.is_finite() {
104        0.0
105    } else if value < 0.0 {
106        -value
107    } else {
108        value
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn the_first_reading_seeds_the_estimate() {
118        let mut kalman = Kalman::new(0.1, 0.1, 0.0);
119        assert_eq!(kalman.update(42.0), 42.0);
120    }
121
122    #[test]
123    fn it_tracks_toward_a_new_level() {
124        let mut kalman = Kalman::new(0.1, 1.0, 0.0);
125        kalman.update(0.0); // seed at zero
126        let mut value = 0.0;
127        for _ in 0..50 {
128            value = kalman.update(10.0);
129        }
130        assert!((value - 10.0).abs() < 0.5);
131    }
132
133    #[test]
134    fn it_smooths_a_noisy_signal_toward_the_mean() {
135        let mut kalman = Kalman::new(0.01, 1.0, 0.0);
136        let mut value = 0.0;
137        for reading in [10.0, 8.0, 12.0, 9.0, 11.0, 10.0, 10.0, 9.5, 10.5, 10.0] {
138            value = kalman.update(reading);
139        }
140        assert!((value - 10.0).abs() < 1.0);
141    }
142
143    #[test]
144    fn zero_measurement_noise_follows_the_reading() {
145        let mut kalman = Kalman::new(1.0, 0.0, 0.0);
146        kalman.update(5.0); // seeds
147        assert_eq!(kalman.update(8.0), 8.0); // full trust in the reading
148    }
149
150    #[test]
151    fn no_noise_at_all_still_gives_a_number() {
152        let mut kalman = Kalman::new(0.0, 0.0, 0.0);
153        for reading in [5.0, 6.0, 7.0, 8.0] {
154            assert_eq!(kalman.update(reading), reading);
155        }
156    }
157
158    #[test]
159    fn a_reading_that_is_not_a_number_leaves_the_estimate_where_it_was() {
160        let mut kalman = Kalman::new(0.1, 1.0, 0.0);
161        let settled = kalman.update(10.0);
162        assert_eq!(kalman.update(f32::NAN), settled);
163        assert_eq!(kalman.update(f32::NEG_INFINITY), settled);
164        assert!(kalman.update(11.0).is_finite());
165        assert_eq!(Kalman::new(0.1, 1.0, 3.0).update(f32::NAN), 3.0);
166    }
167
168    #[test]
169    fn a_noise_that_is_not_a_number_is_taken_as_zero() {
170        let mut kalman = Kalman::new(f32::NAN, f32::INFINITY, 0.0);
171        kalman.update(5.0);
172        assert_eq!(kalman.update(8.0), 8.0);
173    }
174}