Tuning the gain
Sluggish, good, oscillating. Loop speed and driving speed.
Do this lesson in the simulatorThe gain is one number, and it decides whether a controller is sluggish, good, or shaking itself to pieces. Engineers spend real time on it. In this lesson you measure what it does.
Two numbers describe a run: the worst error seen, and how many times the error changed sign. A sign change means the robot crossed the target and went the other way. A good controller crosses once or not at all.
Too little
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def wrapped(h):
return (h + 180) % 360 - 180
worst, flips, last = 0, 0, 0
while position()[1] < 60:
error = wrapped(0 - heading())
worst = max(worst, abs(error))
if error * last < 0:
flips += 1
last = error
# gain 1
drive(70, 0, error * 1)
# pause 0.1 s (the robot keeps doing what it was told)
wait(0.1)
# all motors off
stop()
# where am I? (cm from where I started)
x, y = position()
print(f"gain 1: worst error {round(worst)} deg, {flips} sign changes, ended at x={x}")
With a gain of 1 the correction is too weak to hold the curving robot: the error grows to 15 degrees before the controller matches it, and the robot ends nearly 10 cm off the line. It never crosses the target at all. A controller that is too weak is called sluggish.
Too much
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def wrapped(h):
return (h + 180) % 360 - 180
worst, flips, last = 0, 0, 0
while position()[1] < 60:
error = wrapped(0 - heading())
worst = max(worst, abs(error))
if error * last < 0:
flips += 1
last = error
# gain 40
drive(70, 0, error * 40)
# pause 0.1 s (the robot keeps doing what it was told)
wait(0.1)
# all motors off
stop()
# where am I? (cm from where I started)
x, y = position()
print(f"gain 40: worst error {round(worst)} deg, {flips} sign changes, ended at x={x}")
Watch the robot: it snakes. A tiny error becomes a huge rotation, which overshoots, which becomes a huge rotation the other way, over a dozen times in four seconds. That is oscillation, and in a real machine it wears parts out. The rotation is also capped at 100, so at gain 40 anything over 2.5 degrees is just bang-bang again.
Why the loop speed matters
Between one look and the next, the robot keeps doing whatever you last said. A high gain with a slow loop is the worst combination: a big correction that runs for too long.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def wrapped(h):
return (h + 180) % 360 - 180
worst, flips, last = 0, 0, 0
while position()[1] < 60:
error = wrapped(0 - heading())
worst = max(worst, abs(error))
if error * last < 0:
flips += 1
last = error
# forward, sideways, rotation: -100 to 100 each, until the next command
drive(70, 0, error * 40)
# a slow loop
wait(0.3)
# all motors off
stop()
print(f"gain 40, slow loop: worst error {round(worst)} deg, {flips} sign changes")
The worst error is now over 20 degrees: worse than no controller at all. On a real robot the loop runs over a radio link, so the delay is never zero. That alone limits how high a gain can go.
Measure the sweep
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def wrapped(h):
return (h + 180) % 360 - 180
for gain in [1, 2, 4, 8, 16, 32]:
# count from here as (0, 0)
reset_position()
# this way is now heading 0
reset_heading()
worst, flips, last = 0, 0, 0
while position()[1] < 15:
error = wrapped(0 - heading())
worst = max(worst, abs(error))
if error * last < 0:
flips += 1
last = error
# forward, sideways, rotation: -100 to 100 each, until the next command
drive(70, 0, error * gain)
# pause 0.1 s (the robot keeps doing what it was told)
wait(0.1)
# all motors off
stop()
# pause 0.3 s (the robot keeps doing what it was told)
wait(0.3)
print(f"gain {gain}: worst error {round(worst, 1)} deg, {flips} sign changes")
Each run is short, so the numbers are small, but the shape is what matters: the worst error falls as the gain rises, and then the sign changes start climbing. The good gain is just before that.
Speed and gain together
Faster driving means the fault acts faster too: this robot turns about twice as hard at speed 100 as at 50. A gain that is fine at 50 may be sluggish at 100, and a loop that was fast enough may not be.
Task: hold the heading, fast
The zone is narrower and you have six seconds. The starter's gain of 40 wobbles and wastes time, and its speed is too low. Tune both.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def wrapped(h):
return (h + 180) % 360 - 180
while position()[1] < 72:
error = wrapped(0 - heading())
# forward, sideways, rotation: -100 to 100 each, until the next command
drive(60, 0, error * 40)
# pause 0.05 s (the robot keeps doing what it was told)
wait(0.05)
# all motors off
stop()
Challenges
- Find the largest gain that gives at most one sign change at speed 100.
- Make the gain depend on the speed:
gain = 4 * speed / 70. - Log the error every tick into a list and print the list at the end. Where does it peak?