Probability for robots
A distance sensor says the wall is 2.10 m away. A second sensor says 1.96 m. Neither is lying: both are noisy. Which one do you believe, and what should the robot conclude? Every sensor in robotics is wrong by some amount, so a robot has to reason about how wrong, and combine evidence sensibly. That reasoning is probability, and this lesson turns it into code you can run.
You will be able to
- Describe a noisy measurement as a random variable with a mean and a variance.
- Simulate Gaussian sensor noise and show that averaging reduces it by the square root of N.
- Fuse two Gaussian measurements into one better estimate.
- Apply Bayes rule to a discrete detector and to a robot localizing itself in a corridor.
- Read a covariance matrix.
Noise, and a way to talk about it
Point a range sensor at a wall exactly 2.00 m away and read it a hundred times. You will not get 2.00 each time. You get a cloud of values clustered around the truth. We describe this with a random variableA random variable is a quantity whose value varies from measurement to measurement, described by a probability distribution..
Two numbers summarise it:
- The mean is the centre: the average value.
- The variance is the spread, and its square root is the standard deviation, which has the same units as the measurement.
The most common model for sensor noise is the Gaussian (normal) distribution, written , with density
For a Gaussian, about 68% of readings fall within one of the mean, 95% within two, and 99.7% within three. Why Gaussian? Many small independent error sources add up, and the sum of many small random effects tends toward a Gaussian. It is also mathematically friendly, as you will see in the fusion step.
import numpy as np
rng = np.random.default_rng(42) # fixed seed for repeatable results
true_range = 2.00 # metres
sigma = 0.05 # sensor standard deviation, metres
readings = true_range + rng.normal(0.0, sigma, size=100)
print(readings.mean()) # close to 2.00
print(readings.std(ddof=1)) # close to 0.05
# Average N readings. How much does the average wobble?
for n in (1, 4, 16, 64, 256):
means = (true_range + rng.normal(0.0, sigma, size=(5000, n))).mean(axis=1)
print(n, means.std().round(4), (sigma / np.sqrt(n)).round(4))
The two printed columns match. The spread of an average of readings is . Four times as many readings cut the noise in half. That is why robots average, and also why there is a limit: to be ten times more precise you need a hundred times the readings.
Fusing two measurements
Back to the two sensors. Sensor A says 2.10 m with . Sensor B says 1.96 m with . Sensor B is more precise, so it should count for more. The optimal combination of two independent Gaussian measurements weights each by the inverse of its variance:
Two things fall out. The fused mean lies between the two readings, closer to the more certain one. And the fused variance is smaller than either input, so combining sensors always helps, even if one is poor.
import numpy as np
def fuse(mu1, var1, mu2, var2):
"""Combine two independent Gaussian estimates of the same quantity."""
var = 1.0 / (1.0 / var1 + 1.0 / var2)
mu = var * (mu1 / var1 + mu2 / var2)
return mu, var
mu, var = fuse(2.10, 0.05 ** 2, 1.96, 0.02 ** 2)
print(round(mu, 3), round(float(np.sqrt(var)), 4)) # 1.979 0.0186
The result is 1.979 m with a standard deviation of 1.9 cm: better than the best sensor alone, at 2 cm. This exact formula is the heart of the Kalman filter in Module 05. Keep it in mind.
Covariance: noise in more than one dimension
A 2D position has an x error and a y error, and they may be linked. For example, a camera usually measures depth less accurately than sideways position, and the errors can tilt along a direction. The covariance matrix records this:
The diagonal holds the variances. The off-diagonal says whether the errors move together: positive means when x is too high, y tends to be too high as well.
import numpy as np
rng = np.random.default_rng(1)
cov = np.array([[0.04, 0.03],
[0.03, 0.09]])
xy = rng.multivariate_normal([1.0, 2.0], cov, size=10_000) # shape (10000, 2)
print(np.cov(xy.T).round(3)) # close to the matrix we started with
np.cov expects variables in rows, so we transpose. You will meet this matrix again as the uncertainty ellipse around a robot's estimated position.
Bayes rule: updating what you believe
Suppose an obstacle detector has a hit rate of 90%: if an obstacle is there, it fires 90% of the time. It also has a false alarm rate of 20%: if nothing is there, it still fires 20% of the time. Obstacles are rare, present on 10% of the path. The detector fires. How likely is it that an obstacle is really there?
Most people say 90%. The right answer is lower. Bayes rule gives it:
The numerator is . The denominator adds both ways to get an alarm: . So . Only one alarm in three is real. The prior probability (obstacles are rare) matters a lot.
In words: posterior is proportional to likelihood times prior. You start with a belief (the prior), look at what the sensor says and how likely that reading would be under each possibility (the likelihood), multiply, and renormalise so everything sums to 1.
A robot in a corridor
You will now implement that loop. A robot sits in a circular corridor of 8 cells with doors in cells 0 and 3, and walls elsewhere. It does not know which cell it is in, so it starts with a uniform belief. This is a histogram filter.
import numpy as np
world = np.array(["door", "wall", "wall", "door", "wall", "wall", "wall", "wall"])
belief = np.full(len(world), 1.0 / len(world)) # start: no idea
def sense(belief, z, p_hit=0.8, p_miss=0.2):
"""Bayes update. z is what the sensor reports: 'door' or 'wall'."""
likelihood = np.where(world == z, p_hit, p_miss)
posterior = belief * likelihood
return posterior / posterior.sum()
def move(belief, steps, p_exact=0.8, p_under=0.1, p_over=0.1):
"""Shift the belief by steps cells, allowing for slip (undershoot / overshoot)."""
return (p_exact * np.roll(belief, steps)
+ p_under * np.roll(belief, steps - 1)
+ p_over * np.roll(belief, steps + 1))
belief = sense(belief, "door")
print(belief.round(3)) # peaks at cells 0 and 3
belief = move(belief, 1)
belief = sense(belief, "wall")
belief = move(belief, 2)
belief = sense(belief, "door")
print(belief.round(3), belief.argmax()) # strongest peak at cell 3
Walk through it. The first reading "door" leaves two equally good candidates, cells 0 and 3, because the corridor looks the same at both. Moving and sensing a wall does not separate them. After moving two more cells and seeing "door" again, only one starting guess fits the story, and the belief piles up on cell 3 at about 0.49. The robot has localized itself from nothing but noisy readings and noisy motion.
The belief does not become 1.0. The sensor is wrong 20% of the time and the motion slips 20% of the time, so honest uncertainty remains. A good localizer keeps that doubt visible instead of hiding it.
Hands-on lab
You will build a small fusion and localization toolkit.
- In
kinemata-workspace/foundations, createfusion.pyand add thefusefunction. - Simulate two sensors observing a true range of 3.0 m with standard deviations 0.08 and 0.03, using 1,000 paired readings from
rng.normal. - Fuse each pair with
fuse(a, 0.08**2, b, 0.03**2), then compute the standard deviation of the fused results across all 1,000 pairs. - The check: that standard deviation should be close to 0.028, which is
np.sqrt(1 / (1/0.08**2 + 1/0.03**2)). It should be smaller than both 0.08 and 0.03. - Copy
senseandmoveintohistogram_filter.pyin the same folder, and run the corridor sequence above. - Change the sequence to sense "door", move 3, sense "door", and print the belief. Which cell is the most likely now, and why?
- Commit both files with a clear message.
Exercises
How many readings?Beginner
A sensor has m. How many independent readings must you average to get a standard deviation of 0.01 m? The spread of the average is , so you need , which gives and . Ten times the precision costs a hundred times the data.Show solutionHide solution
A better detectorIntermediate
Change the obstacle detector's false alarm rate from 0.2 to 0.05 and keep the other numbers. Compute the probability that an obstacle is present given an alarm. The numerator is still . The denominator is . So . Cutting the false alarm rate from 20% to 5% doubles how much you can trust an alarm. When events are rare, false alarm rate matters more than hit rate.Show solutionHide solution
Fuse three sensorsIntermediate
Write With equal variances the fused mean is the plain average, and the variance is one third of the original. The general rule: add the inverse variances, and the result is the inverse of the sum.fuse_many(mus, variances) that combines any number of independent Gaussian estimates of one quantity, and test it with readings 2.0, 2.1 and 1.9 with equal variance 0.01.Show solutionHide solution
Check your understanding
Takeaways
- Sensor readings are random variables, usually modelled as Gaussians with a mean and a variance.
- Averaging N readings reduces noise by the square root of N, but never removes bias.
- Fusing independent Gaussians weights each one by its inverse variance and always reduces uncertainty.
- A covariance matrix records variances and how errors move together.
- Bayes rule says posterior is proportional to likelihood times prior, and priors matter for rare events.
- Localization alternates sensing (multiply by likelihood) and moving (shift and blur).
Go deeper
- Probabilistic Robotics (Thrun, Burgard, Fox), chapters 2 to 4: the standard reference for this whole area.
- Kalman and Bayesian Filters in Python (Roger Labbe): a free, code-first book that extends this lesson.
- 3Blue1Brown: Bayes theorem: a visual explanation of the update rule.
- NumPy random generator reference: the sampling functions used here.
Finished the lab and the quiz?
Mark it done to track your progress through the course.