Tuning the gain

Sluggish, good, oscillating. Loop speed and driving speed.

3.3ControlRobot club20 min

Do this lesson in the simulator

The 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}")

Run this in the simulator

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}")

Run this in the simulator

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")

Run this in the simulator

The worst error is now 22 degrees: worse than the sluggish gain of 1 (15), and not far off no controller at all (26 degrees in lesson 3.2). 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 sweep takes 14 seconds, longer than the fast task allows, so this cell runs on the lesson 3.2 mat. Same robot, same fault.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

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

speed = 70
for gain in [1, 2, 4, 8, 16, 32]:
    # this way is now heading 0
    reset_heading()
    worst, flips, last = 0, 0, 0
    # two seconds of driving for each gain
    for tick in range(20):
        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(speed, 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)
    # the next run goes back the way this one came, so the robot stays on the mat
    speed = -speed
    print(f"gain {gain}: worst error {round(worst, 1)} deg, {flips} sign changes")

Run this in the simulator

The worst error falls as the gain rises, from 12.4 degrees at gain 1 to 1.3 at gain 16, with no sign changes. At gain 32 the sign changes jump to 5. The good gain is just before that: about 16 here.

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

  1. Find the largest gain that gives at most one sign change at speed 100.
  2. Make the gain depend on the speed: gain = 4 * speed / 70.
  3. Log the error every tick into a list and print the list at the end. Where does it peak?