Robot Motion
Chapter 15PART IVPlanning with UncertaintyDifficulty: IntermediateEstimated reading time: 55 min

Kalman Filtering: The Geometric Observer

The Kalman filter as Choset teaches it — a Luenberger observer whose gain is re-chosen every step so the correction is the shortest Mahalanobis step onto the most likely measurement hyperplane — plus observability, EKF localization, and Kalman SLAM, compressed to what a planner needs.

The Kalman filter is both an observer and a filter.
Howie Choset, Kevin Lynch, Seth Hutchinson, George Kantor, Wolfram Burgard, Lydia Kavraki, and Sebastian ThrunPrinciples of Robot Motion (2005), §8.2.1

In this chapter

Fourteen chapters of planners assumed the planner knew qq. This chapter admits that Rusty does not. Encoders drift, landmarks are noisy, and the configuration a planner imagines is only a belief about the state the robot occupies. Choset's Chapter 8 teaches the Kalman filter as no other robotics text does — not as Bayes' rule with Gaussians, which the sister book does completely, but as an observer: a copy of the system corrected toward the set of states consistent with the latest measurement.

The whole chapter is one picture. An ellipse of plausible states touches a line of measurement-consistent states, and the point of contact is the update. Once that picture is in place, the Kalman gain, the innovation covariance, data association by χ2\chi^2, and Kalman SLAM all fall out of it — and so does the warning that an optimal observer still cannot learn a direction the sensors never see. Everything deeper is a link into the sister volume. What stays here is exactly the estimation vocabulary Chapters 16 and 23 consume.

The problem: a planner that does not know where it is

Put Rusty in the Apartment corridor and ask it to drive a loop on encoders alone. Each tick the wheels report how far they turned; integrate that and you have a position. The orange track below is that integral. The gray dashed track is where the robot actually went.

Two things go wrong, and they are different in kind. The right wheel is a fraction of a percent larger than the odometry believes, so every straight run bends into an arc — a bias, invisible to any amount of averaging. On top of it the wheels slip by a random amount each tick — noise, which random-walks the heading and, through the heading, the position. Choset's Figure 8.1 shows the same thing on a B21: a rotational error approaching 45° after one lap. The command-integration estimate "is continuously losing information about its location," and nothing in the integration can tell you how much.

The cure is also in the widget. Tick landmark fixes. Whenever a mapped corner comes within range, one range-and-bearing sighting pulls the estimate back and collapses the ellipse. That pull is the subject of this chapter: how hard to tug, in which direction, and what the tug can and cannot fix.

One sentence of bookkeeping, said once for all of Part IV. For Rusty the planner's configuration q∈SE(2)q \in \SEtwo and the filter's state xtx_t are the same object; the sister book writes xt,ut,ztx_t, u_t, z_t and this book keeps Choset's x(k),u(k),y(k)x(k), u(k), y(k) because his geometry reads better in it. The notation table below sits the two side by side so nobody translates twice.

Building intuition

The knob the filter turns

Strip the problem to Choset's §8.2.6 toy: a robot on a rail with state x=[xr,vr]Tx = [x_r, v_r]\T, a force input, Newton's law sampled at T=0.5T = 0.5 s, and a noisy velocity sensor. An observer for it is a copy of the dynamics plus a correction proportional to the innovation — the difference between what the sensor said and what it would have said had the prediction been exact:

x^(k+1∣k)=F x^(k∣k)+G u(k),x^(k+1∣k+1)=x^(k+1∣k)+K [ y(k+1)−H x^(k+1∣k) ].\htmlClass{term-prediction}{\hat x(k{+}1 \mid k)} = F\,\hat x(k \mid k) + G\,u(k), \qquad \htmlClass{term-posterior}{\hat x(k{+}1 \mid k{+}1)} = \htmlClass{term-prediction}{\hat x(k{+}1 \mid k)} + K\,\bigl[\,\htmlClass{term-measurement}{y(k{+}1)} - H\,\htmlClass{term-prediction}{\hat x(k{+}1 \mid k)}\,\bigr].

KK is a knob. Turn it to zero and you have dead reckoning; turn it up and the estimate chases the sensor, noise and all; turn it far enough and the estimate oscillates and diverges. The Observer Bench lets you turn it. Then it lets the filter turn it for you.

Watch three things. The ellipse in the phase plane grows orange on every predict and squishes purple on every update, always toward the green line of states consistent with the reading. The dots on the unit disc are the eigenvalues of the error dynamics; with the velocity sensor one of them sits at exactly 11 no matter what you do, and the position variance climbs forever. And when you tick let the filter choose, the gain stops being a constant: it becomes a sequence R(k)R(k) that starts large, settles, and the "equivalent knob" readout drifts to 11 on its own. The Kalman gain is not a tuning constant. It is a trajectory, and the knob you were turning is what the filter computes.

Notation used in this chapter
SymbolMeaningNote
x(k)∈Rn, u(k)∈Rm, y(k)∈Rpx(k) \in \R^n,\ u(k) \in \R^m,\ y(k) \in \R^pState, input, output at sample k (period T).Sister book: x_t, u_t, z_t
F(k), G(k), H(k)F(k),\ G(k),\ H(k)Dynamics, input, and output matrices.Sister book: A_t, B_t, C_t
v(k)∼N(0,V), w(k)∼N(0,W)v(k) \sim \Normal(0, V),\ w(k) \sim \Normal(0, W)Process and measurement noise and their covariances.Sister book: N(0, R_t), N(0, Q_t)
x^(k1∣k2), P(k1∣k2)\hat x(k_1 \mid k_2),\ P(k_1 \mid k_2)Estimate and error covariance at k₁ given outputs through k₂.Sister book: μ_t, Σ_t (bars when predicted)
Ω={x:Hx=y}, Ω∗\Omega = \{x : Hx = y\},\ \Omega^*States consistent with the output y; with the most likely output y*.
ν=y−Hx^(k+1∣k),S=HPHT+W\nu = y - H\hat x(k{+}1 \mid k),\quad S = HPH\T + WInnovation and its covariance.Sister book: z_t − C_t μ̄_t, S_t
R=PHTS−1R = PH\T S^{-1}The Kalman gain — Choset's letter. Not a covariance.Sister book: K_t. Thrun's R is the process covariance.
∥x∥M2=xTP−1x\norm{x}_M^2 = x\T P^{-1} xMahalanobis norm under the predicted covariance P(k+1 | k).
Q=[H; HF; … ; HFn−1]Q = [H;\ HF;\ \dots;\ HF^{n-1}]Observability matrix (Thm. 8.2.2).
a(⋅),χij2=νijTSij−1νija(\cdot),\quad \chi^2_{ij} = \nu_{ij}\T S_{ij}^{-1} \nu_{ij}Association map; Mahalanobis innovation of measurement i against landmark j.Sister book: c_t^i, d_ij

The symbol hazard, stated once

Choset's RR is the gain and his V,WV, W are covariances. The sister book, in Thrun's convention, uses KK for the gain and R,QR, Q for the covariances. The vendored code speaks Thrun; the estimate crate translates at its boundary and nowhere else, and every widget labels both.

The picture

Here is the one picture the chapter is built on, drawn from the numbers of Choset's example. The orange ellipse is the predicted belief. The green line is Ω\Omega: every state that would produce the reading exactly. Where should the estimate go?

