Advanced Estimation · PhD Track

Bayesian Smoothing:
RTS & Friends

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.

Prerequisites: Kalman Filter — predict, update, K, and P should feel like old friends. (Refresher here.)
12
Chapters
8
Simulations
KF
Assumed Knowledge

Chapter 0: Why — The Post-Flight Truth

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.

The machine we're building: the Rauch–Tung–Striebel (RTS) smoother is one ordinary Kalman forward pass plus one backward sweep over the stored results. Roughly twice the cost of filtering, strictly better everywhere except the very last instant — where there is no future left to use.

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.

Same Data, Two Posteriors

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.

Dropout width12
What to notice: the filter's tube balloons one-sidedly across the gap and snaps back with a kink — that shape is a structural property of causality, not of the data. The smoother's tube is a gentle symmetric lens through the same gap, with no kink anywhere. Hindsight is extra information you already own.
Misconception: "Smoothing is a cosmetic low-pass on the filter's output — curve prettification." No. It computes a different posterior: p(xk | z1:N) instead of p(xk | z1:k). Conditioning on strictly more data is strictly more informed — and no amount of filtering or re-filtering the same log can produce it.
PhD lens: the opening quals move is "write down the density a filter computes, and the one a smoother computes." Candidates who produce p(xk|z1:k) vs p(xk|z1:N) without hesitation signal they think in posteriors, not in code — everything an examiner asks afterwards hangs off that pair of conditioning sets.
Check: A colleague reruns the saved measurement log through the same Kalman filter a second time, hoping for better estimates. What happens?

Chapter 1: Three Posteriors, One Timeline

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):

Pk|k−1 ≥ Pk|k ≥ Pk|N

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: 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.

A taxonomy sentence you'll want at quals: prediction, filtering, and smoothing are the k > b, k = b, and k < b cases of one question — estimate xk given z1:b. One family, three regimes, one machinery.
The Nested Ribbons

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.

Time index k30
Meas. noise R2.0
Misconception: "The filtered estimate is THE best estimate of xk." It is only best-given-data-up-to-k. Once the full record exists, the smoothed estimate strictly dominates it (equality only at k = N), because its conditioning set contains the filter's.
PhD lens: examiners draw a timeline, mark a k, and ask you to shade what each of the three estimates has seen. Then the follow-up that separates candidates: "prove the variance ordering." The expected answer is one line about nested conditioning sets — conditioning on more data cannot increase expected posterior variance — not a Riccati grind.
Check: At which time index are the filtered and smoothed posteriors identical?

Chapter 2: The Forward Pass — and What You Must Keep

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:

Filtered means
k|k — where the filter believed it was
Filtered covariances
Pk|k — how sure it was
Predicted means
k+1|k — where it expected to go next
Predicted covariances
Pk+1|k — how sure it was about that

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.

A covariance curiosity worth planting now: Pk|k and Pk|k−1 never depend on the measured VALUES — only on the model matrices. The whole variance pipeline, including everything the smoother will do to it, can be computed before the drone ever takes off. (Chapter 5's palindrome and Chapter 6's lag ladder are both consequences.)
What the sweep consumesAppears in the recursion asCommonly discarded?
k|k, Pk|kThe base the correction is added to; Pk|k builds CkNo — everyone keeps these
k+1|kSubtracted inside the correction (x̂k+1|N − x̂k+1|k)Yes — the classic mistake
Pk+1|kInverted inside Ck = Pk|k FT P−1k+1|kYes — the classic mistake
z1:N, innovations, gains KkNowhere — already spent during the forward passIrrelevant to the sweep
Misconception: "The smoother only needs the filter's outputs x̂k|k, Pk|k." The backward recursion explicitly consumes the PREDICTED pair as well — Ck inverts Pk+1|k, and the correction term subtracts x̂k+1|k. Discard them and the sweep cannot even be written down.
PhD lens: a favorite oral prompt is simply "state the smoother's inputs, precisely." Many candidates begin writing the recursion and discover live, at the board, that they never noticed the predicted terms. Examiners also probe the corollary — that the P sequences are data-independent — by asking whether smoothing accuracy can be predicted pre-mission. (Yes. Exactly.)
Check: Which quantities must the forward pass hand to the RTS backward sweep?

Chapter 3: Conditioning on the Future

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 = H xk+1 + v = H(F xk + w) + v

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.

