Weighing the guesses

The measurement update: how likely is this reading if the robot were there?

U7.4LocalisationUniversity35 min

Do this lesson in the simulator

The measurement update asks each particle one question: if the robot really were here, how likely is the reading I just got?

for each particle:
    predicted = what the sensor would read at this pose
    weight = likelihood(measured given predicted)

For a sensor with Gaussian noise of standard deviation sigma:

d = predicted - measured
weight = math.exp(-d * d / (2 * sigma * sigma))

A particle that predicts the reading exactly gets weight 1. One that is two sigma out gets 0.14. One that is ten sigma out gets a number so small it might as well be zero, which is the point: it is almost certainly not where the robot is.

Four particles' predicted readings beside the measured one, 0, 2, 4 and 10 sigma out, each labelled with the weight it gets130142160predicted reading, cmticks every sigma = 3 cmmeasured: 130exact: 12 sigma: 0.144 sigma: 0.000310 sigma: about 2 × 10⁻²²
The weight falls away fast. At 2 sigma (6 cm) it is 0.14, at 4 sigma 0.0003, and at 10 sigma about 2 in 10 to the power 22: that guess is as good as ruled out.

The sensor model is the interesting part

predicted is a simulation of the sensor from that particle's pose, and writing it is most of the work in a real system. On this mat, facing the far wall, it is easy:

predicted = 200 - particle_y

That line has a condition hidden in it: the robot must be facing the far wall square on. Turn it and the sensor looks along a slant, which is longer. The simulator's depth sensor also looks along a narrow cone, about 3 degrees either side of straight ahead, and reports the nearest thing in it. Square on, the nearest point in the cone is straight ahead, so the cone and 200 - y agree, and they keep agreeing to within a centimetre or so for the first 10 degrees of a turn. After that they part company fast. Standing 80 cm from the far wall and turning left, the robot read:

Turned by Reading 200 - y says One straight line at that angle says
0 degrees 79 cm 80 cm 80 cm
30 degrees 88 cm 80 cm 92 cm
45 degrees 111 cm 80 cm 113 cm
60 degrees 152 cm 80 cm 162 cm

Weigh a cloud against the 60 degree reading with 200 - y and every guess 70 cm from the truth wins. Even a model that knows the heading and draws one straight line is 10 cm out, because the near edge of the cone reaches the wall first. The simple fix, and the one the rest of this module uses, is to weigh the cloud only while the robot is square on to the wall the model is about, and to skip readings while it turns. The heading the robot has from its gyro (odometry()) drifts by 2 to 3 degrees, which the cone forgives.

A robot 80 cm from the far wall, turned by 0 to 60 degrees: the reading grows along the slantfar wallleft wall0 degreads 7930 degreads 8845 degreads 11160 degreads 152robot,y = 120200 - y reads 152 as y = 48,72 cm from the truth
The four readings from the table, drawn to scale from the robot's spot. Every one of them is taken 80 cm from the wall, but at 60 degrees the sensor looks along a slant and reads 152, and a model that only knows 200 - y puts the robot 72 cm further back than it is. The dashed lines are the edges of the sensor's cone.

With obstacles it becomes a ray cast: march along the particle's heading until you hit something. With a full laser scanner it is a ray cast per beam per particle, which is why real implementations precompute a likelihood field instead of casting rays.

Add a floor to the likelihood, always:

weight = math.exp(...) + 1e-12

Without it, one unexpected reading can make every weight exactly zero, the normalisation divides by zero, and the filter dies. With it, the filter survives a surprise and recovers.

Normalising, and the effective sample size

Weights are relative, so divide by their total. Then a single number tells you how healthy the cloud is:

neff = 1.0 / sum(w * w for w in weights)

The effective sample size. If all weights are equal it is N. If one particle has all the weight it is 1. It is the honest count of how many particles are still contributing, and it is what U7.5 uses to decide when to resample.

from bugbot import *
import math, random
connect()
random.seed(1)

N, SIGMA = 500, 3.0
particles = [random.uniform(0, 200) for i in range(N)]
# standing still, square on to the far wall
measured = sum(distance() for i in range(5) if wait(0.1) is None) / 5

weights = []
for y in particles:
    d = (200 - y) - measured
    weights.append(math.exp(-d * d / (2 * SIGMA * SIGMA)) + 1e-12)
total = sum(weights)
weights = [w / total for w in weights]

best = sum(w * y for w, y in zip(weights, particles))
print("weighted mean", round(best, 1))
print("neff", round(1.0 / sum(w * w for w in weights), 1), "of", N)

Run this in the simulator

The robot is at y = 70 and the weighted mean comes out at 70.3. But look at neff: five hundred particles, and after one reading only about 22 of them are doing any work. The rest were more than a few sigma from the reading and now weigh next to nothing. That collapse is exactly why resampling exists.

The 500 weights after one reading, largest first: a couple of dozen carry nearly all of it010203040506000.020.040.06particles, heaviest first (the first 60 of 500)weightan equal share, 1/500neff = 22the first 22 hold 90 percentthe other 478 (grey) share 10 percent
The lesson's cell, with the robot at y = 70. An even cloud would give every particle the red dashed share. After one reading, 22 particles, the effective sample size, hold 90 percent of the weight, and most of the rest is too small to see.

Do not lie about sigma

Using a sigma smaller than the sensor's real noise makes the filter over-confident: it will throw away the true pose because one reading was unlucky. Slightly larger than the truth is the safer error, and a common practical choice.

Task: weigh the guesses

Scatter particles over the mat, take a reading, weight them, and print best y: (the weighted mean) and neff:.

from bugbot import *
import math, random
connect()

N, SIGMA = 500, 3.0
particles = [random.uniform(0, 200) for i in range(N)]

Challenges

  1. Use sigma = 0.5 and look at neff. How many particles survive?
  2. Weight the cloud against two readings taken from the same spot. Does neff fall further?
  3. Plot the weights against the particle's y. What shape is it?