The derivative term

Damping the overshoot, and why D on a noisy signal needs a filter or it is useless.

U5.3Feedback controlUniversity30 min

Do this lesson in the simulator

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

Run this in the simulator

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.

The heading error and the D term over the first three seconds, with the slope between two looks marked00.511.522.530306090error, degrees52° to 21° in 0.25 s:d = -123° per second00.511.522.53-100-500time, secondsD term, %0.8 × -123 = -98 %
The D term is the slope of the line between two looks. Between 0.5 s and 0.75 s the error falls 31 degrees, the steepest part of the turn, so the D term is at its most negative, -98 percent, taking that much off the P push. Once the error stops changing, the slope is flat and the D term is within a few percent of zero from 2.75 s.

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

Run this in the simulator

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:

  1. Filter the measurement before differentiating. Costs delay, which partly defeats the D term.
  2. Filter the derivative itself, which is the same idea with a different knob.
  3. 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.

Distance readings from a robot standing still, and the D term worked out from them and from the flow sensor012345273135distance(), cm012345-50-2502550time, secondsD term, %differencing distance()from -flow()[1]
The robot has not moved, but its distance readings wobble by about a centimetre, and two readings 0.1 s apart differ by up to 3.7 cm. Divided by 0.1 s and multiplied by Kd = 1.2, that is a D term of up to 44 percent. Worked out from flow() instead, it stays within 2.

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.
The heading error on the quarter turn with no D, a little, a lot and too much04590no D: 20° pastKd 0.8: 1° past, within 3° from 1.75 s0246804590time, secondsKd 2: stops 1.4° short02468time, secondsKd 4: twitches, up to 10° outerror, degrees
The same turn four times, with Kp 4 throughout and only Kd changed. A little D takes out the swing. A lot brakes so early that the last push falls inside the dead band. Too much throws the command from side to side every look, and the robot never settles.

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

  1. Use -flow()[1] as the derivative instead of differencing the distance. Compare the command traces.
  2. On the distance loop, turn Kd up until the robot buzzes while standing still. What is the noise doing?
  3. Differentiate the measurement rather than the error, then change the setpoint mid-run and compare.