Sensor Fusion: Classical to Modern · Lesson 4 of 22

The Recursive Bayes FilterThe Engine of Fusion Over Time

A robot lost in a hallway, fusing a stream of noisy door-and-wall sightings into a sharpening guess of where it is. Strip away the Gaussians and the matrices, and every filter you'll ever meet — Kalman, EKF, UKF, particle — is the same two-step heartbeat: predict, then update, forever. This is that heartbeat, derived by hand on a row of cells.

Prerequisites: basic probability (what a distribution is) + arithmetic. That's it.
11
Chapters
6
Simulations
0
Assumed Knowledge

Chapter 0: A Robot That Does Not Know Where It Is

Picture a small robot dropped into a long, straight hallway. The hallway has ten doorways spaced along it, and between the doors are blank stretches of wall. The robot can roll forward, and it carries one crude sensor: a sideways-looking detector that reports either "I see a door" or "I see a wall." Here is the cruel part — the robot does not know where in the hallway it started. It was set down, switched on, and asked: where am I?

You might think one door-sighting solves it. It does not. The hallway has ten doors, and they all look identical. "I see a door" is consistent with the robot being at any of the ten doorways. A single reading collapses ten possibilities into ten possibilities — it tells the robot "you're at a door," but not which door. This is the whole difficulty of localization, and it is why a robot cannot just trust its latest sensor reading and call it a day.

Common misconception. "The robot just reads its sensor and that's its position." No sensor reads out position directly — a door detector reads "door or wall," a camera reads pixels, a laser reads distances. Position has to be inferred from a stream of such readings, and any single reading is ambiguous (ten identical doors) and noisy (the detector sometimes lies). The robot's job is not to read its position but to maintain a belief about it, sharpening that belief as more readings arrive. That maintained-and-updated belief is the entire subject of this lesson.

So the robot does something cleverer than trusting one reading. It keeps a running guess — not a single number, but a spread of confidence over every place it could possibly be. At the start, knowing nothing, that spread is flat: "I could be anywhere, equally." Then the robot rolls forward a step, sees a door, rolls again, sees a wall, sees a door again — and with each new piece of evidence it reshapes the spread, piling up confidence where the evidence fits and draining it from where it doesn't. After a handful of steps, the flat spread has collapsed into a sharp spike: "I am almost certainly here." The robot found itself not from one reading but from fusing a whole stream of them over time.

That running spread of confidence is called a belief — a probability distribution over every possible position. And the recipe for updating it, one reading at a time, is the recursive Bayes filter: the single most important idea in this entire series, because every estimator you'll meet later (the Kalman filter, the EKF, the UKF, the particle filter) is just a special way of representing and updating this same belief. Learn the Bayes filter and you've learned the skeleton of all of them.

Watch the belief sharpen — before we name a single formula

Let's see it happen before we explain anything. Below is the hallway, drawn as a row of cells. Beneath each cell is a bar — the taller the bar, the more the robot believes it is in that cell. The doors are marked. Press Step to let the robot move one cell and take a reading; watch the flat field of bars buckle and concentrate as evidence accumulates.

A lost robot finds itself — belief over hallway cells

The row of cells is the hallway; orange tabs mark doorways. Each bar is how strongly the robot believes it sits in that cell. It starts flat (knows nothing). Press Step — the robot moves one cell right and senses door/wall — and watch the belief sharpen into a spike. The true (hidden) position is the small dot; notice the belief homes in on it.

flat belief — robot knows nothing yet

For the first step or two the belief stays a mess — several tall bars, because "door, then wall" still fits a few starting points. But keep stepping and the ambiguity dies: only one starting position produces the exact sequence of doors and walls the robot has seen, and the belief converges to a single confident spike on it. The robot never read its position; it computed it, by fusing a sequence of ambiguous readings until only one explanation survived.

The one-sentence thesis of this lesson. A robot's knowledge of where it is lives not in any single sensor reading but in a belief — a probability distribution over all possible states — and the recursive Bayes filter is the rule for updating that belief, one reading at a time, by fusing the prior belief with each new piece of evidence. Do it forever and the belief tracks the truth. That recurring fusion is the engine; everything else in estimation theory is a way of running it efficiently.

Why this is sensor fusion — the link back to Chapter 3

You met fusion already in this series as combining two sensors at one instant — the two-measurement fuse from the earlier lessons, where a GPS reading and an IMU reading were blended by their precisions. The Bayes filter is exactly that idea, but unrolled across time. At every step it fuses two things: the robot's prior belief (everything it has learned from all past readings, rolled into one distribution) and the new measurement (the latest door-or-wall sighting). The prior belief plays the role of "sensor one"; the new reading plays the role of "sensor two." The filter fuses them into a sharper posterior belief, which becomes the prior for the next step.

So the two-measurement fuse you already understand is not a one-off trick — it is the atom of a process the Bayes filter repeats endlessly. Fuse the running belief with the next reading; the result becomes the running belief; fuse again. The Bayes filter is the two-sensor fuse, done recursively, forever. That is why this lesson sits at the heart of a fusion series: it turns instantaneous fusion into fusion-over-time, which is what any real robot actually needs.

Why naive approaches fail — three tempting wrong turns

Before we build the right thing, it pays to see why the obvious shortcuts collapse, because each failure motivates a piece of the filter:

The recursive Bayes filter threads all three needles at once: it keeps a belief (not a point), it accounts for motion (the predict step), and it summarizes all of history in a single distribution of fixed size (recursion). The rest of this lesson builds it from those three requirements.

The plan for this lesson. Chapter 1: what a belief actually is, as a list of numbers that sum to 1. Chapter 2: the predict step — how motion blurs the belief and grows uncertainty (worked by hand on grid cells). Chapter 3: the update step — how a measurement sharpens the belief via Bayes' rule (worked, including the all-important renormalization). Chapter 4: stitching them into the endless predict–update loop. Chapter 5: the Markov assumption that makes it all tractable, and why the loop deserves the name "fusion engine." Chapter 6: the full interactive hallway filter. Then the four recurring chapters: where this ships, how to build it, how it breaks, and where it connects.
A robot in a hallway with ten identical doors senses "door." Why can't it conclude its position from this single reading?

Chapter 1: Belief — The State Is Not a Number, It's a Distribution

We saw the belief sharpen on the screen. Now we make it precise, because everything that follows is just arithmetic on this one object. The central mental shift of this whole field is this: the robot does not store a single guess of where it is — it stores a probability for every place it could be. That collection of probabilities is the belief, written bel(x), read "belief over the state x."

For our hallway with, say, 10 cells, the belief is simply a list of 10 numbers: how likely the robot thinks it is in cell 0, cell 1, …, cell 9. Concretely, at the very start, knowing nothing, the robot spreads its confidence evenly:

bel = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]

Ten cells, each holding probability 0.1 — a uniform distribution, the mathematical statement of "I have no idea, all cells equally likely." Notice the numbers add up to 1.0 (ten times 0.1). That is the one unbreakable law of a belief: the probabilities must always sum to exactly 1, because the robot is somewhere — the total probability of "the robot is in some cell" is, by definition, certainty.

The mental model: a unit of confidence, spread across cells. Imagine you have exactly one cup of water (total probability = 1) and a row of ten glasses (the cells). The belief is how you've poured that one cup among the glasses. Pour it evenly and you've expressed "could be anywhere" (uniform). Pour it all into one glass and you've expressed "I'm certain I'm here" (a spike). You can slosh the water around — that's what predict and update do — but you can never create or destroy water: the total is always one cup. Every operation in this lesson is a rule for sloshing the water while keeping the total at one cup.

The shape of the belief tells you what the robot knows

The shape of the belief distribution is a direct readout of the robot's state of knowledge. Three shapes worth recognizing on sight:

Belief shapeWhat it meansExample
Uniform (flat)total ignorance — equally likely anywhere[0.1, 0.1, …, 0.1] — just switched on
Unimodal spikeconfident in one location, small uncertainty[0, 0, 0.9, 0.1, 0, …] — "almost surely cell 2"
Multimodal (several peaks)narrowed to a few candidates, can't yet decide[0, 0.4, 0, 0, 0.4, 0, 0.2, 0…] — "one of cells 1, 4, or 6"

That third row is the killer feature, and it's why the belief is so much more powerful than a single guess. A single number can only ever say "I think I'm at cell 2." A belief can say "I'm probably at cell 1, 4, or 6, but definitely not anywhere else" — it can honestly represent ambiguity. In the hallway of identical doors, the early belief is exactly this multimodal shape: after one door-sighting, the belief has equal peaks on all ten doorways. No single number could capture that; the belief captures it effortlessly.

Why a distribution beats a point estimate. A point estimate ("I'm at cell 2") is a lie of false confidence — it hides how sure the robot is and erases alternative hypotheses. A belief carries both the best guess (the tallest bar) and the uncertainty (how spread out the bars are) and the alternatives (the other peaks). When you later meet the Kalman filter, its mean is the "best guess" and its variance is the "spread" — it is a belief too, just one restricted to a single-peaked bell shape. The Bayes filter here is the fully general version that can be any shape at all, including the multi-peaked shapes a bell curve can't represent.

Reading a belief, by hand

Let's practice reading one. Suppose after a few steps the robot's belief over six cells is:

bel = [0.05, 0.05, 0.60, 0.20, 0.05, 0.05]

What does this say? Three readings straight off the numbers:

That last check — do the numbers sum to 1? — is the cheapest and most powerful debugging tool in all of filtering. We'll lean on it constantly.

A belief in code — it's just an array

