Probabilistic Robotics
Chapter 06PART IIThe Bayes Filter FamilyDifficulty: IntermediateEstimated reading time: 55 min

Kalman Filters

Two assumptions buy the Bayes filter a closed form. What comes back is the gain — not a tuning knob but a precision-weighted balance — a second coordinate system where the costs are reversed, and the first hint that waiting for the future beats recursion.

The fact that the residual uncertainty is smaller than the contributing Gaussians may appear counter-intuitive, but it is a general characteristic of information integration in Kalman filters.
Sebastian Thrun, Wolfram Burgard, and Dieter FoxProbabilistic Robotics, Chapter 3

In this chapter

Chapter 5 left us with an exact recursion and no way to run it. The prediction step is an integral over a continuous state space; the correction step needs a function evaluated everywhere. The histogram filter escapes by chopping the space into cells, and pays for it: to localize Rusty on a ten-metre rail to the nearest centimetre you need a thousand cells for one dimension, and a thousand cubed the moment you want a full pose.

This chapter buys computability with two commitments — linearity and Gaussianity — and gets back the most consequential algorithm in engineering. The belief collapses from a function to two numbers per dimension, and the Bayes filter's two lines become matrix algebra you can run at ten kilohertz on a microcontroller.

Three things come out of the algebra that are worth the price of the chapter on their own. The Kalman gain is not a parameter; it falls out of the derivation as the ratio of two precisions, and it is completely determined before any data arrives. The information form is the same Gaussian in different coordinates, in which the other filter step is the cheap one — a duality that quietly sets up Chapter 15. And the RTS smoother shows that filtering, for all its elegance, is only the best causal use of the data.

The problem with a grid

Rusty is on a rail. One degree of freedom, a beacon that pings its position, and a motor that never quite delivers the commanded step. The histogram filter of Chapter 5 will localize it perfectly well — at a thousand cells for centimetre resolution, recomputed every step, to describe a belief that is, every single time, one smooth bump.

That is the observation this chapter turns into an algorithm. If the belief is always going to be a single bump, stop storing the bump and store its mean and its width. Two numbers instead of a thousand, and the update becomes arithmetic.

Leave it running for a few cycles before reading on. Four things in that widget are the chapter.

The posterior is taller than both its parents — taller meaning narrower, since all four curves are densities. The green measurement blob and the orange prediction blob combine into a purple blob tighter than either. This is the sentence in the epigraph, and it is not a rounding artifact: two independent opinions genuinely beat one, and the algebra below says by exactly how much.

Prediction always widens; correction always narrows. Orange is never taller than the blue prior that preceded it. That is "moving smears" from Chapter 5, now in closed form: the covariance gains an additive Rt\mat{R}_t every step and there is nothing it can do about it.

The gain settles by itself. Watch the KK readout. It converges within a dozen steps and then holds, even though the measurements keep changing. Nothing told it to. We will prove why: the covariance recursion never looks at the data.

A confident filter is not an accurate one. Press divergent. The filter is told the cart obeys its model almost perfectly (R0\mat{R} \approx 0) while the cart in fact slips. Its covariance collapses, its 2σ band shrinks to a hairline, and the truth walks straight out of it. The NIS readout goes red well before the RMSE readout looks alarming — and NIS is the one you can compute on a real robot.

One implementation note, because it matters for Exercise 7. The bench carries two states — the cart's position and its velocity — while the beacon reports position only. The four blobs are the position marginal. Velocity is never measured at any point in the run, and the filter estimates it anyway, through the off-diagonal terms of Σ\Sigma that couple the two. That is the quiet superpower of a Gaussian belief: correlations turn one sensor into evidence about everything the sensor is correlated with.

Building intuition

Before any algebra, get the mechanism into your hands. The scalar case contains the entire idea, and the entire idea is a balance.

You have a prediction that says the cart is near μˉ\bar\mu, with uncertainty σˉ2\bar\sigma^2. A beacon says it is near zz, with uncertainty σz2\sigma_z^2. Neither is right. The Kalman filter's answer is not to average them, and it is not to pick the more trustworthy one. It is to hang a weight at each claim whose mass is that claim's precision — the reciprocal of its variance — and take the centre of mass.

Two facts that widget makes physical, and that the derivations below make exact:

  1. The posterior mean is always strictly between μˉ\bar\mu and zz. A Kalman filter can be wrong, but it cannot be wild.
  2. The posterior precision is the sum of the two input precisions. Not the larger of them — the sum. That is why the purple curve is taller than both parents, and it is the whole reason fusing sensors is worth doing.

Everything else in this chapter is those two facts written in matrices.

The mathematics

Linear-Gaussian systems

The Kalman filter is the Bayes filter under three additional assumptions — linearity, Gaussian noise, and a Gaussian starting point — layered on top of the Markov assumption Chapter 5 already consumed.

Assumption 1 — linear motion with additive Gaussian noise.

xt=Atxt1+Btut+εt,εtN(0,Rt)x_t = \mat{A}_t x_{t-1} + \mat{B}_t u_t + \varepsilon_t, \qquad \varepsilon_t \sim \Normal(0, \mat{R}_t)

so that

p(xtut,xt1)=N ⁣(xt;  Atxt1+Btut,  Rt)\htmlClass{term-prediction}{p(x_t \mid u_t, x_{t-1})} = \Normal\!\left(x_t;\; \mat{A}_t x_{t-1} + \mat{B}_t u_t,\; \mat{R}_t\right)

Assumption 2 — linear measurements with additive Gaussian noise.

zt=Ctxt+δt,δtN(0,Qt),p(ztxt)=N ⁣(zt;  Ctxt,  Qt)z_t = \mat{C}_t x_t + \delta_t, \qquad \delta_t \sim \Normal(0, \mat{Q}_t), \qquad \htmlClass{term-measurement}{p(z_t \mid x_t)} = \Normal\!\left(z_t;\; \mat{C}_t x_t,\; \mat{Q}_t\right)

Assumption 3 — Gaussian prior. bel(x0)=N(x0;μ0,Σ0)\htmlClass{term-prior}{\bel(x_0)} = \Normal(x_0; \mu_0, \Sigma_0).

Together these are exactly enough. Under them the posterior is Gaussian at every tt — not approximately, not asymptotically — so the filter never needs to represent anything but a mean and a covariance. That closure claim is what the next two derivations prove.

Notation used in this chapter
SymbolMeaningNote
μt, Σt\mu_t,\ \Sigma_tMoments form: the belief mean and covariance at time t.
μˉt, Σˉt\bar\mu_t,\ \bar\Sigma_tThe predicted belief, after the control and before the measurement.
ξt=Σt1μt\xi_t = \Sigma_t^{-1}\mu_tInformation vector — the canonical form’s linear coefficient.
Ωt=Σt1\Omega_t = \Sigma_t^{-1}Information matrix — the curvature of the negative log-belief.
At, Bt, Ct\mathbf{A}_t,\ \mathbf{B}_t,\ \mathbf{C}_tState transition, control, and measurement matrices.
Rt\mathbf{R}_tMotion noise covariance.Thrun’s convention. Most control texts call this Q.
Qt\mathbf{Q}_tMeasurement noise covariance.Thrun’s convention. Most control texts call this R.
Kt\mathbf{K}_tKalman gain — how far towards the measurement to move.
St=CtΣˉtCtT+QtS_t = \mathbf{C}_t\bar\Sigma_t\mathbf{C}_t^{\mathsf{T}} + \mathbf{Q}_tInnovation covariance: how surprised the filter is entitled to be.
μtT, ΣtT\mu_{t|T},\ \Sigma_{t|T}Smoothed belief: state t conditioned on all T measurements.

The R/Q trap. In this book, following Thrun, R\mat{R} is motion noise and Q\mat{Q} is measurement noise. In most of the control literature — and, deliberately, in the TypeScript port that runs the widgets on this page — the letters are the other way around, so you can see the translation happening at the call site. There is no principled reason for either choice, and there is no chance the field will fix it. Whenever you read Kalman filter code, find one line that tells you which convention it uses before you touch a number. Every slider in this chapter is labelled with both the symbol and its meaning, for exactly this reason.

Prediction

DerivationThe prediction step, from the Bayes filter's integral

We want line 2 of the Bayes filter,

bel(xt)=p(xtut,xt1)  bel(xt1)  dxt1\belbar(x_t) = \int \htmlClass{term-prediction}{p(x_t \mid u_t, x_{t-1})}\; \htmlClass{term-prior}{\bel(x_{t-1})}\; dx_{t-1}