The naive answer is the gray foot: drop perpendicularly onto Ω\Omega, correct the velocity the sensor measured and leave the position alone. The right answer is the purple foot: perpendicular in the metric of the covariance. Position and velocity are correlated in PP — a robot that was going faster than we thought has also gone farther — so correcting the velocity should drag the position with it, and the correlated, more uncertain direction moves more. Add sensor noise and the line itself moves to Ω∗\Omega^* through the most likely output y∗y^*; the Kalman update is the Mahalanobis foot on that line. Three derivations, one picture.

The mathematics

Definitions

An observer for the system x(k+1)=Fx(k)+Gu(k)x(k{+}1) = F x(k) + G u(k), y(k)=Hx(k)y(k) = H x(k) is a recursion that reconstructs x(k)x(k) from the known signals u(⋅),y(⋅)u(\cdot), y(\cdot) and the model (F,G,H)(F, G, H) when HH is not invertible — which it usually is not, being p×np \times n with p<np < n (§8.2.1; App. J.4). The Kalman filter is both an observer and a filter: it reconstructs the state and suppresses the noise.

The innovation ν=y(k+1)−Hx^(k+1∣k)\nu = y(k{+}1) - H\hat x(k{+}1 \mid k) is what the sensor said minus what it would have said had the prediction been exact. The consistent set Ω={x∈Rn:Hx=y(k+1)}\Omega = \{x \in \R^n : H x = y(k{+}1)\} is the affine subspace of states that reproduce the output; its dimension is n−pn - p when HH has full row rank, which Choset assumes throughout §8.2.

The Mahalanobis distance dM(x,x^)=∥x−x^∥Md_M(x, \hat x) = \norm{x - \hat x}_M with ∥x∥M2=xTP(k+1∣k)−1x\norm{x}_M^2 = x\T P(k{+}1 \mid k)^{-1} x measures distance in standard deviations under the predicted covariance (§8.2.3, footnote 5). It comes from an inner product ⟨x1,x2⟩M=x1TP−1x2\langle x_1, x_2 \rangle_M = x_1\T P^{-1} x_2, so "orthogonal" has a meaning under it.

A linear system is observable when the initial state is determined by u,yu, y over a finite window; for LTI systems, iff rank⁡Q=n\rank Q = n (App. J.4, Thm. J.4.1). The Luenberger observer (eq. J.9) is x^˙=Ax^+Bu+K(y−Cx^)\dot{\hat x} = A\hat x + Bu + K(y - C\hat x), with error dynamics e˙=(A−KC)e\dot e = (A - KC)e; its discrete cousin is what the Bench runs.

Data association is the choice of a(i)a(i), the landmark that explains measurement ii (§8.3.2).

The simple observer is an orthogonal projection

DerivationProjecting the prediction onto Ω

Write Δx=x^(k+1∣k+1)−x^(k+1∣k)\Delta x = \hat x(k{+}1 \mid k{+}1) - \hat x(k{+}1 \mid k) and ask for the shortest Δx\Delta x that lands in Ω\Omega.

Step 1 — the true state lies in Ω\Omega. With no measurement noise, Hx(k+1)=y(k+1)Hx(k{+}1) = y(k{+}1) exactly. So the update should land in Ω\Omega, and since the prediction is our best guess, it should be the point of Ω\Omega nearest the prediction.