There is no magic here. A belief is a list of non-negative numbers that sum to 1. Creating a uniform belief and checking the conservation law is a few lines:

python
import numpy as np

N = 10                          # ten hallway cells
bel = np.full(N, 1.0 / N)        # uniform: [0.1, 0.1, ..., 0.1]

assert abs(bel.sum() - 1.0) < 1e-9   # the unbreakable law: probabilities sum to 1
best = bel.argmax()              # index of the tallest bar = best guess
conf = bel.max()                 # height of that bar = confidence in the best guess
# a confident, sharp belief instead:
bel_sharp = np.zeros(N); bel_sharp[2] = 0.9; bel_sharp[3] = 0.1   # sums to 1, peaked at cell 2

The whole rest of the lesson is two functions that take this array, slosh the water around, and hand back a new array that still sums to 1. predict() blurs it (Chapter 2); update() sharpens it (Chapter 3). That's the entire Bayes filter — two array operations, applied in a loop.

Below, build your own belief by clicking cells to add or remove confidence, and watch the running total. The widget enforces nothing — it just shows you the sum, so you can feel what it means for a belief to be valid (sum = 1) or broken (sum ≠ 1).

Build a belief — click cells, watch the total

Click a cell to raise its bar (left-click adds, right-click removes a slice of confidence). The number on top of each bar is that cell's probability; the readout shows the running sum. A valid belief sums to 1.00. Try making a flat belief, a single spike, and a two-peak (multimodal) belief — then hit Normalize to see the filter's trick for forcing any pile of numbers back to sum 1.

click cells to build a belief

The Normalize button is worth dwelling on because it reappears in every chapter. Normalizing means: take whatever pile of non-negative numbers you have, add them up to get the total S, then divide every number by S. The result is guaranteed to sum to 1 (because S/S = 1) while preserving the relative heights. It's how the filter forces a broken belief back to legality — and as you'll see in Chapter 3, the update step always produces an un-normalized pile that must be normalized as its final move.

What a belief is, in one box. A belief is a probability distribution over the state: a list of non-negative numbers, one per possible state, that sum to exactly 1. Its tallest bar is the best guess; its spread is the uncertainty; its multiple peaks are honest ambiguity. The robot's entire knowledge of where it is lives in this single object, and the Bayes filter is nothing but a pair of rules for reshaping it as the robot moves and senses.
Why does a robot store a full distribution over positions (a belief) rather than a single best-guess number?

Chapter 2: The Predict Step — Motion Blurs the Belief

The belief is the object; now we learn the first of the two operations that reshape it. Whenever the robot moves, it must update its belief to reflect the move — and here's the counterintuitive part: moving makes the robot more uncertain, not less. The predict step takes the current belief and blurs it, spreading confidence outward, because the robot can never move perfectly. This is the opposite of what a measurement does (which sharpens), and getting clear on this push-pull is the key to the whole filter.

Why does motion add uncertainty? Because the robot's motion is itself noisy. You command "move forward one cell," but in reality the wheels slip a little, the floor is uneven, the command isn't executed perfectly. So after commanding one step, the robot might actually have moved one cell (most likely), or zero cells (slipped and stayed put), or two cells (overshot). The robot doesn't know which happened — so it has to spread its belief to cover all the plausible outcomes of the move.

Common misconception. "If I know where I was and I move forward one step, I know where I am now." Only in a perfect world. Real motion is uncertain: wheels slip, you over- or under-shoot. If you were sure you were in cell 3 and command "forward one," you are now probably in cell 4 — but you might be in cell 3 (slipped) or cell 5 (overshot). Certainty in cell 3 becomes a small spread over cells 3, 4, 5. Every move leaks a little certainty. Left alone, repeated moves with no measurements would eventually wash the belief back toward flat — which is exactly the IMU-drift story from Lesson 1, now told in the language of beliefs.

The motion model — the recipe for how to blur

To blur the belief precisely, the robot needs a motion model: a small table saying, "if I command one step forward, here is the probability I actually moved 0, 1, or 2 cells." Suppose calibration on this robot gives:

P(moved 0 cells) = 0.1,   P(moved 1 cell) = 0.8,   P(moved 2 cells) = 0.1

Read it plainly: 80% of the time the robot moves exactly the commanded one cell, 10% of the time it slips and stays, 10% of the time it overshoots by one. These three numbers sum to 1 (a little distribution of their own — the conservation law applies here too). This table is the robot's honest self-knowledge about how sloppy its own motion is. A precise robot has 0.95 on "moved 1"; a sloppy one might have only 0.6.

Predicting from a spike — the cleanest possible worked example

Start with the simplest belief: the robot is certain it's in cell 3. Its belief is a spike:

bel = [0, 0, 0, 1.0, 0, 0, 0, 0]   (certain: cell 3)

Now command "forward one cell" and apply the motion model. The single unit of confidence sitting in cell 3 gets scattered according to the model: 80% of it lands where it intended (cell 4), 10% stays behind (cell 3, the slip), and 10% overshoots (cell 5). Distribute the 1.0:

OutcomeLands in cellProbability mass
moved 0 (slip)cell 31.0 × 0.1 = 0.1
moved 1 (as commanded)cell 41.0 × 0.8 = 0.8
moved 2 (overshoot)cell 51.0 × 0.1 = 0.1

So the new (predicted) belief is:

bel = [0, 0, 0, 0.1, 0.8, 0.1, 0, 0]   (sum = 1.0 ✓)

Look what happened. The belief went from a perfect spike (certain: cell 3) to a small hill centered on cell 4. The robot's best guess shifted forward by one (good — it did command a move), but its certainty fell: where it had been 100% sure of one cell, it's now only 80% sure of its best cell and admits two neighbors. Motion shifted the belief and flattened it. The hill is wider and shorter than the spike it came from — uncertainty grew. And the numbers still sum to 1 (0.1 + 0.8 + 0.1), because the predict step conserves probability: it moves the water around, never creates or destroys it.

Predict = shift + blur. The predict step does two things at once. It shifts the belief in the direction of motion (the best guess moves forward), and it blurs it (the spread widens, the peak drops). Picture a drop of ink in water that's gently flowing: the whole blob drifts downstream (shift) while diffusing into a wider, fainter cloud (blur). That ink-in-flowing-water image is the predict step exactly. The faster you flow it without ever adding a fresh drop, the more it spreads — which is why a robot that only ever moves (and never senses) slowly loses all confidence.

Predicting from a spread-out belief — convolution, by hand

The spike was easy because all the confidence sat in one cell. What if the belief is already spread out? Then every occupied cell scatters its own confidence forward by the motion model, and the scattered piles overlap and add up. Let's do a real one. Suppose the current belief over eight cells is two lumps:

bel = [0, 0, 0.5, 0.5, 0, 0, 0, 0]   (half in cell 2, half in cell 3)

Command "forward one" with the same motion model (0.1 / 0.8 / 0.1 for move 0 / 1 / 2). Scatter each lump separately, then add. The cell-2 lump (0.5) scatters to cells 2, 3, 4; the cell-3 lump (0.5) scatters to cells 3, 4, 5:

Source→ cell 2→ cell 3→ cell 4→ cell 5
cell 2's 0.5 scatters0.5×0.1 = 0.050.5×0.8 = 0.400.5×0.1 = 0.05
cell 3's 0.5 scatters0.5×0.1 = 0.050.5×0.8 = 0.400.5×0.1 = 0.05
column total0.050.450.450.05

Add the columns and the predicted belief is:

bel = [0, 0, 0.05, 0.45, 0.45, 0.05, 0, 0]   (sum = 0.05+0.45+0.45+0.05 = 1.0 ✓)

Two sharp half-lumps became a single broad hill draped over four cells, its center shifted forward by one. This overlapping-and-adding operation has a name: convolution — you're sliding the motion model across the belief and summing the overlaps. You don't need the word to do it; the table is the convolution, computed by hand. Notice again: the peaks dropped (0.5 became 0.45) and the spread grew (from 2 cells to 4). Motion always does this.

The predict equation, named

What you just did by hand has a one-line formula. The predicted belief at cell x is the sum, over every cell x′ the robot could have come from, of "how much I believed I was at x′" times "the chance a move from x′ lands me at x":

bel̅(x) = ∑x′ P(x | x′, u) · bel(x′)

where bel̅(x) (the bar on top means "predicted, before any measurement") is the new confidence in cell x; bel(x′) is the old confidence in cell x′; P(x | x′, u) is the motion model — the probability that commanding motion u from cell x′ lands you in cell x; and the x′ means "add up the contributions from all the cells you might have come from." That's exactly the table arithmetic: for each destination cell, sum the slices that flowed into it. The formula looks heavy; the operation is just "scatter every lump forward and add up the overlaps."

Why sum over where you came from? To know the chance you're now in cell 4, you must consider every way to arrive at cell 4: from cell 3 by moving 1, from cell 2 by moving 2, from cell 4 by slipping (moving 0). Each route contributes "(belief you were there) × (chance that move lands you in 4)." Adding all routes gives the total predicted belief in cell 4. The sum is just bookkeeping over all the paths into each cell — the law of total probability, applied cell by cell.

Predict, applied twice — watch certainty drain

One predict step gently softened the spike; the real lesson is what happens when you predict again without sensing, because that's the situation of a robot rolling through a long blank corridor with nothing to look at. Take the hill we just made and run a second "forward one" through the same motion model. The hill from cell 3–5 (heights 0.1 / 0.8 / 0.1) scatters once more — each cell spreading its mass to itself, +1, and +2:

