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.
Files changed (50) hide show
  1. simple_autonomous_car/__init__.py +96 -0
  2. simple_autonomous_car/alerts/__init__.py +5 -0
  3. simple_autonomous_car/alerts/track_bounds_alert.py +276 -0
  4. simple_autonomous_car/car/__init__.py +5 -0
  5. simple_autonomous_car/car/car.py +234 -0
  6. simple_autonomous_car/constants.py +112 -0
  7. simple_autonomous_car/control/__init__.py +7 -0
  8. simple_autonomous_car/control/base_controller.py +152 -0
  9. simple_autonomous_car/control/controller_viz.py +282 -0
  10. simple_autonomous_car/control/pid_controller.py +153 -0
  11. simple_autonomous_car/control/pure_pursuit_controller.py +578 -0
  12. simple_autonomous_car/costmap/__init__.py +12 -0
  13. simple_autonomous_car/costmap/base_costmap.py +187 -0
  14. simple_autonomous_car/costmap/grid_costmap.py +507 -0
  15. simple_autonomous_car/costmap/inflation.py +126 -0
  16. simple_autonomous_car/detection/__init__.py +5 -0
  17. simple_autonomous_car/detection/error_detector.py +165 -0
  18. simple_autonomous_car/filters/__init__.py +7 -0
  19. simple_autonomous_car/filters/base_filter.py +119 -0
  20. simple_autonomous_car/filters/kalman_filter.py +131 -0
  21. simple_autonomous_car/filters/particle_filter.py +162 -0
  22. simple_autonomous_car/footprint/__init__.py +7 -0
  23. simple_autonomous_car/footprint/base_footprint.py +128 -0
  24. simple_autonomous_car/footprint/circular_footprint.py +73 -0
  25. simple_autonomous_car/footprint/rectangular_footprint.py +123 -0
  26. simple_autonomous_car/frames/__init__.py +21 -0
  27. simple_autonomous_car/frames/frenet.py +267 -0
  28. simple_autonomous_car/maps/__init__.py +9 -0
  29. simple_autonomous_car/maps/frenet_map.py +97 -0
  30. simple_autonomous_car/maps/grid_ground_truth_map.py +83 -0
  31. simple_autonomous_car/maps/grid_map.py +361 -0
  32. simple_autonomous_car/maps/ground_truth_map.py +64 -0
  33. simple_autonomous_car/maps/perceived_map.py +169 -0
  34. simple_autonomous_car/perception/__init__.py +5 -0
  35. simple_autonomous_car/perception/perception.py +107 -0
  36. simple_autonomous_car/planning/__init__.py +7 -0
  37. simple_autonomous_car/planning/base_planner.py +184 -0
  38. simple_autonomous_car/planning/goal_planner.py +261 -0
  39. simple_autonomous_car/planning/track_planner.py +199 -0
  40. simple_autonomous_car/sensors/__init__.py +6 -0
  41. simple_autonomous_car/sensors/base_sensor.py +105 -0
  42. simple_autonomous_car/sensors/lidar_sensor.py +145 -0
  43. simple_autonomous_car/track/__init__.py +5 -0
  44. simple_autonomous_car/track/track.py +463 -0
  45. simple_autonomous_car/visualization/__init__.py +25 -0
  46. simple_autonomous_car/visualization/alert_viz.py +316 -0
  47. simple_autonomous_car/visualization/utils.py +169 -0
  48. simple_autonomous_car-0.1.2.dist-info/METADATA +324 -0
  49. simple_autonomous_car-0.1.2.dist-info/RECORD +50 -0
  50. 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
+ ]