Step 2 — shortest means orthogonal. The shortest vector from a point to an affine subspace is orthogonal to it: aTΔx=0a\T \Delta x = 0 for every aa parallel to Ω\Omega. A vector is parallel to Ω\Omega iff Ha=0Ha = 0, i.e. a∈null⁡(H)a \in \operatorname{null}(H) (Choset's problems 4–5).

Step 3 — orthogonal to the null space means in the column space of HTH\T. This is the fundamental theorem of linear algebra (problem 6): null⁡(H)⊥=column⁡(HT)\operatorname{null}(H)^\perp = \operatorname{column}(H\T). So Δx=HTγ\Delta x = H\T\gamma for some γ∈Rp\gamma \in \R^p.

Step 4 — guess γ\gamma linear in the innovation. Try γ=Kν\gamma = K\nu with K∈Rp×pK \in \R^{p \times p}, so Δx=HTKν\Delta x = H\T K \nu. Enforce membership in Ω\Omega: H(x^−+Δx)=yH(\hat x^- + \Delta x) = y, i.e. HΔx=νH\Delta x = \nu.

Step 5 — solve. HHTKν=νHH\T K\nu = \nu for every ν\nu forces K=(HHT)−1K = (HH\T)^{-1}, which exists because HH has full row rank. The guess is verified and the observer is complete.

Why it is not enough. The update is always perpendicular to Ω\Omega, so only the component of the state that directly affects the current reading is corrected. Errors parallel to Ω\Omega — position, for a velocity sensor — are never touched, and the estimate does not in general converge. The covariance is what fixes this.

Prediction grows the ellipse; the update is the Mahalanobis-closest point

DerivationCovariance prediction and the skewed projection

Step 1 — expand the definition. P(k+1∣k)=E[(x(k+1)−x^(k+1∣k))(⋅)T]P(k{+}1 \mid k) = \E[(x(k{+}1) - \hat x(k{+}1 \mid k))(\cdot)\T]. Substitute x(k+1)=Fx+Gu+vx(k{+}1) = Fx + Gu + v and x^(k+1∣k)=Fx^+Gu\hat x(k{+}1 \mid k) = F\hat x + Gu; the input cancels and the error is F(x−x^)+vF(x - \hat x) + v.

Step 2 — the cross terms vanish. v(k)v(k) is zero-mean and independent of both x(k)x(k) and x^(k∣k)\hat x(k \mid k), so E[F(x−x^)vT]=F E[x−x^] E[v]T=0\E[F(x - \hat x)v\T] = F\,\E[x - \hat x]\,\E[v]\T = 0. What remains is F E[(x−x^)(x−x^)T]FT+E[vvT]=FPFT+VF\,\E[(x - \hat x)(x - \hat x)\T]F\T + \E[vv\T] = FPF\T + V. This is eq. (8.12). Note that FPFTFPF\T can shrink if FF is contractive; prediction only ever adds uncertainty because of +V+V.

Step 3 — most likely point on Ω\Omega. The Gaussian's exponent is −12(x−x^−)TP−1(x−x^−)-\tfrac12 (x - \hat x^-)\T P^{-1}(x - \hat x^-), so maximizing it over Ω\Omega means minimizing ∥Δx∥M\norm{\Delta x}_M subject to x^−+Δx∈Ω\hat x^- + \Delta x \in \Omega. Replace Euclidean orthogonality by ⟨a,Δx⟩M=aTP−1Δx=0\langle a, \Delta x\rangle_M = a\T P^{-1}\Delta x = 0 for all a∈null⁡(H)a \in \operatorname{null}(H).

Step 4 — the column space moves. P−1Δx∈column⁡(HT)P^{-1}\Delta x \in \operatorname{column}(H\T), hence Δx∈column⁡(PHT)\Delta x \in \operatorname{column}(PH\T): Δx=PHTγ\Delta x = PH\T\gamma. Figure 8.3 draws this: the Mahalanobis-equidistant ellipses around the prediction grow until one touches Ω\Omega, and the touch point is not under the prediction unless PP is a multiple of the identity.

Step 5 — solve as before. Guess γ=Kν\gamma = K\nu, enforce HΔx=νH\Delta x = \nu: HPHTKν=νHPH\T K\nu = \nu, so K=(HPHT)−1K = (HPH\T)^{-1} and R=PHT(HPHT)−1R = PH\T(HPH\T)^{-1} — eq. (8.21). The covariance update P+=P−RHPP^+ = P - RHP, eq. (8.15), is Choset's problem 7 and this chapter's Exercise 1.

Why it is still not enough. Noiseless outputs make P+P^+ singular in the measured directions — exactly zero uncertainty along HH — and a singular PP makes the Mahalanobis norm meaningless on the next step. The last ingredient is sensor noise.

The Kalman filter: most likely output, then project

DerivationFrom Ω to Ω*: Theorem 8.2.1 and the final gain

Step 1 — project the prediction into output space. The state Gaussian (x^−,P−)(\hat x^-, P^-) maps under HH to an output-space Gaussian with mean y^=Hx^−\hat y = H\hat x^- and covariance W^=HP−HT\hat W = HP^-H\T.

Step 2 — the measurement is a second Gaussian there. The reading y(k+1)y(k{+}1) carries noise of covariance WW, so the sensor's claim about the output is N(y,W)\Normal(y, W). The two are independent.

Step 3 — Theorem 8.2.1 (product of Gaussians). The product of Gaussians with pairs (z1,C1)(z_1, C_1) and (z2,C2)(z_2, C_2) is proportional to a Gaussian with z3=z1+Q(z2−z1)z_3 = z_1 + \mathcal{Q}(z_2 - z_1), C3=C1−QC1C_3 = C_1 - \mathcal{Q}C_1, Q=C1(C1+C2)−1\mathcal{Q} = C_1(C_1 + C_2)^{-1}. The proof (Choset's problem 8, Exercise 2 here) multiplies the exponentials and matches the terms quadratic and linear in zz.

Step 4 — the most likely output. Apply it with (z1,C1)=(y^,W^)(z_1, C_1) = (\hat y, \hat W) and (z2,C2)=(y,W)(z_2, C_2) = (y, W): the peak is y∗=y^+W^(W^+W)−1(y−y^)y^* = \hat y + \hat W(\hat W + W)^{-1}(y - \hat y). Define Ω∗={x:Hx=y∗}\Omega^* = \{x : Hx = y^*\} — Figure 8.4's second line.

Step 5 — project onto Ω∗\Omega^* as before. The noiseless result of the previous derivation gives Δx=P−HT(HP−HT)−1(y∗−Hx^−)\Delta x = P^-H\T(HP^-H\T)^{-1}(y^* - H\hat x^-). Substitute y∗−y^=W^(W^+W)−1νy^* - \hat y = \hat W(\hat W + W)^{-1}\nu with W^=HP−HT\hat W = HP^-H\T: the two inverses collapse,

Δx=P−HT (HP−HT)−1 HP−HT (HP−HT+W)−1 ν=P−HT (HP−HT+W)−1 ν.\Delta x = P^-H\T\,(HP^-H\T)^{-1}\,HP^-H\T\,(HP^-H\T + W)^{-1}\,\nu = P^-H\T\,(HP^-H\T + W)^{-1}\,\nu .

That is eq. (8.24) with RR as stated, and P+=P−−RHP−P^+ = P^- - RHP^- follows as in problem 7.

Two honesty notes Choset insists on. For Gaussian noise these are not merely the "best" estimates but the correct posterior: if the belief at kk is Gaussian, so is the belief at k+1k{+}1, and this is it. For non-Gaussian noise the same equations give the best linear estimator, and a nonlinear one may do better. The sister book reaches the same formulas by completing the square — see its Chapter 6 for that route, the information form, and the proof the two coincide.

The Kalman filter is a Luenberger observer with a scheduled gain

This is the chapter's lens, and Choset implies it across §8.2 and Appendix J without stating it.

DerivationError dynamics of the fixed-gain observer

Step 1 — write the observer. x^+=Fx^+Gu+K(y−H(Fx^+Gu))\hat x^+ = F\hat x + Gu + K(y - H(F\hat x + Gu)).

Step 2 — subtract from the true dynamics. x+=Fx+Gu+vx^+ = Fx + Gu + v and y=Hx++w=H(Fx+Gu+v)+wy = Hx^+ + w = H(Fx + Gu + v) + w. The inputs cancel.

Step 3 — collect the error e=x−x^e = x - \hat x: e+=Fe+v−K(HFe+Hv+w)=(I−KH)Fe+(I−KH)v−Kwe^+ = Fe + v - K\bigl(HFe + Hv + w\bigr) = (I - KH)Fe + (I - KH)v - Kw.

Step 4 — read off stability. The noise-free part is the unforced linear system e+=(I−KH)Fee^+ = (I - KH)Fe; by Thm. J.5.1 it decays iff ∣λi∣<1|\lambda_i| < 1 for every eigenvalue of (I−KH)F(I - KH)F. Choosing KK is choosing where those eigenvalues sit — Thm. J.3.2 transposed. For a single output this is Ackermann's formula on the pair (F,HF)(F, HF), which placeObserverGain implements, and which fails exactly when (F,H)(F, H) is unobservable: for the velocity-sensor rail robot the position eigenvalue is pinned at 11 and no KK moves it.

Step 5 — the filter's choice. Take the error covariance of the fixed-gain observer under the noise model, in the form valid for any KK (the Joseph form): P+(K)=(I−KH)P−(I−KH)T+KWKTP^+(K) = (I - KH)P^-(I - KH)\T + KWK\T. Differentiating tr⁡P+(K)\tr P^+(K) in KK and setting it to zero gives K=P−HT(HP−HT+W)−1K = P^-H\T(HP^-H\T + W)^{-1} — the Kalman gain. For a scalar state with P−=1.7P^- = 1.7, H=1H = 1, W=0.5W = 0.5 a brute-force search over KK lands on 0.7727=1.7/2.20.7727 = 1.7/2.2 with P+=0.3864P^+ = 0.3864; the check file does this search so you need not trust the calculus.

From knob to schedule. Iterate predict and update on PP alone and, for an observable pair, P−P^- converges to the fixed point of the Riccati recursion; the gain converges with it to a constant R∞R_\infty. That constant is a perfectly good Luenberger gain — the Bench's knob is scaled so that g=1g = 1 is R∞R_\infty. For the position sensor, R∞=[0.589,0.287]TR_\infty = [0.589, 0.287]\T and the error eigenvalues are 0.634±0.097i0.634 \pm 0.097i, modulus 0.6410.641. For the velocity sensor PP never converges but RR still does, to [0.500,0.358]T[0.500, 0.358]\T, with eigenvalues {1,0.642}\{1, 0.642\} — the 11 is the position direction nobody measures.

Observability: the rank test

DerivationWhy stacking powers of F decides everything

Step 1 — stack the outputs. With zero noise, y(0)=Hx(0)y(0) = Hx(0), y(1)=HFx(0)+HGu(0)y(1) = HFx(0) + HGu(0), y(2)=HF2x(0)+known input termsy(2) = HF^2x(0) + \text{known input terms}, and so on. Move the known input terms to the left: the stacked vector y~=Q x(0)\tilde y = Q\,x(0).

Step 2 — recoverability is injectivity. x(0)x(0) is determined by y~\tilde y iff QQ has a trivial null space, i.e. rank nn. Knowing x(0)x(0) and the inputs, every later state follows.

Step 3 — higher powers add nothing. By Cayley–Hamilton FnF^n is a linear combination of I,F,…,Fn−1I, F, \dots, F^{n-1}, so rows HFkHF^k for k≥nk \ge n lie in the span of those already stacked.

Step 4 — duality. (F,H)(F, H) is observable iff (FT,HT)(F\T, H\T) is controllable (App. J.5.2), which is why the same rank test, transposed, appears in Chapter 20 as the controllability matrix.

The dead-reckoning case. Velocity sensor: Q=[0 1; 0 1]Q = [0\ 1;\ 0\ 1], rank 11. The null direction is [1,0]T[1, 0]\T — position — and the position variance grows without bound through optimal updates. Choset's verdict: "This failure is not the fault of the Kalman filter … The problem instead lies with the system itself." Position sensor: Q=[1 0; 1 0.5]Q = [1\ 0;\ 1\ 0.5], rank 22, and both variances settle.

For nonlinear systems the test is applied to the linearized pair (∂f/∂x,∂h/∂x)(\partial f/\partial x, \partial h/\partial x) at the current estimate and is only local — true at this pose, this input, these landmarks. The Observability Probe recomputes it every frame for that reason.

Two hand-checked steps

Choset's §8.2.6 robot, with his numbers: T=0.5T = 0.5, m=1m = 1, F=[10.501]F = \bigl[\begin{smallmatrix}1 & 0.5\\ 0 & 1\end{smallmatrix}\bigr], G=[0, 0.5]TG = [0,\, 0.5]\T, u=0u = 0, V=[0.20.050.050.1]V = \bigl[\begin{smallmatrix}0.2 & 0.05\\ 0.05 & 0.1\end{smallmatrix}\bigr], H=[0  1]H = [0\ \ 1], W=0.5W = 0.5, x^(k∣k)=[2,4]T\hat x(k \mid k) = [2, 4]\T, P(k∣k)=diag⁡(1,2)P(k \mid k) = \diag(1, 2). He reports no measurement value; this chapter fixes y(k+1)=2.7y(k{+}1) = 2.7 and y(k+2)=2.0y(k{+}2) = 2.0. These two readings are ours, not his.

Step 1. Predict: x^−=F[2,4]T=[4,4]T\hat x^- = F[2, 4]\T = [4, 4]\T and P−=FPFT+V=[1.71.051.052.1]P^- = FPF\T + V = \bigl[\begin{smallmatrix}1.7 & 1.05\\ 1.05 & 2.1\end{smallmatrix}\bigr]. Update: ν=2.7−4=−1.3\nu = 2.7 - 4 = -1.3, S=2.1+0.5=2.6S = 2.1 + 0.5 = 2.6, R=[1.05, 2.1]T/2.6=[0.4038, 0.8077]TR = [1.05,\ 2.1]\T / 2.6 = [0.4038,\ 0.8077]\T, so ν/S=−0.5\nu / S = -0.5 exactly and

x^+=[4, 4]T+R (−1.3)=[3.475, 2.95]T,P+=P−−R [1.05  2.1]=[1.27600.20190.20190.4038].\htmlClass{term-posterior}{\hat x^+} = \htmlClass{term-prediction}{[4,\ 4]\T} + R\,\htmlClass{term-measurement}{(-1.3)} = [3.475,\ 2.95]\T, \qquad \htmlClass{term-posterior}{P^+} = \htmlClass{term-prediction}{P^-} - R\,[1.05\ \ 2.1] = \begin{bmatrix}1.2760 & 0.2019\\ 0.2019 & 0.4038\end{bmatrix}.

Step 2. Predict: x^−=[4.95, 2.95]T\hat x^- = [4.95,\ 2.95]\T, P−=[1.77880.45380.45380.5038]P^- = \bigl[\begin{smallmatrix}1.7788 & 0.4538\\ 0.4538 & 0.5038\end{smallmatrix}\bigr]. Update: ν=−0.95\nu = -0.95, S=1.0038S = 1.0038, R=[0.4521, 0.5019]TR = [0.4521,\ 0.5019]\T, x^+=[4.520, 2.473]T\hat x^+ = [4.520,\ 2.473]\T, P+=[1.57370.22610.22610.2510]P^+ = \bigl[\begin{smallmatrix}1.5737 & 0.2261\\ 0.2261 & 0.2510\end{smallmatrix}\bigr].

Read the diagonal. The velocity variance goes 2→0.404→0.2512 \to 0.404 \to 0.251: the squish of Figure 8.7. The position variance goes 1→1.276→1.5741 \to 1.276 \to 1.574: growing, through two optimal updates, because the sensor never sees position. That is Theorem 8.2.2 in two numbers.

Carry the example one more step with y(k+3) = 2.3. What is the position variance P₁₁(k+3 | k+3)?

EKF for range-and-bearing localization

DerivationLinearizing the unicycle and the landmark sensor

Step 1 — Taylor about the estimate. Expand ff about x^(k∣k)\hat x(k \mid k) and hh about x^(k+1∣k)\hat x(k{+}1 \mid k) to first order; the Kalman equations then apply to the deviation. Nothing else changes: the EKF is eqs. (8.26)–(8.32) with f,hf, h for the means and their Jacobians for the covariances.

Step 2 — the motion Jacobian.

F=[10−u1sin⁡θr01u1cos⁡θr001].F = \begin{bmatrix} 1 & 0 & -u_1\sin\theta_r \\ 0 & 1 & u_1\cos\theta_r \\ 0 & 0 & 1\end{bmatrix}.

The third column is the whole story of heading error: a small δθ\delta\theta becomes a position error u1 δθu_1\,\delta\theta sideways to the motion, every step.

Step 3 — the sensor Jacobian. With Δ=x^−xℓj\Delta = \hat x - x_{\ell j} and ρ=∥Δ∥\rho = \norm{\Delta},

Hj=[Δx/ρΔy/ρ0−Δy/ρ2Δx/ρ2−1].H_j = \begin{bmatrix} \Delta x/\rho & \Delta y/\rho & 0 \\ -\Delta y/\rho^2 & \Delta x/\rho^2 & -1\end{bmatrix}.

Choset prints the bearing row before simplifying, as the raw derivative of atan2⁡\operatorname{atan2} through 1/(1+(Δy/Δx)2)1/(1 + (\Delta y/\Delta x)^2); the check file keeps both forms and confirms they agree to 10−1510^{-15}, and both agree with a central-difference Jacobian to 10−910^{-9}. Read the second row: bearing sensitivity to position falls off as 1/ρ1/\rho while its sensitivity to heading is exactly −1-1 at any range. A distant landmark is nearly a pure heading sensor.

Step 4 — stack the visible landmarks. p(k)p(k) sightings give a 2p×32p \times 3 matrix HH by stacking Ha(1),…,Ha(p)H_{a(1)}, \dots, H_{a(p)} and a block-diagonal WW. For block-diagonal WW this equals applying the sightings one at a time.

Step 5 — range-only is a deletion. Drop the bearing rows from hh and HH (§8.3.3). The observability consequences are the subject of the Probe below.

Wrap the bearing innovation. The 2005 text subtracts angles; near ±π\pm\pi that produces an innovation of 2π2\pi and a catastrophic update. The implementation here takes the residual through angleDiff and the heading through ⊞\bplus. The sister book's Chapter 7 does this properly — the UKF and the error-state EKF on SE(2)\SEtwo — and its Chapter 11 covers gating and multi-hypothesis tracking when association is genuinely ambiguous.

The algorithm

Choset numbers none of Chapter 8's algorithms; the names below follow his section titles.

Algorithmkalman_filter(x̂(k|k), P(k|k), u(k), y(k+1); F, G, H, V, W)CostO(n³) in general; O(n²p + p³) when p ≪ n
In
the current belief, the input applied, the output observed, the model
Out
x̂(k+1|k+1), P(k+1|k+1), and (ν, S, R) for association and diagnostics
  1. x^(k+1∣k)=Fx^(k∣k)+Gu(k)\hat x(k{+}1 \mid k) = F\hat x(k \mid k) + Gu(k) — eq. 8.26
  2. P(k+1∣k)=FP(k∣k)FT+VP(k{+}1 \mid k) = FP(k \mid k)F\T + V — eq. 8.27
  3. ν=y(k+1)−Hx^(k+1∣k)\nu = y(k{+}1) - H\hat x(k{+}1 \mid k) — eq. 8.30
  4. S=HP(k+1∣k)HT+WS = HP(k{+}1 \mid k)H\T + W — eq. 8.31
  5. R=P(k+1∣k)HTS−1R = P(k{+}1 \mid k)H\T S^{-1} — eq. 8.32
  6. x^(k+1∣k+1)=x^(k+1∣k)+Rν\hat x(k{+}1 \mid k{+}1) = \hat x(k{+}1 \mid k) + R\nu — eq. 8.28
  7. P(k+1∣k+1)=P(k+1∣k)−RHP(k+1∣k)P(k{+}1 \mid k{+}1) = P(k{+}1 \mid k) - RHP(k{+}1 \mid k) — eq. 8.29; in code, the Joseph form, which equals this at the optimal gain and stays positive-definite under round-off
  8. return x^(k+1∣k+1), P(k+1∣k+1), (ν,S,R)\hat x(k{+}1 \mid k{+}1),\ P(k{+}1 \mid k{+}1),\ (\nu, S, R)

Choset's reading of RR: if RR is "large" the sensor is more believable than the prediction and the update leans on it; if "small", the reading barely moves the estimate. Line 5 says exactly how large — the ratio of predicted uncertainty to total innovation uncertainty, in matrix form.

Algorithmobservability_rank(F, H)Costone rank computation on an np × n matrix
In
the LTI pair, or the Jacobian pair of a nonlinear system at the current estimate
Out
rank Q, and a basis of null(Q): the directions no output can correct
  1. Q←HQ \leftarrow H
  2. for k=1,…,n−1k = 1, \dots, n-1: append HFkHF^k to QQ
  3. return rank⁡Q\rank Q under a relative tolerance; observable iff it equals nn — Thm. 8.2.2
Algorithmekf_localize(x̂, P, u, {y_i}; f, h, ∂f, ∂h, V, W)Costas kalman_filter plus the Jacobians; association O(n_ℓ p³) per sighting
In
belief, input, the sightings this step, the nonlinear models and their Jacobians
Out
updated belief and the association map a(·)
  1. x^−=f(x^,u)\hat x^- = f(\hat x, u); F=∂f/∂x∣x^F = \partial f/\partial x|_{\hat x}; P−=FPFT+VP^- = FPF\T + V — eqs. 8.37–8.39
  2. for each sighting yiy_i and each landmark jj: νij=yi−hj(x^−)\nu_{ij} = y_i - h_j(\hat x^-), Sij=HjP−HjT+WiS_{ij} = H_jP^-H_j\T + W_i, χij2=νijTSij−1νij\chi^2_{ij} = \nu_{ij}\T S_{ij}^{-1}\nu_{ij} — §8.3.2
  3. a(i)=arg⁡min⁡jχij2a(i) = \arg\min_j \chi^2_{ij}; reject the sighting if the minimum exceeds a gate, found a new landmark if it exceeds a high threshold (§8.4.2)
  4. stack ha(i)h_{a(i)}, Ha(i)H_{a(i)} over accepted sightings; wrap bearing residuals
  5. one kalman_filter update with the stacked (H,W)(H, W) — eqs. 8.40–8.45
  6. return x^+,P+,a\hat x^+, P^+, a

Choset minimizes χij2\chi^2_{ij} alone. When the candidates' gates differ in size this is not quite maximum likelihood: the log⁡det⁡Sij\log\det S_{ij} term matters, and the sister's mlAssociate, which the range–bearing path here delegates to, keeps it. The range-only path uses Choset's rule verbatim because the sister's routine assumes a bearing row.

The ellipse that never shrinks

Now the question the Probe was built to answer. Rusty drives a square in room A of the Apartment while the EKF above localizes it against known landmarks. How many does it need?

With one range-only landmark the rank of the linearized pair is 22. The single range row and its products with FF all share the same (x,y)(x, y) direction — radial from the landmark — so the direction tangent to the constant-range circle never appears in QQ. The posterior ellipse stretches along that circle and stays stretched: after 240 steps its trace is 0.4820.482 against 0.0340.034 with two landmarks, an order of magnitude, and no number of further iterations closes the gap. Switch to two non-collinear landmarks and the rank is 33; put both landmarks and the robot on one line and it falls to 22 again (Exercise 4). Range and bearing from one landmark is observable while the robot moves — the third column of HFHF carries u1u_1 — and loses a rank the moment it stops: two numbers cannot pin three. The NEES tile reads near 33 for the observable cases, which is what a consistent 3-DOF filter should report (sister Ch. 6 on NEES and NIS).

Kalman SLAM

If the landmarks are unknown, put them in the state (§8.4; Smith, Self and Cheeseman). For an omnidirectional vehicle measuring the relative displacement to nℓn_\ell uniquely identified landmarks, the system is linear: x=[xr,yr,xℓ1,yℓ1,… ]Tx = [x_r, y_r, x_{\ell 1}, y_{\ell 1}, \dots]\T, F=IF = I, G=[I2;0]G = [I_2; 0], V=blkdiag⁡(Vr,0,… )V = \operatorname{blkdiag}(V_r, 0, \dots), and each HiH_i has −1-1 in the robot columns and +1+1 in landmark ii's. The sister's plain Kf runs it unchanged. Its observability matrix has rank 44 of 66 for two landmarks: the two null directions shift robot and map together. The filter learns the map's shape — in the check, the covariance of xℓ1−xrx_{\ell 1} - x_r falls monotonically from trace 0.0790.079 to 0.0200.020 — while the map's absolute covariance stalls at 0.0590.059 and goes no lower. No sighting can say where the world is.

For range and bearing on the unicycle (§8.4.2) the state is [xr,yr,θr,xℓ1,… ][x_r, y_r, \theta_r, x_{\ell 1}, \dots] and HiH_i is eq. (8.47): dense in the first three columns, two nonzero entries at landmark ii, zero elsewhere. Association is eq. (8.48) against every landmark in the state, and a minimum χ2\chi^2 above a high threshold founds a new one. The forget the map toggle in the Probe runs this: three landmarks founded from their first sightings, 588588 of the remaining 597597 sightings matched, the worst map error 0.120.12 m at 1.6σ1.6\sigma. Honest caveats: the covariance update is O(nℓ2)O(n_\ell^2) per step, and on long loops the linearization makes the filter inconsistent. The sister book's Chapter 14 shows the cost and the inconsistency, and its Chapter 15 shows what replaced it.

Implementation in Rust

The estimate crate is thin by design: every file is either an adapter over the sister book's vendored filters or a piece Choset has and the sister does not. No filter is re-implemented.

crates/estimate/src/lti.rs
use nalgebra::{DMatrix, SMatrix};

/// An LTI system in Choset's letters. Every field names the Thrun symbol the
/// sister book uses, because the two conventions collide in the worst place:
/// Choset's `R` is the Kalman *gain*; Thrun's `R` is the process *covariance*.
pub struct Lti<const N: usize, const U: usize, const M: usize> {
    pub f: SMatrix<f64, N, N>, // A_t
    pub g: SMatrix<f64, N, U>, // B_t
    pub h: SMatrix<f64, M, N>, // C_t, assumed full row rank
    pub v: SMatrix<f64, N, N>, // R_t  (process covariance)
    pub w: SMatrix<f64, M, M>, // Q_t  (measurement covariance)
}

impl<const N: usize, const U: usize, const M: usize> Lti<N, U, M> {
    /// App. J.5: F = I + T A, G = T B, H = C.
    pub fn discretize(a: SMatrix<f64, N, N>, b: SMatrix<f64, N, U>, c: SMatrix<f64, M, N>, t: f64,
                      v: SMatrix<f64, N, N>, w: SMatrix<f64, M, M>) -> Self {
        Self { f: SMatrix::identity() + t * a, g: t * b, h: c, v, w }
    }

    /// Thm. 8.2.2: Q = [H; HF; …; HF^{N−1}]. Dynamic because N·M is not a
    /// const expression — the one place this crate leaves the stack.
    pub fn observability_matrix(&self) -> DMatrix<f64> {
        let mut q = DMatrix::zeros(N * M, N);
        let mut hf = self.h;
        for k in 0..N {
            q.view_mut((k * M, 0), (M, N)).copy_from(&hf);
            hf = hf * self.f; // Cayley–Hamilton says stopping at N−1 loses nothing
        }
        q
    }

    /// rank Q == N under a *relative* SVD tolerance — a parameter, not a magic
    /// number: a 0/1 matrix needs none, a linearized Jacobian mixing metres
    /// and radians needs a generous one.
    pub fn is_observable(&self, tol: f64) -> bool {
        self.observability_matrix().rank(tol) == N
    }
}

The observer is the knob. Its step is three lines; error_eigs is the unit-disc inset, and place_gain is Theorem J.3.2 transposed — Ackermann's formula on the pair (F,HF)(F, HF), which is why it can only succeed when (F,H)(F, H) is observable.

crates/estimate/src/observer.rs
use nalgebra::{Complex, SMatrix, SVector};
use crate::lti::Lti;

/// Fixed-gain Luenberger observer, App. J.4 discretized by J.5:
///   x̂⁺ = F x̂ + G u + K (y − H(F x̂ + G u)).
pub struct Observer<const N: usize, const U: usize, const M: usize> {
    pub sys: Lti<N, U, M>,
    pub k: SMatrix<f64, N, M>,
    pub xhat: SVector<f64, N>,
}

impl<const N: usize, const U: usize, const M: usize> Observer<N, U, M> {
    pub fn step(&mut self, u: &SVector<f64, U>, y: &SVector<f64, M>) {
        let predicted = self.sys.f * self.xhat + self.sys.g * u;
        let nu = y - self.sys.h * predicted; // the innovation: what the sensor saw minus what it should have
        self.xhat = predicted + self.k * nu;
    }

    /// e(k+1) = (I − K H) F e(k): inside the unit disc ⇔ the error decays (Thm. J.5.1).
    pub fn error_matrix(&self) -> SMatrix<f64, N, N> {
        (SMatrix::identity() - self.k * self.sys.h) * self.sys.f
    }

    pub fn error_eigs(&self) -> Vec<Complex<f64>> {
        self.error_matrix().complex_eigenvalues().iter().copied().collect()
    }

    /// The error covariance a fixed gain *actually* produces (Joseph form) —
    /// valid for any K, not only the optimal one. The Kalman gain is the
    /// argmin of its trace; see `kf::kalman_step`, which never calls this.
    pub fn covariance_update(&self, p: &SMatrix<f64, N, N>) -> SMatrix<f64, N, N> {
        let ikh = SMatrix::<f64, N, N>::identity() - self.k * self.sys.h;
        ikh * p * ikh.transpose() + self.k * self.sys.w * self.k.transpose()
    }
}

The Kalman step itself is an adapter. filters::kalman::Kf is the sister book's filter, vendored unchanged; this function only renames its inputs into Choset's letters and hands back (ν,S,R)(\nu, S, R) because data association and the widgets need them.

crates/estimate/src/kf.rs
pub use filters::kalman::{Gaussian, Kf}; // sister Ch. 6 — not re-derived, not re-implemented
use nalgebra::{SMatrix, SVector};
use crate::lti::Lti;

/// One Choset step, eqs. 8.26–8.32, through the sister's `Kf`.
pub fn kalman_step<const N: usize, const U: usize, const M: usize>(
    sys: &Lti<N, U, M>, bel: &mut Gaussian<N>, u: &SVector<f64, U>, y: &SVector<f64, M>,
) -> (SVector<f64, M>, SMatrix<f64, M, M>, SMatrix<f64, N, M>) {
    let mut kf = Kf::from(bel.clone());
    kf.predict(&sys.f, &sys.v, Some((&sys.g, u)));            // Thrun: A, R, (B, u)
    let info = kf.update(y, &sys.h, &sys.w);                   // Thrun: C, Q
    *bel = kf.belief();
    (info.innovation, info.s, info.k)                          // Choset: ν, S, R
}

/// §8.2.6 constants: T = 0.5, m = 1, V, W = 0.5, x̂ = [2, 4], P = diag(1, 2).
pub fn dead_reckoning() -> (Lti<2, 1, 1>, Gaussian<2>) {
    let (t, m) = (0.5, 1.0);
    let sys = Lti {
        f: SMatrix::<f64, 2, 2>::new(1.0, t, 0.0, 1.0),
        g: SMatrix::<f64, 2, 1>::new(0.0, t / m),
        h: SMatrix::<f64, 1, 2>::new(0.0, 1.0),                 // velocity sensor
        v: SMatrix::<f64, 2, 2>::new(0.2, 0.05, 0.05, 0.1),
        w: SMatrix::<f64, 1, 1>::new(0.5),
    };
    (sys, Gaussian { x: SVector::<f64, 2>::new(2.0, 4.0), p: SMatrix::from_diagonal(&[1.0, 2.0].into()) })
}
crates/estimate/examples/dead_reckoning.rs
use estimate::{kf::{dead_reckoning, kalman_step}, lti::Lti};
use nalgebra::{SMatrix, SVector};

fn main() {
    let (sys, mut bel) = dead_reckoning();
    for (k, y) in [2.7, 2.0].into_iter().enumerate() {
        let (nu, s, r) = kalman_step(&sys, &mut bel, &SVector::zeros(), &SVector::<f64, 1>::new(y));
        println!("step {}: ν = {:+.3}  S = {:.4}  R = [{:.4}, {:.4}]", k + 1, nu[0], s[(0, 0)], r[(0, 0)], r[(1, 0)]);
        println!("        x̂⁺ = [{:.3}, {:.3}]   P⁺ = [[{:.4}, {:.4}], [{:.4}, {:.4}]]",
            bel.x[0], bel.x[1], bel.p[(0, 0)], bel.p[(0, 1)], bel.p[(1, 0)], bel.p[(1, 1)]);
    }
    let pos = Lti { h: SMatrix::<f64, 1, 2>::new(1.0, 0.0), ..sys };
    println!("rank Q = {} (velocity sensor), {} (position sensor)",
        sys.observability_matrix().rank(1e-9), pos.observability_matrix().rank(1e-9));
}
cargo run --example dead_reckoning -p estimate
step 1: ν = -1.300  S = 2.6000  R = [0.4038, 0.8077]
        x̂⁺ = [3.475, 2.950]   P⁺ = [[1.2760, 0.2019], [0.2019, 0.4038]]
step 2: ν = -0.950  S = 1.0038  R = [0.4521, 0.5019]
        x̂⁺ = [4.520, 2.473]   P⁺ = [[1.5737, 0.2261], [0.2261, 0.2510]]
rank Q = 1 (velocity sensor), 2 (position sensor)
crates/estimate/src/kf.rs (tests)
#[test]
fn reproduces_choset_8_2_6_two_steps() {
    let (sys, mut bel) = dead_reckoning();
    kalman_step(&sys, &mut bel, &SVector::zeros(), &SVector::<f64, 1>::new(2.7));
    assert_relative_eq!(bel.x, SVector::<f64, 2>::new(3.475, 2.95), epsilon = 1e-3);
    kalman_step(&sys, &mut bel, &SVector::zeros(), &SVector::<f64, 1>::new(2.0));
    assert_relative_eq!(bel.p[(0, 0)], 1.5737, epsilon = 1e-3);
    assert_relative_eq!(bel.p[(1, 1)], 0.2510, epsilon = 1e-3);
    assert_eq!(sys.observability_matrix().rank(1e-9), 1);
}

/// Derivation 4 made executable: an `Observer` whose gain is set to R(k) each
/// step reproduces the Kalman mean bit-for-bit.
#[test]
fn kf_is_observer_with_scheduled_gain() {
    let (sys, mut bel) = dead_reckoning();
    let mut obs = Observer { sys: sys.clone(), k: SMatrix::zeros(), xhat: bel.x };
    for y in [2.7, 2.0, 2.4, 1.9] {
        let (_, _, r) = kalman_step(&sys, &mut bel, &SVector::zeros(), &SVector::<f64, 1>::new(y));
        obs.k = r;
        obs.step(&SVector::zeros(), &SVector::<f64, 1>::new(y));
        assert_eq!(obs.xhat, bel.x);
    }
}

The landmark models carry Choset's conventions — the bearing is measured from the landmark, a quirk the translation into the sister's Feature type absorbs with one rotation by π\pi.

crates/estimate/src/landmarks.rs
use filters::nonlinear::MeasurementModel; // sister Ch. 7
use geom::SE2;
use nalgebra::{SMatrix, SVector};
use sim::Landmark;

/// h_j = [ρ, atan2(y_r − y_ℓ, x_r − x_ℓ) − θ_r]; rows of ∂h/∂x: [Δx/ρ, Δy/ρ, 0], [−Δy/ρ², Δx/ρ², −1].
pub struct RangeBearing<'m> { pub map: &'m [Landmark] }
/// §8.3.3: the same model with the bearing row removed.
pub struct RangeOnly<'m> { pub map: &'m [Landmark] }