Source (after 1st predict)→ here (0.1)→ +1 (0.8)→ +2 (0.1)
cell 3 holds 0.1cell 3: 0.01cell 4: 0.08cell 5: 0.01
cell 4 holds 0.8cell 4: 0.08cell 5: 0.64cell 6: 0.08
cell 5 holds 0.1cell 5: 0.01cell 6: 0.08cell 7: 0.01

Summing each destination column gives the belief after the second predict:

bel = […, 0.01(c3), 0.16(c4), 0.66(c5), 0.16(c6), 0.01(c7), …]   (sum = 0.01+0.16+0.66+0.16+0.01 = 1.0 ✓)

Track the peak across the three states: it went 1.00 → 0.80 → 0.66, and the spread went from 1 cell → 3 cells → 5 cells. Two moves, no sensing, and the robot's best-cell confidence has nearly halved while its uncertainty has fanned out across five cells. Keep predicting and this continues relentlessly — the peak keeps falling, the spread keeps widening, until after enough blank-corridor steps the belief is nearly flat and the robot is, for all practical purposes, lost. This is the exact "IMU drifts during the tunnel" story from Lesson 1, retold in beliefs: motion without correction is a slow leak of certainty, and the rate of the leak is set by your motion noise. It is also why the next chapter exists — the update step is the only thing that refills the certainty a long corridor drains.

The predict step in code

The whole convolution is a short loop — for each destination cell, gather contributions from the cells that could move into it:

python
import numpy as np

# motion model: P(actually moved k cells) for k = 0, 1, 2  (after commanding "forward 1")
MOTION = {0: 0.1, 1: 0.8, 2: 0.1}

def predict(bel):
    N = len(bel)
    pred = np.zeros(N)
    for src in range(N):              # every cell the robot might have come from
        if bel[src] == 0: continue
        for k, p in MOTION.items():     # scatter that cell's mass forward by k
            dst = min(src + k, N - 1)   # clamp at the wall (mass piles up at the end)
            pred[dst] += bel[src] * p    # overlaps from different sources ADD
    return pred                       # still sums to 1 (motion conserves probability)

predict(np.array([0,0,0,1.,0,0,0,0]))   # -> [0,0,0, 0.1, 0.8, 0.1, 0,0]   spike -> hill
predict(np.array([0,0,.5,.5,0,0,0,0]))  # -> [0,0, .05, .45, .45, .05, 0,0]  two lumps -> one hill

