Skip to content

Pure Pursuit

Imagine you have a robot car, and you want it to follow a winding road/curved path. If the robot just tries to go straight or only follows the exact points on the path, it might turn too sharply, wobble, or even go off track!

How Does Pure Pursuit Help?

Instead of following the path like a dot-to-dot drawing, the robot looks a little ahead and aims for a point in front of it. This way, it turns smoothly and follows the path naturally, just like how you ride a bike or drive a car. This can be compared to a donkey chasing a carrot (as the donkey will keep moving forward to reach the carrot, but never actually reach it and eat it, till the path ends).

Mapping Diagram

Advantages/Disadvantages

Although Pure Pursuit is not the most accurate path-following algorithm to follow a path as the one shown below, it has a decent amount of accuracy when used, and compared to other algorithms, has a speed advantage.

Mapping Diagram
What is the main purpose of the Pure Pursuit algorithm in robotics?

The Math

Draw a Circle With the Robot as the center

\[(x - h)^2 + (y - k)^2 = r^2\]

This equation represents a circle where:

  • \((h, k)\) is the center of the circle and the robot.
  • \(r\) is the lookahead distance, which is how far ahead on the path we want to look ahead. This is also the radius of the circle.
  • \((x, y)\) represents any point on the circle we draw around our robot.
  • Wherever our circle intersects the path is the target point our robot seeks to travel to. If there's multiple intersections, Pure Pursuit will pick the forward-most intersection that is ahead of the robot.

Calculating Steering Angle using Wheelbase

To steer the front wheels toward that target point, Pure Pursuit uses the vehicle's geometry. Specifically, its Wheelbase (\(L\)), which is the distance between the front and rear axles.

First, we calculate the required path curvature (\(\gamma\)) with lateral offset (\(x\)) and lookahead distance (\(L_d\)):

\[\gamma = \frac{2x}{L_d^2}\]

Then, we can convert that curvature into the actual steering angle (\(\delta\)) using the wheelbase (\(L\)):

\[\delta = \arctan(\gamma \cdot L) \]
  • Why Wheelbase Matters: A vehicle with a longer wheelbase \(L\) (like a long truck) needs to turn its front wheels at a larger steering angle to follow the exact same turning arc as a smaller robot with a short wheelbase.

Algorithm Steps

The Pure Pursuit algorithm follows these steps:

  1. Draw a circle around the robot with your chosen lookahead-value (Bigger lookahead means it will follow the path less close and smaller means it will follow the path more close).
  2. See on the path where the circle intersects with the path (this is the goal point).
  3. Calculate the lateral offset distance \(x\) from the robot's heading line to the lookahead point.
  4. Use the wheelbase (\(L\)) and curvature equation to compute the required front steering angle \(\delta\).
  5. Command the steering servo/motors to angle \(\delta\) and move forward.
  6. Repeat at each timestep until reaching the destination.
If the lookahead distance is increased, what is the likely effect on the robot's path following?

Demo

Drag the blue waypoints below to reshape the path, then watch the robot steer itself along it using the Pure Pursuit geometry described above. The panel on the right shows the live lateral offset (\(x\)) and steering angle (\(\delta\)) the robot is computing at each step.

Things to try:

  1. Increase lookahead — the dashed lookahead circle grows, and the robot cuts corners more, producing a smoother but less exact path.
  2. Decrease lookahead — the robot hugs the path more tightly, but can wobble on sharp turns.
  3. Drag a waypoint into a sharp corner — watch the steering angle spike as the required curvature increases.
  4. Increase speed — the robot covers the path faster, but doesn't slow for turns, so it swings wider on curves.

Path View (drag the waypoints)

Live Readout (from Pure Pursuit Geometry)

x (lateral offset)0.0
δ (steering angle)0.0°
Driving straight

The Code

View Pure Pursuit code on GitHub

KEY SNIPPETS OF CODE

Here we find the lookahead point using the intersection of the circle and the path. We use the "discriminant" to tell the amount of intersection points (if any) with the circle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
     def find_lookahead_point(self): # finding the next lookahead point
        robot_position = self.jackal_robot.localize() # localizes properly
        for i in range(self.current_index, len(self.path) - 1):
            start, end = self.path[i], self.path[i + 1] # looking in the line between waypoints
            # Compute quadratic coefficients for lookahead
            dx, dy = end[0] - start[0], end[1] - start[1]
            fx, fy = start[0] - robot_position[0], start[1] - robot_position[1]
            a, b, c = dx**2 + dy**2, 2 * (fx * dx + fy * dy), fx**2 + fy**2 - self.lookahead**2
            discriminant = b**2 - 4 * a * c

            if discriminant >= 0: # finding and returning intersection/updating the points crossed to make sure it doesn't go backwards
                discriminant_sqrt = math.sqrt(discriminant)
                t1 = (-b + discriminant_sqrt) / (2 * a)
                t2 = (-b - discriminant_sqrt) / (2 * a)

                if 0 <= t1 <= 1:
                    self.current_index = i  # Update current index
                    return (start[0] + t1 * dx, start[1] + t1 * dy)
                if 0 <= t2 <= 1:
                    self.current_index = i  # Update current index
                    return (start[0] + t2 * dx, start[1] + t2 * dy)

        return self.path[-1] # returns last point if nothing found(shouldnt ever occur but fault tolerance)

View more Pure Pursuit code (simulation) on GitHub

In this file we find the lookahead point and utilize PID to make the robot head toward the lookahead point found using the previous code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  goal=self.follower.find_lookahead_point()
                self.x_goal=goal[0]
                self.y_goal=goal[1]


                # showing the lookahead intersection point
                p.addUserDebugLine([self.x_goal,self.y_goal-0.1,0.05],
                                    [self.x_goal,self.y_goal+0.1 , 0.05],
                                    lineColorRGB=[1, 0, 0], 
                                    lineWidth=300)

                self.x_error=self.x_goal-self.current_x
                self.y_error=self.y_goal-self.current_y
                self.h_error=self.angle_wrap(math.atan2(self.y_error,self.x_error)-self.current_h)

                # calculate with pid for the velocity to go to the lookahead
                self.linear_velocity=self.linear_pid.calculateVelocity(self.x_error)
                self.angular_velocity=self.angular_pid.calculateVelocity(self.h_error)

                # setting the velocities
                self.jackal_robot.inverse_kinematics(self.linear_velocity,self.angular_velocity)
                self.jackal_robot.setVelocity()