The filter tells you where you are. The smoother tells you where you were — and once the whole flight is on disk, refusing to look at the future is throwing information away.
A flight-test engineer replays a drone log. At t = 41 s the GPS dropped out for nine seconds and the live filter's uncertainty ballooned — but the log already knows the drone reappeared dead on course. Why is the replayed estimate still pretending it can't see ahead?
Picture the review room. The drone flew this morning. A Kalman filter ran onboard, and every measurement plus every filter output now sits in a telemetry file. The flight is over. Nothing about it is uncertain to the universe — only to us.
Onboard, the filter had to be causal: the autopilot needed the best estimate NOW, using only measurements that had already arrived. That constraint is physical, not statistical — control cannot wait for data that doesn't exist yet.
But post-flight, the constraint evaporates. For the sample at t = 41 s, the "future" — nine more minutes of measurements — is literally on disk, a few kilobytes to the right in the file. The engineer's first instinct: rerun the filter over the log. Result: bit-identical estimates, same balloon in the gap, same kink at reacquisition. Running a causal algorithm twice doesn't make it clairvoyant.
Name the two objects precisely. The filter computes p(xk | z1:k) — the state at time k given measurements up to k. What the review actually wants is p(xk | z1:N) — the same state, the same model, one changed subscript on the conditioning set. That one subscript is this whole lesson.
Where hindsight estimation earns salaries: flight-test reconstruction, SLAM map building, sensor calibration, EM-based learning of Q and R, and generating ground-truth-quality labels for training perception models. A large fraction of modern robotics runs on smoothed trajectories, not filtered ones.
One flight, one measurement record, one dropout (shaded). Toggle between the causal filter's view and the post-flight RTS view. The measurements and truth never change — only the estimate's tube does.
Ask "where was the drone at t = 41 s?" and there are three honest answers, depending on how much of the log you allow yourself to read.
Line up the three objects the Kalman machinery ever produces about xk. The predicted density p(xk | z1:k−1) has not yet seen the measurement at k. The filtered density p(xk | z1:k) has seen everything up to and including k. The smoothed density p(xk | z1:N) has seen the entire record.
The conditioning sets are nested — z1:k−1 ⊂ z1:k ⊂ z1:N — so the information can only grow along the chain. In the linear-Gaussian world this becomes a hard matrix inequality (throughout this lesson, A ≤ B between covariances means B − A is positive semidefinite):
More data never inflates an exact posterior.
Fix the notation for the rest of the lesson now, because sloppy subscripts are where smoothing derivations die: x̂a|b means E[xa | z1:b], and Pa|b is its covariance. Filtering is a|a. Prediction is a|a−1. Smoothing is a|N.
Two boundary facts to keep as anchors. At k = N the filtered and smoothed posteriors are the same object — there is no future left, so x̂N|N seeds the backward sweep and is never modified. And for k far from N, the smoothed variance settles to a floor strictly below the filter's steady state — Chapter 6 computes exactly how far below.
One noisy trajectory, three uncertainty ribbons: predicted (widest), filtered, smoothed (narrowest, full RTS). Sweep k and watch the three bells in the inset — as k reaches the end, filtered and smoothed merge into one.
You already own the forward pass — the Kalman filter lesson built it from zero. What that lesson never told you: which of its intermediate numbers you must refuse to throw away.
Compressed recap, five lines, no derivation (that lesson's job, not this one's): predict x̂k|k−1 = F x̂k−1|k−1 and Pk|k−1 = F Pk−1|k−1 FT + Q; then update with gain Kk = Pk|k−1 HT (H Pk|k−1 HT + R)−1 to get x̂k|k and Pk|k. If any of that felt new, detour to the Kalman lesson first — this chapter is a contract, not a course.
The contract: the backward sweep will need FOUR sequences, for k = 1..N:
Most people's mental model of a filter keeps only the first pair. The smoother's gain and its innovation-like correction term are built from the second pair. The flight-recorder framing: a filter that runs and discards is a control component; a filter that runs and RETAINS is the front half of a smoother. Same arithmetic, different afterlife for the numbers.
One subtle escape hatch, stated honestly: if you logged the raw measurements plus the exact filter configuration (F, H, Q, R, the initialization, and every runtime decision like gating or adaptation), you can regenerate the predicted statistics by re-running. But any adaptive or gated filter makes bit-exact offline reconstruction fragile — production telemetry teams log the predicted stats and sleep at night.
Cost accounting. Storage is O(N·n²) for the covariances: for a 15-state INS logged at 200 Hz for an hour, that's 15×15×8 bytes × 720,000 steps ≈ 1.3 GB — which is exactly why real systems log at keyframe rate or store Cholesky factors. Compute for the coming backward sweep will be one n×n solve per step: about the price of a second filter pass.
| What the sweep consumes | Appears in the recursion as | Commonly discarded? |
|---|---|---|
| x̂k|k, Pk|k | The base the correction is added to; Pk|k builds Ck | No — everyone keeps these |
| x̂k+1|k | Subtracted inside the correction (x̂k+1|N − x̂k+1|k) | Yes — the classic mistake |
| Pk+1|k | Inverted inside Ck = Pk|k FT P−1k+1|k | Yes — the classic mistake |
| z1:N, innovations, gains Kk | Nowhere — already spent during the forward pass | Irrelevant to the sweep |
Tomorrow's measurement cannot CAUSE anything about today's state. But it can testify about it — the same way a footprint testifies about the foot that made it, hours later.
Confront the causality objection head-on, because every student raises it: "the future can't affect the past." Correct — and irrelevant. Inference runs on correlation, not causation. Write the future measurement in terms of the past state:
zk+1 is a random variable statistically coupled to xk through the dynamics. Observing it constrains xk, full stop.
Make it mechanical with the lesson's running system — a 1D random walk with F = 1, Q = 1, R = 1. Substitute the dynamics into the future measurement: z2 = x1 + w1 + v2. Read that as a sentence: z2 IS a measurement of x1, corrupted by noise of variance Q + R = 2. The future measurement is just a regular measurement wearing a slightly thicker noise coat — thickened by one trip through the dynamics.
Run ordinary Bayes on that pseudo-measurement. The prior on x1 after z1 is N(2.0, 1.0). The pseudo-measurement has value 3.0 and variance 2, so the gain is 1/(1+2) = 1/3 and the posterior is N(2.3333, 0.6667). The past just moved — and got more certain — using only arithmetic the Kalman lesson already taught.
Left: the frozen gray bell is p(x1|z1) = N(2, 1); the purple bell is p(x1|z1,z2), live. Right: drag the future measurement z2 up and down and watch it pull the past. Then slide Q: small Q = hard pull, large Q = the future goes mute.
Route A — Bayes on the future, directly. Write z2 in terms of x1: z2 = x2 + v2 = (x1 + w1) + v2, a pseudo-measurement of x1 with combined noise variance Q + R = 1 + 1 = 2. Ordinary scalar Bayes: gain K = P/(P + Q + R) = 1/(1+2) = 1/3; innovation z2 − x̂1 = 3.0 − 2.0 = 1.0. Result:
Route B — filter forward, then one backward step. Predict: x̂2|1 = 2.0, P2|1 = 1 + 1 = 2.0. Update with z2 = 3.0: K2 = 2/3, x̂2|2 = 2.0 + (2/3)(1.0) = 8/3 ≈ 2.6667, P2|2 = 2/3 ≈ 0.6667. Now one backward step with gain C1 = P1|1·F / P2|1 = 1/2:
The two routes agree to machine precision. The backward step (which Chapter 4 derives) is Bayes on the future, algebraically compressed through the filtered statistics. One future measurement pulled the past estimate from 2.0000 to 2.3333 and cut its variance from 1.0000 to 0.6667.
A scaling worry, addressed before it festers: at k = 1 with N = 1000, the pseudo-measurement route would need the joint distribution of 999 correlated future measurements — hopeless directly. The Markov structure is what will let the RTS recursion compress all of that testimony into one already-smoothed neighbor, xk+1. That compression is the next chapter.
Chapter 3 handled ONE future measurement. A real log has thousands. The rescue is the Markov property: everything the entire future says about xk is already compressed inside one object — the smoothed belief about xk+1.
State the strategy before the algebra. We will (1) write the JOINT Gaussian of the neighbors (xk, xk+1) given only past data z1:k, (2) condition it on xk+1, (3) invoke Markov to show that conditioning set upgrades from z1:k to z1:N for free, and (4) average over the smoothed belief of xk+1. Four moves, each one already in your toolkit.
Move 1 — the joint. Given z1:k: xk has mean x̂k|k and covariance Pk|k; its neighbor xk+1 = F xk + w has mean F x̂k|k = x̂k+1|k and covariance Pk+1|k. The cross-covariance takes one line: Cov(xk, F xk + w) = E[(xk − x̂)(F(xk − x̂) + w)T] = Pk|k FT — the w term dies because process noise is independent of the current state.
Move 2 — Gaussian conditioning (the same formula the Kalman update used, aimed at a different variable):
with conditional covariance Pk|k − Ck Pk+1|k CkT. Point at the structural rhyme: Ck is to the dynamics what Kk was to the measurement.
Move 3 — the Markov upgrade. This is the load-bearing step; slow down here. Given xk+1, the future measurements zk+1:N are conditionally independent of xk — every path from xk to the future physically passes through xk+1. Therefore p(xk | xk+1, z1:k) = p(xk | xk+1, z1:N). The formula from Move 2, derived with past data only, is secretly valid with ALL data. This is the entire reason a single backward sweep suffices.
Move 4 — the tower property. Average Move 2's formula over p(xk+1 | z1:N) = N(x̂k+1|N, Pk+1|N). The mean gives the RTS mean recursion; the law of total covariance gives the covariance recursion:
Since Pk+1|N ≤ Pk+1|k, the added term is negative semidefinite — smoothing can only shrink covariance, at every single step.
And the exam-defining observation: R appears nowhere; z appears nowhere. Every measurement was already absorbed into the forward statistics. The backward sweep adds no data — it REDISTRIBUTES information the forward pass banked, sending it backward along the dynamics.
Twelve steps, already forward-filtered (warm dots with error bars; faint diamonds are the predicted means). Each "Step back" executes one recursion: the neighbor's hindsight surplus is scaled by Ck and lands on node k, which slides to its smoothed position. Then drag Q: small Q lets one late correction ripple nearly undamped across all 12 steps; large Q kills it within one or two.
Steady state. The filtered variance obeys P ← (P + Q)R/(P + Q + R) = (P + 1)/(P + 2). Its fixed point solves P² + P − 1 = 0, giving P = (√5 − 1)/2 ≈ 0.6180 — the golden ratio conjugate, a happy accident of the all-ones system.
| Regime | Q | C = Pfilt/(Pfilt + Q) | Reading |
|---|---|---|---|
| Tight dynamics | 0.1 | 0.6180/0.7180 = 0.8607 | Hindsight flows backward at 86% strength per step |
| Matched noise | 1 | 0.6180/1.6180 = 0.3820 | Each backward step retains 38% of the neighbor's correction (= 1/φ², another golden cameo) |
| Sloppy dynamics | 10 | 0.6180/10.6180 = 0.0582 | The future is almost mute about the past — corrections die within a step |
| Limit Q → 0 | 0 | → 1 (= 1/F) | Deterministic dynamics: undiminished hindsight |
| Limit Q → ∞ | ∞ | → 0 | Total forgetting: no smoothing benefit at all |
Time to earn the recursion. Four altitude readings — 2, 3, 1, 2 — and we will push every digit through both sweeps with nothing but fractions. The answers come out in sevenths, and the variances hide Fibonacci.
Set the table: random-walk altitude, F = Q = R = 1, measurements z = (2.0, 3.0, 1.0, 2.0). Initialize at the first measurement in the diffuse-prior limit: x̂1|1 = z1 = 2.0, P1|1 = R = 1.0. This is honest, not a fudge: a prior with P0 → ∞ makes the first update collapse onto the measurement — the standard way to start a track with no prior knowledge.
Forward pass, row by row, every fraction shown.
k = 2: predict x̂2|1 = 2.0, P2|1 = 1 + 1 = 2. Gain K2 = 2/(2+1) = 2/3. Innovation 3 − 2 = 1. Filtered: x̂2|2 = 2 + (2/3)(1) = 8/3 = 2.6667, P2|2 = (1/3)(2) = 2/3 = 0.6667.
k = 3: predict x̂3|2 = 8/3, P3|2 = 2/3 + 1 = 5/3. Gain K3 = (5/3)/(8/3) = 5/8 = 0.6250. Innovation 1 − 8/3 = −5/3. Filtered: x̂3|3 = 8/3 − (5/8)(5/3) = 13/8 = 1.6250, P3|3 = (3/8)(5/3) = 5/8 = 0.6250.
k = 4: predict x̂4|3 = 13/8, P4|3 = 5/8 + 1 = 13/8. Gain K4 = (13/8)/(21/8) = 13/21 = 0.6190. Innovation 2 − 13/8 = 3/8. Filtered: x̂4|4 = 13/8 + (13/21)(3/8) = 13/7 = 1.8571, P4|4 = (8/21)(13/8) = 13/21 = 0.6190.
Backward sweep, seeded untouched at (x̂4|4, P4|4) = (13/7, 13/21). Every step is: take the neighbor's hindsight surplus, scale by C, add.
k = 3: C3 = P3|3/P4|3 = (5/8)/(13/8) = 5/13 = 0.3846. Mean: x̂3|4 = 13/8 + (5/13)(13/7 − 13/8) = 12/7 = 1.7143. Variance: P3|4 = 5/8 + (5/13)²(13/21 − 13/8) = 10/21 = 0.4762.
k = 2: C2 = (2/3)/(5/3) = 2/5 = 0.4000. Mean: x̂2|4 = 8/3 + (2/5)(12/7 − 8/3) = 16/7 = 2.2857. Variance: P2|4 = 2/3 + (4/25)(10/21 − 5/3) = 10/21 = 0.4762.
k = 1: C1 = 1/2 = 0.5000. Mean: x̂1|4 = 2 + (1/2)(16/7 − 2) = 15/7 = 2.1429. Variance: P1|4 = 1 + (1/4)(10/21 − 2) = 13/21 = 0.6190.
Now read the answer like an examiner. Means: 15/7, 16/7, 12/7, 13/7 = 2.1429, 2.2857, 1.7143, 1.8571 — the smoothed track is visibly calmer than the measurements (2, 3, 1, 2) and than the filtered track. Variances: 13/21, 10/21, 10/21, 13/21 = 0.6190, 0.4762, 0.4762, 0.6190 — a perfect palindrome, sagging in the middle.
The four steps as columns; red ticks are the measurements (2, 3, 1, 2). Step through the forward pass (gains labeled), then the backward pass (C labeled) — and watch the final column refuse to move. The table below fills in sync: it is the worked example above, replayed.
| k | zk | Kk | x̂k|k | Pk|k | Ck | x̂k|4 | Pk|4 |
|---|---|---|---|---|---|---|---|
| 1 | 2 | — | |||||
| 2 | 3 | ||||||
| 3 | 1 | ||||||
| 4 | 2 | seed |
The general matrix form, honoring Chapter 2's storage contract. The self-test at the bottom is exactly the hand-worked case — if your implementation doesn't print sevenths, you have the classic off-by-one between Pk|k and Pk+1|k.
python import numpy as np def kalman_forward(x0, P0, F, Q, H, R, zs): """Forward pass. Returns filtered AND predicted (x, P) - the smoother's fuel.""" n = x0.shape[0] xf, Pf, xp, Pp = [], [], [], [] x, P = x0.copy(), P0.copy() for z in zs: x_pred = F @ x # predict P_pred = F @ P @ F.T + Q S = H @ P_pred @ H.T + R # update K = P_pred @ H.T @ np.linalg.inv(S) x = x_pred + K @ (z - H @ x_pred) P = (np.eye(n) - K @ H) @ P_pred xp.append(x_pred); Pp.append(P_pred) # the storage contract: xf.append(x); Pf.append(P) # keep BOTH pairs, every step return xf, Pf, xp, Pp def rts_smooth(xf, Pf, xp, Pp, F): """Backward sweep. Touches no measurement - only forward statistics.""" N = len(xf) xs, Ps = [None] * N, [None] * N xs[-1], Ps[-1] = xf[-1], Pf[-1] # seed: final point untouched for k in range(N - 2, -1, -1): # solve, never invert: C = Pf[k] F^T inv(Pp[k+1]) C = np.linalg.solve(Pp[k + 1].T, (Pf[k] @ F.T).T).T xs[k] = xf[k] + C @ (xs[k + 1] - xp[k + 1]) Ps[k] = Pf[k] + C @ (Ps[k + 1] - Pp[k + 1]) @ C.T return xs, Ps if __name__ == "__main__": # Chapter-5 scalar case: F = Q = R = 1, z = (2, 3, 1, 2), diffuse init at z1. F = Q = R = H = np.array([[1.0]]) zs = [np.array([v]) for v in (3.0, 1.0, 2.0)] # z2, z3, z4 x0, P0 = np.array([2.0]), np.array([[1.0]]) # x1|1 = 2, P1|1 = 1 xf, Pf, xp, Pp = kalman_forward(x0, P0, F, Q, H, R, zs) xf = [x0] + xf; Pf = [P0] + Pf # prepend the init step xp = [None] + xp; Pp = [None] + Pp xs, Ps = rts_smooth(xf, Pf, xp, Pp, F) print([float(x[0]) for x in xs]) # [2.1429, 2.2857, 1.7143, 1.8571] = 15/7 16/7 12/7 13/7 print([float(P[0, 0]) for P in Ps]) # [0.6190, 0.4762, 0.4762, 0.6190]
Close with the habit this chapter installs: whenever you implement a smoother, run a 4-step scalar case by hand first and check the anchors — endpoint untouched, variances never above filtered, interior lowest. Those three checks catch nearly every indexing bug in real RTS code.
Post-flight review can wait for the whole log. A broadcast tracker can afford 200 milliseconds. A crash investigator cares about one single instant. Three customers, three smoothing problems, one recursion.
Name the three problems by their customers. Fixed-interval: the record is complete; estimate every xk given z1:N — post-flight reconstruction, SLAM map building. This is what Chapters 4–5 built. Fixed-lag: you're live but may answer L steps late, emitting x̂k−L|k forever — de-noising a broadcast feed, offline-quality perception at small latency. Fixed-point: one special instant k* refines forever as data streams — where exactly was the rocket at engine cutoff; where was the tumor at beam time.
Fixed-lag is just a truncated RTS: from the live head at time k, sweep backward L steps. So its accuracy question reduces to: how fast does the backward recursion converge? Answer, straight from the covariance recursion: each extra lag step multiplies the remaining gap (Plag − P∞) by C² — geometric decay at the SQUARE of the smoother gain, because covariance corrections pass through C twice.
Make it concrete at the golden steady state (Q = R = 1): C = 0.3820, C² = 0.1459. The ladder:
| Lag L | Pk|k+L | % of full smoothing benefit |
|---|---|---|
| 0 (pure filter) | 0.6180 | 0% |
| 1 | 0.4721 | 85.4% |
| 2 | 0.4508 | 97.9% |
| 3 | 0.4477 | 99.7% |
| 4 | 0.4473 | 99.96% |
| ∞ (fixed-interval) | 0.4472 = 1/√5 | 100% |
One step of lag captures 85.4% of everything smoothing can ever give; two steps capture 97.9%; three, 99.7%.
Fixed-point smoothing, run on the Chapter-5 data for the first state: x̂1|k evolves 2.0000 → 2.3333 → 2.1250 → 2.1429 as z2, z3, z4 arrive, while its variance descends monotonically 1.0000 → 0.6667 → 0.6250 → 0.6190. Read the two columns differently: the MEAN can wander both ways — each new measurement re-litigates the past — but the VARIANCE only falls. Information about a fixed instant accumulates, with geometrically vanishing increments.
Implementation note for honesty: production fixed-lag smoothers don't literally re-run RTS every step. They either run an augmented-state filter — stack (xk, …, xk−L) into one big state and let a standard KF do the bookkeeping — or a sliding two-filter scheme (Chapter 7). Same math, different ledger; the augmented-state trick is a nice one-liner to know at quals.
A live stream. The warm head is the causal filter at the right edge; the teal head trails L steps behind with a visibly tighter bar — the shaded band between them is the latency you're paying. The corner readout computes the %-of-benefit live from the C² ladder. Toggle to fixed-point mode and tap the stream to anchor an instant: watch its uncertainty ratchet down, increments shrinking.
There is a second, completely different road to the same summit: run one filter forward from the beginning, run another backward from the end, and multiply. Watching the two answers collide — 1.5 + 0.6 = 2.1 — is the most honest X-ray of what smoothing is.
The factorization, from Bayes plus Markov in two lines:
The first factor is the ordinary forward filter. The second is NOT a posterior — it is a likelihood: how loudly the future testifies about xk. Two witnesses, disjoint data, so their (Fisher) informations add.
Why the backward object must live in information form: at k = N it knows nothing — the likelihood over an empty future is flat, "infinite covariance," unrepresentable as a finite P but perfectly representable as information Y = 0, y = 0. The backward recursion then absorbs measurements — with R appearing explicitly! Contrast with RTS — and passes through the dynamics toward the past. The mechanics of information filtering are sf-09's turf; here we use only Y = P−1, y = Y x̂, and the fact that fusion is addition.
Forward witness, straight from the Chapter-5 table: x2 | z1, z2 ~ N(8/3, 2/3) = N(2.6667, 0.6667), information 1/(2/3) = 1.5000.
Backward witness, unrolling the dynamics from x2: z3 = x2 + w2 + v3 has variance Q + R = 2 about x2; z4 = x2 + w2 + w3 + v4 has variance 2Q + R = 3; and they SHARE w2, so Cov(z3, z4 | x2) = Q = 1. Crunch the 2×2: with a = (1, 1) and Σ = [[2, 1], [1, 3]], Σ−1 = (1/5)[[3, −1], [−1, 2]], so the future's information about x2 is J = aTΣ−1a = (3 − 1 − 1 + 2)/5 = 3/5 = 0.6000, and its point estimate is η = aTΣ−1(1, 2) = 0.8, i.e. mean 0.8/0.6 = 1.3333 with variance 1.6667. The future alone thinks x2 was low, and says so with modest confidence.
Multiply the witnesses (disjoint data ⇒ informations add):
Flip back to Chapter 5's table: x̂2|4 = 16/7 = 2.2857, P2|4 = 10/21 = 0.4762. Two entirely different algorithms, identical digits — because they are two factorizations of one posterior.
When two-filter wins in practice: the sweeps are independent, so they parallelize (map forward and backward passes to two cores, meet in the middle); fixed-lag implementations fall out naturally (backward filter over a short window); and in some numerical regimes the information-form backward pass is better conditioned. When RTS wins: half the filtering work, no F-inverse anxiety (the information-form backward dynamics need care when F is near-singular), and simpler storage.
The forward filter tube grows left-to-right; the backward likelihood grows right-to-left, starting infinitely wide (flat) at the end. Pick the meeting index k: the inset shows both bells and their product, with the information ledger Ifwd + Ibwd = Ismooth updating live.
Everything so far assumed linear dynamics. Real attitude, real bearings, real SLAM are not. The fix is the same one the EKF taught you — linearize — plus a revelation: the smoother you built is secretly an optimizer.
The Extended RTS (ERTS) recipe is almost anticlimactic: run the EKF forward (nonlinear propagation for means, Jacobians Fk = ∂f/∂x evaluated at the filtered means for covariances — ekf owns those mechanics), store the usual four sequences, then run the IDENTICAL backward sweep with Ck = Pk|k FkT P−1k+1|k. No new Jacobians, no re-derivation: the sweep never touches measurements, so it never needs H or R.
The flaw hiding in that convenience: the Jacobians were evaluated at the FILTERED means — the very estimates hindsight is about to declare wrong. Where the filter erred badly (mid-dropout, high dynamics), the linearization is planted on bad ground and the smoothed answer inherits the tilt. The cure is iterated smoothing: relinearize the whole pass around the SMOOTHED trajectory and sweep again, repeating until the trajectory stops moving.
Sigma-point variant in one sentence for completeness: the unscented RTS smoother replaces Pk|k FkT with a sigma-point cross-covariance Cov(xk, xk+1) computed by propagating sigma points through f — derivative-free, same recursion shape (ukf owns sigma-point mechanics).
Now the bridge that reorganizes the whole lesson in hindsight. Write the negative log posterior of the full trajectory:
Minimizing J is MAP estimation — maximum a posteriori over the whole trajectory at once. For linear f, h this is a quadratic; its Hessian — the information matrix — is block-tridiagonal, because each term couples at most neighboring states. Chains have no long-range couplings.
Solving a tridiagonal system takes one forward elimination and one back-substitution — and that back-substitution IS the RTS sweep. RTS is sparse Cholesky on a chain, discovered before anyone called it that.
Assemble Λ. Diagonal entries collect one measurement (+1) plus one dynamics coupling per neighbor: x1 gets 1+1 = 2, x2 and x3 get 1+2 = 3, x4 gets 1+1 = 2; off-diagonals are −1/Q = −1:
Note Λ is persymmetric — unchanged by time reversal — which is WHY Chapter 5's variance profile had to be a palindrome. Only measurements source the right-hand side η; dynamics factors are zero-mean.
Solve Λx = η (one tridiagonal solve): x = (2.1429, 2.2857, 1.7143, 1.8571) — exactly 15/7, 16/7, 12/7, 13/7, the Chapter-5 smoothed means. Marginal variances are diag(Λ−1) = (0.6190, 0.4762, 0.4762, 0.6190) — exactly 13/21, 10/21, 10/21, 13/21, the Chapter-5 smoothed variances. Bit for bit.
The consequence for the nonlinear case: iterating (relinearize → solve the tridiagonal system → repeat) is EXACTLY Gauss–Newton on J. Iterated ERTS and batch nonlinear least squares on the chain are the same algorithm wearing different notation. And the moment your problem stops being a chain — a loop closure ties pose 1000 to pose 5 — the information matrix grows off-tridiagonal fill, the one-sweep guarantee dies, and you need general sparse solvers. That story — iSAM2 and friends — belongs to factor-graphs.
python # pip install filterpy import numpy as np from filterpy.kalman import KalmanFilter dt = 0.1 kf = KalmanFilter(dim_x=2, dim_z=1) kf.F = np.array([[1.0, dt], [0.0, 1.0]]) # constant velocity kf.H = np.array([[1.0, 0.0]]) # position-only sensor kf.R = np.array([[9.0]]) # GPS: +/-3 m -> variance 9 q = 0.5 # white-accel intensity kf.Q = q * np.array([[dt**4 / 4, dt**3 / 2], # discrete white-noise Q [dt**3 / 2, dt**2]]) kf.x = np.array([[0.0], [0.0]]) kf.P = np.eye(2) * 100.0 # diffuse-ish start zs = [np.array([[v]]) for v in (1.2, 2.4, 3.1, 4.6, 5.9)] mu, cov, _, _ = kf.batch_filter(zs) # forward pass (stores what RTS needs) xs, Ps, Cs, Pps = kf.rts_smoother(mu, cov) # backward sweep # xs: smoothed states Ps: smoothed covs Cs: smoother gains Pps: predicted covs assert np.all(np.diagonal(Ps, axis1=1, axis2=2) <= np.diagonal(cov, axis1=1, axis2=2) + 1e-12) # smoothing never inflates # Production notes: # - Post-processing logs: this exact pattern (batch_filter -> rts_smoother). # - Live low-latency: fixed-lag via an augmented-state KF, or GTSAM's # IncrementalFixedLagSmoother. # - Non-chain problems (loop closures, landmarks): GTSAM/iSAM2 factor graphs - # on a pure chain they reproduce RTS to machine precision (this chapter's bridge).
One more payoff planted for later fields: because smoothing = MAP on a chain, the smoother is also the E-step of EM when learning Q and R for linear systems — it supplies the smoothed means, variances, AND the lag-one cross-covariances the M-step needs.
Back to the flight that opened the lesson: nine seconds of GPS silence. This time you have the whole machinery — watch the filter and the smoother cross the same gap and read their characters in the shapes of their tubes. This chapter is played, not read.
What to notice about the filter (warm tube): entering the gap it has only dynamics, so variance grows without bound — and for a constant-velocity model the POSITION variance grows super-linearly, because the unobserved velocity error integrates into position (the classic t³ random-acceleration term). At reacquisition the tube snaps down discontinuously and the mean takes a visible kink — the one-sided estimator's signature.
What to notice about the smoother (teal tube): a symmetric LENS. It pinches at BOTH edges of the gap — the left edge is informed by the past, the right edge by the future — and bulges maximally at the center. For random-walk dynamics the interior variance follows the Brownian-bridge law σ²·t(T−t)/T: conditioned at both ends, uncertainty is a parabola. No kink anywhere: hindsight never gets surprised.
Close the loop with the opening scene: the flight-test engineer's replayed filter could never produce the lens, no matter how many replays — the lens is what conditioning on the future looks like. This is the picture to carry into any quals room.
Everything above, weaponized. Attempt each question at a whiteboard before expanding the answer — the gap between recognizing an answer and producing one is exactly the gap a quals committee measures.
How to use this chapter: one question at a time, out loud, standing up if you can. The answers below are calibrated to what a strong pass sounds like — structured, quantitative, and honest about edge cases. The seven questions span the lesson's spine: the derivation, the structure (where R went, why variances only shrink), the alternatives (two-filter trade-offs, the HMM twin), the applications (EM moments), and the numerics.
Given z1:k, write the joint Gaussian of (xk, xk+1): means (x̂k|k, x̂k+1|k), covariances Pk|k and Pk+1|k = F Pk|kFT + Q, and cross-covariance Cov(xk, xk+1) = E[(xk−x̂)(F(xk−x̂) + w)T] = Pk|kFT (the w term dies by independence). Gaussian conditioning gives E[xk | xk+1, z1:k] = x̂k|k + Ck(xk+1 − x̂k+1|k) with Ck = Pk|kFTP−1k+1|k. The Markov property enters ONCE, here: given xk+1, the future zk+1:N is conditionally independent of xk, so p(xk|xk+1, z1:k) = p(xk|xk+1, z1:N) — the conditioning set upgrades for free. Finally, the tower property averages over p(xk+1|z1:N): x̂k|N = x̂k|k + Ck(x̂k+1|N − x̂k+1|k); the law of total covariance gives Pk|N = Pk|k + Ck(Pk+1|N − Pk+1|k)CkT.
Every measurement was absorbed during the forward pass into the filtered and predicted statistics (x̂k|k, Pk|k, x̂k+1|k, Pk+1|k); the sweep is a function of those statistics only. Implication: smoothing ADDS no information — it REDISTRIBUTES information the filter already banked, propagating each measurement's influence backward along the dynamics at rate C per step. Contrast with the two-filter formulation, where the backward pass genuinely processes measurements and R appears explicitly — the two views bracket the same posterior. A crisp one-liner for the examiner: "the forward pass spends the data; the backward pass settles the accounts."
Backward induction. Base: PN|N − PN|N−1 = −KNSNKNT ≤ 0 directly from the measurement update. Inductive step: assume Pk+1|N ≤ Pk+1|k. The covariance recursion gives Pk|N − Pk|k = Ck(Pk+1|N − Pk+1|k)CkT, a congruence of a negative-semidefinite matrix, hence ≤ 0. The needed hypothesis follows because Pk+1|N ≤ Pk+1|k+1 (same recursion one step later) and Pk+1|k+1 ≤ Pk+1|k (measurement update). Corollary used for debugging: a smoothed variance exceeding its filtered counterpart is ALWAYS a bug or numerics, never data — variances are data-independent in the linear-Gaussian model.
Identical in exact arithmetic — both compute p(xk|z1:N). Choose TWO-FILTER when: you want parallelism (forward and backward sweeps run concurrently from opposite ends); you're building fixed-lag smoothers (a backward filter over a short window is natural); or the diffuse backward start is handled cleanly in information form (YN = 0). Its costs: the backward pass re-processes measurements (double the filtering work) and backward dynamics propagation needs care — information-form updates involving F−1 or its information analog — which is fragile when F is near-singular. Choose RTS when: you already ran a filter and stored the four sequences (the sweep costs one n×n solve per step); you want no F-inversion anywhere; storage of O(Nn²) is acceptable. Production reality: RTS for post-processing logs, two-filter/augmented-state for live fixed-lag.
Both are sum-product message passing on a chain; only the message PARAMETRIZATION differs. HMM forward αk(x) ∝ p(xk, z1:k) ↔ Kalman filtered N(x̂k|k, Pk|k). HMM backward βk(x) = p(zk+1:N|xk) ↔ the two-filter backward likelihood (information form, diffuse start βN ≡ 1 ↔ YN = 0). Marginal γk ∝ αkβk ↔ information addition giving N(x̂k|N, Pk|N). The HMM's α–γ recursion — computing γk from γk+1 without re-touching observations — is precisely RTS: its Gaussian closed form is the Ck recursion. Same graph, same messages; tables versus (mean, covariance). See hmm for the discrete machinery.
The expected complete-data log-likelihood needs three families of smoothed moments: (1) means x̂k|N; (2) marginal second moments E[xkxkT|z] = Pk|N + x̂k|Nx̂k|NT; (3) LAG-ONE cross moments E[xk+1xkT|z] = Pk+1,k|N + x̂k+1|Nx̂k|NT, because the M-step's Q-update expands E[(xk+1−Fxk)(xk+1−Fxk)T] into all three. The lag-one covariance has the closed form Pk+1,k|N = Pk+1|NCkT (verified numerically in Chapter 8 against the batch inverse: 0.2381, 0.1905, 0.2381 on the running example). So the smoother — not the filter — is the E-step of every LDS learner: filtered moments would bias Q downward because they ignore future evidence of model error.
Pk+1|k = FPk|kFT + Q loses rank when Q has zero (or tiny) directions and the filter has converged hard — e.g. a bias state modeled with q ≈ 0, or post-convergence attitude states. Then the explicit inverse amplifies round-off; Ck picks up huge spurious entries and the sweep can push Pk|N indefinite. Remedies, in escalating rigor: (1) never form the inverse — solve Pk+1|kCT = FPk|kT by Cholesky, falling back to a pivoted/LDL solve; (2) symmetrize Pk|N ← (P + PT)/2 each step and clip negative eigenvalues only as a tourniquet; (3) square-root smoothing — propagate Cholesky/QR factors through both passes so P ≥ 0 by construction and effective precision doubles; (4) for genuinely deterministic substates, partition the state and smooth only the stochastic block. The quals-grade observation: the failure is structural (rank), so the fix must be structural (factorized or partitioned), not a fudge-factor on Q.
Finally, three debate prompts — no correct side. They are calibration exercises for engineering judgment, the part of a quals that separates a pass from a strong pass. State both defensible positions before you pick.
You now own the chain. Here is where every road out of it leads — and which lesson guards each road.
The comparison table this lesson has been building toward, one row per estimator:
| Estimator | Conditions on | Superpower | Lives in |
|---|---|---|---|
| Kalman filter | z1:k | Causal, O(1) memory — real-time control | kalman-filter |
| Fixed-lag smoother | z1:k+L | ~All the benefit at L-step latency — live pipelines | Chapter 6 |
| RTS fixed-interval | z1:N | Exact on chains, one backward sweep — post-processing | This lesson |
| Two-filter smoother | z1:N | Same answer, parallel sweeps, diffuse-start machinery | Chapter 7 + sf-09 |
| Moving-horizon estimation | window + arrival cost | Windowed MAP with CONSTRAINTS — what no closed form can do | moving-horizon-estimation |
| Factor-graph smoothing | arbitrary graph | Loop closures, landmarks, multi-robot — the SLAM back-end | factor-graphs |
Roads and their guardians. The forward pass and all its intuition — kalman-filter. Information form, which the two-filter backward pass needs for its zero-information start — sf-09-information-filter. The discrete twin: forward-backward for HMMs is EXACTLY two-filter smoothing with tables instead of Gaussians (α = forward witness, β = backward likelihood) — hmm. Nonlinear forward passes whose Jacobians ERTS borrows — ekf and ukf.
Two forward roads. Moving-horizon estimation takes Chapter 8's MAP view, truncates the window, and adds inequality constraints — solving the "my state-of-charge estimate must live in [0, 1]" problem that no closed-form smoother touches; its arrival cost is exactly a compressed filter posterior. Factor graphs take the same view and free the TOPOLOGY: loop closures, landmarks, multi-robot — with sparse solvers (iSAM2) doing incrementally what our sweep did in one pass. sf-17 shows that machinery holding a LiDAR-inertial mapping stack together in production.
Where smoothing quietly runs the modern stack: every SLAM map you've seen was smoothed, not filtered; sensor-calibration and flight-test reports are smoothed; EM-trained linear dynamical systems have a smoother inside the E-step; even training data for learned odometry is labeled by smoothers, because hindsight is the cheapest ground truth there is.
The filter lives forwards; the smoother understands backwards. Engineering keeps both on staff — the filter flies the aircraft, and the smoother writes the flight report.