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,316 @@
1
+ """Visualization utilities for alert systems."""
2
+
3
+ from typing import Any
4
+
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ from matplotlib.patches import Polygon
8
+
9
+ from simple_autonomous_car.maps.frenet_map import FrenetMap
10
+ from simple_autonomous_car.perception.perception import PerceptionPoints
11
+ from simple_autonomous_car.track.track import Track
12
+
13
+
14
+ class AlertVisualizer:
15
+ """Visualization utilities for alert systems."""
16
+
17
+ def __init__(
18
+ self,
19
+ track: Track,
20
+ frenet_map: FrenetMap | None = None,
21
+ figsize: tuple = (16, 8),
22
+ ):
23
+ """
24
+ Initialize alert visualizer.
25
+
26
+ Args:
27
+ track: Track object
28
+ frenet_map: Optional Frenet map (will be created if not provided)
29
+ figsize: Figure size
30
+ """
31
+ self.track = track
32
+ self.frenet_map = frenet_map if frenet_map is not None else FrenetMap(track)
33
+ self.figsize = figsize
34
+ self.fig: Any = None
35
+ self.axes: Any = None
36
+
37
+ def create_figure(self) -> None:
38
+ """Create figure with world and car frame views."""
39
+ fig, (ax_world, ax_car) = plt.subplots(1, 2, figsize=self.figsize)
40
+ self.fig = fig
41
+ self.ax_world = ax_world
42
+ self.ax_car = ax_car
43
+ self._setup_axes()
44
+
45
+ def _setup_axes(self) -> None:
46
+ """Set up axis labels and properties."""
47
+ # World frame
48
+ self.ax_world.set_title("World Frame - Track Bounds Alert", fontsize=14, fontweight="bold")
49
+ self.ax_world.set_xlabel("X (m)")
50
+ self.ax_world.set_ylabel("Y (m)")
51
+ self.ax_world.set_aspect("equal")
52
+ self.ax_world.grid(True, alpha=0.3)
53
+
54
+ # Car frame
55
+ self.ax_car.set_title("Car Frame - Perception Points", fontsize=14, fontweight="bold")
56
+ self.ax_car.set_xlabel("Forward (m)")
57
+ self.ax_car.set_ylabel("Left (m)")
58
+ self.ax_car.set_aspect("equal")
59
+ self.ax_car.grid(True, alpha=0.3)
60
+
61
+ def plot_track(self, ax: Any = None) -> None:
62
+ """
63
+ Plot full track boundaries.
64
+
65
+ Args:
66
+ ax: Matplotlib axis (uses self.ax_world if None)
67
+ """
68
+ if ax is None:
69
+ ax = self.ax_world
70
+
71
+ ax.plot(
72
+ self.track.inner_bound[:, 0],
73
+ self.track.inner_bound[:, 1],
74
+ "k-",
75
+ linewidth=2.5,
76
+ label="Map (Inner)",
77
+ alpha=0.8,
78
+ )
79
+ ax.plot(
80
+ self.track.outer_bound[:, 0],
81
+ self.track.outer_bound[:, 1],
82
+ "k-",
83
+ linewidth=2.5,
84
+ label="Map (Outer)",
85
+ alpha=0.8,
86
+ )
87
+
88
+ def plot_perception(
89
+ self,
90
+ perception_points: PerceptionPoints,
91
+ car_state: Any,
92
+ car: Any,
93
+ highlight_alerts: bool = False,
94
+ alert_points: list[Any] | None = None,
95
+ ) -> None:
96
+ """
97
+ Plot perception points in both world and car frames.
98
+
99
+ Args:
100
+ perception_points: Perception points
101
+ car: Car object
102
+ highlight_alerts: Whether to highlight alert points
103
+ alert_points: List of alert points (if highlight_alerts is True)
104
+ """
105
+ # Ensure points are in ego frame
106
+ if perception_points.frame != "ego":
107
+ perception_points = perception_points.to_ego_frame(car.state)
108
+
109
+ # Car frame view
110
+ if len(perception_points) > 0:
111
+ self.ax_car.scatter(
112
+ perception_points.points[:, 0],
113
+ perception_points.points[:, 1],
114
+ c="red",
115
+ s=15,
116
+ alpha=0.6,
117
+ label="Perception",
118
+ marker=".",
119
+ )
120
+
121
+ # World frame view
122
+ if len(perception_points) > 0:
123
+ perception_global = perception_points.to_global_frame(car.state)
124
+ self.ax_world.scatter(
125
+ perception_global.points[:, 0],
126
+ perception_global.points[:, 1],
127
+ c="red",
128
+ s=15,
129
+ alpha=0.6,
130
+ label="Perception",
131
+ marker=".",
132
+ )
133
+
134
+ # Highlight alert points if requested
135
+ if highlight_alerts and alert_points:
136
+ alert_points_global = []
137
+ for alert_point in alert_points:
138
+ s, d = alert_point["s"], alert_point["d"]
139
+ point_global = self.frenet_map.frenet_frame.frenet_to_global(s, d)
140
+ alert_points_global.append(point_global)
141
+
142
+ if alert_points_global:
143
+ alert_points_global_arr = np.asarray(alert_points_global, dtype=np.float64) # type: ignore[assignment]
144
+ self.ax_world.scatter(
145
+ alert_points_global_arr[:, 0],
146
+ alert_points_global_arr[:, 1],
147
+ c="orange",
148
+ s=150,
149
+ marker="x",
150
+ linewidths=3,
151
+ label="Alert Points",
152
+ zorder=10,
153
+ )
154
+
155
+ def plot_car(self, car: Any, ax: Any = None) -> None:
156
+ """
157
+ Plot car position.
158
+
159
+ Args:
160
+ car: Car object
161
+ ax: Matplotlib axis (uses self.ax_world if None)
162
+ """
163
+ if ax is None:
164
+ ax = self.ax_world
165
+
166
+ car_corners = car.get_corners()
167
+ car_poly = Polygon(car_corners, closed=True, color="blue", alpha=0.7)
168
+ ax.add_patch(car_poly)
169
+
170
+ # Also plot in car frame (at origin)
171
+ car_corners_car = np.array([[-2, -0.9], [-2, 0.9], [2, 0.9], [2, -0.9]])
172
+ car_poly_car = Polygon(car_corners_car, closed=True, color="blue", alpha=0.7)
173
+ self.ax_car.add_patch(car_poly_car)
174
+
175
+ def plot_alert_result(
176
+ self,
177
+ alert_result: dict[str, Any],
178
+ perception_points: PerceptionPoints,
179
+ car_state: Any,
180
+ car: Any,
181
+ ) -> None:
182
+ """
183
+ Plot complete alert visualization.
184
+
185
+ Args:
186
+ alert_result: Result from alert system check
187
+ perception_points: Perception points
188
+ car: Car object
189
+ """
190
+ self.ax_world.clear()
191
+ self.ax_car.clear()
192
+ self._setup_axes()
193
+
194
+ # Plot track
195
+ self.plot_track()
196
+
197
+ # Plot perception with alerts
198
+ self.plot_perception(
199
+ perception_points,
200
+ car_state,
201
+ car,
202
+ highlight_alerts=True,
203
+ alert_points=alert_result.get("alert_points", []),
204
+ )
205
+
206
+ # Plot car
207
+ self.plot_car(car)
208
+
209
+ # Add alert info as text
210
+ if alert_result.get("has_critical"):
211
+ status_text = f"CRITICAL: Max deviation = {alert_result['max_deviation']:.2f}m"
212
+ color = "red"
213
+ elif alert_result.get("has_warning"):
214
+ status_text = f"WARNING: Max deviation = {alert_result['max_deviation']:.2f}m"
215
+ color = "orange"
216
+ else:
217
+ status_text = f"OK: Max deviation = {alert_result['max_deviation']:.2f}m"
218
+ color = "green"
219
+
220
+ self.ax_world.text(
221
+ 0.02,
222
+ 0.98,
223
+ status_text,
224
+ transform=self.ax_world.transAxes,
225
+ fontsize=12,
226
+ verticalalignment="top",
227
+ bbox=dict(boxstyle="round", facecolor="white", alpha=0.8, edgecolor=color, linewidth=2),
228
+ )
229
+
230
+ # Set limits
231
+ all_points = np.vstack([self.track.inner_bound, self.track.outer_bound])
232
+ padding = 10.0
233
+ self.ax_world.set_xlim(
234
+ np.min(all_points[:, 0]) - padding, np.max(all_points[:, 0]) + padding
235
+ )
236
+ self.ax_world.set_ylim(
237
+ np.min(all_points[:, 1]) - padding, np.max(all_points[:, 1]) + padding
238
+ )
239
+
240
+ self.ax_car.set_xlim(-50, 50)
241
+ self.ax_car.set_ylim(-50, 50)
242
+
243
+ self.ax_world.legend(loc="upper right", fontsize=9)
244
+ self.ax_car.legend(loc="upper right", fontsize=9)
245
+
246
+ plt.tight_layout()
247
+
248
+ def plot_alert_history(self, alert_history: list) -> None:
249
+ """
250
+ Plot alert history over time.
251
+
252
+ Args:
253
+ alert_history: List of alert history entries
254
+ """
255
+ if not alert_history:
256
+ return
257
+
258
+ fig, ax = plt.subplots(figsize=(12, 6))
259
+
260
+ steps = [h["step"] for h in alert_history]
261
+ deviations = [h["max_deviation"] for h in alert_history]
262
+ warnings = [h.get("has_warning", False) for h in alert_history]
263
+ criticals = [h.get("has_critical", False) for h in alert_history]
264
+
265
+ ax.plot(steps, deviations, "b-", label="Max Deviation", linewidth=2)
266
+
267
+ # Add threshold lines if available
268
+ if hasattr(self, "warning_threshold"):
269
+ ax.axhline(
270
+ y=self.warning_threshold,
271
+ color="orange",
272
+ linestyle="--",
273
+ label="Warning Threshold",
274
+ )
275
+ if hasattr(self, "critical_threshold"):
276
+ ax.axhline(
277
+ y=self.critical_threshold,
278
+ color="red",
279
+ linestyle="--",
280
+ label="Critical Threshold",
281
+ )
282
+
283
+ # Fill warning and critical zones
284
+ if any(warnings):
285
+ ax.fill_between(
286
+ steps,
287
+ 0,
288
+ deviations,
289
+ where=np.array(warnings), # type: ignore[arg-type]
290
+ alpha=0.3,
291
+ color="orange",
292
+ label="Warning Zone",
293
+ )
294
+ if any(criticals):
295
+ ax.fill_between(
296
+ steps,
297
+ 0,
298
+ deviations,
299
+ where=np.array(criticals), # type: ignore[arg-type]
300
+ alpha=0.3,
301
+ color="red",
302
+ label="Critical Zone",
303
+ )
304
+
305
+ ax.set_xlabel("Simulation Step")
306
+ ax.set_ylabel("Max Deviation (m)")
307
+ ax.set_title("Alert History - Deviation Over Time")
308
+ ax.legend()
309
+ ax.grid(True, alpha=0.3)
310
+ plt.tight_layout()
311
+ plt.show()
312
+
313
+ def show(self) -> None:
314
+ """Display the figure."""
315
+ if self.fig is not None:
316
+ plt.show()
@@ -0,0 +1,169 @@
1
+ """Utility visualization functions for non-component data (perception, car)."""
2
+
3
+ from typing import Any
4
+
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ from matplotlib.patches import Polygon
8
+
9
+ from simple_autonomous_car.car.car import Car, CarState
10
+ from simple_autonomous_car.perception.perception import PerceptionPoints
11
+
12
+
13
+ def plot_perception(
14
+ perception_points: PerceptionPoints,
15
+ car_state: CarState,
16
+ ax: plt.Axes | None = None,
17
+ frame: str = "global",
18
+ color: str = "red",
19
+ label: str = "Perception",
20
+ **kwargs: Any,
21
+ ) -> plt.Axes:
22
+ """
23
+ Plot perception points from sensors.
24
+
25
+ Parameters
26
+ ----------
27
+ perception_points : PerceptionPoints
28
+ Perception data points.
29
+ car_state : CarState
30
+ Car state for coordinate transformation.
31
+ ax : plt.Axes, optional
32
+ Axes to plot on. If None, creates new figure.
33
+ frame : str, default="global"
34
+ Frame to plot in: "global" or "ego".
35
+ color : str, default="red"
36
+ Color for perception points.
37
+ label : str, default="Perception"
38
+ Label for legend.
39
+ **kwargs
40
+ Additional arguments passed to scatter.
41
+
42
+ Returns
43
+ -------
44
+ plt.Axes
45
+ Axes object with perception plotted.
46
+ """
47
+ if ax is None:
48
+ fig, ax = plt.subplots(figsize=(10, 8))
49
+
50
+ if len(perception_points.points) == 0:
51
+ return ax
52
+
53
+ # Convert to desired frame
54
+ if frame == "global":
55
+ if perception_points.frame != "global":
56
+ points = perception_points.to_global_frame(car_state).points
57
+ else:
58
+ points = perception_points.points
59
+ else: # ego frame
60
+ if perception_points.frame != "ego":
61
+ points = perception_points.to_ego_frame(car_state).points
62
+ else:
63
+ points = perception_points.points
64
+
65
+ # Extract alpha from kwargs if provided, otherwise use default
66
+ scatter_alpha = kwargs.pop("alpha", 0.6)
67
+
68
+ ax.scatter(
69
+ points[:, 0],
70
+ points[:, 1],
71
+ c=color,
72
+ s=25,
73
+ alpha=scatter_alpha,
74
+ label=label,
75
+ marker=".",
76
+ edgecolors="darkred" if color in ["crimson", "red"] else None,
77
+ linewidths=0.5 if color in ["crimson", "red"] else 0,
78
+ **kwargs,
79
+ )
80
+
81
+ ax.set_aspect("equal")
82
+ ax.grid(True, alpha=0.3)
83
+
84
+ if frame == "global":
85
+ ax.set_xlabel("X (m)")
86
+ ax.set_ylabel("Y (m)")
87
+ else:
88
+ ax.set_xlabel("Forward (m)")
89
+ ax.set_ylabel("Left (m)")
90
+
91
+ return ax
92
+
93
+
94
+ def plot_car(
95
+ car: Car,
96
+ ax: plt.Axes | None = None,
97
+ frame: str = "global",
98
+ color: str = "blue",
99
+ show_heading: bool = True,
100
+ **kwargs: Any,
101
+ ) -> plt.Axes:
102
+ """
103
+ Plot car position and orientation.
104
+
105
+ Parameters
106
+ ----------
107
+ car : Car
108
+ Car to plot.
109
+ ax : plt.Axes, optional
110
+ Axes to plot on. If None, creates new figure.
111
+ frame : str, default="global"
112
+ Frame to plot in: "global" or "ego".
113
+ color : str, default="blue"
114
+ Color for car.
115
+ show_heading : bool, default=True
116
+ Whether to show heading arrow.
117
+ **kwargs
118
+ Additional arguments passed to Polygon.
119
+
120
+ Returns
121
+ -------
122
+ plt.Axes
123
+ Axes object with car plotted.
124
+ """
125
+ if ax is None:
126
+ fig, ax = plt.subplots(figsize=(10, 8))
127
+
128
+ if frame == "global":
129
+ car_corners = car.get_corners()
130
+ car_pos = car.state.position()
131
+ else: # ego frame
132
+ car_corners = np.array([[-2, -0.9], [-2, 0.9], [2, 0.9], [2, -0.9]])
133
+ car_pos = np.array([0.0, 0.0])
134
+
135
+ car_poly = Polygon(car_corners, closed=True, color=color, alpha=0.7, label="Car", **kwargs)
136
+ ax.add_patch(car_poly)
137
+
138
+ if show_heading and frame == "global":
139
+ # Draw heading arrow
140
+ arrow_length = 3.0
141
+ dx = arrow_length * np.cos(car.state.heading)
142
+ dy = arrow_length * np.sin(car.state.heading)
143
+ ax.arrow(
144
+ car_pos[0],
145
+ car_pos[1],
146
+ dx,
147
+ dy,
148
+ head_width=1.0,
149
+ head_length=0.8,
150
+ fc=color,
151
+ ec=color,
152
+ )
153
+ elif show_heading and frame == "ego":
154
+ # Draw forward arrow in ego frame
155
+ ax.arrow(
156
+ 0,
157
+ 0,
158
+ 3.0,
159
+ 0,
160
+ head_width=1.2,
161
+ head_length=1.0,
162
+ fc=color,
163
+ ec=color,
164
+ )
165
+
166
+ ax.set_aspect("equal")
167
+ ax.grid(True, alpha=0.3)
168
+
169
+ return ax