Pure pursuit
Chase a point a fixed distance ahead on the path, and choose the one number that decides everything.
Do this lesson in the simulatorPrint it: worksheet (PDF) · answers (PDF) · as a web page
Guide: Pure pursuit path following explained
Cross-track control works on a straight leg. On a curve it has a problem: correcting towards the nearest point on the path aims the robot at the path, not along it, so it arrives at the curve pointing across it and has to correct again. The result is a robot that is always slightly late to every bend.
Pure pursuit fixes it with one idea: do not aim at the nearest point, aim at a point a fixed distance further along.
1. project the robot onto the path -> arc length s
2. take the point at arc length s + L -> the look-ahead point
3. drive towards that point
L is the look-ahead distance, and it is the only number in the algorithm with any real choice in it. Everything good and everything bad about pure pursuit is a consequence of that one value.
Why it works
Aiming at a point ahead builds the curvature of the path into the demand automatically. On a bend, the look-ahead point is already round the corner, so the robot starts turning before it reaches the corner, which is what a human driver does and what a nearest-point controller refuses to do.
For a car, the classic result is that chasing a point at distance L with lateral offset y in the body frame gives an arc of curvature
kappa = 2 * y / (L * L)
which is why the algorithm is popular: it turns a geometry problem straight into a steering command with no controller tuning at all. Note the L * L in the denominator. The effective cross-track gain is 2 / L^2, so doubling the look-ahead makes the controller four times gentler.
On a holonomic robot the geometry is simpler still. There is no steering to solve for: point the velocity vector at the look-ahead point and let the inverse kinematics from U2 work out the commands. The heading is then free, and can be used for something else entirely, such as keeping a camera pointed at a landmark.
The trade-off
This is the one real trade-off in the module and it is worth stating carefully.
| look-ahead | what you get | what it costs |
|---|---|---|
| small (3 to 6 cm) | tight tracking on straights, corners taken almost exactly | on a robot that steers like a car, a weave: the correction is aggressive and the drive's lag turns aggression into overshoot. Pointing the velocity, as here, does not weave |
| medium (15 to 25 cm) | smooth, stable, corners cut a little | the compromise everybody ships |
| large (50 cm) | very smooth, very stable, ignores noise | corners cut badly, and on a tight bend the robot can cut across the inside and leave the path entirely |
Two specific failure modes are worth recognising on sight.
Weaving with small L, on a robot that steers. A car, or a robot driven like one, can only move sideways by turning, so it corrects an error in two steps: turn towards the path, then let the turn carry it across. With a small L the turn it asks for is large, and the drive's lag (0.25 s here) keeps the heading swinging after the robot is back on the line, so it crosses the path and the next correction is the other way. It weaves down a straight with a period of about 2 s. You can see it in the simulator: a follower that turns to face the look-ahead point and drives forwards only weaves up to 4.1 cm either side of this tape at L = 3 and 14 cm/s, and stays within 0.4 cm at L = 5. The weave stops once L is about 0.3 s of travel. Raising L fixes it. Raising the gain does not.
The task's follower does not weave, and it is worth knowing why. Pointing the velocity at the look-ahead point moves the robot sideways directly, with no heading in between to swing past the line, so the lag only slows the correction down. At L = 6 it never leaves the tape by more than 0.9 cm, and even at L = 2 it stays within 0.5 cm at 11, 14 and 20 cm/s. For this robot a short look-ahead costs very little on the mat, which is one of the quiet advantages of being able to move in any direction.
Corner cutting with large L. On a corner, the look-ahead point is across the bend rather than round it, and the robot takes the chord. The error is roughly the sagitta of the chord, which for a 90 degree corner is a good fraction of L. If the path was planned to keep clear of an obstacle, that cut is exactly where the obstacle is, which is how a perfectly good plan produces a collision.
There is also a steady-state offset on a constant curvature path. Pointing the velocity straight at the look-ahead point means moving along a chord, so the robot settles inside the circle, at the radius where that chord is tangent to its own path: an offset of R * (1 - cos(L / R)), about L^2 / (2 * R). On the 40 cm bend here that is 4.0 cm at L = 18, and a follower like the task's came 4.5 cm off at worst. Worth knowing before you spend an afternoon tuning it out.
Speed-dependent look-ahead
The standard practice is L = L0 + k * v. The argument is time: at speed v, a look-ahead of L is L / v seconds of preview, and what the controller needs is a roughly constant preview time, not a constant distance. Fast means look further. For a robot that steers it also falls out of the stability analysis, because the lag costs a fixed number of seconds, not a fixed number of centimetres.
Finding the look-ahead point
Textbooks describe it as the intersection of a circle of radius L centred on the robot with the path. That is correct and it is fragile: when the robot is more than L from the path there is no intersection at all, and you need a special case for exactly the situation where the controller matters most.
Projecting first is better. Find the nearest point on the path, take its arc length, add L, and read off that point. It is defined everywhere, it is monotone (the robot cannot be sent backwards by noise), and it is a handful of lines for a polyline.
from bugbot import *
import math
connect()
arc = [(80 + 40 * math.cos(math.radians(180 - k * 11.25)),
120 + 40 * math.sin(math.radians(180 - k * 11.25))) for k in range(9)]
PATH = [(40.0, 40.0)] + arc + [(150.0, 160.0)]
cum = [0.0]
for (ax, ay), (bx, by) in zip(PATH, PATH[1:]):
cum.append(cum[-1] + math.hypot(bx - ax, by - ay))
print("the path is", round(cum[-1], 1), "cm long, in", len(PATH) - 1, "segments")
Task: chase the look-ahead point
The tape runs straight from (40, 40) to (40, 120), round a quarter circle of radius 40 centred on (80, 120), then straight to (150, 160). Follow it with pure pursuit, plot off path and speed, print path: (the length of the path) and drove: (how far the robot actually travelled), and stop at the far end.
from bugbot import *
import math
connect()
DT = 0.1
START = (40.0, 40.0)
CRUISE, LOOK = 11.0, 18.0
Challenges
- Run it at
LOOK = 6and atLOOK = 45. Which one cuts the corner, and by how much? Does the short one weave at all? - Steer it like a car instead: drive forwards only, and turn at
v * 2 * sin(alpha) / Lradians a second, wherealphais the angle of the look-ahead point from the way the robot faces. RunL = 3at 14 cm/s and watch the first straight, then tryL = 5. - Make the look-ahead grow with speed and see whether you can keep both the straights and the corner.
- Measure the largest
off pathvalue in the corner and compare it withL * L / (2 * 40).