Skip to content

Commit a8a4503

Browse files
authored
feat: Add reverse mode in pure pursuit (#1194)
* feat: Add vehicle model rendering function and pause simulation function * feat: Add support for reverse mode and related function adjustments * feat: Add reverse mode test case * feat: Limit the maximum steering angle to improve path tracking control * feat: Add a newline for improved readability in plot_arrow function * fix: Replace max/min with np.clip for steering angle limitation in pure pursuit control
1 parent ef8c154 commit a8a4503

File tree

2 files changed

+155
-17
lines changed

2 files changed

+155
-17
lines changed

PathTracking/pure_pursuit/pure_pursuit.py

Lines changed: 151 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,49 @@
1010
import math
1111
import matplotlib.pyplot as plt
1212

13+
import sys
14+
import pathlib
15+
sys.path.append(str(pathlib.Path(__file__).parent.parent.parent))
16+
from utils.angle import angle_mod
17+
1318
# Parameters
1419
k = 0.1 # look forward gain
1520
Lfc = 2.0 # [m] look-ahead distance
1621
Kp = 1.0 # speed proportional gain
1722
dt = 0.1 # [s] time tick
1823
WB = 2.9 # [m] wheel base of vehicle
1924

20-
show_animation = True
2125

26+
# Vehicle parameters
27+
LENGTH = WB + 1.0 # Vehicle length
28+
WIDTH = 2.0 # Vehicle width
29+
WHEEL_LEN = 0.6 # Wheel length
30+
WHEEL_WIDTH = 0.2 # Wheel width
31+
MAX_STEER = math.pi / 4 # Maximum steering angle [rad]
2232

23-
class State:
2433

25-
def __init__(self, x=0.0, y=0.0, yaw=0.0, v=0.0):
34+
show_animation = True
35+
pause_simulation = False # Flag for pause simulation
36+
is_reverse_mode = False # Flag for reverse driving mode
37+
38+
class State:
39+
def __init__(self, x=0.0, y=0.0, yaw=0.0, v=0.0, is_reverse=False):
2640
self.x = x
2741
self.y = y
2842
self.yaw = yaw
2943
self.v = v
30-
self.rear_x = self.x - ((WB / 2) * math.cos(self.yaw))
31-
self.rear_y = self.y - ((WB / 2) * math.sin(self.yaw))
44+
self.direction = -1 if is_reverse else 1 # Direction based on reverse flag
45+
self.rear_x = self.x - self.direction * ((WB / 2) * math.cos(self.yaw))
46+
self.rear_y = self.y - self.direction * ((WB / 2) * math.sin(self.yaw))
3247

3348
def update(self, a, delta):
3449
self.x += self.v * math.cos(self.yaw) * dt
3550
self.y += self.v * math.sin(self.yaw) * dt
36-
self.yaw += self.v / WB * math.tan(delta) * dt
51+
self.yaw += self.direction * self.v / WB * math.tan(delta) * dt
52+
self.yaw = angle_mod(self.yaw)
3753
self.v += a * dt
38-
self.rear_x = self.x - ((WB / 2) * math.cos(self.yaw))
39-
self.rear_y = self.y - ((WB / 2) * math.sin(self.yaw))
54+
self.rear_x = self.x - self.direction * ((WB / 2) * math.cos(self.yaw))
55+
self.rear_y = self.y - self.direction * ((WB / 2) * math.sin(self.yaw))
4056

4157
def calc_distance(self, point_x, point_y):
4258
dx = self.rear_x - point_x
@@ -51,13 +67,15 @@ def __init__(self):
5167
self.y = []
5268
self.yaw = []
5369
self.v = []
70+
self.direction = []
5471
self.t = []
5572

5673
def append(self, t, state):
5774
self.x.append(state.x)
5875
self.y.append(state.y)
5976
self.yaw.append(state.yaw)
6077
self.v.append(state.v)
78+
self.direction.append(state.direction)
6179
self.t.append(t)
6280

6381

@@ -117,14 +135,18 @@ def pure_pursuit_steer_control(state, trajectory, pind):
117135
if ind < len(trajectory.cx):
118136
tx = trajectory.cx[ind]
119137
ty = trajectory.cy[ind]
120-
else: # toward goal
138+
else:
121139
tx = trajectory.cx[-1]
122140
ty = trajectory.cy[-1]
123141
ind = len(trajectory.cx) - 1
124142

125143
alpha = math.atan2(ty - state.rear_y, tx - state.rear_x) - state.yaw
126144

127-
delta = math.atan2(2.0 * WB * math.sin(alpha) / Lf, 1.0)
145+
# Reverse steering angle when reversing
146+
delta = state.direction * math.atan2(2.0 * WB * math.sin(alpha) / Lf, 1.0)
147+
148+
# Limit steering angle to max value
149+
delta = np.clip(delta, -MAX_STEER, MAX_STEER)
128150

129151
return delta, ind
130152

@@ -142,18 +164,119 @@ def plot_arrow(x, y, yaw, length=1.0, width=0.5, fc="r", ec="k"):
142164
fc=fc, ec=ec, head_width=width, head_length=width)
143165
plt.plot(x, y)
144166

