How far behind

Dead band, lag and loop period, measured as the centimetres the robot is behind its own plan.

U10.6Following a trajectoryUniversity30 min

Do this lesson in the simulator

A trajectory is a promise about time, and the robot does not keep it. This lesson is about measuring the shortfall, in centimetres and in seconds, and deciding what to do about it.

Where the delay comes from

source on this robot fix
dead band the first 3 cm/s of the profile produce nothing, so the robot sets off late dead band inverse
first order lag the velocity reaches the demand with a time constant of 0.25 s feedforward the acceleration, or accept it
loop period a command is up to 0.1 s stale before it is acted on run the loop faster
estimator lag a filtered position is by definition old news (U4, U6) do not over-filter the signal you control on

They add. A position error of e while travelling at v is a time error of e / v, and it is often more useful to quote it that way: "the robot is a fifth of a second behind" is a statement anyone can act on, while "the robot is 3 cm behind" depends on knowing how fast it was going.

The shape of the error

Run the trapezoid with feedforward and a correction, plot the error, and it has a shape that is the same on every machine.

  1. It grows on the accelerating ramp. The first order lag holds the velocity roughly a * tau below the demand. At a = 12 cm/s/s and tau = 0.25 s that is 3 cm/s, and over a 1.2 s ramp it builds a couple of centimetres of deficit. This is the peak of the whole run.
  2. It shrinks during the cruise. The demand has stopped changing, so the lag has nothing to do, and the feedback works the deficit off down to whatever the model error leaves behind: the residue of U10.5.
  3. It goes negative on the decelerating ramp. The same lag, with the sign reversed. The reference slows down and the robot, still carrying its velocity, runs past it. The robot is now early, and then it overshoots the end point.
  4. It settles. With the reference standing still, a proportional controller finally catches up, except for the last fraction of a centimetre that the dead band cannot reach.

The useful consequence: the error is built during the changes in the profile, not during the cruise. A gentler acceleration is not only kinder to the machine, it is a smaller error at both ends, and the overshoot at the finish is the acceleration limit asking to be lowered.

Two ways to deal with it

Plan for it. Add a margin to the duration, and do not treat the end of the profile as the moment of arrival. Real systems quote a settling time after the profile ends, and check the position then.

Close the loop on time as well as space. Instead of advancing the reference by the clock, advance it by where the robot actually got to: take the projected arc length, add a step, and use that as the next target. The reference then waits for the robot instead of running away from it, and the tracking error can never grow without bound.

That second idea is worth recognising, because you have already implemented it. Pure pursuit advances its target from the robot's own projection onto the path, not from a clock. It is a path follower rather than a trajectory follower, and that is exactly why it degrades gracefully when the robot falls behind. The price is that it has no opinion about timing at all: a pure pursuit controller will happily take all day.

Serious systems do both: a trajectory with a clock, and a rule that slows the clock down when the tracking error grows. That is called time scaling, and it is how a machine tool stays on the geometry when it cannot keep up with the feed rate. Geometry first, timing second, because being in the wrong place is worse than being late.

from bugbot import *
connect()

# a trapezoid to 80 cm, followed with feedforward and a correction
D, A, V, K, DT = 80.0, 12.0, 14.0, 1.5, 0.1
t_acc = V / A
d_acc = 0.5 * A * t_acc * t_acc
t_flat = (D - 2 * d_acc) / V
duration = 2 * t_acc + t_flat
print("the profile says", round(duration, 2), "s")

base, t0 = position()[1], clock()
while clock() - t0 < duration + 3:
    t = clock() - t0
    if t < t_acc:
        s, v = 0.5 * A * t * t, A * t
    elif t < t_acc + t_flat:
        s, v = d_acc + V * (t - t_acc), V
    elif t < duration:
        td = t - t_acc - t_flat
        s, v = d_acc + V * t_flat + V * td - 0.5 * A * td * td, V - A * td
    else:
        s, v = D, 0.0
    err = s - (position()[1] - base)
    drive(max(-100, min(100, 100 * (v + K * err) / 20.0)), 0, 0)
    plot("ref", s)
    plot("actual", position()[1] - base)
    plot("error", err)
    wait(DT)
stop()

Run this in the simulator

Watch the error line and count the four phases off against the list above. The peak is on the first ramp, the dip is on the last one, and neither of them is anywhere near the cruise, which is the part everybody instinctively worries about.

Task: how far behind the plan

Build a trapezoid to 80 cm at 12 cm/s/s and 14 cm/s, follow it with feedforward and a correction, and keep holding the target for a few seconds afterwards. Plot ref, actual and error, and print duration:, what the profile says the move takes, and worst:, the furthest the robot ever fell behind the reference in centimetres.

from bugbot import *
connect()

DT = 0.1
DISTANCE, A_MAX, V_CRUISE, K = 80.0, 12.0, 14.0, 1.5

Challenges

  1. Convert your worst into seconds using the speed at the moment it happened. Which number is more useful to whoever asked?
  2. Halve the acceleration limit and measure again. The move takes longer on paper: does it finish sooner in practice?
  3. Advance the reference from the robot's own position instead of from the clock. What can no longer go wrong, and what have you given up?