Run the first call in your head and you reproduce the spike-to-hill table; the second reproduces the convolution table. Both still sum to 1, because each source cell's mass is fully redistributed (the motion model's slices sum to 1) and nothing leaks out — except at the wall, where the min(..., N-1) clamps overshoot back into the last cell, piling mass there (a real robot can't roll past the end of the hallway).

Below, watch the predict step act on a belief you choose. Set a starting belief (spike or spread), pick how noisy the motion is, and press Predict repeatedly to watch the hill march forward and flatten. With many predicts and no measurements, see the belief wash out toward uniform — pure drift.

The predict step — motion shifts and blurs the belief

Pick a starting belief, then press Predict to apply one "forward one cell" move. The belief bars shift right and spread out (the peak drops). The motion-noise slider sets how much the robot slips/overshoots — more noise blurs faster. Press Predict many times with no measurement and watch the belief drift toward flat: moving without sensing destroys certainty.

Motion noise 0.1
pick a starting belief, then press Predict
The signature of the predict step. Triggered by motion. It convolves the belief with the motion model: every cell scatters its confidence forward and the scatters add up. The effect is always shift (best guess moves with the motion) plus blur (uncertainty grows, peak drops). Probability is conserved (still sums to 1). Run predict alone, forever, and the belief decays toward uniform — this is why a robot needs measurements to inject fresh certainty, which is the next chapter.
A robot is certain it's in cell 3 (belief = a spike). It commands "forward one cell." What does the predict step do to the belief, and why?

Chapter 3: The Update Step — A Measurement Sharpens the Belief via Bayes' Rule

Predict blurred the belief; now the second operation pulls it back into focus. Whenever the robot senses something — "I see a door" — it must fold that evidence into its belief, and the effect is the mirror image of predict: a measurement sharpens the belief, concentrating confidence where the reading fits and draining it from where it doesn't. This is the fusion step — it fuses the prior belief (everything known before) with the new sensor evidence (the latest reading) — and it runs on Bayes' rule, the deepest idea in the lesson and the one the whole filter is named for.

Here is the intuition before any formula. The robot has a belief — some spread over cells. Then it senses "door." Some of those cells are doorways; others are blank wall. The cells that are doorways are now more plausible (the reading agrees with them); the wall cells are less plausible (the reading disagrees). So the update step multiplies each cell's prior belief by how well that cell explains the reading, then renormalizes. Cells that fit the evidence get boosted; cells that don't get suppressed. The belief sharpens around the evidence.

The mental model: a prosecutor weighing witnesses. Your prior belief is your hunch about who committed a crime (spread over suspects). Then a witness says "the culprit was tall." You don't throw away your hunch and you don't ignore the witness — you combine them: every suspect's prior plausibility gets multiplied by how well they match "tall." A short suspect you'd strongly suspected gets knocked down; a tall suspect you'd barely considered gets boosted. After the witness, you renormalize so your suspicions sum to certainty again. That's Bayes' rule: posterior ∝ prior × how-well-each-explains-the-evidence. The robot does this with cells and door-sightings instead of suspects and witnesses.

The measurement model — how trustworthy is the sensor?

Just as predict needed a motion model, update needs a measurement model: a table of how the sensor behaves. The door detector is imperfect — it usually reads correctly but occasionally lies. Suppose calibration gives:

Truth at this cellSensor says "door"Sensor says "wall"
cell IS a door0.8 (correct)0.2 (missed it)
cell IS a wall0.3 (false alarm)0.7 (correct)

Read this carefully — it's the heart of the update. If a cell really is a doorway, the sensor reports "door" 80% of the time and falsely reports "wall" 20% of the time (it missed the door). If a cell really is wall, the sensor correctly says "wall" 70% of the time but cries "door" 30% of the time (a false alarm — maybe a poster looked like a door). These are the sensor's honest error rates. A great sensor would have 0.99 on the diagonal; our crummy detector has 0.8 and 0.7, so a single reading is suggestive, not decisive — which is exactly why we need to fuse many of them.

One update, fully worked — the door-sighting

Let's run a complete update with real numbers. Take a 6-cell hallway where cells 1 and 4 are doors (the rest are wall). Suppose after a predict the robot's belief is the broad hill:

prior bel = [0.05, 0.25, 0.20, 0.20, 0.25, 0.05]   (cells 1 and 4 are doors)

Now the sensor reports "door." We apply Bayes' rule cell by cell: multiply each cell's prior by the probability the sensor would say "door" if the robot were in that cell. For door cells (1 and 4) that probability is 0.8; for wall cells (0, 2, 3, 5) it's 0.3 (the false-alarm rate). Multiply:

CellTypePriorP("door" | this cell)Prior × likelihood (un-normalized)
0wall0.050.30.05 × 0.3 = 0.015
1door0.250.80.25 × 0.8 = 0.200
2wall0.200.30.20 × 0.3 = 0.060
3wall0.200.30.20 × 0.3 = 0.060
4door0.250.80.25 × 0.8 = 0.200
5wall0.050.30.05 × 0.3 = 0.015

The door cells (1 and 4) shot up to 0.200; the wall cells stayed small. But here's the catch — these numbers no longer sum to 1. Add them: 0.015 + 0.200 + 0.060 + 0.060 + 0.200 + 0.015 = 0.550. We've broken the conservation law! Multiplying by likelihoods shrank the total (because the likelihoods are all below 1). We must renormalize: divide every entry by the total 0.550 to force the sum back to 1.

Renormalization — the step everyone forgets, worked out

Divide each un-normalized value by the total S = 0.550:

CellUn-normalized÷ S = 0.550Posterior belief
00.0150.015 / 0.5500.027
1 (door)0.2000.200 / 0.5500.364
20.0600.060 / 0.5500.109
30.0600.060 / 0.5500.109
4 (door)0.2000.200 / 0.5500.364
50.0150.015 / 0.5500.027

The posterior belief is:

posterior bel = [0.027, 0.364, 0.109, 0.109, 0.364, 0.027]   (sum = 1.0 ✓)

Look what the "door" reading did. Before, the belief was a broad hill with no strong opinion (the biggest bars, 0.25, sat on cells 1 and 4 but cells 2 and 3 were nearly as tall). After, the two door cells (1 and 4) leap to 0.364 each — the reading boosted them — while the wall cells collapsed to 0.027–0.109. The belief sharpened toward the doors. But notice it's still bimodal: cells 1 and 4 are tied at 0.364, because one door-sighting can't distinguish two identical doors. The robot now knows "I'm at a door, probably cell 1 or 4" — honest ambiguity, exactly the multimodal shape from Chapter 1. It will take a later reading (after moving) to break the tie. That's the recursion of Chapter 4.

Update = multiply by likelihood, then renormalize. Two moves, always in this order. (1) Multiply each cell's prior belief by the likelihood — the probability the sensor would produce the reading it produced if the robot were in that cell. This boosts cells that explain the reading and suppresses cells that don't. (2) Renormalize — divide everything by the new total so the belief sums to 1 again. Forgetting step (2) is the single most common Bayes-filter bug; your belief silently stops being a probability distribution and every downstream number is wrong. We'll hunt this exact bug in Chapter 9.

Bayes' rule, named — and why it's exactly what we just did

What we computed by hand is Bayes' rule, the formula that tells you how to revise a belief in light of evidence. Stated for our cells:

bel(x) = η · P(z | x) · bel̅(x)

where bel̅(x) is the prior (the predicted belief from Chapter 2 — what you believed before this reading); P(z | x) is the likelihood — the probability of getting reading z (here "door") if the true state were cell x (this is the measurement model, 0.8 for doors, 0.3 for walls); bel(x) is the posterior — the updated belief after folding in the reading; and η (eta) is the normalizer — the number you divide by to make the posterior sum to 1. In our example η = 1/0.550 = 1.818, and dividing by 0.550 is the same as multiplying by 1.818. Map it to the tables: "prior × likelihood" is the multiply step, "η" is the renormalize step. The famous equation is the two-column arithmetic you just did.

Common misconception. "The likelihood P(z|x) is the probability the robot is at x." Backwards — and the backwardness is the whole subtlety of Bayes. P(z|x) reads "the probability of the reading, given you're at x" — it's a property of the sensor (how often it cries 'door' from a door vs. a wall), computed for a hypothetical position. What you actually want, P(x|z) ("the probability you're at x, given the reading"), is the posterior — and Bayes' rule is precisely the machine that flips the sensor's P(z|x) into the answer P(x|z) you want, by weighting it with your prior and renormalizing. Confusing the two directions is the classic Bayes error; the rule exists exactly to convert one into the other.

A second worked update — a "wall" reading does the reverse

To make sure the mechanism, not the example, is what stuck, run the opposite case on the same prior. Same 6-cell hallway (doors at 1 and 4), same prior bel = [0.05, 0.25, 0.20, 0.20, 0.25, 0.05] — but this time the sensor reports "wall." Now the likelihoods flip: a door cell produces "wall" with probability 0.2 (it missed the door), a wall cell produces "wall" with probability 0.7 (correct). Multiply each prior by that:

CellTypePriorP("wall" | this cell)Prior × likelihood
0wall0.050.70.035
1door0.250.20.050
2wall0.200.70.140
3wall0.200.70.140
4door0.250.20.050
5wall0.050.70.035

The un-normalized total is S = 0.035 + 0.050 + 0.140 + 0.140 + 0.050 + 0.035 = 0.450. Renormalize (divide each by 0.450):

posterior bel = [0.078, 0.111, 0.311, 0.311, 0.111, 0.078]   (sum = 1.0 ✓)

Exactly the mirror image of the "door" case: now the wall cells (2 and 3) are boosted to 0.311 while the door cells are suppressed. The reading "wall" pushed belief away from the doors and toward the walls — the same machine (multiply by likelihood, renormalize), just fed the other reading. Notice the door cells didn't vanish: 0.111 each, not 0, because the sensor sometimes misses a door (likelihood 0.2, not 0). An honest sensor model leaves a sliver of belief on the "I might have just missed a door" hypothesis — and that sliver is exactly what saves the filter from the overconfidence trap of Chapter 9.

Why a flaky sensor barely moves the belief — worked out

One more numerical point, because it explains the sensor-reliability slider in the widget below and a deep truth about fusion. What if the sensor is nearly useless — it reports "door" 55% of the time at a door and 45% of the time at a wall (barely better than a coin flip)? Take a flat prior bel = [0.5, 0.5] over two cells (one door, one wall) and update on "door":

door cell: 0.5 × 0.55 = 0.275,   wall cell: 0.5 × 0.45 = 0.225   (sum S = 0.5)
posterior = [0.275/0.5, 0.225/0.5] = [0.55, 0.45]

The belief barely budged — from 50/50 to 55/45. A near-worthless reading produces a near-worthless update, and that's correct: a sensor whose "door" and "wall" likelihoods are nearly equal carries almost no information, so it should leave the belief almost unchanged. Contrast a great sensor (0.95 vs 0.05): the same flat prior would update to [0.95, 0.05] — one reading nearly nails it. The amount a measurement sharpens the belief is set entirely by how different its likelihoods are across the states — the gap between "what this reading looks like here vs. there." This is the discrete cousin of the inverse-variance weighting you met earlier in the series: a precise sensor (likelihoods far apart) gets a loud vote; a sloppy one (likelihoods close together) is politely ignored, automatically, by the arithmetic. No special-casing — the multiply does the down-weighting for free.

The update step in code

Update is even shorter than predict — an element-wise multiply followed by a divide-by-sum:

python
import numpy as np

DOORS = {1, 4}     # which cells are doorways
# measurement model P(reading | cell type)
P_DOOR = {'door': 0.8, 'wall': 0.2}   # if cell IS a door
P_WALL = {'door': 0.3, 'wall': 0.7}   # if cell IS a wall

def update(bel, reading):
    N = len(bel)
    post = np.zeros(N)
    for x in range(N):
        like = (P_DOOR if x in DOORS else P_WALL)[reading]   # P(z | x): the likelihood
        post[x] = bel[x] * like        # Bayes: prior x likelihood (un-normalized)
    post /= post.sum()                 # RENORMALIZE -- the step everyone forgets!
    return post

bel = np.array([.05,.25,.20,.20,.25,.05])
update(bel, 'door')   # -> [.027, .364, .109, .109, .364, .027]  doors boosted, sum=1

The two lines post[x] = bel[x]*like and post /= post.sum() are the entire update step — multiply by the likelihood, then normalize. The post.sum() divisor is exactly the 0.550 we computed by hand; dividing by it is exactly η. Reproduce the call by hand and you get the posterior table above.

Below, drive the update step interactively. Start from a belief, choose whether the robot sees a door or a wall, and watch the bars over door-cells and wall-cells get boosted or suppressed, then snap back to summing 1 after renormalization. Toggle the sensor reliability slider to see a trustworthy sensor sharpen hard and a flaky one barely move the belief.

The update step — a reading sharpens the belief (Bayes' rule)

Cells with the orange tab are doors. Pick a reading (see door or see wall) and press Update: cells matching the reading get boosted, the rest suppressed, then everything is renormalized to sum 1. The sensor-reliability slider sets how trustworthy the detector is — a reliable sensor sharpens the belief hard; a flaky one barely changes it (it's not worth listening to).

Sensor reliability 0.8
pick a reading and press Update
The signature of the update step. Triggered by a measurement. It multiplies the (prior) belief by the likelihood P(z|x) — boosting cells that explain the reading, suppressing those that don't — then renormalizes (divides by η) to restore sum-to-1. The effect is always to sharpen: uncertainty falls, peaks rise. This is the fusion step — it fuses the prior belief with new sensor evidence. A reliable sensor sharpens hard; a flaky one (likelihoods near each other for door vs. wall) barely changes the belief, which is correct — a uninformative reading should leave you about where you were.
In the update step, after multiplying each cell's prior belief by the sensor likelihood P(z|x), what crucial final operation must you perform, and what goes wrong if you skip it?

Chapter 4: The Recursive Loop — Predict, Update, Forever

We have the two operations. Predict blurs (motion adds uncertainty); update sharpens (a measurement removes it). The recursive Bayes filter is simply these two, alternating, in a loop that never ends. Every time the robot moves, run predict; every time it senses, run update; the belief that comes out of one step is the belief that goes into the next. That feeding-the-output-back-in is what "recursive" means, and it is the source of all the filter's power.

Here is the heartbeat, drawn as a cycle:

START: belief bel(x)
everything known so far, rolled into one distribution
↓ robot moves
PREDICT
convolve with motion model — shift & blur; uncertainty GROWS
↓ robot senses
UPDATE
multiply by likelihood P(z|x), renormalize; uncertainty SHRINKS
↻ the new belief becomes the START of the next cycle

The two steps are in perpetual tension, and that tension is the whole machine. Predict pushes the belief toward chaos (every move leaks certainty); update pulls it back toward order (every reading injects certainty). When the robot moves a lot and senses rarely, predict wins and the belief spreads — the robot gets lost. When it senses often, update wins and the belief stays sharp — the robot stays found. A healthy filter keeps the two roughly balanced, so the belief tracks the truth without either washing out or freezing.

The heartbeat analogy. Think of the belief as a lung. Predict is the inhale — it expands the belief, drawing in uncertainty. Update is the exhale — it contracts the belief, expelling uncertainty as the measurement firms things up. The filter breathes: expand, contract, expand, contract, one breath per move-and-sense. A robot that only moves (predicts) is holding an endless inhale until it bursts into total uncertainty; one that only senses the same spot forever (updates) holds an exhale until it's rigidly — and possibly wrongly — certain. Life is in the rhythm of both.

Two full cycles, worked by hand — breaking the door tie

The single update in Chapter 3 left the robot stuck between two identical doors (cells 1 and 4, tied at 0.364). Watch how one more cycle — predict, then update — breaks the tie. This is the payoff of recursion: information from a new reading, combined with the memory of the old, resolves what neither could alone.

Where we left off (posterior after the first "door" reading):

bel = [0.027, 0.364, 0.109, 0.109, 0.364, 0.027]   (tied on cells 1 and 4)

Cycle 2, step PREDICT (robot moves forward one cell). Apply the motion model (0.1/0.8/0.1 for move 0/1/2). Each cell scatters forward. The two big lumps at cells 1 and 4 each become little hills:

Carrying the small terms and summing per destination cell (using an 8-cell hallway now so nothing falls off), the predicted belief comes to approximately:

bel̅ ≈ [0.003, 0.058, 0.305, 0.135, 0.135, 0.305, 0.058, 0.003]   (sum = 1.0 ✓)

The two peaks shifted forward (to roughly cells 2 and 5) and blurred — exactly the predict signature. Still bimodal, still ambiguous. But now the robot is about to sense again, and crucially, the hallway looks different at the two candidate locations.

Cycle 2, step UPDATE (robot now senses "wall"). Here's the magic. Suppose in this hallway cell 2 is a wall but cell 5 is a door. The robot reads "wall." Apply the measurement model: multiply each cell by P("wall" | cell). For wall cells P("wall") = 0.7; for door cells P("wall") = 0.2 (the sensor missing a door). Crucially, the peak near cell 2 (a wall) gets multiplied by 0.7 — boosted relative to the peak near cell 5 (a door), which gets multiplied by only 0.2. The two formerly-tied peaks now diverge:

CellTypePredicted belP("wall"|cell)Product
2wall0.3050.70.213
5door0.3050.20.061
(every cell multiplied similarly — wall cells ×0.7, the cell-5 door ×0.2; full sum of all products S ≈ 0.55)

After renormalizing by S ≈ 0.55, the cell-2 peak (a wall, matching the "wall" reading) emerges at ≈0.39 — three and a half times taller than the cell-5 peak (a door, contradicting the reading), which sinks to ≈0.11. The posterior is now dominated by one peak near cell 2. The tie is broken. The robot has localized itself, and it did so by combining the memory of the first "door" reading (which set up the two candidates) with the new "wall" reading (which distinguished them) — threaded together by the predict step that accounted for the move between them. No single reading could do this; the sequence did.

This is why it must be recursive. The first "door" reading produced two equal hypotheses (cells 1 and 4). Throwing that reading away and trusting only the latest "wall" reading would lose the very memory that set up the candidates. Keeping a separate score for every possible path would explode combinatorially. The recursive filter does neither: it folds the first reading into the belief, carries that belief through the move (predict), then folds in the second reading (update). All of history lives in the current belief, compressed into a fixed-size array — so the work per step is constant no matter how long the robot has been running. Recursion is what makes lifelong localization affordable.

The whole filter in code — the loop is four lines

Stitching predict and update from the last two chapters into the recursive loop is almost anticlimactic — it's a for loop over the stream of (move, reading) events:

python
import numpy as np

bel = np.full(N, 1.0/N)        # start flat: total ignorance

for (moved, reading) in stream:    # the robot's life: a sequence of moves & sensings
    if moved:
        bel = predict(bel)         # motion -> blur (Chapter 2)
    if reading is not None:
        bel = update(bel, reading) # measurement -> sharpen (Chapter 3)
    # bel is now the posterior; it becomes the prior for the next iteration
    best_cell = bel.argmax()       # current best guess of position

That's the entire recursive Bayes filter. predict and update are the two functions you already wrote; the loop just calls them as moves and readings arrive, feeding each output back in as the next input. The fixed-size bel array is the memory of all of history — nothing else is stored. Every filter in the rest of this series (Kalman, EKF, UKF, particle) is this exact loop with a different internal representation of bel.

Below, run the loop manually so you can see the breathing. Press Predict to inhale (blur), Update to exhale (sharpen), or Auto-cycle to watch the filter run itself — predict, update, predict, update — while the belief tracks the moving robot. Watch the belief widen on every predict and snap back on every update.

The recursive loop — predict / update breathing

Step the filter by hand: Predict moves the robot one cell and blurs the belief; Update takes a reading and sharpens it. Or press Auto-cycle to alternate them automatically. The bars are the belief; the dot is the true position; the readout names which step just ran and the belief's current peak. Notice the rhythm: widen, snap, widen, snap — and the belief stays glued to the truth.

flat belief — press Predict or Update to begin the loop
The recursive loop, in one box. bel → predict (blur, on motion) → bel̅ → update (sharpen, on measurement) → bel → repeat. The output of each cycle is the input to the next; the belief carries the entire history in fixed-size form. Predict and update are in permanent tension — one adds uncertainty, the other removes it — and the filter tracks the truth by alternating them. This loop is the universal skeleton of every recursive estimator you will ever build.
What does it mean that the Bayes filter is "recursive," and why is that essential?

Chapter 5: The Markov Assumption — And Why This Is a Fusion Engine

The recursive loop hides an assumption so quiet you may not have noticed it, yet the whole filter would be impossible without it. When we said "the current belief is all the memory you need" — that all of history compresses into one fixed-size distribution — we leaned hard on a promise: the future depends only on the present, not on the whole past. That promise is the Markov assumption, and it is the load-bearing wall of every recursive filter.

Stated precisely for our robot: given where the robot is right now (the current state), where it goes next depends only on its current position and its next move — not on the tangled path it took to get here. And given where the robot is right now, what it senses depends only on its current position — not on what it sensed five steps ago. The present state "screens off" the past: once you know the state now, the past tells you nothing extra about the future.

The Markov assumption in one image. Imagine the robot's position is written on a sticky note. The Markov assumption says: everything the robot needs to predict its future is on that one sticky note. You could burn the entire logbook of where it has been — every past position and reading — and as long as you keep the current sticky note, you lose nothing about what happens next. That's why the belief (a distribution over "what's on the sticky note") is sufficient memory: the past influences the future only through the present, and the belief already captures everything we know about the present.

Why the filter needs the Markov assumption

Trace it through the loop and you'll see the dependence. The predict step used a motion model P(x | x′, u) — "where I land depends only on where I was (x′) and what I commanded (u)." It did not condition on the cell before x′, or the one before that. The update step used a measurement model P(z | x) — "what I sense depends only on where I am now (x)." It did not condition on past readings. Both models are Markov by construction. Without the Markov assumption, the motion model would have to depend on the entire history of positions, and the measurement model on the entire history of readings — and then the belief could not be a fixed-size summary, because the past would keep mattering. The combinatorial path-explosion we dodged in Chapter 4 would come roaring back.

So the Markov assumption is exactly the property that makes recursion legal. It's the formal statement of "the current belief is enough." Drop it and you lose the filter; keep it and the belief is a perfect, complete summary of the past for the purpose of predicting the future.

Common misconception — and when Markov breaks. "The Markov assumption is always true." No — it's an assumption, a modeling choice, and it can be violated, which causes real bugs. If your state is "just position" but the robot's wheels have a slowly-drifting bias (so where it goes next depends on a hidden trend, not just the current position), the process isn't Markov in position alone — and the filter will be subtly wrong. The classic fix is to enlarge the state: fold the hidden quantity (the bias, or the velocity, or the wheel-slip rate) into the state vector, so the augmented state is Markov again. "Make it Markov by adding to the state" is one of the most important design moves in all of estimation, and we'll see it explicitly when the Kalman filter adds velocity to position in Lesson 5.

Why this loop deserves the name "fusion engine," not just "estimator"

Now the deepest point of the lesson, the one that earns its place in a fusion series. It's tempting to call the Bayes filter merely an "estimator" — a thing that guesses the robot's position. It is far more than that. Every single update step is an act of fusion, and the filter is fusing continuously, forever.

Recall what update does: it takes the prior belief (bel̅, everything learned from all past readings) and fuses it with the new measurement (the latest reading's likelihood P(z|x)) to produce a sharper posterior. That is structurally identical to the two-sensor fuse from earlier in this series — combine source A with source B, weighting each by its reliability, to get something better than either. Here source A is "the accumulated past" and source B is "the present reading." The Bayes filter is the two-measurement fuse, with one of the two measurements being the fused result of all previous measurements.

The filter is recursive fusion. Each update fuses the prior belief (the rolled-up result of fusing every past reading) with one new reading. The posterior — the fusion of "all of history" with "now" — becomes the prior for the next step, where it's fused with the next reading. So the Bayes filter doesn't fuse two things once; it fuses the entire stream of measurements, two at a time, endlessly, each new reading folded into the running total. It is the instantaneous fuse of Chapter 3 of this series, unrolled across time into a perpetual-motion fusion machine. That is why it's the engine of fusion over time.

Fusing two different sensors — the filter does it for free

Here's a beautiful consequence that makes the "fusion engine" claim concrete. Suppose the robot has two sensors: the door detector and a noisy light sensor (bright near windows, dark elsewhere). How do you fuse them? You don't need new machinery — you just run an update step for each, back to back, on the same belief. Update with the door reading (multiply by P(door-reading | x), renormalize); then update with the light reading (multiply by P(light-reading | x), renormalize). Because update is "multiply by a likelihood," fusing two sensors is just multiplying by two likelihoods in sequence. The math:

bel(x) = η · P(zlight | x) · P(zdoor | x) · bel̅(x)

Both readings' likelihoods multiply into the belief, weighting each cell by how well it explains both sensors at once. A cell that fits the door reading but contradicts the light reading gets boosted by one factor and crushed by the other — net, it's penalized for disagreeing with one sensor. The Bayes filter fuses arbitrarily many sensors by simply chaining their update steps, and it fuses them over time by chaining updates across cycles. One mechanism — multiply by likelihood, renormalize — handles both multi-sensor fusion (within a step) and temporal fusion (across steps). That unification is why the Bayes filter sits at the foundation of everything in this series.

Fusing two sensors, worked by hand

Let's make the two-sensor fuse concrete with numbers, because watching it disambiguate is the whole "fusion engine" claim in miniature. Four cells. The door detector says "door"; cells 1 and 3 are doors, with the usual P("door"|door)=0.8, P("door"|wall)=0.3. A second sensor — a light detector — says "bright"; only cell 3 is near a window, with P("bright"|window)=0.9, P("bright"|dark)=0.2. Start from a flat prior and fuse both readings, one update at a time.

Step 0 — flat prior:

bel = [0.25, 0.25, 0.25, 0.25]   (doors at 1, 3; window at 3)

Update 1 — the "door" reading. Multiply each cell by P("door"|cell): door cells (1, 3) by 0.8, wall cells (0, 2) by 0.3:

un-norm = [0.25×0.3, 0.25×0.8, 0.25×0.3, 0.25×0.8] = [0.075, 0.200, 0.075, 0.200]

Sum = 0.55; renormalize → bel = [0.136, 0.364, 0.136, 0.364]. Tied between the two doors (1 and 3), exactly as a single door-sighting always leaves us. Now fuse the second sensor into this belief.

Update 2 — the "bright" reading. Multiply each cell by P("bright"|cell): the window cell (3) by 0.9, the dark cells (0, 1, 2) by 0.2:

un-norm = [0.136×0.2, 0.364×0.2, 0.136×0.2, 0.364×0.9] = [0.027, 0.073, 0.027, 0.328]

Sum = 0.455; renormalize → bel = [0.060, 0.160, 0.060, 0.720] (sum = 1.0 ✓). Look what just happened: the door reading alone left cells 1 and 3 tied at 0.364, but the light reading broke the tie in one step — cell 3 (a door and near a window) shot to 0.720 while cell 1 (a door but in the dark) fell to 0.160. Two sensors, fused, pinned the position that neither could pin alone. The door sensor said "you're at a door," the light sensor said "and you're by the window," and only cell 3 satisfies both. That is fusion in its purest form — and the filter did it with nothing but two multiplications and two renormalizations, the exact same update step run twice.

Multi-sensor fusion is just back-to-back updates. To fuse N sensors at one instant, run N update steps in a row on the same belief — multiply by each sensor's likelihood, renormalize once at the end (or after each; the answer is identical). To fuse over time, run the predict–update loop. There is no separate "fusion module" — the update step is the fusion operator, and you invoke it once per sensor per instant. A robot with a door detector, a light sensor, a wheel encoder, and a laser fuses all four by chaining four updates; the same code that fuses one sensor fuses any number.
Where every classical filter comes from. Because update is "multiply likelihoods," the order in which two independent readings arrive doesn't change the answer (multiplication commutes) — a tiny fact with huge consequences, because it means the filter can fuse sensors that report at different rates without special-casing. And because the belief is a full distribution, you're free to represent it however is convenient: as a histogram over cells (this lesson), as a single Gaussian bump (the Kalman filter, Lesson 5 — fast, but only one peak allowed), or as a cloud of sample points (the particle filter, Lesson 10 — any shape, at a cost). All three are the same Bayes filter; they differ only in how they store and reshape bel(x). Learn this loop and you've learned the parent of the entire family.
What does the Markov assumption state, and why does the recursive Bayes filter depend on it?

Chapter 6: Showcase — The Full 1-D Hallway Bayes Filter

Everything from chapters 0–5 now runs at once, live, with you at the controls. This is the payoff: a complete recursive Bayes filter localizing a robot in a hallway of doors and walls. You drive the robot; it predicts on every move and updates on every sensing; you watch the belief breathe — widen on motion, sharpen on sensing — and converge (or stay stuck) depending on the hallway and the noise. No new theory here, just every idea made visible and breakable in one machine.

This is the entire lesson in one sim. The belief bars are bel(x) from Chapter 1. Pressing Drive runs the Chapter-2 predict step (watch the bars shift right and blur). Each sensing runs the Chapter-3 update step (watch the bars sharpen toward door- or wall-cells). The alternation is the Chapter-4 loop; the fact that the current belief is all the robot remembers is the Chapter-5 Markov property; and every update fusing past-with-present is the recursive fusion that makes this the engine of the series. The noise sliders let you break it.
Drive the robot — watch the belief localize

The top strip is the hallway (orange = door, gray = wall); the green dot is the robot's true (hidden) position. The bars below are the belief over cells. Press Drive 1 step (predict + sense + update one cycle) or Auto-drive to loop. Motion noise controls how much each move blurs the belief; sensor noise controls how much each reading sharpens it. Start from a flat belief and watch it converge to a spike on the true cell — or crank the noise and watch it struggle, stay multimodal, or drift.

Motion noise 0.15 Sensor noise 0.2
flat belief — press Drive to start localizing

A guided experiment — reproduce every claim in five drives

Don't just watch — falsify. Run this sequence and confirm each prediction with your own eyes:

  1. Reset to flat, set both noise sliders low, press Auto-drive. The belief starts a flat field of equal bars (total ignorance, Chapter 1). Within a few steps it collapses to one tall spike riding on the green dot — the filter localized from nothing by fusing readings over time (Chapter 0's thesis, live).
  2. Watch a single cycle in slow motion (Drive 1 step, repeatedly). Each step you'll see the bars first widen (the predict half — motion blur, Chapter 2) then snap sharper (the update half — the reading, Chapter 3). Widen, snap, widen, snap: the breathing of Chapter 4.
  3. Crank motion noise to max, keep driving. The belief now blurs faster than the sensor can sharpen it — the spike fattens, sometimes nearly washing out to flat between readings. This is predict winning the tug-of-war: a robot that moves sloppily and senses rarely loses confidence (Chapter 2's drift warning).
  4. Drop motion noise, crank sensor noise to max. Now each reading barely moves the belief (a flaky sensor has near-equal likelihoods for door and wall), so localization is slow and the belief stays broad — the update step can't sharpen with an untrustworthy sensor (Chapter 3's reliability slider, live).
  5. With a converged spike, press "Kidnap robot." The true position teleports elsewhere, but the belief stays confidently spiked on the old spot — now wrong. Watch how many steps it takes the filter to recover (it must accumulate enough contradicting readings to overcome its false confidence). This is the famous kidnapped-robot problem, and it foreshadows the overconfidence failure mode of Chapter 9.

The crucial thing to feel is the tension between the two noise sliders. They are the two halves of the loop fighting: motion noise is predict's strength (blurring), sensor reliability is update's strength (sharpening). When sharpening beats blurring, the belief converges and stays sharp. When blurring beats sharpening, the belief spreads and the robot drifts toward lost. A well-tuned filter keeps them balanced — which, as Chapter 8 will show, is exactly the modeling job of choosing your motion and measurement noise honestly.

You just watched the whole filter run. The flat-to-spike convergence is recursive fusion working: dozens of ambiguous readings, each one fused into the running belief, collapsing the hypothesis space until one explanation survives. The breathing is predict-versus-update. The kidnap recovery is the filter overcoming stale confidence with fresh evidence. Every behavior traces directly to a chapter — because the filter is just those chapters, alternating forever. This same "drive it and watch the belief" intuition is the diagnostic tool we formalize in Chapter 9.

Build it yourself — the full recursive filter in one loop

The showcase ran the filter for you. Now wire the loop with your own hands — predict and update are provided (the two functions from chapters 2 and 3); your job is to call them in the right order on a stream of moves and readings, and watch the belief converge.

Chapter 7: Use Cases & Real Products — The Filter In the Wild

Every lesson in this series ends with the same four chapters — Use Cases, Practical Application, Debugging, Connections — because a concept you can't point at in a shipped product is a concept you don't really own yet. So before any more theory, we ground the recursive Bayes filter in hardware you can buy or have used today.

The recursive Bayes filter is not a textbook curiosity — it is, in one representation or another, running inside almost every device that tracks a moving thing through noisy senses. And here is the payoff of having learned it: the Bayes filter is the parent of nearly every estimator on the spec sheets below. When a product says "Kalman filter," "particle filter," "sensor fusion," or "tracking," it is running a recursive Bayes filter with a particular belief representation. Let's tour them.

Robot vacuums & warehouse robots — grid localization, literally this lesson. A robot vacuum maintains a belief over a grid of floor cells and runs predict (on wheel odometry) and update (on LiDAR or camera readings against its map) exactly as we did with the hallway — just in 2D instead of 1D. The "histogram filter" of this lesson is the grid-localization method in Thrun's Probabilistic Robotics that powers these machines. When your vacuum "figures out where it is" after you move it, it's running this loop until the belief re-converges.

Phone & car navigation — the Kalman filter (Gaussian Bayes). Your phone fusing GPS with the IMU to keep the blue dot moving in a tunnel is a recursive Bayes filter whose belief is a single Gaussian bump (a Kalman filter). Predict = dead-reckon on the IMU (blur the bump); update = snap to a GPS fix (sharpen it). Same predict–update heartbeat, just with a bell-curve belief instead of a histogram.

Radar & air-traffic target tracking — the original killer app. Tracking an aircraft from sparse, noisy radar blips is the problem the Kalman filter was invented for (Apollo, 1960s) and it's still a recursive Bayes filter: predict the target's next position from a motion model, update when a new blip arrives. Air-traffic control, missile defense, and ship navigation all run this loop on every track.

Self-driving cars & drones — particle filters for the hard cases. When the belief can be multimodal or the models nonlinear (a car unsure which of several lanes it's in, a drone relocalizing in a repetitive warehouse), the belief is represented as a cloud of weighted samples — a particle filter. Predict moves the particles by the motion model; update reweights them by the measurement likelihood and resamples. Still the same Bayes loop; the histogram of this lesson is just replaced by samples.

The pattern across products — same loop, different belief

Notice the recurring structure: every one of these is the same predict–update recursion; they differ only in how they store the belief:

ProductWhat's trackedBelief representationName you'll see on the spec
Robot vacuum2D pose on a floorgrid / histogramgrid localization, MCL
Phone / car navposition + velocitysingle GaussianKalman filter, sensor fusion
Radar / ATCaircraft stateGaussian (often EKF)Kalman / EKF tracking
Self-driving / dronefull robot poseweighted particlesparticle filter, MCL
Finance / signal proc.hidden trend / signalGaussian or gridstate-space model, HMM

That last row is worth a beat: a hidden Markov model (HMM) — the workhorse of speech recognition, gene-finding, and the "dishonest casino" — is a recursive Bayes filter over discrete states, which is exactly the histogram filter of this lesson. The "forward algorithm" in an HMM is the predict–update loop. You've already learned HMM inference without anyone calling it that.

The deep case: how a robot vacuum re-finds itself when you move it

The most relatable demonstration of recursive fusion is the moment you pick up a running robot vacuum and set it down across the room. Watch what it does — it's the kidnapped-robot problem from the showcase, live:

Good vacuums speed this up by occasionally re-flattening the belief (injecting fresh uncertainty so it can't get permanently stuck) — a practical patch for the overconfidence trap we'll dissect in Chapter 9. But the core recovery is pure Bayes filter: the same loop you ran in the showcase, fighting stale confidence with fresh evidence.

How to spot a Bayes filter on a spec sheet

You can now reverse-engineer the estimator inside almost any tracking product from its marketing words:

The vocabulary changes by field — robotics says "localization," aerospace says "tracking," speech says "decoding," finance says "state-space" — but underneath, it's always a belief reshaped by predict and update, which is the one loop you now own.

The deployable insight. If a system tracks a hidden, moving quantity through a stream of noisy measurements, it is — or should be — a recursive Bayes filter. The only real design decisions are: how do I represent the belief (histogram for arbitrary shapes, Gaussian for speed, particles for nonlinear-multimodal) and what are my motion and measurement models. Pick those and the predict–update loop writes itself. Every named filter in the field is this lesson plus a choice of belief representation.
A phone navigation app fuses GPS and IMU to keep the position dot moving through a tunnel, and the spec calls it a "Kalman filter." How does it relate to the recursive Bayes filter from this lesson?

Chapter 8: Practical Application — Building a Bayes Filter for Real

You now know the loop, the two steps, the Markov assumption, and where it ships. But knowing the parts is not the same as knowing how to build one when a real tracking problem lands on your desk. This recurring "now build it" chapter turns the theory into a procedure: the three design decisions every Bayes filter forces you to make, and how to make each one well.

There are exactly three things you must choose, and everything else follows from them: (1) the belief representation, (2) the motion model, and (3) the measurement model. Choose these three honestly and the predict–update loop runs itself. Choose them carelessly and you get the failure modes of Chapter 9.

Decision 1 — How do you represent the belief?

This is the biggest fork in the road, because it determines which named filter you're building. The choice is a three-way tradeoff between flexibility, speed, and scalability to high dimensions:

RepresentationBelief shape it can expressCostUse when…
Grid / histogram
(this lesson)
any shape — multimodal, lopsided, anythingcellsdimensions — explodes in high-Dlow-dimensional state (1D, 2D); you need arbitrary shapes; global localization (start flat)
Single Gaussian
(Kalman / EKF)
one bell curve only — mean + spreadtiny — just a few numbersbelief is genuinely single-peaked; you need speed; high-dimensional state (position+velocity+…)
Particles
(particle filter)
any shape, via sample cloudscales with #particles, not grid sizemultimodal AND high-dimensional or nonlinear; the kidnapped-robot problem

The deciding question is usually: can my belief ever have more than one peak? In the hallway of identical doors, yes — so a single Gaussian (which can only ever be one bump) would be wrong: it can't represent "I'm at door 1 OR door 4." That's why global localization uses a grid or particles, not a bare Kalman filter. But once the robot has localized and is just tracking a single confident position (phone nav), a Gaussian is perfect and far cheaper. The grid of this lesson is the most flexible and the easiest to reason about — which is exactly why it's the teaching representation — but it's also the first to blow up in high dimensions (a 6D state on a 100-cell grid is 1006 = a trillion cells). High dimensions force you to Gaussians or particles.

The representation is the filter. Histogram belief → histogram/grid filter (this lesson, HMM forward algorithm). Gaussian belief → Kalman filter (linear) or EKF/UKF (nonlinear). Sampled belief → particle filter. They are all the same predict–update Bayes loop — the only thing that changes is the data structure holding bel(x) and the math for blurring/sharpening it. Pick your representation by asking "what shapes must my belief express, and how many dimensions does my state have?" — and you've picked your filter.

Decision 2 — What is your motion model?

The motion model is your honest answer to "if I command this move, what's the distribution of where I actually end up?" Three practical rules for getting it right:

Decision 3 — What is your measurement model?

The measurement model is "if the robot is truly at state x, what's the distribution of sensor readings?" — the P(z|x) table from Chapter 3. The same rules apply, mirrored:

A worked design — localizing a warehouse robot

Put it together on a concrete job. A robot must localize on a known warehouse floor map, starting from anywhere (it was just powered on). Walk the three decisions:

Decision 1 (belief): It must start from total ignorance, so the initial belief is flat over the whole floor — and a flat belief over a 2D floor is multimodal until it localizes (many spots look alike). A single Gaussian can't express "anywhere on the floor," so we rule out a bare Kalman filter. The floor is 2D and not too large, so a grid/histogram (or particle filter for a big floor) fits — arbitrary shapes, global start.

Decision 2 (motion): The robot reports wheel odometry. Calibrate by commanding known moves and measuring slip; build a 2D motion model that blurs the belief in the direction of travel, with spread proportional to distance. Err slightly blurry.

Decision 3 (measurement): A LiDAR compares its scan to the known map. The measurement model is "how likely is this scan if the robot is at pose x?" — high where the scan matches the map's walls, low where it doesn't. Calibrate the LiDAR's noise; add an outlier floor for reflections.

The result reads as a sentence: "A grid (or particle) belief, started flat for global localization, predicted on calibrated wheel odometry and updated on LiDAR-vs-map likelihood with an outlier-robust tail." Every choice traces to one of the three decisions — and the moment the belief converges to a single confident peak, you could switch to a cheap Gaussian (Kalman) filter to track from there, because once it's unimodal you no longer need the grid's flexibility. (That switch — grid/particle for global localization, then Kalman for tracking — is a real and common production pattern.)

The reusable recipe. (1) Ask "can my belief have multiple peaks, and how many dimensions is my state?" — pick grid (low-D, any shape), Gaussian (single peak, fast, high-D), or particles (any shape, high-D, costly). (2) Calibrate a motion model from real moves; err toward more blur. (3) Calibrate a measurement model from real readings; model honest noise and add an outlier tail. Then the predict–update loop is just the four-line program from Chapter 4. The same recipe builds a $300 vacuum's localizer and a self-driving car's tracker — only the representation and the models change.
You must globally localize a robot that starts from a completely unknown position in a building where many rooms look alike. Why is a single-Gaussian (bare Kalman) belief the wrong choice, and what should you use?

Chapter 9: Debugging & Failure Modes — When the Belief Lies

Knowing the loop is half the battle; the other half is recognizing when a real Bayes filter has quietly gone wrong. Filter bugs are rarely loud crashes — the belief keeps summing to 1, the code keeps running, and the robot keeps reporting a confident position that happens to be wrong. This recurring chapter catalogs the classic failure modes, the symptom each presents, and the specific test that exposes it before it ships.

If you remember nothing else, remember the two cheapest diagnostics in all of filtering: the sum-to-1 check (does the belief still sum to 1 after every step?) and the plausibility check (does the belief's confidence match how much the robot should actually know?). Between them they catch most of what follows.

Trap 1 — The forgotten renormalization

The most common bug, straight out of Chapter 3. The update step multiplies the belief by likelihoods (all below 1), which shrinks the total below 1 — and if you forget the final divide-by-sum, the belief silently stops being a probability distribution. After a few updates the numbers decay toward zero (or, with a different bug, blow up), and every derived quantity — the best guess, the confidence, the next predict — is computed from a non-distribution and is garbage.

Symptom & detection. Symptom: the belief's total drifts away from 1 over time (toward 0 or ∞); confidences become meaningless; underflow to all-zeros after many steps. Detection: assert the sum is 1 after every step. Literally add assert abs(bel.sum() - 1.0) < 1e-9 at the end of predict and update. This single line catches the forgotten renormalization instantly — it's the cheapest, highest-value check you can write. Fix: divide by the sum at the end of every update (and watch for numerical underflow — with many cells and tiny likelihoods, work in log-space).

Trap 2 — The wrong motion model (belief outruns or lags the robot)

If your motion model says the robot moves more (or less) than it actually does, the predict step shifts the belief by the wrong amount every step, and the belief steadily slides ahead of or behind the true position. The update step fights to pull it back, but a consistently-wrong motion model means the belief is always chasing — it tracks with a persistent offset, or in bad cases loses the robot entirely.

Symptom & detection. Symptom: the belief's peak lags or leads the true position by a roughly constant amount; localization is sluggish or biased even with a good sensor. Detection: run the filter on logged data with ground truth (a known trajectory) and plot belief-peak vs. truth — a systematic offset fingers the motion model. Fix: recalibrate the motion model from measured moves (Chapter 8); if there's a hidden trend (drifting bias), the process may not be Markov in your state — enlarge the state (Chapter 5).

Trap 3 — The overconfident belief that ignores new data

The deadliest and subtlest trap, and the one the showcase's kidnap experiment demonstrated. If the belief becomes too sharp — a spike at near-1.0 on one cell, with essentially 0 everywhere else — then new measurements can't move it. Why? The update multiplies by likelihood: but if a cell's prior belief is already ~0, multiplying it by even a high likelihood leaves it ~0 (zero times anything is zero). The belief is locked: it has decided, and no amount of contradicting evidence can resurrect a hypothesis it has crushed to zero. When such an overconfident belief is wrong (the robot was kidnapped, or an early reading was a fluke), the filter is stuck on a wrong answer forever.

Symptom & detection. Symptom: the filter is confidently, persistently wrong and does not recover even as contradicting readings pour in; the belief is a near-delta spike. Detection: monitor the belief's entropy (its spread) — if it collapses to near-zero and never recovers, you're overconfident. Also watch for many consecutive readings with very low likelihood under the current belief (the readings "don't fit" but the belief won't budge). Fix: keep the motion model honestly noisy (every predict re-injects a little spread, preventing total collapse), floor the likelihoods so no cell is ever multiplied by exactly 0, and for global problems periodically inject uniform "I might be lost" probability (the random-particle trick in MCL). The root cause is almost always a motion or measurement model that claims more certainty than it has — Chapter 8's "err toward more noise" rule exists precisely to prevent this.

The overconfidence trap, worked out

See the lock-up in numbers. Suppose an overconfident belief is essentially a spike on cell 3:

bel = [0, 0, 0, 0.9999, 0.0001, 0, …]

The robot is actually at cell 7 (kidnapped). It senses something that's very likely at cell 7 and very unlikely at cell 3 — say likelihood 0.8 at cell 7, 0.05 at cell 3. Apply the update (multiply by likelihood):

CellPriorLikelihoodProduct
30.99990.050.0500
70.00000.80.0000

Even though the reading strongly favors cell 7, cell 7's prior was 0, so 0 × 0.8 = 0 — the correct cell stays at zero probability. Meanwhile cell 3 keeps the lion's share. After renormalizing, the belief is still a near-spike on the wrong cell 3. The truth has zero prior and can never climb back, because the filter crushed it to zero and zero is multiplicatively absorbing. This is why you must never let a belief reach exactly zero on a reachable state — floor the likelihoods and keep the predict step's blur alive, so every cell always retains a sliver of probability to recover with.

Trap 4 — Violating the Markov assumption

If the real process depends on more than your state captures (a hidden drifting bias, correlated sensor noise, an unmodeled velocity), the filter's Markov models are systematically wrong, and the belief is subtly off in ways no amount of tuning fixes — because the bug isn't in the numbers, it's in the state definition.

Symptom & detection. Symptom: persistent, structured errors that recalibrating the models doesn't remove; the filter is "biased in a way that depends on history." Detection: check whether your residuals (reading minus expected reading) are correlated over time — white (uncorrelated) residuals mean your Markov model is adequate; correlated residuals mean something history-dependent is unmodeled. Fix: enlarge the state to include the hidden quantity (add velocity, add the bias, add the slip rate) until the augmented state is Markov again — the single most important modeling move from Chapter 5.

A debugging checklist you can run on any Bayes filter

CheckHowCatches
Sum-to-1 assertionassert |bel.sum() - 1| < 1e-9 after every stepTrap 1 (forgotten renormalization)
Truth-vs-belief plotrun on logged ground-truth data; plot peak vs. truthTrap 2 (wrong motion model)
Entropy / spread monitorwatch belief spread; flag collapse-and-never-recoverTrap 3 (overconfidence)
Likelihood-fit monitorflag many consecutive low-likelihood readingsTrap 3 (belief ignoring data)
Residual whiteness testare reading-minus-prediction errors correlated over time?Trap 4 (Markov violation)
The meta-lesson. Almost every Bayes-filter bug is a gap between the world's true behavior and the models you fed the filter — a motion model that's too sharp or wrong, a measurement model that's too trusting, a state that isn't really Markov, or a missing renormalization. The filter does exactly what you tell it; if it's confidently wrong, your models are confidently wrong. The cure is always the same: assert sum-to-1, calibrate the models from real data, keep them honestly noisy so the belief can never lock to zero, and enlarge the state until it's Markov.
A robot's belief has collapsed to a near-1.0 spike on cell 3, but the robot was secretly moved to cell 7. Readings strongly favoring cell 7 pour in, yet the belief stays stuck on cell 3. What is the failure, and why can't the readings fix it?

Chapter 10: Connections, References & Cheat-Sheet

You now hold the engine that powers the entire middle third of this series. Before we point to what's next, here is everything in one place — the cheat-sheet to screenshot, the motto, the real sources, and the cross-links.

The recursive Bayes filter — cheat-sheet

The belief: bel(x) = a probability distribution over all possible states; a list of non-negative numbers that sum to 1. Tallest bar = best guess; spread = uncertainty; multiple peaks = honest ambiguity.

The two steps (the heartbeat):
· PREDICT (on motion): bel̅(x) = ∑x′ P(x | x′, u) · bel(x′). Convolve with the motion model. Effect: shift + blur; uncertainty GROWS. (Inhale.)
· UPDATE (on measurement): bel(x) = η · P(z | x) · bel̅(x). Multiply by the likelihood, then renormalize (÷ the sum = η). Effect: sharpen; uncertainty SHRINKS. This is the fusion step. (Exhale.)

The loop: bel → predict → bel̅ → update → bel → repeat forever. Each output is the next input; the belief carries all history in fixed size.

The Markov assumption: the future depends only on the present state, not the full past — this is what makes the belief a sufficient, fixed-size memory and the recursion legal.

Why it's a fusion engine: every update fuses the prior belief (all past readings) with one new reading. The filter is the two-measurement fuse, done recursively, forever — and it fuses multiple sensors per step by chaining their likelihood multiplications.

The family: histogram belief = grid filter / HMM (this lesson) · Gaussian belief = Kalman / EKF / UKF · sampled belief = particle filter. All the SAME loop; only the belief representation differs.

The three design decisions: (1) belief representation (grid / Gaussian / particles), (2) motion model (calibrate, err blurry), (3) measurement model (calibrate, model honest noise + outlier tail).

Top debugging tools: assert sum-to-1 every step (catches lost renormalization) · keep models honestly noisy so no cell hits exactly 0 (prevents the overconfidence lock-up) · enlarge the state if residuals are correlated (restores Markov).

The motto

"What I cannot create, I cannot understand." — Richard Feynman.
You can now create the recursive Bayes filter by hand — predict, update, renormalize — and every Kalman, EKF, UKF, and particle filter you'll ever build is this one loop wearing a different belief.

The filter in one quick-reference table

Screenshot this. It is the entire lesson collapsed into a single lookup:

StepTriggerOperationEffect on beliefConservation
Predictrobot movesconvolve with motion modelshift + blur (uncertainty up)sums to 1 (mass redistributed)
Updaterobot senses× likelihood, then renormalizesharpen (uncertainty down)renormalize restores sum to 1

Where this lesson sits in the series

This was Lesson 4 of 22 — the probabilistic engine that the rest of classical estimation is built on. The recurring chapter structure you just walked (Why → concepts → showcase → Use Cases, Practical Application, Debugging, Connections) repeats in every lesson, so you always know where you are. Here is the road ahead, so you can see how this one loop grows into the whole toolbox:

LessonsArcHow it builds on this filter
1–3Foundationsthe fusion problem & the three relationships; fusion architectures; coordinate frames & time sync
4–8Classical estimationthe Bayes filter (here); the Kalman filter (the Bayes filter with a Gaussian belief); EKF & UKF (Gaussian Bayes for nonlinear models); particle filters (sampled belief for any shape)
9–13Spatial fusionoccupancy grids (the 2D histogram belief); SLAM; visual-inertial odometry; multi-target tracking & data association
14–18Robust & distributedfault detection; covariance intersection; consensus filtering; out-of-sequence measurements
19–22Modern / learneddeep & learned filters; transformer-based fusion; end-to-end perception; the Sensor Fusion Atlas

Next up, Lesson 5: The Kalman Filter — the single most famous estimator in engineering, and now you'll see it for what it really is: the recursive Bayes filter of this lesson, with the belief restricted to a single Gaussian bell curve. That one restriction buys enormous speed (a Gaussian is just a mean and a covariance, a handful of numbers) and lets predict and update become simple matrix formulas — the famous Kalman gain is exactly the "trust weight" that decides how much to sharpen on each measurement. Everything you learned here — predict blurs, update sharpens, the loop fuses past with present — carries over verbatim; only the belief's shape changes.

Related lessons on Engineermaxxing

These existing lessons go deeper on the machinery we touched:

References

  1. Thrun, S., Burgard, W., and Fox, D. Probabilistic Robotics. MIT Press, 2005. Chapter 2 ("Recursive State Estimation") is the canonical derivation of the recursive Bayes filter, the Markov assumption, and the histogram/grid filter used throughout this lesson. probabilistic-robotics.org
  2. Simon, D. Optimal State Estimation: Kalman, H∞, and Nonlinear Approaches. Wiley, 2006. A clear, engineering-first treatment of recursive estimation that bridges the Bayes filter to the Kalman family of Lesson 5. doi:10.1002/0470045345
  3. Fox, D., Burgard, W., and Thrun, S. "Markov Localization for Mobile Robots in Dynamic Environments." Journal of Artificial Intelligence Research, vol. 11, pp. 391–427, 1999. The grid/histogram localization that this lesson's hallway filter is a 1-D instance of. doi:10.1613/jair.616
  4. Chen, Z. "Bayesian Filtering: From Kalman Filters to Particle Filters, and Beyond." Statistics, vol. 182, no. 1, pp. 1–69, 2003. A unifying survey showing the Kalman, grid, and particle filters as one recursive Bayes filter under different belief representations. doi:10.1080/02331880309257
A colleague says the Kalman filter and the particle filter are "totally different algorithms from the Bayes filter." What's the precise correction?