Time, rate and latency
How often the loop runs, how late the answer arrives, and what both do to a controller.
Do this lesson in the simulatorA 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()
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()
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:
- Run the loop faster, so the information is fresher.
- Turn the gain down, so a late push does less harm.
- 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")
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
- Raise the gain until the robot oscillates, then halve the loop rate. Does it take more gain or less to oscillate now?
- Add an artificial delay: keep the last three errors in a list and control on the oldest one. What does it look like?
- Time
camera_image()the way the loop above timesscan(). What loop rate could you afford with the camera in it?