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 simulatorCross-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 (5 cm) | tight tracking on straights, corners taken exactly | oscillation: the correction is aggressive and the drive's lag turns aggression into a wobble |
| 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.
Oscillation with small L. The robot weaves down a straight line with a period of a second or two. That is the lag: by the time the correction arrives the robot has already crossed the path, so the next correction is the other way. Raising L fixes it. Raising the gain does not.
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: pure pursuit tracks a circle slightly inside it, by about L^2 / (8 * R). 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. 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 weaves and which one cuts the corner, and by how much? - 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 / (8 * 40).