Scalar Kalman Filter — 1D Train Tracking

Scalar Kalman Filter — 1D Train Tracking#

This demo runs the canonical scalar Kalman filter on a 1D scenario: a train moves at approximately constant velocity along a track, the only sensor is a noisy position measurement that arrives every few seconds, and the filter has to estimate the true position continuously. It is the simplest setting in which every characteristic Kalman behavior is visible — the gain converging to steady state, the bounds growing between updates and snapping tighter at each update, the estimate jumping at measurements and sliding along the model in between.

The filter#

The five recurring equations from the Block 4 reading, specialized to a scalar state with constant-velocity propagation:

\[\begin{split} \begin{aligned} \text{Predict:}\quad & \hat{x}_{k+1}^- = \hat{x}_k^+ + v_\text{model}\,\Delta t, \qquad P_{k+1}^- = P_k^+ + Q\,\Delta t. \\ \text{Update (when measurement available):}\quad & K_{k+1} = \frac{P_{k+1}^-}{P_{k+1}^- + R}, \\ & \hat{x}_{k+1}^+ = \hat{x}_{k+1}^- + K_{k+1}\,(z_{k+1} - \hat{x}_{k+1}^-), \\ & P_{k+1}^+ = (1 - K_{k+1})\,P_{k+1}^-. \end{aligned} \end{split}\]

Process noise is parameterized as a rate (\(\sigma_\text{process}\) in m/√s, \(Q = \sigma_\text{process}^2\) in m²/s); measurement noise variance is \(R = \sigma_\text{meas}^2\). The Kalman gain \(K\) is the inverse-variance weight from Block 3 — the prior \(P^-\) acts as “sensor 1” and the measurement \(R\) acts as “sensor 2”.

Interactive demo#

Open in full screen

The demo simulates the full scenario deterministically (given the current parameters and seed), and animates a moving “now” cursor through the run. The live track view at the top is a top-down spatial view of the train: the cyan triangle is truth, the navy diamond is the filter estimate, the dark dot is the most recent measurement, and the red bar with dashed caps is the current ±95% covariance interval. The track auto-scrolls to keep the estimate at the center. The three time-series panels below carry a synchronized vertical “now” cursor showing where on the time history the live view corresponds to.

Walkthrough#

The demo opens at the canonical Block 4 scenario and starts playing automatically: \(v_\text{true} = 20\) m/s, \(\sigma_\text{meas} = 30\) m, measurement interval = 10 s, \(\sigma_\text{process} = 10\) m/√s, initial estimate \(-100\) m with \(\sigma_0 = 200\) m. Try the following:

  1. Watch the first measurement at \(t = 10\) s. Pause the animation right after. The estimate (navy diamond) was 100 m off truth at \(t=0\) with a large covariance bar; the prior at \(t=10^-\) is still 100 m off but now slightly more uncertain (\(P^-\) has grown by \(Q \cdot t\)). The first measurement arrives, the gain is essentially 1 (because \(P^-\) at this point dwarfs \(R\)), and the estimate snaps onto the measurement. The covariance bar abruptly tightens. The Kalman gain panel shows this first update as a high dot (close to 1).

  2. Let it play through. After two or three update cycles the gain settles around 0.6. That is the steady-state Kalman gain: the value where the prediction step’s growth (\(Q \cdot \text{interval}\)) is exactly balanced by the measurement update’s shrink. From here on, the bounds widen and snap tighter periodically, but their post-update value stops shrinking.

  3. Slide \(\sigma_\text{meas}\) up to 80 m. The measurement is now much noisier than the prediction; the gain converges to a smaller steady-state value (the filter trusts measurements less). The bounds stay relatively wider; the post-update jumps in the position panel are smaller. Slide it back down to 5 m and the gain converges close to 1 — the filter all but ignores its prior between updates and snaps onto every measurement.

  4. Slide \(\sigma_\text{process}\) up to 25 m/√s. Bounds grow much faster between updates. The gain converges higher — when the prediction is uncertain, the filter trusts measurements more. This is the \(Q\) vs \(R\) tug-of-war the reading describes.

  5. Slide the measurement interval up to 30 s. Bounds grow visibly between the now-rare updates. Steady-state gain shifts higher (because \(P^-\) has more time to grow before each update). At very long intervals the filter is essentially open-loop between fixes.

  6. Set \(v_\text{model} \neq v_\text{true}\) (e.g. 15 vs 20). The estimate slowly slides off truth between updates and gets yanked back at each measurement. The error panel shows characteristic sawtooth behavior; the gain stays high because the filter never converges to a tight prior. Model mismatch shows up as a structurally biased error, not as wider bounds — a tight CI does not protect you from a wrong model.

  7. Pause and scrub. Drag the time slider; the live track, all three time cursors, and the stats card all update in lockstep. Use this to inspect the filter state at any time of interest. Pressing Play resumes from wherever you scrubbed to.

Key observations#

  • The gain converges. The Kalman gain is not a fixed value — it depends on the current \(P^-\), which depends on the run’s history of \(Q\) growth and \(R\) updates. With time-invariant \(Q\) and \(R\), the gain settles into a steady state, and \(P^+\) approaches a fixed point. This is the discrete-time Riccati equation reaching equilibrium.

  • Bounds shrink at updates, grow between. This is the visible signature of \(P^+ = (1-K) P^-\) followed by \(P^- = P^+ + Q\,\Delta t\). Watch the position panel: the dashed red bounds make a sawtooth pattern.

  • Confidence interval \(\neq\) accuracy. When \(v_\text{model} \neq v_\text{true}\), the filter still produces tight bounds but the actual error rides outside them. The CI captures noise variability around the model; if the model is wrong, the CI is wrong about something true. This is the same lesson as the Schuler bias from Block 2.

  • The first few updates do most of the work. The initial covariance shrinks dramatically after the first one or two measurements. This is why in practice the initial uncertainty \(\sigma_0\) has surprisingly little effect on long-run behavior — the filter “forgets” the prior quickly once measurements arrive.

  • Every behavior generalizes. Replace the scalar with a vector, \(F\) with a matrix, \(H\) with a measurement matrix, and the same predict-update cycle is the multi-state Kalman filter from Block 5. Same recursion, same trade-off, same tug-of-war.

Source#

MATLAB · code/KalmanFilterScalar.m