impl MeasurementModel<SE2, 3, 2> for RangeBearing<'_> {
    fn h(&self, x: &SE2, j: usize) -> SVector<f64, 2> {
        let (dx, dy) = (x.x() - self.map[j].x, x.y() - self.map[j].y);
        SVector::new((dx * dx + dy * dy).sqrt(), geom::wrap(dy.atan2(dx) - x.theta()))
    }
    fn jacobian(&self, x: &SE2, j: usize) -> SMatrix<f64, 2, 3> {
        let (dx, dy) = (x.x() - self.map[j].x, x.y() - self.map[j].y);
        let q = (dx * dx + dy * dy).max(1e-12);
        let rho = q.sqrt();
        SMatrix::new(dx / rho, dy / rho, 0.0, -dy / q, dx / q, -1.0)
    }
    /// The innovation wraps: the one place where z − h(x) is wrong.
    fn residual(&self, z: &SVector<f64, 2>, zhat: &SVector<f64, 2>) -> SVector<f64, 2> {
        SVector::new(z[0] - zhat[0], geom::wrap(z[1] - zhat[1]))
    }
}

/// §8.3.2: a(i) = argmin_j ν_ijᵀ S_ij⁻¹ ν_ij. `None` above `chi2_new` ⇒ found a new landmark (§8.4.2).
pub fn associate_ml(y_i: &SVector<f64, 2>, bel: &Gaussian<3>, map: &[Landmark],
                    w: &SMatrix<f64, 2, 2>, chi2_new: f64) -> Option<usize> {
    let model = RangeBearing { map };
    let pose = SE2::from_vector(&bel.x);
    let best = (0..map.len()).map(|j| {
        let h = model.jacobian(&pose, j);
        let s = h * bel.p * h.transpose() + w;
        let nu = model.residual(y_i, &model.h(&pose, j));
        (j, (nu.transpose() * s.try_inverse().expect("S is PD") * nu)[(0, 0)])
    }).min_by(|a, b| a.1.total_cmp(&b.1))?;
    (best.1 <= chi2_new).then_some(best.0)
}