with both factors Gaussian. Write xx for xt1x_{t-1}, drop the time subscripts on the matrices, and put d=xtBud = x_t - \mat{B}u to keep the control out of the way. The integrand is exp(L)\exp(-L) with

L=12(dAx)TR1(dAx)+12(xμ)TΣ1(xμ)L = \tfrac12 (d - \mat{A}x)\T \mat{R}^{-1} (d - \mat{A}x) + \tfrac12 (x - \mu)\T \Sigma^{-1} (x - \mu)

Step 1 — recognize a joint quadratic. LL is quadratic in (xt,x)(x_t, x) jointly, so the integrand is an unnormalized Gaussian over both. Expanding and collecting the terms in xx:

L=12xT(ATR1A+Σ1)=  Ψ1x    (ATR1d+Σ1μ)Tx  +  12dTR1d+12μTΣ1μL = \tfrac12 x\T \underbrace{(\mat{A}\T\mat{R}^{-1}\mat{A} + \Sigma^{-1})}_{=\;\Psi^{-1}} x \;-\; (\mat{A}\T\mat{R}^{-1} d + \Sigma^{-1}\mu)\T x \;+\; \tfrac12 d\T\mat{R}^{-1}d + \tfrac12 \mu\T\Sigma^{-1}\mu

Step 2 — complete the square in xx and integrate it out. Any quadratic splits as

L=12(xx)TΨ1(xx)+L,x=Ψ(ATR1d+Σ1μ)L = \tfrac12 (x - x^\star)\T \Psi^{-1} (x - x^\star) + L^\star, \qquad x^\star = \Psi\,(\mat{A}\T\mat{R}^{-1} d + \Sigma^{-1}\mu)

The first term integrates to det(2πΨ)\sqrt{\det(2\pi\Psi)}, a constant that does not involve xtx_t. It is absorbed into the normalizer and never seen again. Everything that matters is in

L=12dTR1d+12μTΣ1μ12(ATR1d+Σ1μ)TΨ(ATR1d+Σ1μ)L^\star = \tfrac12 d\T\mat{R}^{-1}d + \tfrac12\mu\T\Sigma^{-1}\mu - \tfrac12 (\mat{A}\T\mat{R}^{-1}d + \Sigma^{-1}\mu)\T \Psi\, (\mat{A}\T\mat{R}^{-1}d + \Sigma^{-1}\mu)

Step 3 — read off the quadratic coefficient of dd. Collecting the terms in dT()dd\T(\cdot)d:

12dT(R1R1AΨATR1)d\tfrac12\, d\T \left(\mat{R}^{-1} - \mat{R}^{-1}\mat{A}\,\Psi\,\mat{A}\T\mat{R}^{-1}\right) d

and the inversion lemma below, with U=R\mat{U} = \mat{R}, P=A\mat{P} = \mat{A}, V=Σ\mat{V} = \Sigma, says that bracket is exactly (R+AΣAT)1(\mat{R} + \mat{A}\Sigma\mat{A}\T)^{-1}.

Step 4 — read off the linear coefficient. The cross term is dTR1AΨΣ1μ-\,d\T\mat{R}^{-1}\mat{A}\,\Psi\,\Sigma^{-1}\mu, and

(R+AΣAT)1A=R1AR1AΨATR1A=R1AΨ(Ψ1ATR1A)=R1AΨΣ1(\mat{R} + \mat{A}\Sigma\mat{A}\T)^{-1}\mat{A} = \mat{R}^{-1}\mat{A} - \mat{R}^{-1}\mat{A}\Psi\mat{A}\T\mat{R}^{-1}\mat{A} = \mat{R}^{-1}\mat{A}\Psi\left(\Psi^{-1} - \mat{A}\T\mat{R}^{-1}\mat{A}\right) = \mat{R}^{-1}\mat{A}\Psi\Sigma^{-1}

so that cross term is dT(R+AΣAT)1Aμ-\,d\T(\mat{R} + \mat{A}\Sigma\mat{A}\T)^{-1}\mat{A}\mu. A quadratic with that Hessian and that linear term is, up to a constant,

L=12(dAμ)T(R+AΣAT)1(dAμ)+constL^\star = \tfrac12 (d - \mat{A}\mu)\T (\mat{R} + \mat{A}\Sigma\mat{A}\T)^{-1} (d - \mat{A}\mu) + \text{const}

Undoing d=xtBud = x_t - \mat{B}u gives the result. \blacksquare

The two-line version, and why we did it the long way. A linear map of a Gaussian is Gaussian, so it suffices to push the first two moments through: E[xt]=Aμ+Bu\E[x_t] = \mat{A}\mu + \mat{B}u and Cov(xt)=AΣAT+R\Cov(x_t) = \mat{A}\Sigma\mat{A}\T + \mat{R}, using Cov(Ax+b)=ACov(x)AT\Cov(\mat{A}x + b) = \mat{A}\Cov(x)\mat{A}\T and the independence of εt\varepsilon_t. Correct, and four lines shorter. The long derivation earns its keep in Chapter 7: when A\mat{A} becomes a Jacobian the shortcut silently stops being exact, and only the completing-the-square version shows you which step became an approximation.

The result, in the colors of the widget: the blue prior becomes the orange prediction by

μˉt=Atμt1+Btut,Σˉt=AtΣt1AtT+Rt\htmlClass{term-prediction}{\bar\mu_t} = \mat{A}_t \htmlClass{term-prior}{\mu_{t-1}} + \mat{B}_t u_t, \qquad \htmlClass{term-prediction}{\bar\Sigma_t} = \mat{A}_t \htmlClass{term-prior}{\Sigma_{t-1}} \mat{A}_t\T + \mat{R}_t

Read the covariance line twice. AΣAT\mat{A}\Sigma\mat{A}\T is the old uncertainty dragged through the dynamics — for a contracting system it can shrink. The +Rt+\mat{R}_t is the uncertainty this step creates, and since Rt\mat{R}_t is positive definite, that term only ever adds. In every system we will meet, prediction is a net loss.

Correction

DerivationThe correction step, and where the gain comes from

Line 3 of the Bayes filter is a product of two Gaussians in xtx_t:

bel(xt)=η  N(zt;Cxt,Q)  N(xt;μˉ,Σˉ)\htmlClass{term-posterior}{\bel(x_t)} = \eta\; \htmlClass{term-measurement}{\Normal(z_t; \mat{C}x_t, \mat{Q})}\; \htmlClass{term-prediction}{\Normal(x_t; \bar\mu, \bar\Sigma)}

Write xx for xtx_t. The exponent is a sum of two quadratics,

J(x)=12(zCx)TQ1(zCx)+12(xμˉ)TΣˉ1(xμˉ)J(x) = \tfrac12 (z - \mat{C}x)\T\mat{Q}^{-1}(z - \mat{C}x) + \tfrac12 (x - \bar\mu)\T\bar\Sigma^{-1}(x - \bar\mu)

Step 1 — the Hessian is the posterior information. Differentiating twice,

2Jx2=CTQ1C+Σˉ1Σt=(Σˉ1+CTQ1C)1\frac{\partial^2 J}{\partial x^2} = \mat{C}\T\mat{Q}^{-1}\mat{C} + \bar\Sigma^{-1} \qquad\Longrightarrow\qquad \Sigma_t = \left(\bar\Sigma^{-1} + \mat{C}\T\mat{Q}^{-1}\mat{C}\right)^{-1}

For a Gaussian, "the exponent's curvature" is the inverse covariance. Note that the two information contributions simply added — remember this line, it is the whole information filter.

Step 2 — the minimum is the posterior mean. Setting J/x=0\partial J/\partial x = 0:

CTQ1(zCμt)+Σˉ1(μtμˉ)=0    (Σˉ1+CTQ1C)μt=Σˉ1μˉ+CTQ1z-\mat{C}\T\mat{Q}^{-1}(z - \mat{C}\mu_t) + \bar\Sigma^{-1}(\mu_t - \bar\mu) = 0 \;\Longrightarrow\; \left(\bar\Sigma^{-1} + \mat{C}\T\mat{Q}^{-1}\mat{C}\right)\mu_t = \bar\Sigma^{-1}\bar\mu + \mat{C}\T\mat{Q}^{-1} z

so μt=Σt(Σˉ1μˉ+CTQ1z)\mu_t = \Sigma_t\left(\bar\Sigma^{-1}\bar\mu + \mat{C}\T\mat{Q}^{-1}z\right).

Step 3 — that is already the answer. Steps 1 and 2 are the complete measurement update, in two lines, with no gain anywhere. They are the information filter's correct step, written a section early. Everything that follows is the work of translating them back into moments.

