Part III: State Estimation and Classical Inference
Chapter 9: Bayesian Filtering and the Kalman Family

Tuning process and measurement noise

"Every Kalman filter I have ever met was correct. Most of them were also lying, because someone guessed Q and R over lunch and never checked."

A Skeptical AI Agent

The big picture

Section 9.3 handed you the linear Kalman filter as a finished machine: predict with the dynamics, correct with the measurement, and the algebra is exact. What that derivation quietly assumed is that you already know two covariance matrices, the process noise \(\mathbf{Q}\) and the measurement noise \(\mathbf{R}\). You almost never do. Those two matrices are the filter's only free knobs, and every practical Kalman deployment lives or dies on how they are set. This section explains what \(\mathbf{Q}\) and \(\mathbf{R}\) actually represent, why only their ratio changes the filter's behavior, how to pin each one to something physical rather than a lunchtime guess, and how to let the filter's own residuals tell you when you got it wrong. Get this right and a filter tracks cleanly; get it wrong and it either lags reality or chases noise, with no error message to warn you.

Recall the notation remap flagged back in Chapter 1: throughout this chapter the state is \(\mathbf{x}\) and the measurement is \(\mathbf{z}\), following sixty years of control-engineering tradition. The prediction step propagates the state through the dynamics \(\mathbf{x}_k = \mathbf{F}\mathbf{x}_{k-1} + \mathbf{w}_k\) with process noise \(\mathbf{w}_k \sim \mathcal{N}(\mathbf{0}, \mathbf{Q})\), and the measurement arrives as \(\mathbf{z}_k = \mathbf{H}\mathbf{x}_k + \mathbf{v}_k\) with measurement noise \(\mathbf{v}_k \sim \mathcal{N}(\mathbf{0}, \mathbf{R})\). You need only comfort with covariance and the Gaussian; if that feels shaky, Chapter 4 is the place to firm it up.

What Q and R actually represent

What they are. \(\mathbf{R}\) is the covariance of the sensor's noise: how much each raw measurement scatters around the true value it is reporting. \(\mathbf{Q}\) is the covariance of the process noise: how much the real state wanders away from what your dynamics model predicts in one time step. They answer two different questions. \(\mathbf{R}\) asks "how badly does my sensor lie?" and \(\mathbf{Q}\) asks "how wrong is my model of the world?"

Why they are not symmetric in difficulty. \(\mathbf{R}\) is a property of hardware you can measure on a bench. \(\mathbf{Q}\) is a property of a model you invented, so it is a fudge factor by construction. A constant-velocity tracker applied to a pedestrian who accelerates, turns, and stops has a dynamics model that is wrong in a way no datasheet describes. \(\mathbf{Q}\) is where you confess that wrongness. Setting \(\mathbf{Q}\) too small tells the filter "my model is nearly perfect," so it trusts its prediction, ignores fresh measurements, and lags every real maneuver. Setting it too large tells the filter "my model is worthless," so it chases each noisy sample and the estimate rattles. Neither failure raises an exception; the filter runs happily while being quietly useless.

Key insight

The Kalman gain depends only on the ratio of predicted state uncertainty to measurement uncertainty, not on either alone. Scale \(\mathbf{Q}\) and \(\mathbf{R}\) by the same factor and the estimate is unchanged; only the reported covariance rescales. So tuning is never about two independent numbers. It is about one balance: how much do you trust your model versus your sensor? Fix \(\mathbf{R}\) from the hardware, then move \(\mathbf{Q}\) until that balance is right, and you have collapsed a two-knob problem into a one-knob problem.

The gain feels only the ratio

To see why the ratio is what matters, look at the scalar case. With prior variance \(P^-\), measurement variance \(R\), and \(H = 1\), the Kalman gain is

$$ K = \frac{P^-}{P^- + R}. $$

When \(P^- \gg R\) the gain approaches 1: the filter throws away its prediction and adopts the measurement. When \(P^- \ll R\) the gain approaches 0: the filter ignores the measurement and coasts on its model. And \(P^-\) grows by exactly \(Q\) at every predict step (\(P^- = P + Q\) in the scalar constant-position case), so \(Q\) is the throttle that keeps the filter listening to its sensor. A tiny \(Q\) lets \(P^-\) collapse toward zero, the gain follows, and the filter goes deaf. This is the single most common Kalman pathology in the field, and it never announces itself. The estimate simply drifts away from a slow-moving truth while the reported covariance shrinks with false confidence.

In practice: a delivery robot that fell asleep on GNSS

