The derivative term
Damping the overshoot, and why D on a noisy signal needs a filter or it is useless.
Do this lesson in the simulatorPrint it: worksheet (PDF) · answers (PDF) · as a web page
Guide: PID control explained
Proportional control looks only at where you are. The derivative term looks at how fast you are getting there.
command = Kp * error + Kd * d(error)/dt
When the error is shrinking quickly, the derivative is large and negative, and the D term subtracts from the command. The controller eases off before it arrives. That is damping, and it is what removes overshoot without giving up the gain that made the loop fast.
In code
The quarter turn from U5.2, where Kp = 4 swung about 20 degrees past the target. Same gain, with a D term added.
from bugbot import *
connect()
KP, KD = 4.0, 0.8
TARGET = 90.0 # degrees: a quarter turn on the spot
DT = 0.25
last = TARGET - heading()
worst = 0.0
for tick in range(32): # 8 seconds
error = (TARGET - heading() + 180) % 360 - 180
d = (error - last) / DT
last = error
worst = min(worst, error)
plot("error", error)
plot("d term", KD * d)
drive(0, 0, max(-100, min(100, KP * error + KD * d)))
wait(DT)
stop()
print("overshoot", round(max(0.0, -worst), 1), "degrees")
The overshoot drops from about 20 degrees to about 1, and the robot is within 3 degrees of 90 in under 2 s instead of 4. Watch the two lines. The D term spikes early, down to about -100 percent while the robot is turning fastest, and has died away by about 2.5 s. It does its work on the way in and then gets out of the way.
Why D is difficult in practice
It amplifies noise. Differentiating means subtracting two nearly equal numbers and dividing by a small one. The heading above is clean, so the problem does not show. distance() is not: on the wall mat from U5.2 it wobbles by about 1 cm, and here is the same kind of controller on it.
from bugbot import *
connect()
KP, KD = 3.0, 1.2
TARGET = 30.0
DT = 0.1
last = distance() - TARGET
for tick in range(150):
error = distance() - TARGET
d = (error - last) / DT
last = error
plot("error", error)
plot("d term", KD * d)
drive(max(-70, min(70, KP * error + KD * d)), 0, 0)
wait(DT)
stop()
This time the D term never goes quiet. Two readings 0.1 s apart can differ by 4 cm with the robot all but still, which the derivative reports as 40 cm/s of approach speed that is not happening, about three times this robot's speed at 70 percent. Multiplied by Kd = 1.2 that is a kick of nearly 50 percent, and on the chart the D term swings by about 45 percent either way after the robot has arrived, as much as it did on the way in.
Three standard treatments:
- Filter the measurement before differentiating. Costs delay, which partly defeats the D term.
- Filter the derivative itself, which is the same idea with a different knob.
- Use a measured rate instead of a difference.
flow()measures speed directly, and a measured derivative is far better behaved than a computed one.
That third option is the best available, and it is why serious systems put a rate sensor on anything they intend to differentiate. On this robot it means using -flow()[1] rather than differencing distance(). In the cell above, 1.2 * -flow()[1] stays within about 4 percent after the robot has arrived, against the differenced term's 45.
Derivative kick. If the setpoint jumps, the error jumps, and the derivative of a jump is enormous. The standard fix is to differentiate the measurement rather than the error, since the measurement does not jump. It gives the same damping and no kick.
The shape it produces
On the quarter turn at Kp = 4, D changes the character of the response:
- No D: about 20 degrees of overshoot, a few swings, settles in about 4 s.
- A little D (0.8): about 1 degree of overshoot, settled in under 2 s. Usually the best behaved.
- A lot of D (2): no overshoot, a slower approach, and it stops about 1.4 degrees short, because it brakes so early that the last push falls into the dead band.
- Too much D (4): the D term throws the command from one side to the other every tick and the robot twitches about the target, up to about 10 degrees, without ever settling. On a noisy sensor like
distance()the same gain makes it buzz while standing still.
Task: damp the overshoot
This robot is carrying a load, so it takes about three times as long to speed up and to slow down. That extra lag is exactly the job the D term is for. Turn to face 90 degrees and be within 3 degrees of it from three seconds onwards. Plot error and d term.
On this robot P alone cannot do it, whatever the gain and however fast the loop runs: a gain low enough not to swing past arrives too late, and a gain high enough to arrive in time swings past and is still swinging at three seconds. Start from KP = 4 and find a KD that brakes it in time.
from bugbot import *
connect()
KP, KD = 4.0, 0.0
DT = 0.25
last = 90.0 # the error at the start
Challenges
- Use
-flow()[1]as the derivative instead of differencing the distance. Compare the command traces. - On the distance loop, turn
Kdup until the robot buzzes while standing still. What is the noise doing? - Differentiate the measurement rather than the error, then change the setpoint mid-run and compare.