Step 4 — rearrange into an innovation. Split z=(zCμˉ)+Cμˉz = (z - \mat{C}\bar\mu) + \mat{C}\bar\mu inside Step 2's right-hand side:

Σˉ1μˉ+CTQ1Cμˉ+CTQ1(zCμˉ)=Σt1μˉ+CTQ1(zCμˉ)\bar\Sigma^{-1}\bar\mu + \mat{C}\T\mat{Q}^{-1}\mat{C}\bar\mu + \mat{C}\T\mat{Q}^{-1}(z - \mat{C}\bar\mu) = \Sigma_t^{-1}\bar\mu + \mat{C}\T\mat{Q}^{-1}(z - \mat{C}\bar\mu)

Multiplying by Σt\Sigma_t gives μt=μˉ+ΣtCTQ1(zCμˉ)\mu_t = \bar\mu + \Sigma_t\mat{C}\T\mat{Q}^{-1}(z - \mat{C}\bar\mu), which names the gain: Kt:=ΣtCTQ1\mat{K}_t := \Sigma_t\mat{C}\T\mat{Q}^{-1}.

Step 5 — the usable form of the gain. That expression needs Σt\Sigma_t, which needs an n×nn\times n inversion. The familiar form needs only an m×mm \times m one. They are equal:

(Σˉ1+CTQ1C)1CTQ1  =  ΣˉCT(CΣˉCT+Q)1\left(\bar\Sigma^{-1} + \mat{C}\T\mat{Q}^{-1}\mat{C}\right)^{-1}\mat{C}\T\mat{Q}^{-1} \;=\; \bar\Sigma\mat{C}\T\left(\mat{C}\bar\Sigma\mat{C}\T + \mat{Q}\right)^{-1}

Multiply both sides on the left by (Σˉ1+CTQ1C)(\bar\Sigma^{-1} + \mat{C}\T\mat{Q}^{-1}\mat{C}) and on the right by (CΣˉCT+Q)(\mat{C}\bar\Sigma\mat{C}\T + \mat{Q}). The left-hand side becomes the first line below, the right-hand side the second:

CTQ1(CΣˉCT+Q)=CTQ1CΣˉCT+CT\mat{C}\T\mat{Q}^{-1}\left(\mat{C}\bar\Sigma\mat{C}\T + \mat{Q}\right) = \mat{C}\T\mat{Q}^{-1}\mat{C}\bar\Sigma\mat{C}\T + \mat{C}\T(Σˉ1+CTQ1C)ΣˉCT=CT+CTQ1CΣˉCT\left(\bar\Sigma^{-1} + \mat{C}\T\mat{Q}^{-1}\mat{C}\right)\bar\Sigma\mat{C}\T = \mat{C}\T + \mat{C}\T\mat{Q}^{-1}\mat{C}\bar\Sigma\mat{C}\T

The same thing. \blacksquare

Step 6 — the covariance in moments form. Apply the inversion lemma to Σt=(Σˉ1+CTQ1C)1\Sigma_t = (\bar\Sigma^{-1} + \mat{C}\T\mat{Q}^{-1}\mat{C})^{-1}:

Σt=ΣˉΣˉCT(CΣˉCT+Q)1CΣˉ=ΣˉKtCΣˉ=(IKtC)Σˉ\Sigma_t = \bar\Sigma - \bar\Sigma\mat{C}\T(\mat{C}\bar\Sigma\mat{C}\T + \mat{Q})^{-1}\mat{C}\bar\Sigma = \bar\Sigma - \mat{K}_t\mat{C}\bar\Sigma = (\mat{I} - \mat{K}_t\mat{C})\bar\Sigma

Step 7 — the Joseph form, and why the code uses it. For any gain K\mat{K}, not just the optimal one, propagating the error xtμt=(IKC)(xtμˉ)Kδtx_t - \mu_t = (\mat{I}-\mat{KC})(x_t - \bar\mu) - \mat{K}\delta_t through Cov\Cov gives

Σt=(IKC)Σˉ(IKC)T+KQKT\Sigma_t = (\mat{I} - \mat{K}\mat{C})\bar\Sigma(\mat{I} - \mat{K}\mat{C})\T + \mat{K}\mat{Q}\mat{K}\T

Expanding it and using ΣˉCT=KSt\bar\Sigma\mat{C}\T = \mat{K}S_t at the optimal gain collapses it back to (IKC)Σˉ(\mat{I}-\mat{KC})\bar\Sigma:

(IKC)Σˉ[ΣˉCTK(CΣˉCT+Q)]KT=(IKC)Σˉ(\mat{I} - \mat{KC})\bar\Sigma - \left[\bar\Sigma\mat{C}\T - \mat{K}(\mat{C}\bar\Sigma\mat{C}\T + \mat{Q})\right]\mat{K}\T = (\mat{I} - \mat{KC})\bar\Sigma

So the two agree in exact arithmetic. They do not agree in floating point. The short form is a difference of two matrices and can go indefinite after enough round-off; the Joseph form is a sum of two symmetric positive-semidefinite terms and cannot. Exercise 6 makes this happen on purpose.

Collecting the results, with the colors of the widget:

St=CtΣˉtCtT+Qt,Kt=ΣˉtCtTSt1\htmlClass{term-measurement}{S_t} = \mat{C}_t\htmlClass{term-prediction}{\bar\Sigma_t}\mat{C}_t\T + \mat{Q}_t, \qquad \mat{K}_t = \htmlClass{term-prediction}{\bar\Sigma_t}\mat{C}_t\T \htmlClass{term-measurement}{S_t^{-1}}
μt=μˉt+Kt(ztCtμˉt),Σt=(IKtCt)Σˉt\htmlClass{term-posterior}{\mu_t} = \htmlClass{term-prediction}{\bar\mu_t} + \mat{K}_t\bigl(\htmlClass{term-measurement}{z_t} - \mat{C}_t\htmlClass{term-prediction}{\bar\mu_t}\bigr), \qquad \htmlClass{term-posterior}{\Sigma_t} = (\mat{I} - \mat{K}_t\mat{C}_t)\htmlClass{term-prediction}{\bar\Sigma_t}

The quantity ztCtμˉtz_t - \mat{C}_t\bar\mu_t is the innovation: the part of the measurement the filter did not already know. StS_t is how surprised the filter is entitled to be. Both names earn their keep later — Chapter 11 uses StS_t to gate outliers, and the consistency section below uses it to catch a filter lying about its own confidence.

The gain is precision-weighted trust

Specialize to one dimension with C=1\mat{C} = 1. Then S=σˉ2+σz2S = \bar\sigma^2 + \sigma_z^2 and

K=σˉ2σˉ2+σz2,μ=(1K)μˉ+Kz,σ2=(1K)σˉ2K = \frac{\bar\sigma^2}{\bar\sigma^2 + \sigma_z^2}, \qquad \htmlClass{term-posterior}{\mu} = (1-K)\,\htmlClass{term-prediction}{\bar\mu} + K\,\htmlClass{term-measurement}{z}, \qquad \htmlClass{term-posterior}{\sigma^2} = (1-K)\,\htmlClass{term-prediction}{\bar\sigma^2}

Substituting KK into the variance and inverting turns that last equation into the sentence the Gain Lever draws:

1σ2=1σˉ2+1σz2\frac{1}{\htmlClass{term-posterior}{\sigma^2}} = \frac{1}{\htmlClass{term-prediction}{\bar\sigma^2}} + \frac{1}{\htmlClass{term-measurement}{\sigma_z^2}}

Precisions add. Not variances, not standard deviations — precisions. The posterior is more certain than either input, always, by an amount that does not depend on whether the two sources agreed. That is the epigraph, and it is also why the gain is not a knob: KK is nothing but the fraction of the total precision that the prediction contributed, so choosing it means choosing σˉ2\bar\sigma^2 and σz2\sigma_z^2, which are physical claims about a motor and a beacon.

The inversion lemma

Both derivations leaned on one identity. It is Thrun's Table 3.2, and it is more usually called the Sherman–Morrison–Woodbury formula.

Lemma. For invertible U\mat{U} and V\mat{V} and any P\mat{P} of compatible shape,

(U+PVPT)1=U1U1P(V1+PTU1P)1PTU1\left(\mat{U} + \mat{P}\mat{V}\mat{P}\T\right)^{-1} = \mat{U}^{-1} - \mat{U}^{-1}\mat{P}\left(\mat{V}^{-1} + \mat{P}\T\mat{U}^{-1}\mat{P}\right)^{-1}\mat{P}\T\mat{U}^{-1}

