Skip to content

Commit 1d27935

Browse files
authored
Merge pull request #70 from mohitk3000/docs-stanley-controller
Documentation: Stanley controller algorithm for trajectory following
2 parents 2d0cdc9 + f989a2b commit 1d27935

File tree

3 files changed

+295
-1
lines changed

3 files changed

+295
-1
lines changed
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# 4. Stanley Controller
2+
3+
In this chapter, the Stanley steering controller class is implemented for path tracking. This class implements the Stanley path tracking algorithm, which computes a steering angle to guide a vehicle's front axle back onto a reference course by correcting both heading error and lateral (cross-track) error.
4+
5+
Before getting into the code, let's get some basic understanding behind the algorithm:
6+
### **Stanley Controller**
7+
8+
- This algorithm is also known sometimes as `Hoffmann-Stanley-control`.
9+
- Developed at Stanford for autonomous vehicles DARPA challenge
10+
- Combines heading error and cross-track error.
11+
12+
1. **Heading error** ($\large{\psi_e}$)— the difference between the vehicle's heading and the path's tangent
13+
14+
2. **Cross-track error** ($\large{e}$) — lateral distance from the front axle to the nearest path point
15+
16+
![alt text](./path_ref.png)
17+
18+
$$
19+
\delta = \psi_e + \arctan\!\left(\frac{k \cdot e}{v}\right)
20+
$$
21+
22+
Image credit: https://ai.stanford.edu/~gabeh/papers/hoffmann_stanley_control07.pdf
23+
Ref: https://www.ri.cmu.edu/pub_files/2009/2/Automatic_Steering_Methods_for_Autonomous_Automobile_Path_Tracking.pdf
24+
25+
26+
**Advantages over Pure Pursuit**:
27+
28+
- Directly minimizes cross-track error at the front axle.
29+
- Better performance at higher speeds.
30+
- Naturally speed-adaptive.
31+
- Pure Pursuit is great for robots, low-speed vehicles, and when simplicity matters.
32+
- Stanley is preferred for autonomous cars and situations where you need tighter path adherence at varying speeds.
33+
34+
**Disadvantages**:
35+
- needs tuning of parameter $k$.
36+
- can be unstable at low speed(divison be 0) and steady state error.
37+
38+
39+
## 4.1 StanleyController Class
40+
41+
The controller class is located at:
42+
[stanley_controller.py](/src/components/control/stanley/stanley_controller.py)
43+
44+
```python
45+
"""
46+
stanley_controller.py
47+
48+
Author: Shisato Yano
49+
"""
50+
51+
import sys
52+
from pathlib import Path
53+
from math import sin, cos, tan, atan2
54+
55+
abs_dir_path = str(Path(__file__).absolute().parent)
56+
relative_path = "/../../../components/"
57+
58+
sys.path.append(abs_dir_path + relative_path + "state")
59+
60+
from state import State
61+
62+
63+
class StanleyController:
64+
"""
65+
Controller class by Stanley steering control algorithm
66+
"""
67+
```
68+
69+
This class imports `sin`, `cos`, `tan`, and `atan2` from Python's `math` module, as these trigonometric functions are used throughout the steering calculations. It also imports the `State` class to construct a virtual front axle state from the vehicle's rear axle state.
70+
71+
---
72+
73+
### 4.1.1 Constructor
74+
75+
```python
76+
def __init__(self, spec, course=None):
77+
self.SPEED_PROPORTIONAL_GAIN = 1.0
78+
self.CONTROL_GAIN = 0.5
79+
self.WHEEL_BASE_M = spec.wheel_base_m
80+
81+
self.course = course
82+
self.target_course_index = 0
83+
self.target_accel_mps2 = 0.0
84+
self.target_speed_mps = 0.0
85+
self.target_yaw_rate_rps = 0.0
86+
self.target_steer_rad = 0.0
87+
```
88+
89+
The constructor takes a `VehicleSpecification` object and an optional `CubicSplineCourse` object. The key member variables are:
90+
91+
| Variable | Value | Description |
92+
|---|---|---|
93+
| `SPEED_PROPORTIONAL_GAIN` | 1.0 | Gain for proportional speed control |
94+
| `CONTROL_GAIN` | 0.5 | Stanley gain `k` for cross-track error correction |
95+
| `WHEEL_BASE_M` | from spec | Distance between front and rear axles [m] |
96+
| `target_course_index` | 0 | Index of the nearest point on course |
97+
| `target_accel_mps2` | 0.0 | Computed acceleration command [m/s²] |
98+
| `target_steer_rad` | 0.0 | Computed steering angle command [rad] |
99+
| `target_yaw_rate_rps` | 0.0 | Computed yaw rate command [rad/s] |
100+
101+
---
102+
103+
## 4.2 Private Methods
104+
105+
### 4.2.1 `_calculate_front_axle_state`
106+
107+
```python
108+
def _calculate_front_axle_state(self, state):
109+
curr_rear_x = state.get_x_m()
110+
curr_rear_y = state.get_y_m()
111+
curr_yaw = state.get_yaw_rad()
112+
curr_spd = state.get_speed_mps()
113+
114+
curr_front_x = curr_rear_x + self.WHEEL_BASE_M * cos(curr_yaw)
115+
curr_front_y = curr_rear_y + self.WHEEL_BASE_M * sin(curr_yaw)
116+
curr_front_state = State(curr_front_x, curr_front_y, curr_yaw, curr_spd)
117+
118+
return curr_front_state
119+
```
120+
121+
The Stanley algorithm is applied at the **front axle**, not the rear axle. This method projects the rear axle position forward by `WHEEL_BASE_M` along the vehicle's current heading to compute where the front axle is, and returns it as a new `State` object.
122+
123+
The front axle position is calculated as:
124+
125+
```
126+
front_x = rear_x + L * cos(yaw)
127+
front_y = rear_y + L * sin(yaw)
128+
```
129+
130+
where `L` is the wheelbase.
131+
132+
---
133+
134+
### 4.2.3 `_calculate_target_course_index`
135+
136+
```python
137+
def _calculate_target_course_index(self, state):
138+
nearest_index = self.course.search_nearest_point_index(state)
139+
self.target_course_index = nearest_index
140+
```
141+
142+
This method finds the index of the nearest point on the course to the front axle's current position. This index is used in subsequent steps to look up the target speed and compute tracking errors.
143+
144+
---
145+
146+
### 4.2.4 `_decide_target_speed_mps`
147+
148+
```python
149+
def _decide_target_speed_mps(self):
150+
self.target_speed_mps = self.course.point_speed_mps(self.target_course_index)
151+
```
152+
153+
The target speed is read directly from the course at the nearest point index. This allows the course to specify variable speed profiles along its length.
154+
155+
---
156+
157+
### 4.2.4 `_calculate_target_acceleration_mps2`
158+
159+
```python
160+
def _calculate_target_acceleration_mps2(self, state):
161+
diff_speed_mps = self.target_speed_mps - state.get_speed_mps()
162+
self.target_accel_mps2 = self.SPEED_PROPORTIONAL_GAIN * diff_speed_mps
163+
```
164+
165+
A simple proportional speed controller is used. The acceleration command is proportional to the difference between the target speed and the current front axle speed:
166+
167+
```
168+
accel = Kp * (target_speed - current_speed)
169+
```
170+
171+
---
172+
173+
### 4.2.5 `_calculate_tracking_error`
174+
175+
```python
176+
def _calculate_tracking_error(self, state):
177+
error_lon_m, error_lat_m, error_yaw_rad = self.course.calculate_lonlat_error(state, self.target_course_index)
178+
return error_lon_m, error_lat_m, error_yaw_rad
179+
```
180+
181+
This method delegates to the course object to compute three error terms between the front axle and the nearest course point:
182+
183+
| Error | Description |
184+
|---|---|
185+
| `error_lon_m` | Longitudinal error along the course tangent [m] |
186+
| `error_lat_m` | Lateral (cross-track) error perpendicular to the course [m] |
187+
| `error_yaw_rad` | Heading error between vehicle and course tangent [rad] |
188+
189+
Only `error_lat_m` and `error_yaw_rad` are used for steering.
190+
191+
---
192+
193+
### 4.2.6 `_calculate_control_input`
194+
195+
```python
196+
def _calculate_control_input(self, state, error_lat_m, error_yaw_rad):
197+
curr_spd = state.get_speed_mps()
198+
if abs(curr_spd) != 0.0:
199+
error_steer_rad = atan2(self.CONTROL_GAIN * error_lat_m, curr_spd)
200+
else:
201+
error_steer_rad = 0.0
202+
self.target_steer_rad = -1 * (error_steer_rad + error_yaw_rad)
203+
204+
self.target_yaw_rate_rps = curr_spd * tan(self.target_steer_rad) / self.WHEEL_BASE_M
205+
```
206+
207+
This is the core of the Stanley algorithm. The steering angle is computed as:
208+
209+
```
210+
δ = -(ψ_error + arctan(k * e_lat / v))
211+
```
212+
- minus sign comes due to the sign convention of steering angle.
213+
214+
where:
215+
- `ψ_error` (`error_yaw_rad`) - heading error between vehicle and course tangent
216+
- `e_lat` (`error_lat_m`) - lateral cross-track error
217+
- `k` (`CONTROL_GAIN`) - Stanley gain constant
218+
- `v` (`curr_spd`) - current vehicle speed
219+
220+
The `atan2` term increases steering correction as lateral error grows, and decreases it at higher speeds to prevent overcorrection. A guard against zero speed is included to avoid division by zero.
221+
222+
The yaw rate is then derived from the steering angle using the kinematic bicycle model:
223+
224+
```
225+
yaw_rate = v * tan(δ) / L
226+
```
227+
since, for kinematic bicycle model:
228+
```
229+
tan(δ) = L / R => R = L / tan(δ)
230+
```
231+
232+
---
233+
234+
## 4.3 Public Methods
235+
236+
### 4.3.1 `update`
237+
238+
```python
239+
def update(self, state, time_s):
240+
if not self.course: return
241+
242+
front_axle_state = self._calculate_front_axle_state(state)
243+
self._calculate_target_course_index(front_axle_state)
244+
self._decide_target_speed_mps()
245+
self._calculate_target_acceleration_mps2(front_axle_state)
246+
_, error_lat_m, error_yaw_rad = self._calculate_tracking_error(front_axle_state)
247+
self._calculate_control_input(front_axle_state, error_lat_m, error_yaw_rad)
248+
```
249+
250+
This is the main entry point called every simulation frame by `FourWheelsVehicle`. It orchestrates all private methods in order:
251+
252+
```
253+
update()
254+
1. Project rear axle - front axle state
255+
2. Find nearest course index from front axle
256+
4. Read target speed at that index
257+
4. Compute acceleration from speed error
258+
5. Compute lateral and heading errors
259+
6. Compute steering angle and yaw rate (Stanley law)
260+
```
261+
262+
If no course is set, the method returns early without computing anything.
263+
264+
---
265+
266+
### 4.3.2 Getter Methods
267+
268+
```python
269+
def get_target_accel_mps2(self):
270+
return self.target_accel_mps2
271+
272+
def get_target_yaw_rate_rps(self):
273+
return self.target_yaw_rate_rps
274+
275+
def get_target_steer_rad(self):
276+
return self.target_steer_rad
277+
```
278+
279+
These three getter methods expose the computed control outputs. They are called by `FourWheelsVehicle` after each `update()` to apply the commands to the vehicle's state.
280+
281+
---
282+
283+
### 4.3.3 `draw`
284+
285+
```python
286+
def draw(self, axes, elems):
287+
pass
288+
```
289+
290+
This method is required by the visualizer interface - all objects registered with `GlobalXYVisualizer` must have a `draw` method. For the controller, no visual element needs to be drawn, so the body is left empty with `pass`.
291+
292+
**Author**: Mohit Kumar
293+

doc/4_path_tracking/path_ref.png

79.8 KB
Loading

doc/DESIGN_DOCUMENT.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ This document provides an explanation of each Python programs design in this rep
44
## Table of contents
55
1. [World Visualization](/doc/1_world_visualization/1_world_visualization.md)
66
2. [Vehicle Model](/doc/2_vehicle_model/2_vehicle_model.md)
7-
3. [Sensor Models](/doc/3_sensor_models/3_sensor_models.md)
7+
3. [Sensor Models](/doc/3_sensor_models/3_sensor_models.md)
8+
4. [Path Tracking](/doc/4_path_tracking/4_staley_controller.md)

0 commit comments

Comments
 (0)