Kalman Filter
The math that flew Apollo to the Moon
You have two clocks. One runs fast, one runs slow, and neither is right. What time is it?
The wrong answer is "pick the better clock." The Kalman filter's answer, provably the best answer in the universe, is a single number that splits the difference, weighted by exactly how much each clock lies.
A robot is always holding two clocks: what it predicts from its own motion, and what it measures from a noisy sensor. The filter below fuses them in one dimension. The muted line is the truth the robot can't see; the yellow dots are noisy GPS fixes; the accent line is the filter's belief, wrapped in a shaded band of doubt.
Drag on the canvas to inject a manual GPS fix. K→1 trusts GPS, K→0 trusts odometry.
Try this:
- Watch the band breathe. It swells between GPS fixes (every blind step adds doubt) and snaps tight the instant a yellow dot arrives. That pulse is the whole algorithm.
- GPS blackout. Push the GPS interval to 5 s. Watch the estimate drift and the band balloon. This is what a tunnel does to your phone.
- Garbage GPS. Crank the measurement σ² up. The filter shrugs, trusts its own motion, and barely flinches at each dot.
- Perfect GPS. Drop the measurement σ² near zero. Now the filter snaps onto every dot like a magnet. Same equations, opposite personality.
The dial you're turning, implicitly, is trust. The filter computes it for you, every single tick, optimally.
The one number that runs everything
Last phase we faked sensor fusion by guessing a blend factor: alpha = 0.98, trust the gyro a lot, the accelerometer a little. It worked, but why 0.98? What happens when the floor turns to ice and the gyro starts lying?
The Kalman filter refuses to guess. At every instant it asks one question, who do I trust more right now?, and answers with a number called the Kalman gain, . When your sensor is garbage, and the filter leans on its own prediction. When your prediction is garbage, and the filter snaps to the sensor. It recomputes this a hundred times a second, and the value it lands on isn't a heuristic. For a linear system with Gaussian noise it is optimal: the single best estimate the laws of probability permit. No filter can ever beat it.
Driving through a tunnel
Picture yourself in a tunnel with the radio on.
Predict (the speedometer). It reads 60 mph, so you reason: one more minute, one more mile. Smooth, instant, always available, and quietly wrong, because tires slip and the road curves. Your uncertainty bubble inflates. 🎈
Update (the GPS). You burst out of the tunnel and the GPS barks "Exit 4." It's noisy, but it's an anchor to the real world. Your bubble deflates. 🤏
The Kalman gain is the referee between them. Trust nobody completely; trust everybody exactly as much as they've earned. That's the entire trick, and it's why your phone can hold a believable blue dot through a tunnel where it can't see a single satellite.
A two-hundred-year-old idea
The filter feels modern, but its heart is older than the lightbulb. In 1801 the astronomer Piazzi spotted a new object, the asteroid Ceres, then lost it behind the Sun's glare. A few scattered, error-riddled observations were all anyone had. A 24-year-old named Carl Friedrich Gauss invented the method of least squares to wring the most probable orbit out of that noisy scatter, predicted exactly where Ceres would reappear, and astronomers found it. The Kalman filter is least-squares made recursive: Gauss's idea, run forward in time, one measurement at a time.
The two laws
Strip away the matrices and only two laws remain, and you've already watched both in the demo:
- Predict always grows uncertainty. Moving blind can only ever make you less sure.
- Update always shrinks it. A measurement, however noisy, can only ever make you more sure.
Predict, update, predict, update: the same heartbeat as every estimator in this course, now with the trust factor computed optimally instead of guessed.
The five equations, and where the gain comes from
The filter tracks a state estimate and its covariance , the size of the uncertainty bubble. Each tick runs two stages.
Predict: project the state forward through the motion model , with control , and inflate the covariance by the process noise :
Update: compute the gain, correct the estimate by the measurement residual, and deflate the covariance:
Read the gain as a ratio of doubts: my prediction's uncertainty over prediction plus measurement uncertainty. When the sensor is noisy ( large), and the correction term vanishes, so trust the prediction. When the prediction is shaky ( large), and the estimate jumps to the measurement. The symbols:
- : state estimate (e.g. position, velocity)
- : covariance (the uncertainty bubble)
- : Kalman gain (the trust factor, computed, never guessed)
- : process noise (doubt added by motion)
- : measurement noise (how much the sensor lies)
- : state transition (the motion model)
- : measurement matrix (how the sensor sees the state)
In the demo, the reported error is and the band's half-width is , the largest eigenvalue of the covariance, the longest axis of the doubt ellipse.
Build the 1D filter from scratch
In one dimension the matrices collapse to scalars and the whole filter fits in a struct:
struct KalmanFilter1D {
x: f32, // state estimate
p: f32, // variance (uncertainty)
q: f32, // process noise
r: f32, // measurement noise
}
impl KalmanFilter1D {
fn predict(&mut self, u: f32) {
self.x += u; // motion model
self.p += self.q; // uncertainty grows
}
fn update(&mut self, z: f32) {
let k = self.p / (self.p + self.r); // Kalman gain
self.x += k * (z - self.x); // correct toward measurement
self.p *= 1.0 - k; // uncertainty shrinks
}
}That let k = self.p / (self.p + self.r) is the entire idea: the same ratio of doubts as the matrix version, with the transpose and inverse stripped away.
To take it to a real rover, hand an LLM this brief: implement a 2D Kalman filter in Rust fusing GPS (x, y) at 1 Hz with R = [[10,0],[0,10]] and IMU velocity (vx, vy) at 100 Hz with Q = [[0.1,0],[0,0.1]], state vector [x, y, vx, vy], targeting a Raspberry Pi via the rppal crate. For the sensor side, a u-blox NEO-M9N gives roughly 1.5 m accuracy over UART, while a \$40 Adafruit Ultimate GPS is fine for learning. Parse its $GPGGA NMEA sentences for lat/lon, fix quality, and HDOP, then project to local XY with UTM.
Run the filter yourself
The entire filter is the eight lines below, and it is editable. Change q (process noise) and r (measurement noise), then run it again: push r up and the gain stays small so the filter leans on its own prediction; drop r and the estimate snaps onto every measurement.
Two clocks, neither right. A 24-year-old chasing a lost asteroid with a pencil. A rejected paper that ended up steering a spacecraft to the Moon. They are all the same idea, seen from different centuries: when you cannot know the truth, weigh your guesses by exactly how much each one lies, and the math will hand you the best answer there is.
Your phone is doing it right now, holding your blue dot steady through the tunnel.