(Thrun states it with R\mat{R} and Q\mat{Q} in those two roles; neutral letters here, since in this chapter those symbols are already taken.)

Verification. Multiply the right-hand side by (U+PVPT)(\mat{U} + \mat{P}\mat{V}\mat{P}\T) and write Ψ1=V1+PTU1P\Psi^{-1} = \mat{V}^{-1} + \mat{P}\T\mat{U}^{-1}\mat{P}. The result is

I+U1PVPTU1PΨ(PT+PTU1PVPT)=I+U1PVPTU1PΨΨ1VPT=I\mat{I} + \mat{U}^{-1}\mat{P}\mat{V}\mat{P}\T - \mat{U}^{-1}\mat{P}\Psi\left(\mat{P}\T + \mat{P}\T\mat{U}^{-1}\mat{P}\mat{V}\mat{P}\T\right) = \mat{I} + \mat{U}^{-1}\mat{P}\mat{V}\mat{P}\T - \mat{U}^{-1}\mat{P}\Psi\Psi^{-1}\mat{V}\mat{P}\T = \mat{I}

using PT+PTU1PVPT=(V1+PTU1P)VPT\mat{P}\T + \mat{P}\T\mat{U}^{-1}\mat{P}\mat{V}\mat{P}\T = (\mat{V}^{-1} + \mat{P}\T\mat{U}^{-1}\mat{P})\mat{V}\mat{P}\T. \blacksquare

The two places this chapter uses it: U=R,P=A,V=Σ\mat{U} = \mat{R}, \mat{P} = \mat{A}, \mat{V} = \Sigma in the prediction derivation, and U=Σˉ1,P=CT,V=Q1\mat{U} = \bar\Sigma^{-1}, \mat{P} = \mat{C}\T, \mat{V} = \mat{Q}^{-1} in the correction one.

The practical reading: it converts an n×nn \times n inversion into an m×mm \times m one. When a robot carries a 12-dimensional state and receives 2-dimensional range–bearing readings, that is the difference between inverting a 12×12 matrix and a 2×2 one, on every measurement, forever.

The algorithm

AlgorithmKalman_filter(µ_{t-1}, Σ_{t-1}, u_t, z_t)CostO(n³ + m³ + m n²) per step — the m³ is the inversion of S_t, the n³ the Joseph update
In
previous belief (µ, Σ), control u_t, measurement z_t
Out
µ_t, Σ_t
  1. μˉt=Atμt1+Btut\bar\mu_t = \mat{A}_t\mu_{t-1} + \mat{B}_t u_t
  2. Σˉt=AtΣt1AtT+Rt\bar\Sigma_t = \mat{A}_t\Sigma_{t-1}\mat{A}_t\T + \mat{R}_t
  3. St=CtΣˉtCtT+QtS_t = \mat{C}_t\bar\Sigma_t\mat{C}_t\T + \mat{Q}_t
  4. Kt=ΣˉtCtTSt1\mat{K}_t = \bar\Sigma_t\mat{C}_t\T S_t^{-1}
  5. μt=μˉt+Kt(ztCtμˉt)\mu_t = \bar\mu_t + \mat{K}_t(z_t - \mat{C}_t\bar\mu_t)
  6. Σt=(IKtCt)Σˉt\Sigma_t = (\mat{I} - \mat{K}_t\mat{C}_t)\bar\Sigma_t
  7. return μt,Σt\mu_t, \Sigma_t

Lines 1–2 are prediction, lines 3–6 correction. This is Thrun's Table 3.1 with the innovation covariance given its own line, because every diagnostic in this chapter needs it. Line 6 is the form that appears in every textbook; the Rust below runs the Joseph variant of it instead, for the reason Step 7 of the derivation gives.

The other coordinate system

Step 3 of the correction derivation was an aside with a filter hidden in it. Written in the canonical parameters

Ωt=Σt1,ξt=Σt1μt\Omega_t = \Sigma_t^{-1}, \qquad \xi_t = \Sigma_t^{-1}\mu_t

the entire measurement update is

Ωt=Ωˉt+CtTQt1Ct,ξt=ξˉt+CtTQt1zt\htmlClass{term-posterior}{\Omega_t} = \htmlClass{term-prediction}{\bar\Omega_t} + \mat{C}_t\T\mat{Q}_t^{-1}\mat{C}_t, \qquad \htmlClass{term-posterior}{\xi_t} = \htmlClass{term-prediction}{\bar\xi_t} + \mat{C}_t\T\mat{Q}_t^{-1}\htmlClass{term-measurement}{z_t}

No gain. No innovation covariance. No inversion of anything the size of the state. Two additions.

The reason has a name: Ω\Omega is the Hessian of the negative log-belief, and log-densities of independent sources add. Fusing kk sensors in information form is a fold over kk matrix additions, in any order, with no intermediate normalization. That is why large multi-sensor systems are so often written this way.

The price appears on the other step. Prediction, which was a single matrix product in moments form, now requires going out to covariance and back:

Ωˉt=(AtΩt11AtT+Rt)1,ξˉt=Ωˉt(AtΩt11ξt1+Btut)\bar\Omega_t = \left(\mat{A}_t\Omega_{t-1}^{-1}\mat{A}_t\T + \mat{R}_t\right)^{-1}, \qquad \bar\xi_t = \bar\Omega_t\left(\mat{A}_t\Omega_{t-1}^{-1}\xi_{t-1} + \mat{B}_t u_t\right)
AlgorithmInformation_filter(ξ_{t-1}, Ω_{t-1}, u_t, z_t)Costpredict O(n³) — two inversions; correct O(m³ + m n²) — additive, no n×n inversion
In
previous canonical belief (ξ, Ω), control u_t, measurement z_t
Out
ξ_t, Ω_t
  1. Ωˉt=(AtΩt11AtT+Rt)1\bar\Omega_t = \left(\mat{A}_t\Omega_{t-1}^{-1}\mat{A}_t\T + \mat{R}_t\right)^{-1}
  2. ξˉt=Ωˉt(AtΩt11ξt1+Btut)\bar\xi_t = \bar\Omega_t\left(\mat{A}_t\Omega_{t-1}^{-1}\xi_{t-1} + \mat{B}_t u_t\right)
  3. Ωt=Ωˉt+CtTQt1Ct\Omega_t = \bar\Omega_t + \mat{C}_t\T\mat{Q}_t^{-1}\mat{C}_t
  4. ξt=ξˉt+CtTQt1zt\xi_t = \bar\xi_t + \mat{C}_t\T\mat{Q}_t^{-1} z_t
  5. return ξt,Ωt\xi_t, \Omega_t
OperationMoments (μ,Σ)(\mu, \Sigma)Information (ξ,Ω)(\xi, \Omega)
Predictionone product plus an addition — cheaptwo n×nn\times n inversions — expensive
Correctioninnovation, gain, Joseph update — expensiveone addition per sensor — cheap
Fusing kk sensorskk full updates, order matters numericallykk additions, order irrelevant
Marginalizing a variabledelete a row and column of Σ\SigmaSchur complement — real work
Conditioning on a variablereal workdelete a row and column of Ω\Omega
Natural prior for "I know nothing"Σ\Sigma \to \infty, not representableΩ=0\Omega = 0, perfectly representable

The last three rows are the ones to remember, because they are the reason this section exists. In Chapter 14 a robot builds a map by keeping a covariance over its pose and every landmark, and that matrix becomes dense and enormous. Its inverse, it turns out, is nearly sparse: Ωij\Omega_{ij} is non-zero only when landmarks ii and jj were seen from a common pose. Chapter 15 throws away the covariance entirely and works in the sparse information matrix forever, and the "expensive" prediction step stops being a problem because it is never performed — the whole trajectory is solved at once.

Thrun's draft also develops the extended information filter as a nonlinear SLAM engine. That lineage is told honestly in Chapter 15, where its descendants live. Here the information filter appears only in its linear form, sized to carry the duality.

Is the filter telling the truth?

RMSE answers "how wrong is the estimate?" It cannot answer "does the filter know how wrong it is?" — and the second question is the one that predicts a crash. A filter that reports 5 cm of uncertainty while making 50 cm errors will reject the very measurements that could save it, because they fall outside its gate.

Two statistics answer it. Under a correct linear-Gaussian model, the normalized estimation error squared

ϵt=(xtμt)TΣt1(xtμt)\epsilon_t = (\htmlClass{term-truth}{x_t} - \htmlClass{term-posterior}{\mu_t})\T\, \htmlClass{term-posterior}{\Sigma_t^{-1}}\, (\htmlClass{term-truth}{x_t} - \htmlClass{term-posterior}{\mu_t})