Notice what set the strength of the backward pull: the dynamics noise Q. If Q were huge, the coat thickens to uselessness — the world forgets its past too fast for the future to testify about it. If Q → 0, z2 measures x1 almost as sharply as z1 did. Hold this intuition; it becomes the smoother gain in the next chapter.
The Future Pulls the Past

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.

Process noise Q1.0

Worked, Both Routes: One Future Measurement

Setup: the running system (F = 1, Q = 1, R = 1). After absorbing z1 = 2.0 with a diffuse prior, the filter believes x1 ~ N(2.0, 1.0). A future measurement z2 = 3.0 arrives. Update the belief about x1 two completely different ways and demand identical answers.

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:

1|2 = 2.0 + (1/3)(1.0) = 7/3 ≈ 2.3333     P1|2 = (1 − 1/3)(1.0) = 2/3 ≈ 0.6667

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:

1|2 = 2.0 + 0.5(2.6667 − 2.0) = 2.3333     P1|2 = 1.0 + 0.25(0.6667 − 2.0) = 0.6667

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.

Misconception: "Using future measurements violates causality, so smoothing must be an approximation or a hack." Conditioning is symmetric in time: p(xk | future data) is a perfectly well-defined slice of the joint distribution, because the future data is a noisy FUNCTION of xk through the dynamics. Correlation flows both ways even though causation flows one way.
PhD lens: the trap question is "does smoothing violate causality?" Examiners want the correlation-vs-causation disarm plus the quantitative kicker: backward information flow is throttled by Q, dying like a product of dynamics-trust ratios across the gap. A candidate who says "the future testifies through the dynamics, at a rate the process noise sets" has answered at PhD level.
Check: Why does a measurement taken at time k+1 carry information about the state at time k?

Chapter 4: The RTS Recursion, Derived

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):

E[xk | xk+1, z1:k] = x̂k|k + Ck(xk+1 − x̂k+1|k)     Ck = Pk|k FT P−1k+1|k

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:

k|N = x̂k|k + Ck(x̂k+1|N − x̂k+1|k)
Pk|N = Pk|k + Ck(Pk+1|N − Pk+1|k)CkT

Since Pk+1|N ≤ Pk+1|k, the added term is negative semidefinite — smoothing can only shrink covariance, at every single step.

Anatomy of C — the chapter's soul. In scalar form, C = Pk|kF / (F²Pk|k + Q). As Q → 0 it tends to 1/F — deterministic dynamics let hindsight flow back undiminished. As Q → ∞ it dies to 0 — a world that forgets instantly cannot be told about its past. C is a trust slider like K, but between DYNAMICS and forgetting, not between model and sensor.

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.

Watch the Backward Sweep Run

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.

Process noise Q1.0

Worked: The Gain's Personality, Quantified

Setup: the scalar running system with F = 1, R = 1. First find the steady-state filtered variance for Q = 1 by iterating the Riccati map, then evaluate C = Pfilt/(Pfilt + Q) across three regimes of dynamics noise.

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.

RegimeQC = Pfilt/(Pfilt + Q)Reading
Tight dynamics0.10.6180/0.7180 = 0.8607Hindsight flows backward at 86% strength per step
Matched noise10.6180/1.6180 = 0.3820Each backward step retains 38% of the neighbor's correction (= 1/φ², another golden cameo)
Sloppy dynamics100.6180/10.6180 = 0.0582The future is almost mute about the past — corrections die within a step
Limit Q → 00→ 1 (= 1/F)Deterministic dynamics: undiminished hindsight
Limit Q → ∞→ 0Total forgetting: no smoothing benefit at all
Misconception: "The smoother gain is another measurement-trust knob like the Kalman gain, so R must hide inside it." Ck = Pk|kFTP−1k+1|k contains no R and no z — it arbitrates between the filtered state and the DYNAMICS (via Q inside Pk+1|k). All measurement information entered once, during the forward pass; the sweep merely redistributes it.
PhD lens: THE standard smoothing quals question is this derivation, and the grader listens for three checkpoints: the cross-covariance Pk|kFT derived (not asserted), the Markov argument for why conditioning on xk+1 screens off the whole future, and the tower/total-covariance step. Follow-up, almost guaranteed: "where is R in your backward pass?" — the answer "already spent, inside the forward statistics" in one sentence.
Check: As process noise Q → ∞ with everything else fixed, the smoother gain Ck tends to…

Chapter 5: The Whole Thing, By Hand

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.

