simple-autonomous-car 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simple_autonomous_car/__init__.py +96 -0
- simple_autonomous_car/alerts/__init__.py +5 -0
- simple_autonomous_car/alerts/track_bounds_alert.py +276 -0
- simple_autonomous_car/car/__init__.py +5 -0
- simple_autonomous_car/car/car.py +234 -0
- simple_autonomous_car/constants.py +112 -0
- simple_autonomous_car/control/__init__.py +7 -0
- simple_autonomous_car/control/base_controller.py +152 -0
- simple_autonomous_car/control/controller_viz.py +282 -0
- simple_autonomous_car/control/pid_controller.py +153 -0
- simple_autonomous_car/control/pure_pursuit_controller.py +578 -0
- simple_autonomous_car/costmap/__init__.py +12 -0
- simple_autonomous_car/costmap/base_costmap.py +187 -0
- simple_autonomous_car/costmap/grid_costmap.py +507 -0
- simple_autonomous_car/costmap/inflation.py +126 -0
- simple_autonomous_car/detection/__init__.py +5 -0
- simple_autonomous_car/detection/error_detector.py +165 -0
- simple_autonomous_car/filters/__init__.py +7 -0
- simple_autonomous_car/filters/base_filter.py +119 -0
- simple_autonomous_car/filters/kalman_filter.py +131 -0
- simple_autonomous_car/filters/particle_filter.py +162 -0
- simple_autonomous_car/footprint/__init__.py +7 -0
- simple_autonomous_car/footprint/base_footprint.py +128 -0
- simple_autonomous_car/footprint/circular_footprint.py +73 -0
- simple_autonomous_car/footprint/rectangular_footprint.py +123 -0
- simple_autonomous_car/frames/__init__.py +21 -0
- simple_autonomous_car/frames/frenet.py +267 -0
- simple_autonomous_car/maps/__init__.py +9 -0
- simple_autonomous_car/maps/frenet_map.py +97 -0
- simple_autonomous_car/maps/grid_ground_truth_map.py +83 -0
- simple_autonomous_car/maps/grid_map.py +361 -0
- simple_autonomous_car/maps/ground_truth_map.py +64 -0
- simple_autonomous_car/maps/perceived_map.py +169 -0
- simple_autonomous_car/perception/__init__.py +5 -0
- simple_autonomous_car/perception/perception.py +107 -0
- simple_autonomous_car/planning/__init__.py +7 -0
- simple_autonomous_car/planning/base_planner.py +184 -0
- simple_autonomous_car/planning/goal_planner.py +261 -0
- simple_autonomous_car/planning/track_planner.py +199 -0
- simple_autonomous_car/sensors/__init__.py +6 -0
- simple_autonomous_car/sensors/base_sensor.py +105 -0
- simple_autonomous_car/sensors/lidar_sensor.py +145 -0
- simple_autonomous_car/track/__init__.py +5 -0
- simple_autonomous_car/track/track.py +463 -0
- simple_autonomous_car/visualization/__init__.py +25 -0
- simple_autonomous_car/visualization/alert_viz.py +316 -0
- simple_autonomous_car/visualization/utils.py +169 -0
- simple_autonomous_car-0.1.2.dist-info/METADATA +324 -0
- simple_autonomous_car-0.1.2.dist-info/RECORD +50 -0
- simple_autonomous_car-0.1.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""Pure Pursuit path following controller."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from matplotlib.patches import Arc
|
|
7
|
+
|
|
8
|
+
from simple_autonomous_car.car.car import CarState
|
|
9
|
+
from simple_autonomous_car.constants import (
|
|
10
|
+
ADAPTIVE_GAIN_DENOMINATOR,
|
|
11
|
+
COST_THRESHOLD,
|
|
12
|
+
DEFAULT_ARC_ALPHA,
|
|
13
|
+
DEFAULT_ARC_LINEWIDTH,
|
|
14
|
+
DEFAULT_ARC_LOOKAHEAD_DISTANCE,
|
|
15
|
+
DEFAULT_GOAL_TOLERANCE,
|
|
16
|
+
DEFAULT_LOOKAHEAD_DISTANCE,
|
|
17
|
+
DEFAULT_LOOKAHEAD_GAIN,
|
|
18
|
+
DEFAULT_MAX_STEERING_RATE,
|
|
19
|
+
DEFAULT_MAX_TURNING_RADIUS,
|
|
20
|
+
DEFAULT_MIN_TURNING_RADIUS,
|
|
21
|
+
DEFAULT_TARGET_VELOCITY,
|
|
22
|
+
DEFAULT_VELOCITY_GAIN,
|
|
23
|
+
DEFAULT_WHEELBASE,
|
|
24
|
+
MIN_LOOKAHEAD_DISTANCE,
|
|
25
|
+
SMALL_STEERING_ANGLE_THRESHOLD,
|
|
26
|
+
)
|
|
27
|
+
from simple_autonomous_car.control.base_controller import BaseController
|
|
28
|
+
from simple_autonomous_car.perception.perception import PerceptionPoints
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from simple_autonomous_car.costmap.base_costmap import BaseCostmap
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PurePursuitController(BaseController):
|
|
35
|
+
"""
|
|
36
|
+
Pure Pursuit path following controller.
|
|
37
|
+
|
|
38
|
+
This controller follows a path by looking ahead a certain distance
|
|
39
|
+
and steering towards that point. It's simple, robust, and widely used.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
lookahead_distance : float, default=8.0
|
|
44
|
+
Distance ahead to look for target point (meters).
|
|
45
|
+
Larger = smoother but slower response.
|
|
46
|
+
lookahead_gain : float, default=2.0
|
|
47
|
+
Gain for adaptive lookahead (multiplies velocity).
|
|
48
|
+
max_steering_rate : float, default=1.0
|
|
49
|
+
Maximum steering rate in rad/s.
|
|
50
|
+
target_velocity : float, default=10.0
|
|
51
|
+
Target velocity in m/s.
|
|
52
|
+
velocity_gain : float, default=0.5
|
|
53
|
+
Gain for velocity control.
|
|
54
|
+
name : str, default="pure_pursuit"
|
|
55
|
+
Controller name.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
lookahead_distance: float = DEFAULT_LOOKAHEAD_DISTANCE,
|
|
61
|
+
lookahead_gain: float = DEFAULT_LOOKAHEAD_GAIN,
|
|
62
|
+
max_steering_rate: float = DEFAULT_MAX_STEERING_RATE,
|
|
63
|
+
target_velocity: float = DEFAULT_TARGET_VELOCITY,
|
|
64
|
+
velocity_gain: float = DEFAULT_VELOCITY_GAIN,
|
|
65
|
+
name: str = "pure_pursuit",
|
|
66
|
+
):
|
|
67
|
+
super().__init__(name=name)
|
|
68
|
+
self.lookahead_distance = lookahead_distance
|
|
69
|
+
self.lookahead_gain = lookahead_gain
|
|
70
|
+
self.max_steering_rate = max_steering_rate
|
|
71
|
+
self.target_velocity = target_velocity
|
|
72
|
+
self.velocity_gain = velocity_gain
|
|
73
|
+
|
|
74
|
+
def compute_control(
|
|
75
|
+
self,
|
|
76
|
+
car_state: CarState,
|
|
77
|
+
perception_data: dict[str, PerceptionPoints] | None = None,
|
|
78
|
+
costmap: Optional["BaseCostmap"] = None,
|
|
79
|
+
plan: np.ndarray | None = None,
|
|
80
|
+
goal: np.ndarray | None = None,
|
|
81
|
+
goal_tolerance: float | None = None,
|
|
82
|
+
dt: float = 0.1,
|
|
83
|
+
) -> dict[str, float]:
|
|
84
|
+
"""
|
|
85
|
+
Compute control using Pure Pursuit algorithm.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
car_state : CarState
|
|
90
|
+
Current car state.
|
|
91
|
+
perception_data : dict, optional
|
|
92
|
+
Not used by Pure Pursuit (uses plan instead).
|
|
93
|
+
costmap : BaseCostmap, optional
|
|
94
|
+
Costmap for obstacle avoidance (can be used to adjust velocity).
|
|
95
|
+
plan : np.ndarray, optional
|
|
96
|
+
Planned path as array of shape (N, 2) with [x, y] waypoints.
|
|
97
|
+
If None, returns zero control.
|
|
98
|
+
goal : np.ndarray, optional
|
|
99
|
+
Goal position [x, y] for goal-based velocity adaptation.
|
|
100
|
+
goal_tolerance : float, optional
|
|
101
|
+
Distance to goal to consider reached (default: DEFAULT_GOAL_TOLERANCE).
|
|
102
|
+
When within tolerance, target velocity is set to 0.
|
|
103
|
+
dt : float, default=0.1
|
|
104
|
+
Time step in seconds.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
Dict[str, float]
|
|
109
|
+
Control commands with "acceleration" and "steering_rate".
|
|
110
|
+
"""
|
|
111
|
+
if not self.enabled or plan is None or len(plan) == 0:
|
|
112
|
+
return {"acceleration": 0.0, "steering_rate": 0.0}
|
|
113
|
+
|
|
114
|
+
# Adaptive lookahead based on velocity
|
|
115
|
+
adaptive_lookahead = self.lookahead_distance + self.lookahead_gain * car_state.velocity
|
|
116
|
+
|
|
117
|
+
# Find closest point on path first
|
|
118
|
+
car_pos = car_state.position()
|
|
119
|
+
distances_to_path = np.linalg.norm(plan - car_pos, axis=1)
|
|
120
|
+
closest_idx = np.argmin(distances_to_path)
|
|
121
|
+
|
|
122
|
+
# Find target point at lookahead distance ahead of closest point
|
|
123
|
+
# Calculate cumulative distances along path from closest point
|
|
124
|
+
path_distances = np.zeros(len(plan))
|
|
125
|
+
for i in range(closest_idx, len(plan) - 1):
|
|
126
|
+
path_distances[i + 1] = path_distances[i] + np.linalg.norm(plan[i + 1] - plan[i])
|
|
127
|
+
for i in range(closest_idx - 1, -1, -1):
|
|
128
|
+
path_distances[i] = path_distances[i + 1] - np.linalg.norm(plan[i + 1] - plan[i])
|
|
129
|
+
|
|
130
|
+
# Find point at lookahead distance ahead
|
|
131
|
+
target_distances = path_distances - path_distances[closest_idx]
|
|
132
|
+
ahead_mask = target_distances >= 0
|
|
133
|
+
ahead_distances = target_distances[ahead_mask]
|
|
134
|
+
|
|
135
|
+
if len(ahead_distances) == 0:
|
|
136
|
+
# If no points ahead, use last point
|
|
137
|
+
target_idx = len(plan) - 1
|
|
138
|
+
else:
|
|
139
|
+
# Find point closest to lookahead distance
|
|
140
|
+
lookahead_idx = np.argmin(np.abs(ahead_distances - adaptive_lookahead))
|
|
141
|
+
ahead_indices = np.where(ahead_mask)[0]
|
|
142
|
+
target_idx = ahead_indices[lookahead_idx]
|
|
143
|
+
if target_idx >= len(plan):
|
|
144
|
+
target_idx = len(plan) - 1
|
|
145
|
+
|
|
146
|
+
target_point = plan[target_idx]
|
|
147
|
+
|
|
148
|
+
# Calculate steering using Pure Pursuit
|
|
149
|
+
target_vector = target_point - car_pos
|
|
150
|
+
lookahead_dist = np.linalg.norm(target_vector)
|
|
151
|
+
|
|
152
|
+
# Check if we're very close to goal (if goal provided)
|
|
153
|
+
goal_tolerance_check = (
|
|
154
|
+
goal_tolerance if goal_tolerance is not None else DEFAULT_GOAL_TOLERANCE
|
|
155
|
+
)
|
|
156
|
+
distance_to_goal = None
|
|
157
|
+
if goal is not None:
|
|
158
|
+
distance_to_goal = np.linalg.norm(car_pos - goal)
|
|
159
|
+
|
|
160
|
+
# PRIORITY: Check goal distance FIRST - if within tolerance, NO STEERING AT ALL
|
|
161
|
+
# Initialize steering_rate (will be set in one of the branches below)
|
|
162
|
+
steering_rate = 0.0
|
|
163
|
+
|
|
164
|
+
# If within tolerance, STOP STEERING COMPLETELY
|
|
165
|
+
if distance_to_goal is not None and distance_to_goal <= goal_tolerance_check:
|
|
166
|
+
# Within tolerance - NO STEERING AT ALL
|
|
167
|
+
steering_rate = 0.0
|
|
168
|
+
elif distance_to_goal is not None and distance_to_goal <= goal_tolerance_check * 1.5:
|
|
169
|
+
# Between tolerance and 1.5*tolerance - reduce steering proportionally
|
|
170
|
+
steering_reduction = (distance_to_goal - goal_tolerance_check) / (
|
|
171
|
+
goal_tolerance_check * 0.5
|
|
172
|
+
)
|
|
173
|
+
steering_reduction = max( # type: ignore[arg-type]
|
|
174
|
+
0.0, min(1.0, steering_reduction) # type: ignore[arg-type]
|
|
175
|
+
) # Clamp to [0, 1] # type: ignore[assignment]
|
|
176
|
+
|
|
177
|
+
# Calculate normal steering but scale it down
|
|
178
|
+
target_angle = np.arctan2(target_vector[1], target_vector[0])
|
|
179
|
+
alpha = target_angle - car_state.heading
|
|
180
|
+
alpha = np.arctan2(np.sin(alpha), np.cos(alpha))
|
|
181
|
+
desired_steering = alpha * (1.0 / (1.0 + lookahead_dist / ADAPTIVE_GAIN_DENOMINATOR))
|
|
182
|
+
max_steering = np.pi / 4
|
|
183
|
+
desired_steering = np.clip(desired_steering, -max_steering, max_steering)
|
|
184
|
+
|
|
185
|
+
# Scale down steering based on distance to goal
|
|
186
|
+
desired_steering *= steering_reduction
|
|
187
|
+
steering_error = desired_steering - car_state.steering_angle
|
|
188
|
+
steering_rate = 2.0 * steering_error * steering_reduction # Also scale rate
|
|
189
|
+
steering_rate = np.clip(steering_rate, -self.max_steering_rate, self.max_steering_rate)
|
|
190
|
+
elif lookahead_dist < MIN_LOOKAHEAD_DISTANCE:
|
|
191
|
+
# Very close to target point (but not necessarily goal), reduce steering
|
|
192
|
+
# Scale steering by lookahead distance to avoid spinning
|
|
193
|
+
steering_scale = max(0.0, lookahead_dist / MIN_LOOKAHEAD_DISTANCE) # type: ignore[assignment]
|
|
194
|
+
steering_rate = -2.0 * car_state.steering_angle * steering_scale # type: ignore[operator]
|
|
195
|
+
steering_rate = np.clip(steering_rate, -self.max_steering_rate, self.max_steering_rate)
|
|
196
|
+
else:
|
|
197
|
+
# Normal Pure Pursuit: calculate desired steering angle
|
|
198
|
+
target_angle = np.arctan2(target_vector[1], target_vector[0])
|
|
199
|
+
alpha = target_angle - car_state.heading
|
|
200
|
+
alpha = np.arctan2(np.sin(alpha), np.cos(alpha)) # Normalize to [-pi, pi]
|
|
201
|
+
|
|
202
|
+
# Pure Pursuit: scale steering by lookahead distance
|
|
203
|
+
# When lookahead is small, steering should be reduced proportionally
|
|
204
|
+
# This prevents spinning when close to goal
|
|
205
|
+
lookahead_scale = min(1.0, lookahead_dist / self.lookahead_distance)
|
|
206
|
+
desired_steering = (
|
|
207
|
+
alpha * (1.0 / (1.0 + lookahead_dist / ADAPTIVE_GAIN_DENOMINATOR)) * lookahead_scale
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Limit steering angle
|
|
211
|
+
max_steering = np.pi / 4 # 45 degrees
|
|
212
|
+
desired_steering = np.clip(desired_steering, -max_steering, max_steering)
|
|
213
|
+
|
|
214
|
+
# Compute steering rate (smooth control)
|
|
215
|
+
steering_error = desired_steering - car_state.steering_angle
|
|
216
|
+
steering_rate = 2.0 * steering_error * lookahead_scale # Also scale rate by lookahead
|
|
217
|
+
steering_rate = np.clip(steering_rate, -self.max_steering_rate, self.max_steering_rate)
|
|
218
|
+
|
|
219
|
+
# Velocity control (adjust based on costmap and goal distance)
|
|
220
|
+
target_velocity = self.target_velocity
|
|
221
|
+
|
|
222
|
+
# Reduce velocity if approaching goal (smooth deceleration)
|
|
223
|
+
if goal is not None:
|
|
224
|
+
distance_to_goal = np.linalg.norm(car_state.position() - goal)
|
|
225
|
+
tolerance = goal_tolerance if goal_tolerance is not None else DEFAULT_GOAL_TOLERANCE
|
|
226
|
+
|
|
227
|
+
# If within tolerance, stop completely
|
|
228
|
+
if distance_to_goal <= tolerance:
|
|
229
|
+
target_velocity = 0.0
|
|
230
|
+
else:
|
|
231
|
+
# Start slowing down when within 10 meters of goal
|
|
232
|
+
# Use a smaller slow_down_distance to ensure smooth deceleration even when close
|
|
233
|
+
slow_down_distance = max(10.0, tolerance * 5.0) # At least 5x tolerance, or 10m
|
|
234
|
+
if distance_to_goal < slow_down_distance:
|
|
235
|
+
# Smooth velocity reduction: linear from full speed to zero
|
|
236
|
+
# At distance=tolerance, velocity=0; at distance=slow_down_distance, velocity=target_velocity
|
|
237
|
+
# Map distance from [tolerance, slow_down_distance] to velocity [0, target_velocity]
|
|
238
|
+
if slow_down_distance > tolerance:
|
|
239
|
+
velocity_factor = (distance_to_goal - tolerance) / (
|
|
240
|
+
slow_down_distance - tolerance
|
|
241
|
+
)
|
|
242
|
+
velocity_factor = max( # type: ignore[arg-type]
|
|
243
|
+
0.0, min(1.0, velocity_factor) # type: ignore[arg-type]
|
|
244
|
+
) # Clamp to [0, 1] # type: ignore[assignment]
|
|
245
|
+
target_velocity = self.target_velocity * velocity_factor # type: ignore[assignment]
|
|
246
|
+
else:
|
|
247
|
+
target_velocity = 0.0
|
|
248
|
+
# Ensure we always slow down when very close (even if above tolerance)
|
|
249
|
+
if distance_to_goal < tolerance * 2.0:
|
|
250
|
+
# Extra safety: cap velocity when very close
|
|
251
|
+
max_velocity_near_goal = self.target_velocity * (
|
|
252
|
+
distance_to_goal / (tolerance * 2.0)
|
|
253
|
+
)
|
|
254
|
+
target_velocity = min(target_velocity, max_velocity_near_goal) # type: ignore[assignment]
|
|
255
|
+
|
|
256
|
+
# Reduce velocity if high cost ahead
|
|
257
|
+
if costmap is not None and costmap.is_enabled():
|
|
258
|
+
# Check cost ahead of car
|
|
259
|
+
lookahead_pos = car_state.position() + 5.0 * np.array(
|
|
260
|
+
[np.cos(car_state.heading), np.sin(car_state.heading)]
|
|
261
|
+
)
|
|
262
|
+
cost_ahead = costmap.get_cost(lookahead_pos, frame="global", car_state=car_state)
|
|
263
|
+
# Reduce velocity if cost > threshold
|
|
264
|
+
if cost_ahead > COST_THRESHOLD:
|
|
265
|
+
target_velocity = target_velocity * (1.0 - cost_ahead * 0.5)
|
|
266
|
+
|
|
267
|
+
velocity_error = target_velocity - car_state.velocity
|
|
268
|
+
acceleration = self.velocity_gain * velocity_error
|
|
269
|
+
acceleration = np.clip(acceleration, -2.0, 2.0)
|
|
270
|
+
|
|
271
|
+
return {"acceleration": acceleration, "steering_rate": steering_rate}
|
|
272
|
+
|
|
273
|
+
def get_visualization_data(
|
|
274
|
+
self, car_state: CarState, plan: np.ndarray | None = None, **kwargs: Any
|
|
275
|
+
) -> dict[str, Any]:
|
|
276
|
+
"""
|
|
277
|
+
Get visualization data for Pure Pursuit controller.
|
|
278
|
+
|
|
279
|
+
Returns lookahead point, steering arc parameters, and control commands.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
car_state : CarState
|
|
284
|
+
Current car state.
|
|
285
|
+
plan : np.ndarray, optional
|
|
286
|
+
Planned path.
|
|
287
|
+
**kwargs
|
|
288
|
+
Additional arguments.
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
Dict
|
|
293
|
+
Dictionary with keys:
|
|
294
|
+
- "lookahead_point": np.ndarray, target point [x, y]
|
|
295
|
+
- "lookahead_distance": float, current lookahead distance
|
|
296
|
+
- "steering_angle": float, current steering angle
|
|
297
|
+
- "turning_radius": float, turning radius (if steering > 0)
|
|
298
|
+
- "control": Dict, control commands
|
|
299
|
+
"""
|
|
300
|
+
if not self.enabled or plan is None or len(plan) == 0:
|
|
301
|
+
return {
|
|
302
|
+
"lookahead_point": None,
|
|
303
|
+
"lookahead_distance": self.lookahead_distance,
|
|
304
|
+
"steering_angle": car_state.steering_angle,
|
|
305
|
+
"turning_radius": None,
|
|
306
|
+
"control": {"acceleration": 0.0, "steering_rate": 0.0},
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# Compute control to get current state
|
|
310
|
+
# Remove wheelbase from kwargs before passing to compute_control
|
|
311
|
+
control_kwargs = {k: v for k, v in kwargs.items() if k != "wheelbase"}
|
|
312
|
+
control = self.compute_control(car_state, plan=plan, **control_kwargs)
|
|
313
|
+
|
|
314
|
+
# Find lookahead point (same logic as compute_control)
|
|
315
|
+
adaptive_lookahead = self.lookahead_distance + self.lookahead_gain * car_state.velocity
|
|
316
|
+
car_pos = car_state.position()
|
|
317
|
+
distances_to_path = np.linalg.norm(plan - car_pos, axis=1)
|
|
318
|
+
closest_idx = np.argmin(distances_to_path)
|
|
319
|
+
|
|
320
|
+
path_distances = np.zeros(len(plan))
|
|
321
|
+
for i in range(closest_idx, len(plan) - 1):
|
|
322
|
+
path_distances[i + 1] = path_distances[i] + np.linalg.norm(plan[i + 1] - plan[i])
|
|
323
|
+
for i in range(closest_idx - 1, -1, -1):
|
|
324
|
+
path_distances[i] = path_distances[i + 1] - np.linalg.norm(plan[i + 1] - plan[i])
|
|
325
|
+
|
|
326
|
+
target_distances = path_distances - path_distances[closest_idx]
|
|
327
|
+
ahead_mask = target_distances >= 0
|
|
328
|
+
ahead_distances = target_distances[ahead_mask]
|
|
329
|
+
|
|
330
|
+
if len(ahead_distances) == 0:
|
|
331
|
+
target_idx = len(plan) - 1
|
|
332
|
+
else:
|
|
333
|
+
lookahead_idx = np.argmin(np.abs(ahead_distances - adaptive_lookahead))
|
|
334
|
+
ahead_indices = np.where(ahead_mask)[0]
|
|
335
|
+
target_idx = ahead_indices[lookahead_idx]
|
|
336
|
+
if target_idx >= len(plan):
|
|
337
|
+
target_idx = len(plan) - 1
|
|
338
|
+
|
|
339
|
+
lookahead_point = plan[target_idx]
|
|
340
|
+
|
|
341
|
+
# Calculate turning radius using desired steering angle (from control computation)
|
|
342
|
+
# Always compute desired_steering for visualization to show intended steering, not current state
|
|
343
|
+
# This ensures arc only shows when actually turning, not when steering angle is stale
|
|
344
|
+
wheelbase = kwargs.get("wheelbase", DEFAULT_WHEELBASE)
|
|
345
|
+
turning_radius = None
|
|
346
|
+
steering_angle_for_viz = 0.0 # Default to 0 (straight) for visualization
|
|
347
|
+
|
|
348
|
+
# Always compute desired steering for visualization (same logic as compute_control)
|
|
349
|
+
target_vector = lookahead_point - car_pos
|
|
350
|
+
lookahead_dist = np.linalg.norm(target_vector)
|
|
351
|
+
|
|
352
|
+
if lookahead_dist > MIN_LOOKAHEAD_DISTANCE:
|
|
353
|
+
target_angle = np.arctan2(target_vector[1], target_vector[0])
|
|
354
|
+
alpha = target_angle - car_state.heading
|
|
355
|
+
alpha = np.arctan2(np.sin(alpha), np.cos(alpha)) # Normalize to [-pi, pi]
|
|
356
|
+
|
|
357
|
+
desired_steering = alpha * (1.0 / (1.0 + lookahead_dist / ADAPTIVE_GAIN_DENOMINATOR))
|
|
358
|
+
max_steering = np.pi / 4 # 45 degrees
|
|
359
|
+
desired_steering = np.clip(desired_steering, -max_steering, max_steering)
|
|
360
|
+
|
|
361
|
+
# Use desired steering for visualization (shows intended steering, not stale state)
|
|
362
|
+
steering_angle_for_viz = desired_steering
|
|
363
|
+
# else: lookahead_dist too small, use 0.0 (straight) - don't use stale car_state.steering_angle
|
|
364
|
+
|
|
365
|
+
# Calculate turning radius from steering angle (always calculate, even for straight)
|
|
366
|
+
if abs(steering_angle_for_viz) > SMALL_STEERING_ANGLE_THRESHOLD:
|
|
367
|
+
# Normal steering - calculate turning radius
|
|
368
|
+
turning_radius = wheelbase / np.tan(steering_angle_for_viz)
|
|
369
|
+
else:
|
|
370
|
+
# Very small or zero steering - use large turning radius (essentially straight)
|
|
371
|
+
turning_radius = None # Will be handled in visualization as straight line
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
"lookahead_point": lookahead_point,
|
|
375
|
+
"lookahead_distance": adaptive_lookahead,
|
|
376
|
+
"steering_angle": steering_angle_for_viz, # Use desired steering for visualization
|
|
377
|
+
"turning_radius": turning_radius,
|
|
378
|
+
"control": control,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
def visualize(
|
|
382
|
+
self,
|
|
383
|
+
ax: Any,
|
|
384
|
+
car_state: CarState,
|
|
385
|
+
plan: np.ndarray | None = None,
|
|
386
|
+
frame: str = "global",
|
|
387
|
+
**kwargs: Any,
|
|
388
|
+
) -> None:
|
|
389
|
+
"""
|
|
390
|
+
Visualize Pure Pursuit controller state (lookahead point, steering arc).
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
ax : matplotlib.axes.Axes
|
|
395
|
+
Axes to plot on.
|
|
396
|
+
car_state : CarState
|
|
397
|
+
Current car state.
|
|
398
|
+
plan : np.ndarray, optional
|
|
399
|
+
Planned path.
|
|
400
|
+
frame : str, default="global"
|
|
401
|
+
Frame to plot in: "global" or "ego".
|
|
402
|
+
**kwargs
|
|
403
|
+
Additional visualization arguments:
|
|
404
|
+
- lookahead_color: str, color for lookahead point/line
|
|
405
|
+
- arc_color: str, color for steering arc
|
|
406
|
+
- arc_linewidth: float, linewidth for steering arc
|
|
407
|
+
- arc_alpha: float, alpha for steering arc
|
|
408
|
+
- wheelbase: float, car wheelbase for turning radius calculation
|
|
409
|
+
"""
|
|
410
|
+
if not self.enabled or plan is None or len(plan) == 0:
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
# Get visualization data
|
|
414
|
+
wheelbase = kwargs.pop("wheelbase", DEFAULT_WHEELBASE)
|
|
415
|
+
viz_data = self.get_visualization_data(
|
|
416
|
+
car_state,
|
|
417
|
+
plan=plan,
|
|
418
|
+
wheelbase=wheelbase,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Extract visualization parameters from kwargs
|
|
422
|
+
lookahead_color = kwargs.pop("lookahead_color", "magenta")
|
|
423
|
+
arc_color = kwargs.pop("arc_color", "violet") # Changed to violet/purple
|
|
424
|
+
arc_linewidth = kwargs.pop("arc_linewidth", DEFAULT_ARC_LINEWIDTH)
|
|
425
|
+
arc_alpha = kwargs.pop("arc_alpha", DEFAULT_ARC_ALPHA)
|
|
426
|
+
arc_lookahead_distance = kwargs.pop(
|
|
427
|
+
"arc_lookahead_distance", DEFAULT_ARC_LOOKAHEAD_DISTANCE
|
|
428
|
+
)
|
|
429
|
+
kwargs.pop("max_turning_radius", DEFAULT_MAX_TURNING_RADIUS)
|
|
430
|
+
kwargs.pop("min_turning_radius", DEFAULT_MIN_TURNING_RADIUS)
|
|
431
|
+
|
|
432
|
+
# Get car position and heading for frame transformation
|
|
433
|
+
car_pos = car_state.position()
|
|
434
|
+
car_heading = car_state.heading
|
|
435
|
+
|
|
436
|
+
if frame == "ego":
|
|
437
|
+
car_pos_plot = np.array([0.0, 0.0])
|
|
438
|
+
car_heading_plot = 0.0
|
|
439
|
+
else:
|
|
440
|
+
car_pos_plot = car_pos
|
|
441
|
+
car_heading_plot = car_heading
|
|
442
|
+
|
|
443
|
+
# Plot lookahead point
|
|
444
|
+
lookahead_point = viz_data.get("lookahead_point")
|
|
445
|
+
if lookahead_point is not None:
|
|
446
|
+
if frame == "ego":
|
|
447
|
+
lookahead_plot = car_state.transform_to_car_frame(lookahead_point)
|
|
448
|
+
else:
|
|
449
|
+
lookahead_plot = lookahead_point
|
|
450
|
+
|
|
451
|
+
# Plot lookahead point (line removed per user request)
|
|
452
|
+
ax.plot(
|
|
453
|
+
lookahead_plot[0],
|
|
454
|
+
lookahead_plot[1],
|
|
455
|
+
"o",
|
|
456
|
+
color=lookahead_color,
|
|
457
|
+
markersize=10,
|
|
458
|
+
markeredgecolor="darkmagenta",
|
|
459
|
+
markeredgewidth=2,
|
|
460
|
+
label="Lookahead",
|
|
461
|
+
zorder=6,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Plot steering arc
|
|
465
|
+
turning_radius = viz_data.get("turning_radius")
|
|
466
|
+
steering_angle = viz_data.get("steering_angle", car_state.steering_angle)
|
|
467
|
+
|
|
468
|
+
# Always show steering path (removed all thresholds per user request)
|
|
469
|
+
# Calculate turning radius if not provided
|
|
470
|
+
if turning_radius is None:
|
|
471
|
+
# Handle very small angles to avoid division by zero
|
|
472
|
+
if abs(steering_angle) < SMALL_STEERING_ANGLE_THRESHOLD:
|
|
473
|
+
# Very small steering - draw straight line forward instead of arc
|
|
474
|
+
# Draw straight line segment in direction of car heading
|
|
475
|
+
if frame == "ego":
|
|
476
|
+
end_x = arc_lookahead_distance
|
|
477
|
+
end_y = 0.0
|
|
478
|
+
else:
|
|
479
|
+
end_x = car_pos_plot[0] + arc_lookahead_distance * np.cos(car_heading_plot)
|
|
480
|
+
end_y = car_pos_plot[1] + arc_lookahead_distance * np.sin(car_heading_plot)
|
|
481
|
+
|
|
482
|
+
ax.plot(
|
|
483
|
+
[car_pos_plot[0], end_x],
|
|
484
|
+
[car_pos_plot[1], end_y],
|
|
485
|
+
color=arc_color,
|
|
486
|
+
linestyle="-",
|
|
487
|
+
linewidth=arc_linewidth,
|
|
488
|
+
alpha=arc_alpha,
|
|
489
|
+
zorder=4,
|
|
490
|
+
label="Steering Path",
|
|
491
|
+
)
|
|
492
|
+
return # Done with straight line case
|
|
493
|
+
else:
|
|
494
|
+
turning_radius = wheelbase / np.tan(steering_angle)
|
|
495
|
+
|
|
496
|
+
# Draw arc for non-straight steering
|
|
497
|
+
if (
|
|
498
|
+
turning_radius is not None
|
|
499
|
+
and not np.isnan(turning_radius)
|
|
500
|
+
and not np.isinf(turning_radius)
|
|
501
|
+
):
|
|
502
|
+
turning_radius_abs = abs(turning_radius)
|
|
503
|
+
# Always show arc (removed min_turning_radius check)
|
|
504
|
+
if turning_radius_abs > 0:
|
|
505
|
+
# Calculate arc parameters
|
|
506
|
+
arc_length_rad = arc_lookahead_distance / turning_radius_abs
|
|
507
|
+
|
|
508
|
+
if frame == "ego":
|
|
509
|
+
center_x = 0.0
|
|
510
|
+
if steering_angle > 0: # Left turn
|
|
511
|
+
center_y = turning_radius_abs
|
|
512
|
+
theta_start_deg = -90.0
|
|
513
|
+
theta_end_deg = -90.0 + np.degrees(arc_length_rad)
|
|
514
|
+
else: # Right turn
|
|
515
|
+
center_y = -turning_radius_abs
|
|
516
|
+
theta_start_deg = 90.0
|
|
517
|
+
theta_end_deg = 90.0 - np.degrees(arc_length_rad)
|
|
518
|
+
else:
|
|
519
|
+
# Global frame
|
|
520
|
+
perp_angle = car_heading_plot + np.pi / 2
|
|
521
|
+
if turning_radius < 0:
|
|
522
|
+
perp_angle += np.pi
|
|
523
|
+
|
|
524
|
+
center_x = car_pos_plot[0] + turning_radius_abs * np.cos(perp_angle)
|
|
525
|
+
center_y = car_pos_plot[1] + turning_radius_abs * np.sin(perp_angle)
|
|
526
|
+
|
|
527
|
+
dx_to_car = car_pos_plot[0] - center_x
|
|
528
|
+
dy_to_car = car_pos_plot[1] - center_y
|
|
529
|
+
theta_start_rad = np.arctan2(dy_to_car, dx_to_car)
|
|
530
|
+
theta_start_deg = np.degrees(theta_start_rad)
|
|
531
|
+
|
|
532
|
+
if steering_angle > 0:
|
|
533
|
+
theta_end_deg = theta_start_deg + np.degrees(arc_length_rad)
|
|
534
|
+
else:
|
|
535
|
+
theta_end_deg = theta_start_deg - np.degrees(arc_length_rad)
|
|
536
|
+
|
|
537
|
+
# Draw steering arc
|
|
538
|
+
arc = Arc(
|
|
539
|
+
(center_x, center_y),
|
|
540
|
+
2 * turning_radius_abs,
|
|
541
|
+
2 * turning_radius_abs,
|
|
542
|
+
angle=0,
|
|
543
|
+
theta1=theta_start_deg,
|
|
544
|
+
theta2=theta_end_deg,
|
|
545
|
+
color=arc_color,
|
|
546
|
+
linestyle="-",
|
|
547
|
+
linewidth=arc_linewidth,
|
|
548
|
+
alpha=arc_alpha,
|
|
549
|
+
zorder=4,
|
|
550
|
+
label="Steering Path",
|
|
551
|
+
)
|
|
552
|
+
ax.add_patch(arc)
|
|
553
|
+
|
|
554
|
+
# Add arrow at end of arc
|
|
555
|
+
theta_end_rad = np.radians(theta_end_deg)
|
|
556
|
+
arc_end_x = center_x + turning_radius_abs * np.cos(theta_end_rad)
|
|
557
|
+
arc_end_y = center_y + turning_radius_abs * np.sin(theta_end_rad)
|
|
558
|
+
|
|
559
|
+
if steering_angle > 0:
|
|
560
|
+
direction_angle = theta_end_rad + np.pi / 2
|
|
561
|
+
else:
|
|
562
|
+
direction_angle = theta_end_rad - np.pi / 2
|
|
563
|
+
|
|
564
|
+
arrow_length = 2.0
|
|
565
|
+
dx = arrow_length * np.cos(direction_angle)
|
|
566
|
+
dy = arrow_length * np.sin(direction_angle)
|
|
567
|
+
ax.arrow(
|
|
568
|
+
arc_end_x,
|
|
569
|
+
arc_end_y,
|
|
570
|
+
dx,
|
|
571
|
+
dy,
|
|
572
|
+
head_width=1.5,
|
|
573
|
+
head_length=1.0,
|
|
574
|
+
fc=arc_color,
|
|
575
|
+
ec="darkmagenta",
|
|
576
|
+
alpha=arc_alpha,
|
|
577
|
+
zorder=5,
|
|
578
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Costmap system for obstacle representation and inflation."""
|
|
2
|
+
|
|
3
|
+
from simple_autonomous_car.costmap.base_costmap import BaseCostmap
|
|
4
|
+
from simple_autonomous_car.costmap.grid_costmap import GridCostmap
|
|
5
|
+
from simple_autonomous_car.costmap.inflation import compute_inflation_kernel, inflate_obstacles
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"BaseCostmap",
|
|
9
|
+
"GridCostmap",
|
|
10
|
+
"inflate_obstacles",
|
|
11
|
+
"compute_inflation_kernel",
|
|
12
|
+
]
|