is χ2\chi^2 distributed with nn degrees of freedom, so E[ϵt]=n\E[\epsilon_t] = n. And the normalized innovation squared

νt=(ztCtμˉt)TSt1(ztCtμˉt)\nu_t = (\htmlClass{term-measurement}{z_t} - \mat{C}_t\htmlClass{term-prediction}{\bar\mu_t})\T\, S_t^{-1}\, (\htmlClass{term-measurement}{z_t} - \mat{C}_t\htmlClass{term-prediction}{\bar\mu_t})

is χ2\chi^2 with mm degrees of freedom, so E[νt]=m\E[\nu_t] = m.

The difference between them is operational, and decisive. NEES needs xtx_t, so it exists only in simulation. NIS needs nothing the robot does not already have, so it runs on the real hardware, in flight, forever. Average either over NN steps and compare against the χ2\chi^2 envelope with NnNn (or NmNm) degrees of freedom, divided by NN: outside the band, and the filter's covariance is a work of fiction. Above the band means overconfident, which is dangerous; below means conservative, which merely wastes information.

Go back to the tuning bench and run the three failure presets while watching the two meters. Each one moves a single slider away from balanced, and the three results are genuinely different diseases. Over a 400-step run, seeded and reproducible, they come out like this:

presetR\mat{R} (σa\sigma_a)Q\mat{Q} (σz\sigma_z)RMSEmean NEES (want 2)mean NIS (want 1)
balanced0.450.320.1752.331.15
sluggish0.452.400.4881.590.07
jittery4.000.320.2521.370.84
divergent0.0040.322.069918643.6

Sluggish claims the beacon is seven times noisier than it is, so it discounts every reading, lags every turn, and nearly triples its RMSE. NEES stays in band and NIS collapses to a seventh of its expected value: this filter is not lying, it is ignoring information, and NIS is the instrument that says so.

Jittery claims the cart slips nine times harder than it does. The prediction is worthless, so the filter follows the beacon almost exactly and inherits the sensor's noise — its RMSE is about what you would get by plotting the raw measurements. Yet both consistency statistics are fine. This is the case that matters most for how you tune: a filter can be honest and wasteful, and no amount of NIS-watching will find it. Only RMSE against known truth will.

Divergent is the dangerous one. Told the cart obeys its model almost perfectly, the covariance shrinks every step, the gain goes to zero, and the filter simply stops updating. Its own claimed σ ends the run at 0.05 m while it is 2 m from the truth. In the seeded run above, a sliding 40-step NIS average leaves its envelope at step 48; the same window's RMSE does not look alarming until step 66. Eighteen steps is not much, but it is the difference between a warning and a post-mortem — and unlike RMSE, NIS is available on the robot.

Implementation in Rust

The type is where the chapter's assumptions become enforceable. Three const generic parameters — state, control, and measurement dimension — mean that feeding a 4-state filter a 3×2 measurement matrix is a compile error, not a panic during a field trial.

crates/ch06_kalman/src/kf.rs
use nalgebra::{SMatrix, SVector};
use pr_core::prob::Gaussian; // Ch. 2's moments-form belief — reused, not redeclared

/// A linear-Gaussian system and the belief it carries.
///
/// `N` = state dimension, `U` = control dimension, `M` = measurement dimension.
pub struct Kf<const N: usize, const U: usize, const M: usize> {
    pub a: SMatrix<f64, N, N>, // A_t  state transition
    pub b: SMatrix<f64, N, U>, // B_t  control
    pub c: SMatrix<f64, M, N>, // C_t  measurement
    /// R_t — **motion** noise, Thrun's convention. Most control texts call this Q.
    pub r: SMatrix<f64, N, N>,
    /// Q_t — **measurement** noise, Thrun's convention. Most control texts call this R.
    pub q: SMatrix<f64, M, M>,
    pub belief: Gaussian<N>,
}

/// Everything a correction learned, handed back for gating and diagnostics.
#[derive(Clone, Copy, Debug)]
pub struct Update<const N: usize, const M: usize> {
    pub innovation: SVector<f64, M>,
    pub s: SMatrix<f64, M, M>,
    pub gain: SMatrix<f64, N, M>,
    /// log p(z_t | z_{1:t-1}) = log N(innovation; 0, S) — Chapter 5's evidence,
    /// still free, and the same quantity Chapter 11 gates outliers with.
    pub log_evidence: f64,
}

impl<const N: usize, const U: usize, const M: usize> Kf<N, U, M> {
    /// Table 3.1, lines 1–2.
    pub fn predict(&mut self, u: &SVector<f64, U>) {
        self.belief.mu = self.a * self.belief.mu + self.b * u;
        // A Σ Aᵀ is the old uncertainty dragged through the dynamics — it can
        // shrink. + R is the uncertainty this step *creates*, and it cannot.
        self.belief.sigma = self.a * self.belief.sigma * self.a.transpose() + self.r;
        self.belief.symmetrize();
    }

    /// Table 3.1, lines 3–6, with the Joseph-form covariance.
    pub fn correct(&mut self, z: &SVector<f64, M>) -> Update<N, M> {
        let ct = self.c.transpose();
        let innovation = z - self.c * self.belief.mu;
        let s = self.c * self.belief.sigma * ct + self.q;
        let s_inv = s.try_inverse().expect("S_t singular: a measurement carries no noise");
        let gain = self.belief.sigma * ct * s_inv;

        self.belief.mu += gain * innovation;

        // Joseph form: (I − KC) Σ̄ (I − KC)ᵀ + K Q Kᵀ. Identical to (I − KC)Σ̄
        // at the optimal gain, but it is a *sum* of two positive-semidefinite
        // terms rather than a difference, so round-off cannot make it indefinite.
        let i_kc = SMatrix::<f64, N, N>::identity() - gain * self.c;
        self.belief.sigma =
            i_kc * self.belief.sigma * i_kc.transpose() + gain * self.q * gain.transpose();
        self.belief.symmetrize();

        // Ch. 2's Cholesky-based log-density, evaluated at the innovation.
        let log_evidence = pr_core::prob::log_normal_pdf(&innovation, &SVector::zeros(), &s);
        Update { innovation, s, gain, log_evidence }
    }
}

impl<const N: usize, const U: usize, const M: usize> bayes_core::BayesFilter for Kf<N, U, M> {
    type Belief = Gaussian<N>;
    type Control = SVector<f64, U>;
    type Measurement = SVector<f64, M>;

    fn predict(&mut self, u: &Self::Control) {
        Kf::predict(self, u)
    }

    fn correct(&mut self, z: &Self::Measurement) -> f64 {
        Kf::correct(self, z).log_evidence.exp()
    }

    fn belief(&self) -> &Self::Belief {
        &self.belief
    }
}

The information filter is the same struct with the belief stored the other way round. Notice how much shorter correct is than predict — the exact inverse of the shape above.

crates/ch06_kalman/src/info.rs
use nalgebra::{SMatrix, SVector};
use pr_core::prob::Gaussian;

/// The same belief in canonical coordinates: Ω = Σ⁻¹, ξ = Σ⁻¹µ.
pub struct InfoFilter<const N: usize, const U: usize, const M: usize> {
    pub xi: SVector<f64, N>,
    pub omega: SMatrix<f64, N, N>,
    pub a: SMatrix<f64, N, N>,
    pub b: SMatrix<f64, N, U>,
    pub c: SMatrix<f64, M, N>,
    pub r: SMatrix<f64, N, N>,
    pub q: SMatrix<f64, M, M>,
}

impl<const N: usize, const U: usize, const M: usize> InfoFilter<N, U, M> {
    /// Table 3.4, lines 1–2. Two n×n inversions: the expensive step *here*.
    pub fn predict(&mut self, u: &SVector<f64, U>) {
        let sigma = self.omega.try_inverse().expect("Ω must be positive definite");
        let mu = sigma * self.xi;
        let sigma_bar = self.a * sigma * self.a.transpose() + self.r;
        self.omega = sigma_bar.try_inverse().expect("Σ̄ must be positive definite");
        self.xi = self.omega * (self.a * mu + self.b * u);
    }

    /// Table 3.4, lines 3–4, with this filter's own sensor.
    pub fn correct(&mut self, z: &SVector<f64, M>) {
        let (c, q) = (self.c, self.q);
        self.add_information(z, &c, &q);
    }

