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.
42 /// * `measurement_noise` - how noisy each reading is; larger trusts readings less. Its
43 /// magnitude is used.
44 /// * `initial` - the starting estimate, used until the first reading replaces it.
45 ///
46 /// # Returns
47 ///
48 /// A filter awaiting its first reading.
49 pub fn new(process_noise: f32, measurement_noise: f32, initial: f32) -> Self {
50 Self {
51 estimate: initial,
52 error: 1.0,
53 process: magnitude(process_noise),
54 measurement: magnitude(measurement_noise),
55 started: false,
56 }
57 }
58
59 /// Folds in a reading and returns the updated estimate.
60 ///
61 /// The first reading seeds the estimate and is returned unchanged.
62 ///
63 /// # Arguments
64 ///
65 /// * `reading` - the latest measurement.
66 ///
67 /// # Returns
68 ///
69 /// The filtered estimate after this reading.
70 pub fn update(&mut self, reading: f32) -> f32 {
71 if !self.started {
72 self.estimate = reading;
73 self.started = true;
74 return self.estimate;
75 }
76 let predicted_error = self.error + self.process;
77 let gain = predicted_error / (predicted_error + self.measurement);
78 self.estimate += gain * (reading - self.estimate);
79 self.error = (1.0 - gain) * predicted_error;
80 self.estimate
81 }
82
83 /// Returns the current estimate.
84 pub fn estimate(&self) -> f32 {
85 self.estimate
86 }
87}
88
89// `f32::abs` lives in `std`, so this `no_std` crate takes the magnitude by hand.
90fn magnitude(value: f32) -> f32 {
91 if value < 0.0 {
92 -value
93 } else {
94 value
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn the_first_reading_seeds_the_estimate() {
104 let mut kalman = Kalman::new(0.1, 0.1, 0.0);
105 assert_eq!(kalman.update(42.0), 42.0);
106 }
107
108 #[test]
109 fn it_tracks_toward_a_new_level() {
110 let mut kalman = Kalman::new(0.1, 1.0, 0.0);
111 kalman.update(0.0); // seed at zero
112 let mut value = 0.0;
113 for _ in 0..50 {
114 value = kalman.update(10.0);
115 }
116 assert!((value - 10.0).abs() < 0.5);
117 }
118
119 #[test]
120 fn it_smooths_a_noisy_signal_toward_the_mean() {
121 let mut kalman = Kalman::new(0.01, 1.0, 0.0);
122 let mut value = 0.0;
123 for reading in [10.0, 8.0, 12.0, 9.0, 11.0, 10.0, 10.0, 9.5, 10.5, 10.0] {
124 value = kalman.update(reading);
125 }
126 assert!((value - 10.0).abs() < 1.0);
127 }
128
129 #[test]
130 fn zero_measurement_noise_follows_the_reading() {
131 let mut kalman = Kalman::new(1.0, 0.0, 0.0);
132 kalman.update(5.0); // seeds
133 assert_eq!(kalman.update(8.0), 8.0); // full trust in the reading
134 }
135}