The widgets on this page run the TypeScript port of these files, web/lib/estimate/, over the vendored web/lib/filters/kf.ts and ekf.ts. Thirteen checks in __checks_ch15__.ts pin every number printed above: the two §8.2.6 steps to 10−310^{-3}, both ranks, the placed eigenvalues {0.5,0.2}\{0.5, 0.2\} and the error ratio that converges to 0.50.5, the brute-force gain 0.77270.7727, the Riccati limit, the Jacobian agreement, the linearized ranks 2/3/3/2/22/3/3/2/2, the one-versus-two-landmark traces, both SLAM instances, and Problem 9.

Putting it together

The integration lab is the two widgets above, run in sequence on the Apartment, and it ends with the object Chapter 16 inherits.

Dead reckoning. Rusty's encoders through odometryDelta give Choset's input u=[Δs,Δθ]u = [\Delta s, \Delta\theta] each tick. ChosetEkf::predict alone is Figure 8.1: the orange track in the hook bends off the gray one and the ellipse grows without bound. Its claimed σ\sigma tracks the actual error only as long as the noise model is honest — the wheel-radius bias is not in the model, and when you raise it the error outruns the ellipse. That gap is what the sister book's NEES instrument measures, and the reason Chapter 16 will care about beliefs that are not Gaussian.