    /// No gain, no S, nothing n×n inverted: the measurement's information is
    /// added in, and that is the entire update. Any sensor of any dimension `K`
    /// contributes through the same three lines, which is why fusion in
    /// information form is a fold rather than a special case.
    pub fn add_information<const K: usize>(
        &mut self,
        z: &SVector<f64, K>,
        c: &SMatrix<f64, K, N>,
        q: &SMatrix<f64, K, K>,
    ) {
        let ct_qinv = c.transpose() * q.try_inverse().expect("Q must be invertible");
        self.omega += ct_qinv * c;
        self.xi += ct_qinv * z;
    }

    pub fn to_moments(&self) -> Gaussian<N> {
        let sigma = self.omega.try_inverse().expect("Ω must be positive definite");
        Gaussian::new(sigma * self.xi, sigma)
    }

    /// Start from a moments-form belief and the same system matrices as a `Kf`.
    pub fn from_kf(kf: &Kf<N, U, M>) -> Self {
        let omega = kf.belief.sigma.try_inverse().expect("Σ must be positive definite");
        Self {
            xi: omega * kf.belief.mu,
            omega,
            a: kf.a,
            b: kf.b,
            c: kf.c,
            r: kf.r,
            q: kf.q,
        }
    }
}

The smoother needs the forward run kept rather than thrown away — which is the first time in this book that a filter's defining virtue, forgetting the past, is the thing standing in the way.

crates/ch06_kalman/src/smooth.rs
use nalgebra::{SMatrix, SVector};
use pr_core::prob::Gaussian;

/// One time step of a stored forward run: what the filter believed before the
/// measurement, after it, and the A_t that connected them.
#[derive(Clone, Copy)]
pub struct Step<const N: usize> {
    pub predicted: Gaussian<N>, // (µ̄_t, Σ̄_t)
    pub filtered: Gaussian<N>,  // (µ_t,  Σ_t)
    pub a: SMatrix<f64, N, N>,
}

/// Rauch–Tung–Striebel fixed-interval smoother.
///
/// One backward pass over a completed run. The middle line says everything:
/// correct each state by how wrong its own prediction of the future turned out
/// to be, scaled by how much that state was responsible for the prediction.
pub fn rts_smooth<const N: usize>(run: &[Step<N>]) -> Vec<Gaussian<N>> {
    let mut out: Vec<Gaussian<N>> = run.iter().map(|s| s.filtered).collect();

    for t in (0..run.len().saturating_sub(1)).rev() {
        let next = &run[t + 1];
        let l = run[t].filtered.sigma
            * next.a.transpose()
            * next.predicted.sigma.try_inverse().expect("Σ̄ must be invertible");

        out[t].mu = run[t].filtered.mu + l * (out[t + 1].mu - next.predicted.mu);
        out[t].sigma = run[t].filtered.sigma
            + l * (out[t + 1].sigma - next.predicted.sigma) * l.transpose();
    }
    out
}

/// NEES — needs ground truth, so it exists only in simulation. E[·] = N.
pub fn nees<const N: usize>(truth: &SVector<f64, N>, bel: &Gaussian<N>) -> f64 {
    let d = truth - bel.mu;
    let omega = bel.sigma.try_inverse().expect("Σ must be positive definite");
    (d.transpose() * omega * d)[(0, 0)]
}

/// NIS — needs only the innovation and S, so it runs on the real robot. E[·] = M.
pub fn nis<const M: usize>(innovation: &SVector<f64, M>, s: &SMatrix<f64, M, M>) -> f64 {
    let s_inv = s.try_inverse().expect("S must be invertible");
    (innovation.transpose() * s_inv * innovation)[(0, 0)]
}

A worked example you can check by hand

One dimension, one control, one measurement. The cart is commanded to move one metre per step, and a beacon reports its position.

A=B=C=1,R=0.25,Q=0.5,bel(x0)=N(0,1)\mat{A} = \mat{B} = \mat{C} = 1, \qquad \mat{R} = 0.25, \qquad \mat{Q} = 0.5, \qquad \bel(x_0) = \Normal(0, 1)

Step one. The control u1=1u_1 = 1 gives μˉ1=0+1=1\bar\mu_1 = 0 + 1 = 1 and Σˉ1=1+0.25=1.25\bar\Sigma_1 = 1 + 0.25 = 1.25. The beacon reports z1=1.7z_1 = 1.7, so S1=1.25+0.5=1.75S_1 = 1.25 + 0.5 = 1.75 and

K1=1.251.75=570.714286K_1 = \frac{1.25}{1.75} = \frac{5}{7} \approx 0.714286

The filter moves five sevenths of the way from its prediction to the reading: μ1=1+0.714286×0.7=1.5\mu_1 = 1 + 0.714286 \times 0.7 = 1.5, and Σ1=(15/7)×1.25=2.5/70.357143\Sigma_1 = (1 - 5/7)\times 1.25 = 2.5/7 \approx 0.357143. That posterior variance is smaller than the prediction's 1.25 and smaller than the measurement's own 0.5 — the epigraph, in arithmetic you can do on paper.

Step two. u2=1u_2 = 1 gives μˉ2=2.5\bar\mu_2 = 2.5, Σˉ2=2.5/7+0.25=4.25/70.607143\bar\Sigma_2 = 2.5/7 + 0.25 = 4.25/7 \approx 0.607143. The beacon reports z2=2.1z_2 = 2.1, which is below the prediction, so S2=4.25/7+0.5=7.75/7S_2 = 4.25/7 + 0.5 = 7.75/7 and K2=4.25/7.750.548387K_2 = 4.25/7.75 \approx 0.548387.

stepμˉ\bar\muΣˉ\bar\SigmazzSSKKμ\muΣ\Sigma
11.0000001.2500001.71.7500000.7142861.5000000.357143
22.5000000.6071432.11.1071430.5483872.2806450.274194

Notice the gain fell between the steps, from 0.714 to 0.548, without any instruction. The prediction got better — its variance dropped from 1.25 to 0.607 — so the measurement's share of the total precision shrank. Notice too that Σ\Sigma is converging: 1, 0.357, 0.274, heading for the fixed point of the Riccati recursion, which for these numbers is exactly Σ=0.25\Sigma_\infty = 0.25 and K=0.5K_\infty = 0.5. (Substitute: Σˉ=0.25+0.25=0.5\bar\Sigma = 0.25 + 0.25 = 0.5, S=1S = 1, K=0.5K = 0.5, Σ=0.5×0.5=0.25\Sigma = 0.5 \times 0.5 = 0.25. A fixed point you can verify in one line, and the reason the gain readout in the tuning bench stops moving.)

crates/ch06_kalman/examples/cart_1d.rs
use approx::assert_relative_eq;
use ch06_kalman::{InfoFilter, Kf};
use nalgebra::{SMatrix, SVector};
use pr_core::prob::Gaussian;

/// The chapter's 1-D cart, built once so the example and the test cannot drift.
fn cart() -> Kf<1, 1, 1> {
    Kf {
        a: SMatrix::identity(),
        b: SMatrix::identity(),
        c: SMatrix::identity(),
        r: SMatrix::from_element(0.25), // motion noise      R
        q: SMatrix::from_element(0.50), // measurement noise Q
        belief: Gaussian::new(SVector::zeros(), SMatrix::identity()),
    }
}

fn main() {
    let mut kf = cart();
    for (t, (u, z)) in [(1.0, 1.7), (1.0, 2.1)].into_iter().enumerate() {
        kf.predict(&SVector::from_element(u));
        print!("step {}:  µ̄ = {:.6}  Σ̄ = {:.6}", t + 1, kf.belief.mu[0], kf.belief.sigma[(0, 0)]);
        let up = kf.correct(&SVector::from_element(z));
        println!(
            "   K = {:.6}   µ = {:.6}  Σ = {:.6}",
            up.gain[(0, 0)], kf.belief.mu[0], kf.belief.sigma[(0, 0)]
        );
    }
}

#[test]
fn worked_example_ch06_cart_1d() {
    let mut kf = cart();

    kf.predict(&SVector::from_element(1.0));
    assert_relative_eq!(kf.belief.mu[0], 1.0, epsilon = 1e-12);
    assert_relative_eq!(kf.belief.sigma[(0, 0)], 1.25, epsilon = 1e-12);

    let u1 = kf.correct(&SVector::from_element(1.7));
    assert_relative_eq!(u1.gain[(0, 0)], 5.0 / 7.0, epsilon = 1e-12);
    assert_relative_eq!(kf.belief.mu[0], 1.5, epsilon = 1e-12);
    assert_relative_eq!(kf.belief.sigma[(0, 0)], 2.5 / 7.0, epsilon = 1e-12);

    kf.predict(&SVector::from_element(1.0));
    assert_relative_eq!(kf.belief.mu[0], 2.5, epsilon = 1e-12);
    assert_relative_eq!(kf.belief.sigma[(0, 0)], 4.25 / 7.0, epsilon = 1e-12);

    let u2 = kf.correct(&SVector::from_element(2.1));
    assert_relative_eq!(u2.gain[(0, 0)], 4.25 / 7.75, epsilon = 1e-12);
    assert_relative_eq!(kf.belief.mu[0], 2.280_645_161_290_323, epsilon = 1e-9);
    assert_relative_eq!(kf.belief.sigma[(0, 0)], 0.274_193_548_387_096_8, epsilon = 1e-9);
}

