Tutorial · Intermediate · 30 min

Pure Pursuit: Following a Path With One Knob

The controller that turns a list of waypoints into steering. The lookahead geometry, why the best value moves with speed, and what it costs at corners.

The rung nobody teaches

A planner gives you a path. A* gives you a list of grid cells; a survey gives you a list of waypoints; a mission gives you a line on a map. None of them tell the wheels what to do.

That gap is where pure pursuit lives, and it is one paragraph of geometry:

Find the point on the path a fixed distance ahead of you. Drive the circular arc that passes through it. Repeat.

There is no error signal, no integral term, no gain schedule. There is one number — the lookahead distance — and it decides everything.

The geometry

Put the goal point in the robot’s own frame: x forward, y to the left, at distance L_d. A circular arc from the robot’s current position through that point has curvature

κ = 2y / L_d²

which is worth reading twice, because everything follows from it. The lateral offset y is in the numerator, so the arc bends harder the further off-axis the goal is. The lookahead is squared in the denominator, so doubling it quarters the curvature.

Turned into a radius, and into a steering angle for a vehicle of wheelbase b:

R = L_d² / 2y            δ = atan(2 b sin α / L_d)

For a differential drive there is no steering angle — you command a rotation rate directly, ω = v κ. Same curvature, different actuator.

The one knob, and why it fights itself

Short lookahead. The goal point is close, so a small lateral offset produces a large curvature. The robot tracks the path tightly — and reacts violently to every wobble, including the ones its own steering just caused.

Long lookahead. The goal point is far, curvature is gentle, steering is smooth — and at a corner the goal point has already rounded the bend while the robot has not, so the robot cuts the corner.

Those pull in opposite directions and there is nothing in the algorithm to balance them. You pick a number.

Two stacked plots. The upper one shows RMS cross-track error against lookahead distance for four speeds from 1 to 3 metres per second: each curve is U-shaped, falling steeply from a short lookahead, reaching a marked minimum, then rising slowly, with the minimum moving to the right and upward as speed increases. The lower plot shows the absolute cross-track error along the path for two lookaheads at 2 metres per second: a short lookahead of 0.75 metres jitters everywhere, while a long lookahead of 4 metres is clean on the straights and spikes at each corner.
Measured in the path following simulator. The best lookahead is 0.9 m at 1 m/s and 1.4 m at 3 m/s — and the error at that optimum triples, from 0.048 m to 0.229 m. Speed costs accuracy that no tuning gives back. Download SVG

The trap in every textbook derivation

Write pure pursuit out on paper, with a robot that steers instantly, and shorter is always better. The error falls monotonically as the lookahead shrinks. There is no optimum, and the U-shape above does not exist.

Real robots have lag. Steering takes time to reach the commanded angle; the pose you act on is a few tens of milliseconds old by the time the wheels respond. The simulator models a 0.25 s steering time constant and 0.15 s of sensing and compute delay — 0.4 s of total lag, which is unremarkable for a hobby rover.

That lag is the entire reason the optimum exists. A short lookahead commands a correction, the correction arrives late, the robot has already moved past the error, and the next command over-corrects the other way. A controller that is faster than its own actuator oscillates. This is the same fact that governs PID derivative gain and the same one behind stopping distance: latency is not a detail, it is the thing that sets the tuning.

So how do you pick it?

The measured optima, and what they are in units that transfer:

Speed Best lookahead As a time Versus v × lag
1.0 m/s 0.9 m 0.90 s 2.25×
1.5 m/s 1.1 m 0.73 s 1.83×
2.0 m/s 1.3 m 0.65 s 1.62×
3.0 m/s 1.4 m 0.47 s 1.17×

Two honest readings of that table.

The lookahead grows with speed, but less than proportionally. The common advice “make it a fixed number of seconds of travel” is close but not right — the time falls from 0.90 s to 0.47 s across this range.

It sits at one to two times the distance you travel during your own control lag. That is the useful rule, because it is measurable: find your lag, multiply by your speed, and start there. On this robot v × lag runs from 0.4 m to 1.2 m and the optima are 0.9 m to 1.4 m.

In practice, ship it as a small linear schedule and clamp it:

float lookahead = constrain(K_LOOK * speed + L_MIN, L_MIN, L_MAX);

L_MIN matters more than K_LOOK. At a standstill the schedule wants zero lookahead, which is a division by zero wearing a disguise.

Implementing it

// Pure pursuit over a waypoint list. Returns the curvature to command.
float pursue(float x, float y, float heading, float lookahead) {
  // 1. Where am I on the path? Search only near the arc length I last reached —
  //    a global search latches onto whichever segment happens to be nearest, and a
  //    path that doubles back will teleport your progress to a corner you passed
  //    twenty seconds ago.
  float s = projectOntoPath(x, y, lastS);
  lastS = max(lastS, s);

  // 2. The goal point, one lookahead further along.
  Point goal = pointAtArcLength(lastS + lookahead);

  // 3. Into the robot frame, then straight into the curvature formula.
  float dx = goal.x - x, dy = goal.y - y;
  float lateral = -sin(heading) * dx + cos(heading) * dy;
  float ld = max(0.3f, hypotf(dx, dy));
  return 2.0f * lateral / (ld * ld);
}

Three things in twelve lines are worth more than the rest:

  • lastS is state, and it only moves forward. Without it, any path that crosses itself will strand the controller.
  • Clamp ld. As the goal point approaches, curvature goes to infinity.
  • The goal point is on the path, not the nearest waypoint. Aiming at waypoints makes the robot zig-zag between them; aiming at a point on the segment is what makes the motion smooth.

What it will and will not do

It will not stand still and turn to face the path. Pure pursuit has no notion of standing still — the curvature is a shape, and a shape needs forward motion. Start the robot roughly aligned, or drive a short opening arc first.

It has a minimum turning radius and the path may not respect it. If a corner is sharper than b / tan(δ_max), the robot cannot follow it however you tune. Either soften the path or slow down for the corner — which is what a motion profile is for.

It always cuts corners, by design. In the measured run at 2 m/s it drove 0.8% less than the commanded path. If that is unacceptable, you want a different controller — Stanley steers on cross-track error instead and swings wide rather than cutting.

It cannot fix a bad position estimate. Everything above assumes the robot knows where it is. Feed it a 1 Hz GPS fix and the RMS error goes from 0.09 m to 1.25 m — see GPS waypoint navigation.

When it goes wrong

Symptom Usually
Weaves down a straight Lookahead too short for the lag; lengthen it or measure the lag
Rounds off every corner Lookahead too long — this is the trade, not a bug
Cuts across to a much later part of the path Global projection instead of a windowed one that tracks arc length
Spins on the spot at the start Lookahead schedule hit zero at zero speed; clamp L_MIN
Follows well slowly, wanders fast The optimum moved; schedule the lookahead on speed
Overshoots the last waypoint Nothing tells it to stop — pure pursuit only steers
Perfect in simulation, poor on the robot The simulation had no steering lag. Add it and retune

Drive both controllers over the same course in the path following simulator — drag the lookahead and the whole run replots. Then build the GPS waypoint rover, where the position estimate becomes the hard part.

Explore the graph

Part of these builds

Projects and learning paths that include this tutorial.

Further reading

References