Pause on the pattern the arithmetic just coughed up: the filtered variances are 1/1, 2/3, 5/8, 13/21 — ratios of consecutive Fibonacci numbers. Why: the Riccati map P ← (P+1)/(P+2) is a continued-fraction step, and its iterates ARE the Fibonacci convergents, marching toward 1/φ = 0.6180 from Chapter 4. The steady state arrives absurdly fast — by k = 4 we're at 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.

Three structural facts, each verifiable in the table: (1) the final point is EXACTLY the filter's — smoothing never edits the present. (2) The interior variance 10/21 ≈ 0.476 beats the best variance the filter EVER achieved (13/21 ≈ 0.619) — the middle of the record is where hindsight is richest, informed from both sides. (3) The palindrome is not luck: this model is time-reversible (F = 1, same Q, R, diffuse start), so the batch information matrix is symmetric under time reversal and the variance profile must mirror.
The Ledger, Animated

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.

kzkKkk|kPk|kCkk|4Pk|4
12
23
31
42seed

From Scratch: RTS in NumPy

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.

Misconception: "Smoothing improves every estimate, including the newest one." x̂N|N is the seed of the backward sweep and is never modified — at the final instant there is no future to borrow from, so filter and smoother agree exactly there. The benefit grows as you move backward into the interior of the record.
PhD lens: orals routinely demand exactly this — "here are three measurements and the model; run the smoother at the board." Fluency with the fraction arithmetic (gains as ratios, innovations signed correctly) is the tell between someone who implemented RTS once and someone who owns it. Examiners also plant the classic off-by-one — handing you Pk|k where Pk+1|k belongs — and expect your sanity anchors to catch it.
Check: In the 4-step worked example, which estimate is completely unchanged by the backward sweep?

Chapter 6: Fixed-Interval, Fixed-Lag, Fixed-Point

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 — 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 LPk|k+L% of full smoothing benefit
0 (pure filter)0.61800%
10.472185.4%
20.450897.9%
30.447799.7%
40.447399.96%
∞ (fixed-interval)0.4472 = 1/√5100%

One step of lag captures 85.4% of everything smoothing can ever give; two steps capture 97.9%; three, 99.7%.

The engineering rule: lag worth buying ≈ 2–3 time constants of the C² decay; past that you're paying latency for decimals. The general version: the smoothing time constant is −1/ln(C²) steps. Tight-dynamics systems (C near 1) reward long lags; sloppy ones (C near 0) make even lag 1 pointless.

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.

The Lag Lab

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.

Lag L2
Process noise Q1.0
Misconception: "More lag keeps helping, so a real-time smoother should buy as much delay as the pipeline tolerates." The benefit decays geometrically with ratio C² per step — at the golden steady state, lag 2 already delivers 97.9% of infinite hindsight — so past ~3 decay time-constants you are paying pure latency for noise-level decimals.
PhD lens: examiners pose it as a design decision: "your pipeline may emit state 200 ms late — which smoothing problem is that, and how do you size the lag?" They want the classification (fixed-lag), the decay rate (C² per step, time constant −1/ln C²), and the engineering conclusion (2–3 time constants ≈ all of it). Bonus depth: mention the augmented-state implementation and you've flagged yourself as someone who has shipped one.
Check: Your tracker can tolerate 3 steps of output latency. At the worked steady state (C² ≈ 0.146), roughly what fraction of the full smoothing benefit does lag-3 fixed-lag smoothing capture?

Chapter 7: The Two-Filter View

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:

p(xk | z1:N) ∝ p(xk | z1:k) · p(zk+1:N | xk)

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.

Worked: The Collision at k = 2

Setup: two-filter smoothing at k = 2 on the Chapter-5 data (F = Q = R = 1, z = (2, 3, 1, 2)). Build the forward witness from z1, z2; build the backward witness from z3, z4 alone by unrolling the dynamics; fuse by information addition; demand Chapter 5's RTS answer to four decimals.

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):

I = 1.5 + 0.6 = 2.1     mean = (2.6667×1.5 + 0.8)/2.1 = 4.8/2.1 = 2.2857     var = 1/2.1 = 0.4762

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.

The distinction students blur at exams, in bold: the two-filter backward pass PROCESSES MEASUREMENTS (R appears, z appears, it starts at zero information). The RTS backward sweep processes forward-pass STATISTICS (no R, no z, it starts at the filtered endpoint). RTS is what you get by algebraically collapsing the product form — one object, two derivations.

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.

Two Witnesses Meet in the Middle

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.