/// The same two steps in canonical coordinates must land on the same posterior.
/// If this ever fails, one of the two derivations above is wrong.
#[test]
fn information_form_agrees() {
    let mut kf = cart();
    let mut inf = InfoFilter::from_kf(&kf);

    for (u, z) in [(1.0, 1.7), (1.0, 2.1)] {
        let (u, z) = (SVector::from_element(u), SVector::from_element(z));
        kf.predict(&u);
        kf.correct(&z);
        inf.predict(&u);
        inf.correct(&z);
    }

    let moments = inf.to_moments();
    assert_relative_eq!(moments.mu[0], kf.belief.mu[0], epsilon = 1e-9);
    assert_relative_eq!(moments.sigma[(0, 0)], kf.belief.sigma[(0, 0)], epsilon = 1e-9);
}

The widget at the top of this chapter runs the same algorithm: lib/filters/kf.ts is a line-for-line port of the Rust above, including the Joseph form, and lib/filters/info.ts is the port of the information filter. Both reproduce the table you just checked by hand.

One more habit worth building here, because every later chapter reuses it. The book hand-rolls its filters — that is the point — but a hand-rolled filter with no external witness is a hand-rolled filter with an untested opinion. adskalman is a dev-dependency of ch06_kalman for exactly one purpose: to run the same matrices through somebody else's implementation and assert agreement to 10910^{-9}. It is not in the release build and never appears in a listing outside a #[cfg(test)] block. It is the cheapest available insurance against a transposed matrix — the class of bug that produces plausible numbers rather than a crash.

The failure gallery in the tuning bench is not hand-tuned for the widget either. It lives in the library, so the prose, the simulation, and the tests all cite the same four numbers.

crates/ch06_kalman/src/tuning.rs
/// A claim about how noisy the world is. The cart on the bench really has
/// `accel_sigma = 0.45` and `range_sigma = 0.32`; every other preset is a lie
/// of a particular shape.
#[derive(Clone, Copy, Debug)]
pub struct Tuning {
    /// R_t, as a white-acceleration σ in m/s².
    pub accel_sigma: f64,
    /// Q_t, as a range σ in m.
    pub range_sigma: f64,
}

pub const BALANCED: Tuning = Tuning { accel_sigma: 0.45, range_sigma: 0.32 };
/// Q enormous: the beacon is discounted, the estimate lags, NIS collapses.
pub const SLUGGISH: Tuning = Tuning { accel_sigma: 0.45, range_sigma: 2.40 };
/// R enormous: the model is discarded and the estimate inherits sensor noise.
/// Honest — both consistency tests pass — and wasteful.
pub const JITTERY: Tuning = Tuning { accel_sigma: 4.00, range_sigma: 0.32 };
/// R ≈ 0 while the cart really slips: Σ collapses and the filter stops listening.
pub const DIVERGENT: Tuning = Tuning { accel_sigma: 0.004, range_sigma: 0.32 };

#[test]
fn divergence_is_caught_by_nis_before_rmse() {
    let seeded = crate::bench::run(DIVERGENT, /* steps */ 400, /* seed */ 7);
    assert!(seeded.first_nis_out_of_band < seeded.first_rmse_alarm);
}

Putting it together: waiting for the future

A filter is causal by construction. Its estimate at time tt has seen z1:tz_{1:t} and nothing more, because that is what "online" means. But once a run is over — a logged dataset, a mapping session, a post-flight analysis — the constraint is gone. The state at t=40t = 40 can be re-estimated using the measurements from t=41t = 41 onwards, and it should be, because those measurements are informative about it.

DerivationThe RTS smoother, from a conditional Gaussian

Write μtT,ΣtT\mu_{t|T}, \Sigma_{t|T} for the belief about xtx_t given all TT measurements, and keep μt,Σt\mu_t, \Sigma_t for the filtered ones.

Step 1 — the joint of two consecutive states, given the past. Conditioned on z1:tz_{1:t}, the pair (xt,xt+1)(x_t, x_{t+1}) is jointly Gaussian, because xt+1=At+1xt+Bu+εx_{t+1} = \mat{A}_{t+1}x_t + \mat{B}u + \varepsilon is a linear function of xtx_t plus independent noise. Its mean is (μt,μˉt+1)(\mu_t, \bar\mu_{t+1}), and its covariance is built from three blocks we already have:

Cov(xt)=Σt,Cov(xt,xt+1)=ΣtAt+1T,Cov(xt+1)=Σˉt+1\Cov(x_t) = \Sigma_t, \qquad \Cov(x_t,\, x_{t+1}) = \Sigma_t\mat{A}_{t+1}\T, \qquad \Cov(x_{t+1}) = \bar\Sigma_{t+1}

The middle one is the load-bearing one, and it comes for free: Cov(xt,At+1xt+Bu+ε)=ΣtAt+1T\Cov(x_t,\, \mat{A}_{t+1}x_t + \mat{B}u + \varepsilon) = \Sigma_t\mat{A}_{t+1}\T, because ε\varepsilon is independent of xtx_t.

Step 2 — condition on xt+1x_{t+1}. The conditional-Gaussian identity gives

p(xtxt+1,z1:t)=N ⁣(xt;  μt+Lt(xt+1μˉt+1),  ΣtLtΣˉt+1LtT)p(x_t \mid x_{t+1}, z_{1:t}) = \Normal\!\left(x_t;\; \mu_t + \mat{L}_t(x_{t+1} - \bar\mu_{t+1}),\; \Sigma_t - \mat{L}_t\bar\Sigma_{t+1}\mat{L}_t\T\right)

with the smoother gain Lt=ΣtAt+1TΣˉt+11\mat{L}_t = \Sigma_t\mat{A}_{t+1}\T\bar\Sigma_{t+1}^{-1}.

Step 3 — the future adds nothing more. By the Markov assumption, zt+1:Tz_{t+1:T} is conditionally independent of xtx_t given xt+1x_{t+1}, so

p(xtxt+1,z1:T)=p(xtxt+1,z1:t)p(x_t \mid x_{t+1}, z_{1:T}) = p(x_t \mid x_{t+1}, z_{1:t})

This is the step that makes a backward recursion possible at all: everything the future has to say about xtx_t arrives through xt+1x_{t+1}.

Step 4 — average over the smoothed successor. By the tower rule, with the expectation taken over xt+1N(μt+1T,Σt+1T)x_{t+1} \sim \Normal(\mu_{t+1|T}, \Sigma_{t+1|T}):

μtT=E ⁣[μt+Lt(xt+1μˉt+1)]=μt+Lt(μt+1Tμˉt+1)\mu_{t|T} = \E\!\left[\mu_t + \mat{L}_t(x_{t+1} - \bar\mu_{t+1})\right] = \mu_t + \mat{L}_t\left(\mu_{t+1|T} - \bar\mu_{t+1}\right)

and by the law of total covariance,

ΣtT=ΣtLtΣˉt+1LtTE[Cov(xtxt+1)]+LtΣt+1TLtTCov(E[xtxt+1])=Σt+Lt(Σt+1TΣˉt+1)LtT\Sigma_{t|T} = \underbrace{\Sigma_t - \mat{L}_t\bar\Sigma_{t+1}\mat{L}_t\T}_{\E[\Cov(x_t \mid x_{t+1})]} + \underbrace{\mat{L}_t\Sigma_{t+1|T}\mat{L}_t\T}_{\Cov(\E[x_t \mid x_{t+1}])} = \Sigma_t + \mat{L}_t\left(\Sigma_{t+1|T} - \bar\Sigma_{t+1}\right)\mat{L}_t\T

Initialize at t=Tt = T with the filtered belief — the last state has no future to borrow from — and run backwards. \blacksquare

Since Σt+1TΣˉt+1\Sigma_{t+1|T} \preceq \bar\Sigma_{t+1} always, the correction term is negative semidefinite: smoothing never increases a covariance. The improvement is largest where Σˉt+1\bar\Sigma_{t+1} was largest, which is to say exactly where the filter was least sure.