167+
def plot_vehicle(x, y, yaw, steer=0.0, color='blue', is_reverse=False):
168+
"""
169+
Plot vehicle model with four wheels
170+
Args:
171+
x, y: Vehicle center position
172+
yaw: Vehicle heading angle
173+
steer: Steering angle
174+
color: Vehicle color
175+
is_reverse: Flag for reverse mode
176+
"""
177+
# Adjust heading angle in reverse mode
178+
if is_reverse:
179+
yaw = angle_mod(yaw + math.pi) # Rotate heading by 180 degrees
180+
steer = -steer # Reverse steering direction
181+
182+
def plot_wheel(x, y, yaw, steer=0.0, color=color):
183+
"""Plot single wheel"""
184+
wheel = np.array([
185+
[-WHEEL_LEN/2, WHEEL_WIDTH/2],
186+
[WHEEL_LEN/2, WHEEL_WIDTH/2],
187+
[WHEEL_LEN/2, -WHEEL_WIDTH/2],
188+
[-WHEEL_LEN/2, -WHEEL_WIDTH/2],
189+
[-WHEEL_LEN/2, WHEEL_WIDTH/2]
190+
])
191+
192+
# Rotate wheel if steering
193+
if steer != 0:
194+
c, s = np.cos(steer), np.sin(steer)
195+
rot_steer = np.array([[c, -s], [s, c]])
196+
wheel = wheel @ rot_steer.T
197+
198+
# Apply vehicle heading rotation
199+
c, s = np.cos(yaw), np.sin(yaw)
200+
rot_yaw = np.array([[c, -s], [s, c]])
201+
wheel = wheel @ rot_yaw.T
202+
203+
# Translate to position
204+
wheel[:, 0] += x
205+
wheel[:, 1] += y
206+
207+
# Plot wheel with color
208+
plt.plot(wheel[:, 0], wheel[:, 1], color=color)
209+
210+
# Calculate vehicle body corners
211+
corners = np.array([
212+
[-LENGTH/2, WIDTH/2],
213+
[LENGTH/2, WIDTH/2],
214+
[LENGTH/2, -WIDTH/2],
215+
[-LENGTH/2, -WIDTH/2],
216+
[-LENGTH/2, WIDTH/2]
217+
])
218+
219+
# Rotation matrix
220+
c, s = np.cos(yaw), np.sin(yaw)
221+
Rot = np.array([[c, -s], [s, c]])
222+
223+
# Rotate and translate vehicle body
224+
rotated = corners @ Rot.T
225+
rotated[:, 0] += x
226+
rotated[:, 1] += y
227+
228+
# Plot vehicle body
229+
plt.plot(rotated[:, 0], rotated[:, 1], color=color)
230+
231+
# Plot wheels (darker color for front wheels)
232+
front_color = 'darkblue'
233+
rear_color = color
234+
235+
# Plot four wheels
236+
# Front left
237+
plot_wheel(x + LENGTH/4 * c - WIDTH/2 * s,
238+
y + LENGTH/4 * s + WIDTH/2 * c,
239+
yaw, steer, front_color)
240+
# Front right
241+
plot_wheel(x + LENGTH/4 * c + WIDTH/2 * s,
242+
y + LENGTH/4 * s - WIDTH/2 * c,
243+
yaw, steer, front_color)
244+
# Rear left
245+
plot_wheel(x - LENGTH/4 * c - WIDTH/2 * s,
246+
y - LENGTH/4 * s + WIDTH/2 * c,
247+
yaw, color=rear_color)
248+
# Rear right
249+
plot_wheel(x - LENGTH/4 * c + WIDTH/2 * s,
250+
y - LENGTH/4 * s - WIDTH/2 * c,
251+
yaw, color=rear_color)
252+
253+
# Add direction arrow
254+
arrow_length = LENGTH/3
255+
plt.arrow(x, y,
256+
-arrow_length * math.cos(yaw) if is_reverse else arrow_length * math.cos(yaw),
257+
-arrow_length * math.sin(yaw) if is_reverse else arrow_length * math.sin(yaw),
258+
head_width=WIDTH/4, head_length=WIDTH/4,
259+
fc='r', ec='r', alpha=0.5)
260+
261+
# Keyboard event handler
262+
def on_key(event):
263+
global pause_simulation
264+
if event.key == ' ': # Space key
265+
pause_simulation = not pause_simulation
266+
elif event.key == 'escape':
267+
exit(0)
145268