One landmark. In the Probe with one range-only landmark the rank reads 22 and the ellipse's major axis runs along the constant-range circle. After 240 steps the pose trace is 0.4820.482. The filter is doing nothing wrong; "the problem lies with the system." A planner handed this belief should read the long axis and plan a motion that changes the geometry — toward a second landmark, or around the first — because no amount of sitting still will shrink it.

Two landmarks. Rank 33, trace 0.0340.034, mean NEES 3.83.8 against the expected 33. This is the belief a planner wants: a Gaussian bel⁡(xt)\bel(x_t) with a covariance it can read, in the same SE(2)\SEtwo coordinates its configuration lives in.

Forget the map. Kalman SLAM founds the three landmarks from their first sightings and matches 588588 of 597597 later ones, with the worst landmark 0.120.12 m off at 1.6σ1.6\sigma. The covariance is now 9×99 \times 9 and grows by two rows per landmark; the correlations between landmarks are what let a re-sighting of one improve all the others. The state is also what makes the filter's eventual inconsistency possible, which is the sister book's story, not this one.

What a planner takes from here is small and exact: a mean in SE(2)\SEtwo, a 3×33 \times 3 covariance, an innovation covariance SS for each candidate sighting, and the rank test to tell whether the next action can even reduce the uncertainty. Chapter 16 asks what happens when the belief is not Gaussian, and how to plan on it.

