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,463 @@
1
+ """Track generation with bounds and centerline."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Optional
4
+
5
+ import numpy as np
6
+
7
+ if TYPE_CHECKING:
8
+ from matplotlib.axes import Axes
9
+
10
+ from simple_autonomous_car.car.car import CarState
11
+
12
+
13
+ class Track:
14
+ """Represents a racing track with centerline and bounds."""
15
+
16
+ def __init__(
17
+ self,
18
+ centerline: np.ndarray,
19
+ track_width: float = 5.0,
20
+ inner_bound: np.ndarray | None = None,
21
+ outer_bound: np.ndarray | None = None,
22
+ ):
23
+ """
24
+ Initialize track.
25
+
26
+ Args:
27
+ centerline: Array of shape (N, 2) with [x, y] coordinates of centerline
28
+ track_width: Width of the track (used if bounds not provided)
29
+ inner_bound: Array of shape (N, 2) with inner boundary points
30
+ outer_bound: Array of shape (N, 2) with outer boundary points
31
+ """
32
+ self.centerline = np.asarray(centerline, dtype=np.float64)
33
+ if self.centerline.ndim != 2 or self.centerline.shape[1] != 2:
34
+ raise ValueError("centerline must be shape (N, 2)")
35
+
36
+ self.track_width = track_width
37
+
38
+ if inner_bound is None or outer_bound is None:
39
+ self._generate_bounds()
40
+ else:
41
+ self.inner_bound = np.asarray(inner_bound, dtype=np.float64)
42
+ self.outer_bound = np.asarray(outer_bound, dtype=np.float64)
43
+
44
+ self._validate_bounds()
45
+
46
+ def _generate_bounds(self) -> None:
47
+ """Generate inner and outer bounds from centerline and width."""
48
+ n_points = len(self.centerline)
49
+ inner_bound = np.zeros_like(self.centerline)
50
+ outer_bound = np.zeros_like(self.centerline)
51
+
52
+ for i in range(n_points):
53
+ # Get direction vector
54
+ if i == 0:
55
+ direction = self.centerline[1] - self.centerline[0]
56
+ elif i == n_points - 1:
57
+ direction = self.centerline[-1] - self.centerline[-2]
58
+ else:
59
+ direction = self.centerline[i + 1] - self.centerline[i - 1]
60
+
61
+ # Normalize and get perpendicular
62
+ direction_norm = np.linalg.norm(direction)
63
+ if direction_norm > 1e-6:
64
+ direction = direction / direction_norm
65
+ else:
66
+ direction = np.array([1.0, 0.0])
67
+
68
+ # Perpendicular vector (rotate 90 degrees)
69
+ perp = np.array([-direction[1], direction[0]])
70
+
71
+ # Generate bounds
72
+ half_width = self.track_width / 2.0
73
+ inner_bound[i] = self.centerline[i] - perp * half_width
74
+ outer_bound[i] = self.centerline[i] + perp * half_width
75
+
76
+ self.inner_bound = inner_bound
77
+ self.outer_bound = outer_bound
78
+
79
+ def _validate_bounds(self) -> None:
80
+ """Validate that bounds have correct shape."""
81
+ if (
82
+ self.inner_bound.shape != self.centerline.shape
83
+ or self.outer_bound.shape != self.centerline.shape
84
+ ):
85
+ raise ValueError("Bounds must have same shape as centerline")
86
+
87
+ @classmethod
88
+ def create_oval_track(
89
+ cls,
90
+ length: float = 100.0,
91
+ width: float = 30.0,
92
+ track_width: float = 5.0,
93
+ num_points: int = 200,
94
+ ) -> "Track":
95
+ """
96
+ Create an oval-shaped track.
97
+
98
+ Args:
99
+ length: Length of the straight sections
100
+ width: Width of the track (distance between straight sections)
101
+ track_width: Width of the track boundaries
102
+ num_points: Number of points along the centerline
103
+
104
+ Returns:
105
+ Track instance
106
+ """
107
+ # Generate oval centerline
108
+ points = []
109
+ straight_points = int(num_points * 0.3)
110
+
111
+ # First straight
112
+ for i in range(straight_points):
113
+ x = -length / 2 + (i / straight_points) * length
114
+ y = -width / 2
115
+ points.append([x, y])
116
+
117
+ # First curve
118
+ curve_points = int(num_points * 0.2)
119
+ for i in range(curve_points):
120
+ angle = np.pi * (i / curve_points)
121
+ x = length / 2 + (width / 2) * np.cos(angle)
122
+ y = -width / 2 + (width / 2) * np.sin(angle)
123
+ points.append([x, y])
124
+
125
+ # Second straight
126
+ for i in range(straight_points):
127
+ x = length / 2 - (i / straight_points) * length
128
+ y = width / 2
129
+ points.append([x, y])
130
+
131
+ # Second curve
132
+ for i in range(curve_points):
133
+ angle = np.pi + np.pi * (i / curve_points)
134
+ x = -length / 2 + (width / 2) * np.cos(angle)
135
+ y = width / 2 + (width / 2) * np.sin(angle)
136
+ points.append([x, y])
137
+
138
+ return cls(np.array(points), track_width=track_width)
139
+
140
+ @classmethod
141
+ def create_simple_track(
142
+ cls,
143
+ length: float = 100.0,
144
+ width: float = 50.0,
145
+ track_width: float = 5.0,
146
+ num_points: int = 200,
147
+ ) -> "Track":
148
+ """
149
+ Create a simple rounded rectangle track with gentle curves (no 180 or 360 degree turns).
150
+
151
+ Args:
152
+ length: Overall length of the track
153
+ width: Overall width of the track
154
+ track_width: Width of the track boundaries
155
+ num_points: Number of points along the centerline
156
+
157
+ Returns:
158
+ Track instance
159
+ """
160
+ points = []
161
+
162
+ # Create a rounded rectangle with very gentle corners
163
+ # Use large radius for corners to avoid sharp turns
164
+ corner_radius = min(length, width) * 0.3 # Large radius for gentle curves
165
+
166
+ # Calculate dimensions
167
+ straight_length = length - 2 * corner_radius
168
+ straight_width = width - 2 * corner_radius
169
+
170
+ # Distribute points: more on straights, fewer on gentle curves
171
+ straight_points = int(num_points * 0.35)
172
+ corner_points = int(num_points * 0.15)
173
+
174
+ # Bottom straight (left to right)
175
+ for i in range(straight_points):
176
+ x = -length / 2 + corner_radius + (i / straight_points) * straight_length
177
+ y = -width / 2
178
+ points.append([x, y])
179
+
180
+ # Bottom-right corner (gentle curve, max 90 degrees)
181
+ for i in range(corner_points):
182
+ angle = -np.pi / 2 + (i / corner_points) * (np.pi / 2) # 0 to 90 degrees
183
+ x = length / 2 - corner_radius + corner_radius * np.cos(angle)
184
+ y = -width / 2 + corner_radius + corner_radius * np.sin(angle)
185
+ points.append([x, y])
186
+
187
+ # Right straight (bottom to top)
188
+ for i in range(straight_points):
189
+ x = length / 2
190
+ y = -width / 2 + corner_radius + (i / straight_points) * straight_width
191
+ points.append([x, y])
192
+
193
+ # Top-right corner (gentle curve)
194
+ for i in range(corner_points):
195
+ angle = 0 + (i / corner_points) * (np.pi / 2) # 0 to 90 degrees
196
+ x = length / 2 - corner_radius + corner_radius * np.cos(angle)
197
+ y = width / 2 - corner_radius + corner_radius * np.sin(angle)
198
+ points.append([x, y])
199
+
200
+ # Top straight (right to left)
201
+ for i in range(straight_points):
202
+ x = length / 2 - corner_radius - (i / straight_points) * straight_length
203
+ y = width / 2
204
+ points.append([x, y])
205
+
206
+ # Top-left corner (gentle curve)
207
+ for i in range(corner_points):
208
+ angle = np.pi / 2 + (i / corner_points) * (np.pi / 2) # 90 to 180 degrees
209
+ x = -length / 2 + corner_radius + corner_radius * np.cos(angle)
210
+ y = width / 2 - corner_radius + corner_radius * np.sin(angle)
211
+ points.append([x, y])
212
+
213
+ # Left straight (top to bottom)
214
+ for i in range(straight_points):
215
+ x = -length / 2
216
+ y = width / 2 - corner_radius - (i / straight_points) * straight_width
217
+ points.append([x, y])
218
+
219
+ # Bottom-left corner (gentle curve) - close the loop
220
+ for i in range(corner_points):
221
+ angle = np.pi + (i / corner_points) * (np.pi / 2) # 180 to 270 degrees
222
+ x = -length / 2 + corner_radius + corner_radius * np.cos(angle)
223
+ y = -width / 2 + corner_radius + corner_radius * np.sin(angle)
224
+ points.append([x, y])
225
+
226
+ return cls(np.array(points), track_width=track_width)
227
+
228
+ @classmethod
229
+ def create_figure8_track(
230
+ cls,
231
+ size: float = 60.0,
232
+ track_width: float = 5.0,
233
+ num_points: int = 300,
234
+ ) -> "Track":
235
+ """
236
+ Create a figure-8 (lemniscate) shaped track.
237
+
238
+ The track forms a smooth figure-8 pattern with two loops that cross in the middle.
239
+ This creates a more challenging track with crossing paths and varying curvature.
240
+
241
+ Args:
242
+ size: Overall size of the track (controls the scale, approximately the diameter)
243
+ track_width: Width of the track boundaries
244
+ num_points: Number of points along the centerline
245
+
246
+ Returns:
247
+ Track instance
248
+ """
249
+ points = []
250
+
251
+ # Use parametric equations for a lemniscate of Bernoulli (figure-8 curve)
252
+ # Parametric form: x = a * sin(t) / (1 + cos^2(t)), y = a * sin(t) * cos(t) / (1 + cos^2(t))
253
+ # This creates a smooth, symmetric figure-8
254
+
255
+ # Generate parameter t from 0 to 2*pi
256
+ t_values = np.linspace(0, 2 * np.pi, num_points)
257
+
258
+ # Scale factor for the lemniscate (adjust to match desired size)
259
+ a = size / 2.5 # Adjusted for better size control
260
+
261
+ for t in t_values:
262
+ # Lemniscate parametric equations
263
+ # Denominator ensures smooth curve and proper figure-8 shape
264
+ denom = 1 + np.cos(t) ** 2
265
+ x = a * np.sin(t) / denom
266
+ y = a * np.sin(t) * np.cos(t) / denom
267
+
268
+ points.append([x, y])
269
+
270
+ return cls(np.array(points), track_width=track_width)
271
+
272
+ def get_point_at_distance(self, distance: float) -> tuple[np.ndarray, float]:
273
+ """
274
+ Get point on centerline at given distance along track.
275
+
276
+ Args:
277
+ distance: Distance along track from start
278
+
279
+ Returns:
280
+ Tuple of (point, heading_angle)
281
+ """
282
+ cumulative_distances = np.zeros(len(self.centerline))
283
+ for i in range(1, len(self.centerline)):
284
+ dist = np.linalg.norm(self.centerline[i] - self.centerline[i - 1])
285
+ cumulative_distances[i] = cumulative_distances[i - 1] + dist
286
+
287
+ total_length = cumulative_distances[-1]
288
+ distance = distance % total_length # Wrap around
289
+
290
+ # Find segment
291
+ idx_int = np.searchsorted(cumulative_distances, distance)
292
+ idx: int = int(idx_int)
293
+ if idx == 0:
294
+ idx = 1
295
+ if idx >= len(self.centerline):
296
+ idx = len(self.centerline) - 1
297
+
298
+ # Interpolate
299
+ segment_dist = distance - cumulative_distances[idx - 1]
300
+ segment_length = cumulative_distances[idx] - cumulative_distances[idx - 1]
301
+ if segment_length > 1e-6:
302
+ t = segment_dist / segment_length
303
+ else:
304
+ t = 0.0
305
+
306
+ point = self.centerline[idx - 1] + t * (self.centerline[idx] - self.centerline[idx - 1])
307
+
308
+ # Calculate heading
309
+ direction = self.centerline[idx] - self.centerline[idx - 1]
310
+ heading = np.arctan2(direction[1], direction[0])
311
+
312
+ return point, heading
313
+
314
+ def visualize(
315
+ self,
316
+ ax: "Axes",
317
+ car_state: Optional["CarState"] = None,
318
+ frame: str = "global",
319
+ **kwargs: Any,
320
+ ) -> None:
321
+ """
322
+ Visualize track boundaries on the given axes.
323
+
324
+ Parameters
325
+ ----------
326
+ ax : matplotlib.axes.Axes
327
+ Axes to plot on.
328
+ car_state : CarState, optional
329
+ Current car state (for frame transformations).
330
+ frame : str, default="global"
331
+ Frame to plot in: "global" or "ego".
332
+ **kwargs
333
+ Additional visualization arguments:
334
+ - show_centerline: bool, whether to show centerline
335
+ - show_bounds: bool, whether to show track boundaries
336
+ - centerline_color: str, color for centerline
337
+ - bounds_color: str, color for boundaries
338
+ - bounds_linewidth: float, linewidth for boundaries
339
+ - horizon: float, visualization horizon (for ego frame filtering)
340
+ """
341
+ show_centerline = kwargs.pop("show_centerline", True)
342
+ show_bounds = kwargs.pop("show_bounds", True)
343
+ centerline_color = kwargs.pop("centerline_color", "b")
344
+ bounds_color = kwargs.pop("bounds_color", "k")
345
+ bounds_linewidth = kwargs.pop("bounds_linewidth", 2.5)
346
+ horizon = kwargs.pop("horizon", None)
347
+
348
+ if frame == "ego" and car_state is not None:
349
+ # Transform track to ego frame
350
+ if show_bounds:
351
+ inner_bound_ego = np.array(
352
+ [car_state.transform_to_car_frame(point) for point in self.inner_bound]
353
+ )
354
+ outer_bound_ego = np.array(
355
+ [car_state.transform_to_car_frame(point) for point in self.outer_bound]
356
+ )
357
+
358
+ # Filter points within horizon for performance
359
+ if horizon is not None:
360
+ mask_inner = (np.abs(inner_bound_ego[:, 0]) < horizon * 1.2) & (
361
+ np.abs(inner_bound_ego[:, 1]) < horizon * 1.2
362
+ )
363
+ mask_outer = (np.abs(outer_bound_ego[:, 0]) < horizon * 1.2) & (
364
+ np.abs(outer_bound_ego[:, 1]) < horizon * 1.2
365
+ )
366
+ else:
367
+ mask_inner = np.ones(len(inner_bound_ego), dtype=bool)
368
+ mask_outer = np.ones(len(outer_bound_ego), dtype=bool)
369
+
370
+ if np.any(mask_inner):
371
+ ax.plot(
372
+ inner_bound_ego[mask_inner, 0],
373
+ inner_bound_ego[mask_inner, 1],
374
+ "-",
375
+ color=bounds_color,
376
+ linewidth=bounds_linewidth,
377
+ label="Map",
378
+ alpha=0.9,
379
+ zorder=1,
380
+ **kwargs,
381
+ )
382
+ if np.any(mask_outer):
383
+ ax.plot(
384
+ outer_bound_ego[mask_outer, 0],
385
+ outer_bound_ego[mask_outer, 1],
386
+ "-",
387
+ color=bounds_color,
388
+ linewidth=bounds_linewidth,
389
+ alpha=0.9,
390
+ zorder=1,
391
+ **kwargs,
392
+ )
393
+
394
+ if show_centerline:
395
+ centerline_ego = np.array(
396
+ [car_state.transform_to_car_frame(point) for point in self.centerline]
397
+ )
398
+ if horizon is not None:
399
+ mask = (np.abs(centerline_ego[:, 0]) < horizon * 1.2) & (
400
+ np.abs(centerline_ego[:, 1]) < horizon * 1.2
401
+ )
402
+ else:
403
+ mask = np.ones(len(centerline_ego), dtype=bool)
404
+
405
+ if np.any(mask):
406
+ ax.plot(
407
+ centerline_ego[mask, 0],
408
+ centerline_ego[mask, 1],
409
+ "--",
410
+ color=centerline_color,
411
+ linewidth=1.5,
412
+ alpha=0.5,
413
+ label="Centerline",
414
+ **kwargs,
415
+ )
416
+ else:
417
+ # Global frame
418
+ if show_bounds:
419
+ ax.plot(
420
+ self.inner_bound[:, 0],
421
+ self.inner_bound[:, 1],
422
+ "-",
423
+ color=bounds_color,
424
+ linewidth=bounds_linewidth,
425
+ label="Track Bounds",
426
+ **kwargs,
427
+ )
428
+ ax.plot(
429
+ self.outer_bound[:, 0],
430
+ self.outer_bound[:, 1],
431
+ "-",
432
+ color=bounds_color,
433
+ linewidth=bounds_linewidth,
434
+ **kwargs,
435
+ )
436
+
437
+ if show_centerline:
438
+ ax.plot(
439
+ self.centerline[:, 0],
440
+ self.centerline[:, 1],
441
+ "--",
442
+ color=centerline_color,
443
+ linewidth=1.5,
444
+ alpha=0.5,
445
+ label="Centerline",
446
+ **kwargs,
447
+ )
448
+
449
+ def get_bounds_at_point(self, point: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
450
+ """
451
+ Get inner and outer boundary points closest to given point.
452
+
453
+ Args:
454
+ point: Point [x, y]
455
+
456
+ Returns:
457
+ Tuple of (inner_bound_point, outer_bound_point)
458
+ """
459
+ # Find closest centerline point
460
+ distances = np.linalg.norm(self.centerline - point, axis=1)
461
+ idx = np.argmin(distances)
462
+
463
+ return self.inner_bound[idx], self.outer_bound[idx]
@@ -0,0 +1,25 @@
1
+ """Visualization and animation utilities.
2
+
3
+ All visualization is now component-based. Use component.visualize() methods:
4
+ - track.visualize()
5
+ - planner.visualize()
6
+ - controller.visualize()
7
+ - costmap.visualize()
8
+
9
+ Utility functions are provided for non-component data (perception, car).
10
+ """
11
+
12
+ from simple_autonomous_car.control.controller_viz import (
13
+ plot_control_history,
14
+ plot_pure_pursuit_state,
15
+ )
16
+ from simple_autonomous_car.visualization.alert_viz import AlertVisualizer
17
+ from simple_autonomous_car.visualization.utils import plot_car, plot_perception
18
+
19
+ __all__ = [
20
+ "AlertVisualizer",
21
+ "plot_perception",
22
+ "plot_car",
23
+ "plot_pure_pursuit_state",
24
+ "plot_control_history",
25
+ ]