Meeting index k8
Process noise Q1.0
Meas. noise R1.0
Misconception: "The RTS backward sweep IS the backward filter of the two-filter smoother." They are different objects: the two-filter backward pass processes future MEASUREMENTS (R and z appear; it starts from zero information about xN), while the RTS sweep processes forward-pass STATISTICS (no R, no z; it starts from the filtered endpoint). RTS is the algebraic collapse of the product form, not one of its factors.
PhD lens: the subtlety examiners probe relentlessly: "is the RTS backward sweep a filter?" No — and candidates who conflate it with the two-filter backward pass get dismantled with "then where does R appear in your sweep?". The strong answer exhibits both factorizations, states which pass touches measurements in each, and notes the parallelism and diffuse-start trade-offs that decide between them in practice.
Check: Why must the two-filter smoother's backward pass run in information form?

Chapter 8: Nonlinear Smoothing & the MAP Bridge

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:

J(x1:N) = ½ ∑k ‖zk − h(xk)‖²R + ½ ∑k ‖xk+1 − f(xk)‖²Q

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.

Worked: The MAP System, Solved Once

Setup: verify the bridge numerically on the Chapter-5 problem. Assemble the batch information matrix for x1:4 (each measurement adds R−1 = 1 to its diagonal; each dynamics factor adds the block [[1, −1], [−1, 1]]/Q), solve the tridiagonal system once, and compare with the RTS sweep.

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:

Λ = [ 2  −1   0   0 ]
      [−1   3  −1   0 ]
      [ 0  −1   3  −1 ]
      [ 0   0  −1   2 ]      η = HTR−1z = (2, 3, 1, 2)

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.

Bonus fact the batch view hands us for free: the off-diagonals of Λ−1 are the smoothed CROSS-covariances, and they verify the closed form Cov(xk+1, xk | z1:N) = Pk+1|N·Ck: they come out 0.2381 = (10/21)(1/2), 0.1905 = (10/21)(2/5), 0.2381 = (13/21)(5/13). That lag-one cross-covariance is exactly the quantity EM's M-step consumes when learning Q and R — the Quals Arsenal works one of these.

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.

Production Framing: FilterPy Does the Bookkeeping

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.

Misconception: "The extended RTS smoother needs its own linearization pass — new Jacobians derived for the backward direction." It reuses the forward EKF's Jacobians unchanged (the sweep touches no measurement, so no H is ever needed) — and that convenience is precisely its weakness: Jacobians planted at poor filtered means poison the smoothed answer, which is what iterated smoothing (= Gauss–Newton on the MAP objective) exists to fix.
PhD lens: two examiner favorites converge here. First: "show that RTS is belief propagation / Cholesky on a chain, and explain why one sweep is exact" — the expected keyword is NO LOOPS (a tree-structured graph). Second: "your system now gets a loop-closure measurement between x1000 and x5 — what breaks?" — the tridiagonal structure, hence the one-sweep guarantee, hence the handoff to sparse factor-graph solvers. Candidates who can also produce the lag-one cross-covariance formula for EM are operating at the level the exam is actually calibrated for.
Check: What structural property of the linear-Gaussian chain's information matrix makes a single backward sweep give the EXACT smoothed answer?

Chapter 9: Showcase — The Dropout Bridge

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.

Experiments to run, one sentence each: widen the gap and watch the filter's balloon grow super-linearly while the smoother's lens only widens gently at its waist. Stiffen the dynamics (small Q) and watch the bridge pull taut — with near-deterministic dynamics even a huge gap costs little. Crank R and watch both tubes thicken but the lens keep its shape. Drag the playhead to the gap center and compare the two bells side by side in the inset. Then flip to fixed-lag mode and watch Chapter 6's ladder become geometry: the teal head trails the warm head by L steps, inheriting most of the lens once L reaches a few decay constants.
Filter vs Smoother Through the Gap
Dropout start45
Dropout width18
Process noise q0.15
Meas. noise R9.0
Playhead60
Lag L (fixed-lag)8

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.

Misconception: "During a dropout the smoother is as blind as the filter — no data is no data." The smoother interpolates between the last pre-gap and first post-gap measurements: its gap variance follows the Brownian-bridge lens (max at center, pinched at both edges) rather than the filter's one-sided balloon, because the future edge of the gap is data the filter structurally cannot use.
PhD lens: examiners hand you a marker and say "sketch σ(t) for the filter and the smoother through a 10-second outage, and justify the shapes." The lens (with the t(T−t)/T law named) versus the one-sided balloon (with the t³ CV term named), plus where the two curves must touch, is a complete answer — this simulation is that sketch, running.
Check: Inside a measurement dropout, the smoother's uncertainty profile is…