That widget is this book's longest-range foreshadowing. The smoothed trajectory is the posterior over all states given all measurements, p(x0:Tz1:T,u1:T)p(x_{0:T} \mid z_{1:T}, u_{1:T}) — and computing it took a forward pass, a stored history, and a backward pass. Chapter 15 computes the same posterior a completely different way: assemble every measurement and every motion as a constraint in one large sparse system, and solve it. When the models are linear and Gaussian the two answers are identical to machine precision. When they are not — which is always, on a real robot with rotations in its state — the batch method can iterate to convergence and the filter cannot. That is the argument that ended the filtering era in SLAM, and you have just seen its first move.

The natural next questions are what happens when At\mat{A}_t and Ct\mat{C}_t stop being matrices and start being nonlinear functions (Chapter 7), and what happens when a single Gaussian genuinely cannot describe the belief — three identical doors, a kidnapped robot (Chapter 8). Both keep every equation on this page and change exactly one thing.

Exercises

  1. Foundation exerciseDifficulty 1 of 3Precisions add

    Derive K=σˉ2/(σˉ2+σz2)K = \bar\sigma^2/(\bar\sigma^2 + \sigma_z^2) directly, by completing the square in the 1-D exponent 12(zx)2/σz2+12(xμˉ)2/σˉ2\tfrac12(z-x)^2/\sigma_z^2 + \tfrac12(x-\bar\mu)^2/\bar\sigma^2 rather than by specializing the matrix result. Then show 1/σ2=1/σˉ2+1/σz21/\sigma^2 = 1/\bar\sigma^2 + 1/\sigma_z^2, and use it to reproduce both rows of the chapter's numeric table with a calculator.

    Check Your second row should give K2=4.25/7.75=0.548387K_2 = 4.25/7.75 = 0.548387 and Σ2=0.274194\Sigma_2 = 0.274194. If you get Σ2=0.208333\Sigma_2 = 0.208333 you skipped the +R+\mat{R} in the second prediction; if you get Σ2=0.375\Sigma_2 = 0.375 you predicted from Σˉ1\bar\Sigma_1 instead of Σ1\Sigma_1.

  2. Foundation exerciseDifficulty 2 of 3The gain never sees the data

    Prove that Σt\Sigma_t, and therefore Kt\mat{K}_t, depends only on A1:t,C1:t,R1:t,Q1:t\mat{A}_{1:t}, \mat{C}_{1:t}, \mat{R}_{1:t}, \mat{Q}_{1:t} and Σ0\Sigma_0 — never on z1:tz_{1:t}. What does this let you precompute before the robot is switched on? Then state precisely which line of the derivation breaks when Ct\mat{C}_t is replaced by a Jacobian evaluated at μˉt\bar\mu_t, as it will be in Chapter 7.

  3. Foundation exerciseDifficulty 3 of 3Count the flops and pick a side

    Using the inversion lemma, show that the information filter's correct step and the Kalman filter's correct step produce the same posterior. Then count multiply–adds for both forms of both steps at n=13n = 13, m=2m = 2 (a 3-DoF pose plus five 2-D landmarks, observed by a range–bearing sensor — the Chapter 14 state, in miniature), and again at n=13n = 13 with six independent 2-D sensors fused per step. Which form wins in each case, and what would have to be true of Ω\Omega for the answer to flip again? The ledger in w6.2 prices the same formula at m=1m = 1; drag its dimension slider to 13 and check that your totals have the same shape.

  4. Conceptual exerciseDifficulty 1 of 3Predict, then verify

    In the Kalman Tuning Bench, write down your predictions before touching anything: if Q\mat{Q} is multiplied by 100, what happens to (a) the steady-state gain KK, (b) the RMSE, (c) the mean NIS? Now do it. Two of the three probably surprised you; explain why NIS moves in the direction it does.

  5. Conceptual exerciseDifficulty 2 of 3Find the honest failure

    Switch the bench to sweep mode. Find a region of the (R,Q)(\mat{R}, \mat{Q}) grid where the mean NIS is inside its envelope but the RMSE is poor, and a second region where RMSE is good but NIS is far outside. Explain what each region means physically, and say which of the two filters you would rather deploy on a robot that has to gate outliers.

  6. Practical exerciseDifficulty 2 of 3Break the covariance on purpose

    Implement the naive update Σt=(IKtCt)Σˉt\Sigma_t = (\mat{I} - \mat{K}_t\mat{C}_t)\bar\Sigma_t alongside the Joseph form. Run the 1-D cart for 10610^6 steps in f32, logging the smallest eigenvalue of Σ\Sigma every thousand steps. Plot both. Then repeat with a deliberately suboptimal gain (0.9Kt0.9\mat{K}_t) and report which form survives, and why the Joseph form's algebraic structure predicts the result.

  7. Practical exerciseDifficulty 3 of 3Smooth a logged run

    Log a 2-D constant-velocity run in the Apartment (Chapter 4) with N=4N = 4 (position and velocity) and M=2M = 2 (beacon position fixes only). Confirm that the filter recovers velocity, a state it never measures, through the cross-covariance terms — plot Σ13\Sigma_{13} over time and explain its shape. Then run rts_smooth over the stored trajectory and report filtered vs. smoothed RMSE plus the covariance ratio at mid-trajectory. Cross-check the whole thing against adskalman's implementation in a test; a disagreement above 10910^{-9} is a bug in one of you.

References

  1. Kalman, R. E. (1960) A New Approach to Linear Filtering and Prediction Problems. Journal of Basic Engineering 82(1), 35–45.doi:10.1115/1.3662552 (opens in a new tab)

    The original. Worth reading for how little it resembles the modern presentation: Kalman derives the estimator from orthogonal projection, not from Bayes rule, and the word Gaussian barely appears.

  2. Rauch, H. E., Tung, F., and Striebel, C. T. (1965) Maximum Likelihood Estimates of Linear Dynamic Systems. AIAA Journal 3(8), 1445–1450.doi:10.2514/3.3166 (opens in a new tab)

    The smoother in the last section of this chapter, in its original form. Note the framing: maximum likelihood over the whole trajectory — which is precisely the view Chapter 15 returns to.

  3. Thrun, S., Burgard, W., and Fox, D. (2005) Probabilistic Robotics. MIT Press.link to Probabilistic Robotics (opens in a new tab)

    Chapter 3 is the source of this chapter's notation, the Table 3.1 and 3.4 algorithms, the completing-the-square derivation, and the inversion lemma.

  4. Bar-Shalom, Y., Li, X. R., and Kirubarajan, T. (2001) Estimation with Applications to Tracking and Navigation: Theory, Algorithms and Software. Wiley-Interscience.doi:10.1002/0471221279 (opens in a new tab)

    Where NEES and NIS come from, with the χ² acceptance regions and the multi-run averaging this chapter's consistency meters use. The standard reference for filter tuning done as engineering rather than by eye.

  5. Barfoot, T. D. (2024) State Estimation for Robotics. Cambridge University Press, 2nd edition.link to State Estimation for Robotics (opens in a new tab)

    The modern robotics treatment, and the source for this chapter's RTS presentation. Barfoot derives the filter as a special case of batch estimation rather than the other way round — the reordering this book's Part V follows.

  6. Ortiz, J., Evans, T., and Davison, A. J. (2021) A Visual Introduction to Gaussian Belief Propagation. arXiv:2107.02308.link to A Visual Introduction to Gaussian Belief Propagation (opens in a new tab)

    What the information form becomes when you stop assuming a chain: message passing on a sparse Ω. The interactive figures are the best answer to “why would anyone accept the information filter's expensive prediction step?”

  7. Tracy, K. S. (2022) A Square-Root Kalman Filter Using Only QR Decompositions. arXiv:2208.06452.link to A Square-Root Kalman Filter Using Only QR Decompositions (opens in a new tab)

    The numerical-hygiene chapter this one only gestures at. Propagating a Cholesky factor of Σ instead of Σ doubles the working precision and makes the positive-definiteness that Joseph form defends structurally impossible to lose.

  8. Revach, G., Shlezinger, N., Ni, X., Escoriza, A. L., van Sloun, R. J. G., and Eldar, Y. C. (2022) KalmanNet: Neural Network Aided Kalman Filtering for Partially Known Dynamics. IEEE Transactions on Signal Processing 70, 1532–1547.doi:10.1109/TSP.2022.3158588 (opens in a new tab)

    What happens when the gain stops being derivable because R and Q are unknown: learn K with a recurrent network while keeping the rest of the recursion intact. Chapter 25 differentiates through this chapter's Kf to get there.