The low pass filter

A moving average, then the exponential filter that does the same job in one line and no memory.

U4.3Noise and filteringUniversity30 min

Do this lesson in the simulator

A moving robot cannot stop and average. It needs an estimate that is always available, updated as each reading arrives, and smoother than the raw stream.

The moving average

Keep the last n readings in a list and average them:

window.append(raw)
if len(window) > N:
    window.pop(0)
estimate = sum(window) / len(window)

It works, it is easy to explain, and it costs n numbers of memory and n additions per tick. Its response to a step is a ramp n steps long: the output is half way to the new value after about n/2 steps and all the way there after n. So more smoothing means more delay, in a fixed and obvious way.

A moving average of 8 readings after the readings step down: the output falls in a straight ramp 8 readings longreadingsoldnewhalf way after 4all the way after 8a ramp 8 readings longthe raw readingsthe moving average of the last 8
The readings drop from one steady value to another, with no noise so the shape shows. A moving average of 8 lets one new reading in at a time, so it falls by the same amount each step: half way after 4, and only there once all 8 readings in the window are new ones.

The exponential filter

The same job in one line and no memory at all:

filtered = alpha * raw + (1 - alpha) * filtered

Each new reading gets weight alpha; everything before it keeps the rest. The weights decay geometrically into the past, which is why it is called an exponentially weighted moving average, and in signal processing it is a first order low pass filter.

The weight each past reading carries in an exponential filter with alpha 0.25, shrinking by the same factor each step back02468101200.10.20.3how many readings ago (0 is the newest)weight0.250.190.140.11the newest reading: alpha = 0.25each older one: 0.75 times the one after itall of them together add up to 1
With alpha = 0.25, the newest reading counts for 0.25, the one before for 0.19, then 0.14, and so on, each 0.75 of the last. No reading is ever dropped completely, but one 12 or more readings old counts for less than 1%.
alpha behaviour roughly like a window of
0.5 fast, barely smoothed 3 readings
0.2 a reasonable default 9 readings
0.05 very smooth, very slow 39 readings

The equivalent window is about (2 - alpha) / alpha. Worth knowing so that "alpha = 0.1" turns into a picture rather than a mystery.

from bugbot import *
connect()

ALPHA = 0.25
filtered = distance()
forward(45)
for i in range(60):
    raw = distance()
    filtered = ALPHA * raw + (1 - ALPHA) * filtered
    plot("raw", raw)
    plot("filtered", filtered)
    if filtered < 30:
        stop()
    wait(0.1)
stop()

Run this in the simulator

Two lines on the chart: a jagged one and a smooth one that follows it a little late. Both facts matter, and the second is the subject of U4.5.

It is the same idea as the Kalman filter

Look at the update again, rearranged:

filtered = filtered + alpha * (raw - filtered)

"Take your current belief, and move it towards the new measurement by a fraction of the difference." That is precisely the shape of the Kalman update in U6.3. The whole difference is that alpha there is not a constant you chose: it is computed each tick from how uncertain the belief is compared with the measurement.

One step of the filter: the estimate moves a quarter of the way from where it was towards the new reading82848688distance, cmestimate89.0new reading82.3new estimate 87.3a quarter of the gap of 6.7 cm
One tick from the drive in the cell above, run in the simulator. The estimate was 89.0 cm and a reading of 82.3 arrived. The filter does not jump to it; it moves alpha = 0.25 of the way, to 87.3. A reading that is noise pulls the estimate only a little.

Everything you learn about choosing alpha here is a way of feeling what that filter is doing for you automatically.

Starting it

filtered = 0 as a starting value means the first second of output is the filter climbing from zero to reality, which looks exactly like a real signal and is not one. Start it at the first reading instead. Small detail, and it removes a startup transient that is otherwise blamed on the hardware.

Task: a low pass filter

Drive towards the wall with an exponential filter running, plotting raw and filtered as you go, and print the alpha: you chose.

from bugbot import *
connect()

ALPHA = 0.25
filtered = distance()

Challenges

  1. Run the same drive at alpha 0.05, 0.2 and 0.6 and compare the charts.
  2. Implement the moving average as well and plot all three. Which is smoother for the same delay?
  3. Filter imu()[1] instead. What is a sensible alpha for a signal you want to use in a controller?