A sidewalk delivery robot fused wheel odometry (its dynamics model) with GNSS position fixes (its measurement) in a Kalman filter. On a straight test corridor it tracked beautifully, so the team shipped it. In a plaza with tall buildings the GNSS fixes jumped by two to three meters from multipath, and to suppress that jitter an engineer had quietly cranked \(\mathbf{Q}\) down until the trajectory looked smooth. The smoothing worked by making the filter nearly ignore GNSS. When the robot's wheels slipped on wet tile, odometry drifted a full meter, the GNSS fixes screaming the correct position were down-weighted almost to zero, and the robot confidently drove off the curb. The bug was not in the code. It was a \(\mathbf{Q}\) tuned to make a plot look pretty instead of to match the real motion uncertainty. The reported covariance had been tiny the whole time, which is exactly why nobody caught it until the curb did.

Set R from the sensor, Q from the motion

How to set \(\mathbf{R}\). Measure it. Hold the sensor still (or against a known reference), collect a few thousand samples, and compute the sample covariance of the residual from the known value. For many sensors the datasheet already gives you a noise density or an RMS figure you can square into a variance; grounding \(\mathbf{R}\) in the physics of the transducer is exactly the material of Chapter 2. Two cautions. First, datasheet noise is often quoted under ideal conditions; real \(\mathbf{R}\) inflates with temperature, motion, and aging, so measure it in your deployment regime. Second, \(\mathbf{R}\) is a covariance, not a variance: if two measurement channels share a common error source, the off-diagonal terms are real and dropping them costs you accuracy.

How to set \(\mathbf{Q}\). There is no bench for \(\mathbf{Q}\), so anchor it to physics instead of taste. For a constant-velocity model driven by unknown acceleration, the standard construction treats acceleration as white noise with intensity \(\sigma_a^2\) and integrates it into the state, giving the continuous-white-noise-acceleration \(\mathbf{Q}\)

$$ \mathbf{Q} = \sigma_a^2 \begin{bmatrix} \tfrac{1}{4}\Delta t^4 & \tfrac{1}{2}\Delta t^3 \\[2pt] \tfrac{1}{2}\Delta t^3 & \Delta t^2 \end{bmatrix}, $$

where \(\Delta t\) is the sample interval and \(\sigma_a\) is the biggest acceleration you expect the target to sneak in between updates. Now \(\mathbf{Q}\) has one interpretable knob, \(\sigma_a\), that you can reason about physically: a tracked pedestrian might peak near \(1\ \text{m/s}^2\), an aggressive drone near \(20\). This turns a matrix of mystery numbers into a single question with a real-world answer. Chapter 23 revisits exactly this construction for inertial motion models.

Let the innovations grade your tuning

The elegant part of Kalman tuning is that the filter checks its own homework. At each step it predicts a measurement and the actual one arrives; the difference is the innovation \(\boldsymbol{\nu}_k = \mathbf{z}_k - \mathbf{H}\hat{\mathbf{x}}_k^-\), and the filter also predicts that innovation's covariance \(\mathbf{S}_k = \mathbf{H}\mathbf{P}_k^-\mathbf{H}^\top + \mathbf{R}\). If \(\mathbf{Q}\) and \(\mathbf{R}\) are right, the innovations are white (uncorrelated in time) and their squared, normalized magnitude, the normalized innovation squared (NIS) \(\boldsymbol{\nu}_k^\top \mathbf{S}_k^{-1} \boldsymbol{\nu}_k\), follows a chi-square distribution with degrees of freedom equal to the measurement dimension. Averaged over a run it should sit near that dimension. If the average NIS runs high, the filter is overconfident: \(\mathbf{Q}\) or \(\mathbf{R}\) is too small. If it runs low, the filter is underconfident and you are leaving accuracy on the table. This is a rigorous, quantitative consistency test, not a look-at-the-plot heuristic, and it is the same residual-whitening idea that underlies the diagnostics in Section 9.7.

import numpy as np
from scipy.stats import chi2

def average_nis(innovations, S_list):
    """Mean normalized innovation squared over a run.
    innovations: list of (m,) arrays; S_list: list of (m,m) innovation covariances."""
    nis = [float(nu @ np.linalg.solve(S, nu)) for nu, S in zip(innovations, S_list)]
    return np.mean(nis)

# One-dimensional position measurements (m = 1): NIS should average ~1.
m, N = 1, 500
mean_nis = average_nis(innovations, S_list)      # from your filter run
lo, hi = chi2.ppf([0.025, 0.975], df=m*N) / N     # 95% consistency band
verdict = ("OVERCONFIDENT: raise Q or R" if mean_nis > hi else
           "UNDERCONFIDENT: lower Q or R" if mean_nis < lo else
           "CONSISTENT")
print(f"mean NIS = {mean_nis:.3f}  band = [{lo:.3f}, {hi:.3f}]  -> {verdict}")
Grading a filter's noise tuning with the average NIS test. Each innovation is normalized by its own predicted covariance \(\mathbf{S}_k\), and the mean is compared against a chi-square consistency band. A value above the band means the filter reports smaller errors than it actually makes (overconfident); below means the opposite. Sweep \(\sigma_a\) in your \(\mathbf{Q}\) and re-run until the verdict reads CONSISTENT.

