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.
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.
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.
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.
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.
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.
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.
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.
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:
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 shape of the belief distribution is a direct readout of the robot's state of knowledge. Three shapes worth recognizing on sight:
| Belief shape | What it means | Example |
|---|---|---|
| Uniform (flat) | total ignorance — equally likely anywhere | [0.1, 0.1, …, 0.1] — just switched on |
| Unimodal spike | confident 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.
Let's practice reading one. Suppose after a few steps the robot's belief over six cells is:
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.
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).
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.
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.
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.
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:
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.
Start with the simplest belief: the robot is certain it's in cell 3. Its belief is a spike:
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:
| Outcome | Lands in cell | Probability mass |
|---|---|---|
| moved 0 (slip) | cell 3 | 1.0 × 0.1 = 0.1 |
| moved 1 (as commanded) | cell 4 | 1.0 × 0.8 = 0.8 |
| moved 2 (overshoot) | cell 5 | 1.0 × 0.1 = 0.1 |
So the new (predicted) belief is:
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.
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:
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 scatters | 0.5×0.1 = 0.05 | 0.5×0.8 = 0.40 | 0.5×0.1 = 0.05 | — |
| cell 3's 0.5 scatters | — | 0.5×0.1 = 0.05 | 0.5×0.8 = 0.40 | 0.5×0.1 = 0.05 |
| column total | 0.05 | 0.45 | 0.45 | 0.05 |
Add the columns and the predicted belief is:
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.
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":
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."
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.1 | cell 3: 0.01 | cell 4: 0.08 | cell 5: 0.01 |
| cell 4 holds 0.8 | cell 4: 0.08 | cell 5: 0.64 | cell 6: 0.08 |
| cell 5 holds 0.1 | cell 5: 0.01 | cell 6: 0.08 | cell 7: 0.01 |
Summing each destination column gives the belief after the second predict:
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 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.
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.
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.
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 cell | Sensor says "door" | Sensor says "wall" |
|---|---|---|
| cell IS a door | 0.8 (correct) | 0.2 (missed it) |
| cell IS a wall | 0.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.
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:
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:
| Cell | Type | Prior | P("door" | this cell) | Prior × likelihood (un-normalized) |
|---|---|---|---|---|
| 0 | wall | 0.05 | 0.3 | 0.05 × 0.3 = 0.015 |
| 1 | door | 0.25 | 0.8 | 0.25 × 0.8 = 0.200 |
| 2 | wall | 0.20 | 0.3 | 0.20 × 0.3 = 0.060 |
| 3 | wall | 0.20 | 0.3 | 0.20 × 0.3 = 0.060 |
| 4 | door | 0.25 | 0.8 | 0.25 × 0.8 = 0.200 |
| 5 | wall | 0.05 | 0.3 | 0.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.
Divide each un-normalized value by the total S = 0.550:
| Cell | Un-normalized | ÷ S = 0.550 | Posterior belief |
|---|---|---|---|
| 0 | 0.015 | 0.015 / 0.550 | 0.027 |
| 1 (door) | 0.200 | 0.200 / 0.550 | 0.364 |
| 2 | 0.060 | 0.060 / 0.550 | 0.109 |
| 3 | 0.060 | 0.060 / 0.550 | 0.109 |
| 4 (door) | 0.200 | 0.200 / 0.550 | 0.364 |
| 5 | 0.015 | 0.015 / 0.550 | 0.027 |
The posterior belief is:
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.
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:
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.
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:
| Cell | Type | Prior | P("wall" | this cell) | Prior × likelihood |
|---|---|---|---|---|
| 0 | wall | 0.05 | 0.7 | 0.035 |
| 1 | door | 0.25 | 0.2 | 0.050 |
| 2 | wall | 0.20 | 0.7 | 0.140 |
| 3 | wall | 0.20 | 0.7 | 0.140 |
| 4 | door | 0.25 | 0.2 | 0.050 |
| 5 | wall | 0.05 | 0.7 | 0.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):
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.
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":
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.
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.
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).
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:
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 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):
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:
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:
| Cell | Type | Predicted bel | P("wall"|cell) | Product |
|---|---|---|---|---|
| 2 | wall | 0.305 | 0.7 | 0.213 |
| 5 | door | 0.305 | 0.2 | 0.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.
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.
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.
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.
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.
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.
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:
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.
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:
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:
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:
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.
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.
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.
Don't just watch — falsify. Run this sequence and confirm each prediction with your own eyes:
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.
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.
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.
Notice the recurring structure: every one of these is the same predict–update recursion; they differ only in how they store the belief:
| Product | What's tracked | Belief representation | Name you'll see on the spec |
|---|---|---|---|
| Robot vacuum | 2D pose on a floor | grid / histogram | grid localization, MCL |
| Phone / car nav | position + velocity | single Gaussian | Kalman filter, sensor fusion |
| Radar / ATC | aircraft state | Gaussian (often EKF) | Kalman / EKF tracking |
| Self-driving / drone | full robot pose | weighted particles | particle filter, MCL |
| Finance / signal proc. | hidden trend / signal | Gaussian or grid | state-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 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.
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.
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.
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:
| Representation | Belief shape it can express | Cost | Use when… |
|---|---|---|---|
| Grid / histogram (this lesson) | any shape — multimodal, lopsided, anything | cellsdimensions — explodes in high-D | low-dimensional state (1D, 2D); you need arbitrary shapes; global localization (start flat) |
| Single Gaussian (Kalman / EKF) | one bell curve only — mean + spread | tiny — just a few numbers | belief is genuinely single-peaked; you need speed; high-dimensional state (position+velocity+…) |
| Particles (particle filter) | any shape, via sample cloud | scales with #particles, not grid size | multimodal 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 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:
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:
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.)
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.
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.
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).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.
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.
See the lock-up in numbers. Suppose an overconfident belief is essentially a spike on cell 3:
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):
| Cell | Prior | Likelihood | Product |
|---|---|---|---|
| 3 | 0.9999 | 0.05 | 0.0500 |
| 7 | 0.0000 | 0.8 | 0.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.
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.
| Check | How | Catches |
|---|---|---|
| Sum-to-1 assertion | assert |bel.sum() - 1| < 1e-9 after every step | Trap 1 (forgotten renormalization) |
| Truth-vs-belief plot | run on logged ground-truth data; plot peak vs. truth | Trap 2 (wrong motion model) |
| Entropy / spread monitor | watch belief spread; flag collapse-and-never-recover | Trap 3 (overconfidence) |
| Likelihood-fit monitor | flag many consecutive low-likelihood readings | Trap 3 (belief ignoring data) |
| Residual whiteness test | are reading-minus-prediction errors correlated over time? | Trap 4 (Markov violation) |
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.
Screenshot this. It is the entire lesson collapsed into a single lookup:
| Step | Trigger | Operation | Effect on belief | Conservation |
|---|---|---|---|---|
| Predict | robot moves | convolve with motion model | shift + blur (uncertainty up) | sums to 1 (mass redistributed) |
| Update | robot senses | × likelihood, then renormalize | sharpen (uncertainty down) | renormalize restores sum to 1 |
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:
| Lessons | Arc | How it builds on this filter |
|---|---|---|
| 1–3 | Foundations | the fusion problem & the three relationships; fusion architectures; coordinate frames & time sync |
| 4–8 | Classical estimation | the 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–13 | Spatial fusion | occupancy grids (the 2D histogram belief); SLAM; visual-inertial odometry; multi-target tracking & data association |
| 14–18 | Robust & distributed | fault detection; covariance intersection; consensus filtering; out-of-sequence measurements |
| 19–22 | Modern / learned | deep & 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.
These existing lessons go deeper on the machinery we touched: