Time, rate and latency

How often the loop runs, how late the answer arrives, and what both do to a controller.

U1.4The robot as a systemUniversity25 min

Do this lesson in the simulator

A controller lives in time. How often it runs, and how late its information is, decide as much about whether it works as the arithmetic inside it.

Three different times

  • Period T: how often the loop runs. The BugBot's loops are usually 0.1 s, so 10 Hz.
  • Latency: how old the measurement is by the time you act on it. Sensing takes time, the radio takes time, and the motors take time to respond.
  • Time constant tau: how long the machine itself takes to react. About a quarter of a second for this drive.

A rule of thumb worth remembering: sample about ten times faster than the thing you are controlling. With tau around 0.25 s, a loop at 10 Hz is about right and a loop at 2 Hz is not.

The same controller at two rates

This robot pulls to one side. The controller corrects with rotation in proportion to the heading error, and the only difference between the two runs is how often it does so.

from bugbot import *
connect()

def wrapped(a):
    return (a + 180) % 360 - 180

# fast loop: ten times a second
for tick in range(40):
    error = wrapped(0 - heading())
    plot("fast", error)
    drive(70, 0, error * 3)
    wait(0.1)
stop()

Run this in the simulator

from bugbot import *
connect()

def wrapped(a):
    return (a + 180) % 360 - 180

# slow loop: twice a second, same gain
for tick in range(8):
    error = wrapped(0 - heading())
    plot("slow", error)
    drive(70, 0, error * 3)
    wait(0.5)
stop()

Run this in the simulator

Same gain, same robot, a very different result. The slow loop lets the error grow for half a second before it does anything about it, then corrects for an error that is already out of date.

Why lateness turns into instability

Feedback works by pushing against the current error. If the information is late, you are pushing against an error that has moved on, and at some delay you are pushing in the wrong direction entirely. That is the whole story of instability in one sentence: delay turns negative feedback into positive feedback.

The fixes are all of the same family:

  1. Run the loop faster, so the information is fresher.
  2. Turn the gain down, so a late push does less harm.
  3. Predict, so you push against where the error is going rather than where it was. That is the D term in U5, and the prediction step in U6.

Measuring your own loop

from bugbot import *
connect()

start = clock()
n = 20
for i in range(n):
    d = distance()
    tags = scan()          # this one costs real time on the robot
    wait(0.05)
print("period:", round((clock() - start) / n, 3), "s")

Run this in the simulator

Ask for 0.05 s and you get rather more, because the sensor reads are not free. On a real robot this is the difference between a loop that runs at the rate you designed for and one that quietly runs at half of it.

Task: hold the heading

Drive into the green zone, and from three seconds onwards stay within 8 degrees of heading 0. This robot pulls hard to one side, so the loop has to be doing real work all the way.

from bugbot import *
connect()

def wrapped(a):
    return (a + 180) % 360 - 180

# drive, correcting as you go, and keep correcting once you are there
forward(70, distance=130)
stop()

Challenges

  1. Raise the gain until the robot oscillates, then halve the loop rate. Does it take more gain or less to oscillate now?
  2. Add an artificial delay: keep the last three errors in a list and control on the oldest one. What does it look like?
  3. Time camera_image() the way the loop above times scan(). What loop rate could you afford with the camera in it?