The companion test, when you have ground truth (in simulation, or from a motion-capture reference rig), is the normalized estimation error squared (NEES), which normalizes the true estimation error by the filter's reported state covariance \(\mathbf{P}_k\). NEES catches an overconfident \(\mathbf{P}\) that NIS can miss, so use NEES offline during development and NIS online where truth is unavailable. Both are consumers of the same principle: a well-tuned filter's own uncertainty claims must match the errors it actually commits, and calibration of that kind is a theme this book returns to in Chapter 18.

The right tool: consistency diagnostics off the shelf

Hand-rolling a full tuning loop (running the filter, accumulating innovations and their covariances, computing NIS and NEES, and comparing against chi-square bounds across a sweep of \(\sigma_a\)) is roughly sixty lines of careful bookkeeping. The filterpy library exposes the pieces directly, collapsing it to about ten:

from filterpy.kalman import KalmanFilter
from filterpy.stats import NEES
import numpy as np

kf = KalmanFilter(dim_x=2, dim_z=1)   # set F, H, Q, R, x, P as usual
errs, ps = [], []
for z, x_true in zip(measurements, truth):
    kf.predict(); kf.update(z)
    errs.append(x_true - kf.x); ps.append(kf.P)
    # kf.y is the innovation, kf.S its covariance -> NIS = kf.y.T @ inv(kf.S) @ kf.y
print("mean NEES:", np.mean(NEES(errs, ps)))     # compare to dim_x = 2
Using filterpy to run the filter and score its tuning. The library maintains the innovation kf.y, its covariance kf.S, and the posterior covariance kf.P for you, and filterpy.stats.NEES computes the normalized error directly, so the tuning loop that would otherwise be sixty lines of matrix bookkeeping fits in ten.

When to automate: adaptive and data-driven tuning

Manual NIS sweeps are the right tool when the noise is roughly stationary and you can tune once offline. When it is not, three escalating options exist. Adaptive filtering adjusts \(\mathbf{Q}\) or \(\mathbf{R}\) online from a sliding window of innovations, useful when a sensor's noise changes with regime (GNSS in a tunnel, a camera in glare). Autocovariance least squares (ALS) is an offline method that estimates \(\mathbf{Q}\) and \(\mathbf{R}\) jointly from the autocorrelation of the innovation sequence on logged data, giving a principled fit rather than a hand sweep. Maximum-likelihood tuning treats \(\mathbf{Q}\) and \(\mathbf{R}\) as parameters and maximizes the filter's log-likelihood over a batch, the most rigorous route when you have representative logs. Reach for automation only after the manual NIS test has told you your model structure is sound; an adaptive scheme wrapped around a misspecified dynamics model will chase the wrong target faster. The nonlinear filters of Chapter 10 inherit every one of these tuning concerns, with the added twist that \(\mathbf{Q}\) must also absorb linearization error.

Exercise

Take the constant-velocity position tracker from Section 9.3 and simulate a target that moves at constant velocity for 200 steps, then executes a sharp turn.

  1. Fix \(\mathbf{R}\) at the true measurement variance you injected. Sweep \(\sigma_a\) in the \(\mathbf{Q}\) construction across three orders of magnitude and record the average NIS for each.
  2. Plot mean NIS against \(\sigma_a\) and mark the chi-square consistency band. Which values pass?
  3. Now overlay the tracked trajectory during the turn for the smallest and largest passing \(\sigma_a\). Explain, in terms of the gain, why the small-\(\sigma_a\) filter lags the turn even though its NIS was acceptable on the straight segment.
Hint

A single scalar \(\mathbf{Q}\) that is consistent on average across a run can still be too small during a brief maneuver and too large during the calm stretches. That averaging blind spot is precisely why maneuver tracking motivates the adaptive and nonlinear methods of later sections.

Self-check

  1. If you multiply both \(\mathbf{Q}\) and \(\mathbf{R}\) by 100, what changes in the filter's state estimate, and what changes in its reported covariance?
  2. Your filter's average NIS is far above the chi-square band. Name two distinct fixes and say which noise matrix each one touches.
  3. Why can a filter with a beautifully smooth output trajectory still be dangerously mistuned? What single number would have exposed it in the delivery-robot story?

What's Next

In Section 9.5, we move from tuning to the failure that no amount of tuning can rescue: when the sensor geometry simply does not constrain part of the state. We formalize observability, show how an unobservable mode makes a covariance quietly blow up, and connect a diverging filter back to the consistency diagnostics you just learned, so you can tell a tuning problem from a structural one before it drives off the curb.