Exercises

  1. Foundation exerciseDifficulty 2 of 3The covariance update (Choset's problem 7)

    Starting from P+=E[(x−x^+)(x−x^+)T]P^+ = \E[(x - \hat x^+)(x - \hat x^+)\T] and x^+=x^−+Rν\hat x^+ = \hat x^- + R\nu, derive P+=P−−RHP−P^+ = P^- - RHP^-. Follow Choset's three steps: expand into P−−2 E[Rν(x−x^−)T]+E[RννTRT]P^- - 2\,\E[R\nu(x - \hat x^-)\T] + \E[R\nu\nu\T R\T]; show the middle term is 2RHP−2RHP^-; show E[ννT]=HP−HT\E[\nu\nu\T] = HP^-H\T when the output is noiseless and mark that step — it is where §8.2.3's assumption enters, and where +W+W appears in the §8.2.4 version. Then check that with R=P−HT(HP−HT+W)−1R = P^-H\T(HP^-H\T + W)^{-1} the Joseph form (I−RH)P−(I−RH)T+RWRT(I - RH)P^-(I - RH)\T + RWR\T collapses to the same expression.

  2. Foundation exerciseDifficulty 2 of 3Product of Gaussians, and two gains that are one

    Prove Theorem 8.2.1 via Choset's problem 8: write the product of the exponents, collect the terms quadratic in zz to get C3−1=C1−1+C2−1C_3^{-1} = C_1^{-1} + C_2^{-1}, the terms linear in zz to get C3−1z3=C1−1z1+C2−1z2C_3^{-1}z_3 = C_1^{-1}z_1 + C_2^{-1}z_2, and use the matrix identity (C1−1+C2−1)−1=C1−C1(C1+C2)−1C1(C_1^{-1} + C_2^{-1})^{-1} = C_1 - C_1(C_1 + C_2)^{-1}C_1 to reach Q=C1(C1+C2)−1\mathcal{Q} = C_1(C_1 + C_2)^{-1}. Then show the sister book's gain K=ΣˉCT(CΣˉCT+Q)−1K = \bar\Sigma C\T(C\bar\Sigma C\T + Q)^{-1} and Choset's RR coincide under the notation table's map — and say in one sentence why the sister's QQ is Choset's WW, not his QQ.

  3. Conceptual exerciseDifficulty 2 of 3Predict the schedule's limit
    Predict first

    In the Observer Bench with the default V and W and the velocity sensor, tick 'let the filter choose' and wait. Where do the two eigenvalues of (I − R(k)H)F settle?

    With the velocity sensor and Choset's V, W, what is the steady-state velocity gain R₂(∞)?

  4. Conceptual exerciseDifficulty 2 of 3The collinear trap
    Predict first

    In the Observability Probe, switch to range-only, show two landmarks, and drag them so that both landmarks and the robot's path lie on one line. What does the rank panel read while the robot is on that line?

  5. Practical exerciseDifficulty 2 of 3The steady-state gain

    Implement Lti::steady_state_gain(&self, p0, max_iters, tol) -> (SMatrix<f64, N, M>, bool) by iterating the Riccati recursion in Joseph form until P(k+1∣k)P(k{+}1 \mid k) stops moving, returning the gain and whether it converged. Test that an Observer with that gain and the full Kalman filter agree in their means to 10−610^{-6} after 50 steps on the §8.2.6 system with the position sensor, and that with the velocity sensor the recursion reports non-convergence while the gain itself still settles. The TypeScript riccatiSteadyState and gainSchedule in web/lib/estimate/kf.ts are the port.

  6. Practical exerciseDifficulty 3 of 3Choset's problem 9 as a test (stretch)

    Implement the unicycle with a position-only sensor y=x1+wy = x_1 + w, Var⁡w=0.5\Var w = 0.5, no process noise, and run one EKF step from x^(1∣1)=[1,0.5,π/4]T\hat x(1 \mid 1) = [1, 0.5, \pi/4]\T with u=[3,π]u = [3, \pi], T=0.25T = 0.25, P(1∣1)=IP(1 \mid 1) = I, y(2)=1.7y(2) = 1.7. Print x^(2∣2)\hat x(2 \mid 2) and P(2∣2)P(2 \mid 2), then run the same step through the sister's UKF and report the gap.

    What is x̂₁(2 | 2), the corrected x-position?

References

  1. Choset, H., Lynch, K. M., Hutchinson, S., Kantor, G., Burgard, W., Kavraki, L. E., and Thrun, S. (2005) Principles of Robot Motion: Theory, Algorithms, and Implementations. MIT Press.link to Principles of Robot Motion: Theory, Algorithms, and Implementations (opens in a new tab)

    Chapter 8 is this chapter's source: the observer-first derivation (§8.2.2–8.2.4), the dead-reckoning example (§8.2.6), Theorem 8.2.2, the EKF Jacobians (§8.3), and Kalman SLAM (§8.4). Appendix J supplies the Luenberger observer and the rank tests.

  2. 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. Kalman's derivation is by orthogonal projection in the Hilbert space of random variables — the same geometry Choset draws in state space.

  3. Luenberger, D. G. (1971) An Introduction to Observers. IEEE Transactions on Automatic Control 16(6), 596–602.doi:10.1109/TAC.1971.1099826 (opens in a new tab)

    The observer as a copy of the plant with a correcting term, and the error dynamics (A − KC)e that this chapter's Bench puts on the unit disc.

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

    The Bayes-filter derivation of the same equations, and the convention (K for the gain, R and Q for the covariances) the vendored code speaks. Chapters 3, 7 and 10 of this book are the sister volume's Chapters 6, 11 and 14.

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

    NEES, NIS and the χ² consistency tests the Probe's tiles report; §5.4 is the standard reference for reading them.

  6. Huang, G. P., Mourikis, A. I., and Roumeliotis, S. I. (2010) Observability-based Rules for Designing Consistent EKF SLAM Estimators. International Journal of Robotics Research 29(5), 502–528.doi:10.1177/0278364909353640 (opens in a new tab)

    Why Kalman SLAM goes inconsistent: the linearized system acquires an observable direction the true system lacks, and the filter gains information it should not. The modern coda to §8.4.

  7. Shannon Dynamics (2026) Probabilistic Robotics via Rust — Chapters 5–7, 11, 14. Sister volume, web edition.link to Probabilistic Robotics via Rust — Chapters 5–7, 11, 14 (opens in a new tab)

    Completing the square, the information form and RTS smoothing (Ch. 6); UKF and the error-state EKF on SE(2) (Ch. 7); gating and multi-hypothesis tracking (Ch. 11); EKF-SLAM's cost and inconsistency (Ch. 14). The filters this chapter wraps are vendored from there.