Chapter 10: Quals Arsenal

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.

Q1. Derive the RTS smoother gain Ck from first principles, stating exactly where the Markov property enters.
Model answer

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.

Q2. The backward sweep contains no R and no z. Where did the measurements go, and what does that imply about what smoothing "does"?
Model answer

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."

Q3. Prove that smoothing never inflates uncertainty: Pk|N ≤ Pk|k for all k.
Model answer

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.

Q4. RTS versus the two-filter smoother: identical outputs, different machines. When do you choose which?
Model answer

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.

Q5. Exhibit the exact correspondence between RTS smoothing and the forward-backward algorithm for HMMs.
Model answer

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.

Q6. You want to learn Q and R from data via EM on a linear dynamical system. What does the E-step require from the smoother, exactly?
Model answer

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|Nk|NT; (3) LAG-ONE cross moments E[xk+1xkT|z] = Pk+1,k|N + x̂k+1|Nk|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.

Q7. Numerics: Ck = Pk|kFTP−1k+1|k. What goes wrong when Pk+1|k is near-singular, and what are the production remedies?
Model answer

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.

Debate 1 — "In the era of factor-graph solvers (GTSAM, iSAM2), the RTS smoother is a historical curiosity — teach it for culture, never ship it." Defend or refute. Consider: chain-structured problems, embedded compute budgets, the O(N) exactness guarantee, and relinearization control — versus loop closures, robust losses, and ecosystem maturity.
Debate 2 — Your flight-test group proposes logging only filtered states to halve telemetry bandwidth, arguing the smoother can be "reconstructed later from the measurements." Defend or refute as the reviewing engineer. Consider: what the sweep actually consumes, whether re-running the filter offline is bit-exact when gating/adaptation ran onboard, and what is truly irrecoverable.
Debate 3 — "Every production tracker should emit fixed-lag smoothed output by default, with L ≈ 3 decay constants; raw filtered output is a legacy habit." Defend or refute. Consider: which consumers can tolerate latency (analytics, display, mapping) and which cannot (control, safety interlocks) — and whether shipping two streams is the real answer.
Misconception: "Quals smoothing questions test formula recall." Examiners probe WHY-structure almost exclusively: where Ck comes from, why no R appears in the sweep, when one sweep is exact, what breaks off-chain. A candidate with the four whys beats one with the ten formulas every time.
PhD lens: this chapter IS the lens. The debates especially: committees end orals with judgment questions ("would you ship fixed-lag by default?") precisely because they cannot be answered from memory — only from a model of the trade-offs.
Check: An examiner asks: "Your RTS implementation produces a smoothed variance LARGER than the filtered variance at one timestep. What do you conclude?" The strongest answer:

Chapter 11: Connections — Where Smoothing Lives

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:

EstimatorConditions onSuperpowerLives in
Kalman filterz1:kCausal, O(1) memory — real-time controlkalman-filter
Fixed-lag smootherz1:k+L~All the benefit at L-step latency — live pipelinesChapter 6
RTS fixed-intervalz1:NExact on chains, one backward sweep — post-processingThis lesson
Two-filter smootherz1:NSame answer, parallel sweeps, diffuse-start machineryChapter 7 + sf-09
Moving-horizon estimationwindow + arrival costWindowed MAP with CONSTRAINTS — what no closed form can domoving-horizon-estimation
Factor-graph smoothingarbitrary graphLoop closures, landmarks, multi-robot — the SLAM back-endfactor-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.

Misconception: "Smoothing supersedes filtering — why would anyone accept the worse estimate?" The filter's causality is a feature, not a bug: control loops, collision avoidance, and anything that acts NOW can only consume causal estimates. Smoothing owns the record; filtering owns the moment. Production systems run both.
PhD lens: committees end with map questions: "when would you refuse to smooth?" (real-time control, unbounded memory, non-chain topology better served by iSAM) and "what single idea connects RTS, forward-backward, and bundle adjustment?" (exact inference by message passing on trees; least squares when Gaussian). Answering with the map, not a list, is the difference between coverage and understanding.
Check: Your battery state-of-charge estimate must respect 0 ≤ SoC ≤ 1, online, and your Kalman filter keeps reporting 1.08. Which tool from the map is built for this?
"Life can only be understood backwards; but it must be lived forwards."
— Søren Kierkegaard

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.