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 drive's rise after a command, with the moments a 10 Hz loop and a 2 Hz loop look at it00.250.511.500.51time after the command, secondsshare of the way10 Hz: a look every 0.1 s2 Hz: a look every 0.5 stau = 0.25 s
The drive gets most of the way in 0.25 s. The 10 Hz loop looks 8 times while it is rising, and can react to it. The 2 Hz loop's first look after the command comes when the rise is already 86 percent done.

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 * 10)
    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 * 10)
    wait(0.5)
stop()

Run this in the simulator

Same gain, same robot, a very different result. At ten times a second the error never gets past 1.6 degrees. At twice a second it swings 1.6, -3.8, 8.6, -16.5 degrees, each swing bigger than the last, and the robot finishes facing 51 degrees off. 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.

The slow loop's heading error over time, the value it saw at each look, held until the next, and the fast loop for comparison01234-50-25020time, secondsheading error, degreesat 2 s it sees +8.6and corrects for thatuntil 2.5 s-51°fast
The slow loop. The red dots are its looks, and each dashed line is the error it goes on correcting for until the next look. From 1.5 s the real error (the solid line) has crossed zero well before the next look, so the loop is pushing the wrong way. The green line is the fast loop, which never gets past 1.6 degrees.

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

In the simulator this prints period: 0.05 s, exactly what was asked for, and that is worth being suspicious of. Simulated time moves in steps of 0.02 s, so wait(0.05) is two steps one time and three the next, averaging out to the time you asked for. And the sensor reads cost no simulated time at all. On a real robot they do: scan() and the camera take real milliseconds, you ask for 0.05 s and get rather more, and that is the difference between a loop that runs at the rate you designed for and one that quietly runs at half of it.

Where each wait(0.05) ends on the simulator's clock, against where 0.05 s steps would end3 steps2 steps3 steps2 steps3 steps00.060.10.160.20.26asked for: every 0.05 sclock steps: 0.02 s
The simulator's clock moves in steps of 0.02 s, so each wait(0.05) ends on a step: 3 steps, then 2, then 3 again. Every other time the loop comes round between the 0.05 s marks, but the times average out to exactly what was asked.

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 the loop above with wait(0.03) and with wait(0.07). The simulator gives exactly 0.03 and 0.07. Add a line inside the loop that works something out for a long time, such as sum(range(1000000)). Does the period change in the simulator? Would it on the robot?