146269
def main():
147270
# target course
148-
cx = np.arange(0, 50, 0.5)
271+
cx = -1 * np.arange(0, 50, 0.5) if is_reverse_mode else np.arange(0, 50, 0.5)
149272
cy = [math.sin(ix / 5.0) * ix / 2.0 for ix in cx]
150273

151274
target_speed = 10.0 / 3.6 # [m/s]
152275

153276
T = 100.0 # max simulation time
154277

155278
# initial state
156-
state = State(x=-0.0, y=-3.0, yaw=0.0, v=0.0)
279+
state = State(x=-0.0, y=-3.0, yaw=math.pi if is_reverse_mode else 0.0, v=0.0, is_reverse=is_reverse_mode)
157280

158281
lastIndex = len(cx) - 1
159282
time = 0.0
@@ -173,22 +296,33 @@ def main():
173296

174297
time += dt
175298
states.append(time, state)
176-
177299
if show_animation: # pragma: no cover
178300
plt.cla()
179301
# for stopping simulation with the esc key.
180-
plt.gcf().canvas.mpl_connect(
181-
'key_release_event',
182-
lambda event: [exit(0) if event.key == 'escape' else None])
183-
plot_arrow(state.x, state.y, state.yaw)
302+
plt.gcf().canvas.mpl_connect('key_release_event', on_key)
303+
# Pass is_reverse parameter
304+
plot_vehicle(state.x, state.y, state.yaw, di, is_reverse=is_reverse_mode)
184305
plt.plot(cx, cy, "-r", label="course")
185306
plt.plot(states.x, states.y, "-b", label="trajectory")
186307
plt.plot(cx[target_ind], cy[target_ind], "xg", label="target")
187308
plt.axis("equal")
188309
plt.grid(True)
189310
plt.title("Speed[km/h]:" + str(state.v * 3.6)[:4])
311+
plt.legend() # Add legend display
312+
313+
# Add pause state display
314+
if pause_simulation:
315+
plt.text(0.02, 0.95, 'PAUSED', transform=plt.gca().transAxes,
316+
bbox=dict(facecolor='red', alpha=0.5))
317+
190318
plt.pause(0.001)
191319

320+
# Handle pause state
321+
while pause_simulation:
322+
plt.pause(0.1) # Reduce CPU usage
323+
if not plt.get_fignums(): # Check if window is closed
324+
exit(0)
325+
192326
# Test
193327
assert lastIndex >= target_ind, "Cannot goal"
194328

tests/test_pure_pursuit.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ def test1():
66
m.show_animation = False
77
m.main()
88

9+
def test_backward():
10+
m.show_animation = False
11+
m.is_reverse_mode = True
12+
m.main()
913

1014
if __name__ == '__main__':
1115
conftest.run_this_test(__file__)

0 commit comments

Comments
 (0)