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,107 @@
1
+ """Perception data structures - points in local (ego) frame."""
2
+
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ import numpy as np
6
+
7
+ if TYPE_CHECKING:
8
+ from simple_autonomous_car.car.car import CarState
9
+
10
+
11
+ class PerceptionPoints:
12
+ """Represents perception data as a vector of points in local (ego) frame."""
13
+
14
+ def __init__(self, points: np.ndarray, frame: str = "ego"):
15
+ """
16
+ Initialize perception points.
17
+
18
+ Args:
19
+ points: Array of shape (N, 2) with [x, y] coordinates
20
+ frame: Frame of the points ("ego", "sensor", "global")
21
+ """
22
+ self.points = np.asarray(points, dtype=np.float64)
23
+ if self.points.ndim != 2 or self.points.shape[1] != 2:
24
+ raise ValueError("points must be shape (N, 2)")
25
+
26
+ if frame not in ["ego", "sensor", "global"]:
27
+ raise ValueError(f"frame must be one of ['ego', 'sensor', 'global'], got {frame}")
28
+
29
+ self.frame = frame
30
+
31
+ def to_ego_frame(self, car_state: Optional["CarState"] = None) -> "PerceptionPoints":
32
+ """
33
+ Convert points to ego frame.
34
+
35
+ Args:
36
+ car_state: Car state (required if converting from global frame)
37
+
38
+ Returns:
39
+ New PerceptionPoints instance in ego frame
40
+ """
41
+ if self.frame == "ego":
42
+ return PerceptionPoints(self.points.copy(), frame="ego")
43
+
44
+ if self.frame == "sensor":
45
+ # Assume sensor at ego origin for now
46
+ # Could be extended with sensor_pose_ego parameter
47
+ return PerceptionPoints(self.points.copy(), frame="ego")
48
+
49
+ if self.frame == "global":
50
+ if car_state is None:
51
+ raise ValueError("car_state required for global to ego conversion")
52
+ ego_points = np.array(
53
+ [car_state.transform_to_car_frame(point) for point in self.points]
54
+ )
55
+ return PerceptionPoints(ego_points, frame="ego")
56
+
57
+ raise ValueError(f"Unknown frame: {self.frame}")
58
+
59
+ def to_global_frame(self, car_state: "CarState") -> "PerceptionPoints":
60
+ """
61
+ Convert points to global frame.
62
+
63
+ Args:
64
+ car_state: Car state
65
+
66
+ Returns:
67
+ New PerceptionPoints instance in global frame
68
+ """
69
+ if self.frame == "global":
70
+ return PerceptionPoints(self.points.copy(), frame="global")
71
+
72
+ if self.frame == "ego":
73
+ # Import here to avoid circular import
74
+
75
+ global_points = np.array(
76
+ [car_state.transform_to_world_frame(point) for point in self.points]
77
+ )
78
+ return PerceptionPoints(global_points, frame="global")
79
+
80
+ if self.frame == "sensor":
81
+ # Convert sensor -> ego -> global
82
+ ego_points = self.to_ego_frame()
83
+ return ego_points.to_global_frame(car_state)
84
+
85
+ raise ValueError(f"Unknown frame: {self.frame}")
86
+
87
+ def filter_by_distance(self, max_distance: float) -> "PerceptionPoints":
88
+ """
89
+ Filter points by distance from origin.
90
+
91
+ Args:
92
+ max_distance: Maximum distance to keep
93
+
94
+ Returns:
95
+ New PerceptionPoints instance with filtered points
96
+ """
97
+ distances = np.linalg.norm(self.points, axis=1)
98
+ mask = distances <= max_distance
99
+ return PerceptionPoints(self.points[mask], frame=self.frame)
100
+
101
+ def __len__(self) -> int:
102
+ """Return number of points."""
103
+ return len(self.points)
104
+
105
+ def __getitem__(self, index: int | slice) -> np.ndarray:
106
+ """Get point(s) by index."""
107
+ return self.points[index]
@@ -0,0 +1,7 @@
1
+ """Path planning system for autonomous vehicles."""
2
+
3
+ from simple_autonomous_car.planning.base_planner import BasePlanner
4
+ from simple_autonomous_car.planning.goal_planner import GoalPlanner
5
+ from simple_autonomous_car.planning.track_planner import TrackPlanner
6
+
7
+ __all__ = ["BasePlanner", "TrackPlanner", "GoalPlanner"]
@@ -0,0 +1,184 @@
1
+ """Base planner class for path planning."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Any, Optional
5
+
6
+ import numpy as np
7
+
8
+ if TYPE_CHECKING:
9
+ from simple_autonomous_car.car.car import CarState
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ from simple_autonomous_car.costmap.base_costmap import BaseCostmap
14
+
15
+
16
+ class BasePlanner(ABC):
17
+ """
18
+ Base class for all path planners.
19
+
20
+ Planners generate paths (sequences of waypoints) that the car should follow.
21
+ Controllers can then use these plans to compute control commands.
22
+
23
+ Attributes
24
+ ----------
25
+ name : str
26
+ Planner name/identifier.
27
+ enabled : bool
28
+ Whether the planner is enabled.
29
+ """
30
+
31
+ def __init__(self, name: str = "planner", enabled: bool = True):
32
+ """
33
+ Initialize base planner.
34
+
35
+ Parameters
36
+ ----------
37
+ name : str, default="planner"
38
+ Planner name/identifier.
39
+ enabled : bool, default=True
40
+ Whether the planner is enabled.
41
+ """
42
+ self.name = name
43
+ self.enabled = enabled
44
+
45
+ @abstractmethod
46
+ def plan(
47
+ self,
48
+ car_state: "CarState",
49
+ perception_data: dict | None = None,
50
+ costmap: Optional["BaseCostmap"] = None,
51
+ goal: np.ndarray | None = None,
52
+ ) -> np.ndarray:
53
+ """
54
+ Generate a plan (path) for the car to follow.
55
+
56
+ Parameters
57
+ ----------
58
+ car_state : CarState
59
+ Current car state.
60
+ perception_data : dict, optional
61
+ Dictionary of perception data from sensors.
62
+ costmap : BaseCostmap, optional
63
+ Costmap for obstacle avoidance.
64
+ goal : np.ndarray, optional
65
+ Goal position as [x, y] array.
66
+
67
+ Returns
68
+ -------
69
+ np.ndarray
70
+ Planned path as array of shape (N, 2) with [x, y] waypoints.
71
+ Empty array if planning fails or planner is disabled.
72
+ """
73
+ pass
74
+
75
+ def is_enabled(self) -> bool:
76
+ """Check if planner is enabled."""
77
+ return self.enabled
78
+
79
+ def enable(self) -> None:
80
+ """Enable the planner."""
81
+ self.enabled = True
82
+
83
+ def disable(self) -> None:
84
+ """Disable the planner."""
85
+ self.enabled = False
86
+
87
+ def get_visualization_data(
88
+ self, car_state: "CarState", plan: np.ndarray | None = None, **kwargs: Any
89
+ ) -> dict[str, Any]:
90
+ """
91
+ Get visualization data for this planner.
92
+
93
+ This method should be overridden by subclasses to provide
94
+ planner-specific visualization data (e.g., waypoints, path segments,
95
+ planning metadata).
96
+
97
+ Parameters
98
+ ----------
99
+ car_state : CarState
100
+ Current car state.
101
+ plan : np.ndarray, optional
102
+ Planned path (if already computed).
103
+ **kwargs
104
+ Additional arguments.
105
+
106
+ Returns
107
+ -------
108
+ Dict
109
+ Dictionary containing visualization data. Default implementation
110
+ returns the plan if provided. Subclasses should override to provide
111
+ additional data.
112
+ """
113
+ if plan is not None:
114
+ return {"plan": plan}
115
+ return {}
116
+
117
+ def visualize(
118
+ self,
119
+ ax: Any,
120
+ car_state: "CarState",
121
+ plan: np.ndarray | None = None,
122
+ frame: str = "global",
123
+ **kwargs: Any,
124
+ ) -> None:
125
+ """
126
+ Visualize planned path on the given axes.
127
+
128
+ This method should be overridden by subclasses to plot
129
+ planner-specific visualizations (e.g., waypoints, path segments).
130
+
131
+ Parameters
132
+ ----------
133
+ ax : matplotlib.axes.Axes
134
+ Axes to plot on.
135
+ car_state : CarState
136
+ Current car state.
137
+ plan : np.ndarray, optional
138
+ Planned path (if already computed, otherwise will be computed).
139
+ frame : str, default="global"
140
+ Frame to plot in: "global" or "ego".
141
+ **kwargs
142
+ Additional visualization arguments (colors, linewidths, etc.).
143
+ """
144
+ # Default implementation: plot plan if available
145
+ if plan is not None and len(plan) > 0:
146
+ # Transform plan to desired frame
147
+ if frame == "ego":
148
+ plan_plot = np.array([car_state.transform_to_car_frame(point) for point in plan])
149
+ else:
150
+ plan_plot = plan
151
+
152
+ # Extract visualization parameters
153
+ color = kwargs.pop("color", "green")
154
+ linewidth = kwargs.pop("linewidth", 2.5)
155
+ linestyle = kwargs.pop("linestyle", "--")
156
+ show_waypoints = kwargs.pop("show_waypoints", frame == "global")
157
+ alpha = kwargs.pop("alpha", 0.8)
158
+
159
+ # Plot plan
160
+ if show_waypoints:
161
+ ax.plot(
162
+ plan_plot[:, 0],
163
+ plan_plot[:, 1],
164
+ "o-",
165
+ color=color,
166
+ linewidth=linewidth,
167
+ linestyle=linestyle,
168
+ markersize=4,
169
+ label="Plan",
170
+ alpha=alpha,
171
+ **kwargs,
172
+ )
173
+ else:
174
+ ax.plot(
175
+ plan_plot[:, 0],
176
+ plan_plot[:, 1],
177
+ "-",
178
+ color=color,
179
+ linewidth=linewidth,
180
+ linestyle=linestyle,
181
+ label="Plan",
182
+ alpha=alpha,
183
+ **kwargs,
184
+ )
@@ -0,0 +1,261 @@
1
+ """Goal-based path planner using A* algorithm."""
2
+
3
+ import heapq
4
+ from typing import TYPE_CHECKING, Any, Optional
5
+
6
+ import numpy as np
7
+
8
+ from simple_autonomous_car.car.car import CarState
9
+ from simple_autonomous_car.constants import (
10
+ COST_THRESHOLD,
11
+ DEFAULT_PLAN_LINEWIDTH,
12
+ )
13
+ from simple_autonomous_car.planning.base_planner import BasePlanner
14
+
15
+ if TYPE_CHECKING:
16
+ from simple_autonomous_car.costmap.base_costmap import BaseCostmap
17
+ from simple_autonomous_car.maps.grid_map import GridMap
18
+
19
+
20
+ class GoalPlanner(BasePlanner):
21
+ """
22
+ Goal-based path planner using A* algorithm.
23
+
24
+ This planner finds a path from the car's current position to a goal,
25
+ avoiding obstacles using a costmap or grid map.
26
+
27
+ Parameters
28
+ ----------
29
+ grid_map : GridMap, optional
30
+ Grid map with obstacles. If None, uses costmap.
31
+ resolution : float, default=0.5
32
+ Planning resolution in meters.
33
+ name : str, default="goal_planner"
34
+ Planner name.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ grid_map: Optional["GridMap"] = None,
40
+ resolution: float = 0.5,
41
+ name: str = "goal_planner",
42
+ ):
43
+ super().__init__(name=name)
44
+ self.grid_map = grid_map
45
+ self.resolution = resolution
46
+
47
+ def _heuristic(self, pos1: np.ndarray, pos2: np.ndarray) -> float:
48
+ """Euclidean distance heuristic for A*."""
49
+ return float(np.linalg.norm(pos1 - pos2))
50
+
51
+ def _get_neighbors(
52
+ self,
53
+ pos: np.ndarray,
54
+ grid_map: Optional["GridMap"] = None,
55
+ costmap: Optional["BaseCostmap"] = None,
56
+ car_state: CarState | None = None,
57
+ ) -> list[np.ndarray]:
58
+ """Get valid neighboring positions, checking both grid_map and costmap."""
59
+ neighbors = []
60
+ directions = [
61
+ [self.resolution, 0],
62
+ [-self.resolution, 0],
63
+ [0, self.resolution],
64
+ [0, -self.resolution],
65
+ [self.resolution, self.resolution],
66
+ [self.resolution, -self.resolution],
67
+ [-self.resolution, self.resolution],
68
+ [-self.resolution, -self.resolution],
69
+ ]
70
+
71
+ for dx, dy in directions:
72
+ neighbor = pos + np.array([dx, dy])
73
+
74
+ # Check validity using grid_map if available
75
+ if grid_map is not None:
76
+ if not grid_map.is_valid_position(neighbor):
77
+ continue
78
+ elif costmap is not None and car_state is not None:
79
+ # Use costmap to check if position is valid
80
+ cost = costmap.get_cost(neighbor, frame="global", car_state=car_state) # type: ignore[union-attr]
81
+ if cost >= COST_THRESHOLD: # Threshold for occupied
82
+ continue
83
+
84
+ neighbors.append(neighbor)
85
+
86
+ return neighbors
87
+
88
+ def _world_to_grid(self, pos: np.ndarray, grid_map: "GridMap") -> tuple[int, int]:
89
+ """Convert world coordinates to grid coordinates."""
90
+ grid_x = int((pos[0] + grid_map.width / 2) / self.resolution)
91
+ grid_y = int((pos[1] + grid_map.height / 2) / self.resolution)
92
+ return grid_x, grid_y
93
+
94
+ def _grid_to_world(self, grid_x: int, grid_y: int, grid_map: "GridMap") -> np.ndarray:
95
+ """Convert grid coordinates to world coordinates."""
96
+ x = (grid_x * self.resolution) - grid_map.width / 2
97
+ y = (grid_y * self.resolution) - grid_map.height / 2
98
+ return np.array([x, y])
99
+
100
+ def plan(
101
+ self,
102
+ car_state: CarState,
103
+ perception_data: dict | None = None,
104
+ costmap: Optional["BaseCostmap"] = None,
105
+ goal: np.ndarray | None = None,
106
+ ) -> np.ndarray:
107
+ """
108
+ Plan path from car position to goal using A*.
109
+
110
+ Parameters
111
+ ----------
112
+ car_state : CarState
113
+ Current car state.
114
+ perception_data : dict, optional
115
+ Not used by goal planner.
116
+ costmap : BaseCostmap, optional
117
+ Costmap for obstacle avoidance (used if grid_map not provided).
118
+ goal : np.ndarray, optional
119
+ Goal position [x, y]. Required for goal-based planning.
120
+
121
+ Returns
122
+ -------
123
+ np.ndarray
124
+ Planned path as array of shape (N, 2) with [x, y] waypoints.
125
+ Empty array if planning fails.
126
+ """
127
+ if not self.enabled:
128
+ return np.array([]).reshape(0, 2)
129
+
130
+ if goal is None:
131
+ return np.array([]).reshape(0, 2)
132
+
133
+ # Use grid_map if available, otherwise use costmap
134
+ use_costmap = self.grid_map is None and costmap is not None and costmap.enabled
135
+
136
+ if not use_costmap and self.grid_map is None:
137
+ return np.array([]).reshape(0, 2)
138
+
139
+ start = car_state.position()
140
+
141
+ # Check if start and goal are valid
142
+ if self.grid_map is not None:
143
+ if not self.grid_map.is_valid_position(start):
144
+ return np.array([]).reshape(0, 2)
145
+ if not self.grid_map.is_valid_position(goal):
146
+ return np.array([]).reshape(0, 2)
147
+ if use_costmap and costmap is not None:
148
+ # Check using costmap
149
+ start_cost = costmap.get_cost(start, frame="global", car_state=car_state)
150
+ goal_cost = costmap.get_cost(goal, frame="global", car_state=car_state)
151
+ if start_cost >= COST_THRESHOLD or goal_cost >= COST_THRESHOLD:
152
+ return np.array([]).reshape(0, 2)
153
+
154
+ # A* algorithm
155
+ open_set: list[tuple[float, tuple[float, ...]]] = [
156
+ (0.0, tuple(start))
157
+ ] # (f_score, position)
158
+ came_from: dict[tuple[float, ...], tuple[float, ...]] = {}
159
+ g_score: dict[tuple[float, ...], float] = {tuple(start): 0.0}
160
+ f_score: dict[tuple[float, ...], float] = {tuple(start): self._heuristic(start, goal)}
161
+ closed_set = set()
162
+
163
+ while open_set:
164
+ current_f, current_tuple = heapq.heappop(open_set)
165
+ current = np.asarray(current_tuple, dtype=np.float64)
166
+
167
+ if tuple(current) in closed_set:
168
+ continue
169
+
170
+ closed_set.add(tuple(current))
171
+
172
+ # Check if reached goal
173
+ if np.linalg.norm(current - goal) < self.resolution * 2:
174
+ # Reconstruct path
175
+ path = [goal.copy()]
176
+ current_tuple = tuple(current)
177
+ while current_tuple in came_from:
178
+ path.append(current.copy())
179
+ current_tuple = came_from[current_tuple]
180
+ current = np.asarray(current_tuple, dtype=np.float64)
181
+ path.append(start)
182
+ path.reverse()
183
+ return np.array(path)
184
+
185
+ # Explore neighbors (use costmap if grid_map not available)
186
+ neighbors = self._get_neighbors(current, self.grid_map, costmap, car_state)
187
+ for neighbor in neighbors:
188
+ neighbor_tuple = tuple(neighbor)
189
+
190
+ if neighbor_tuple in closed_set:
191
+ continue
192
+
193
+ # Cost to reach neighbor (1 step + costmap cost if available)
194
+ base_cost = np.linalg.norm(neighbor - current)
195
+ if costmap is not None and costmap.enabled:
196
+ # Add costmap cost as penalty
197
+ neighbor_cost = costmap.get_cost(neighbor, frame="global", car_state=car_state)
198
+ # Scale costmap cost (0.0-1.0) to add penalty
199
+ cost_penalty = neighbor_cost * 5.0 # Scale factor for obstacle avoidance
200
+ tentative_g = g_score[tuple(current)] + base_cost + cost_penalty
201
+ else:
202
+ tentative_g = g_score[tuple(current)] + base_cost
203
+
204
+ if neighbor_tuple not in g_score or tentative_g < g_score[neighbor_tuple]:
205
+ came_from[neighbor_tuple] = tuple(current)
206
+ g_score[neighbor_tuple] = float(tentative_g)
207
+ f_score[neighbor_tuple] = float(tentative_g + self._heuristic(neighbor, goal))
208
+ heapq.heappush(open_set, (f_score[neighbor_tuple], neighbor_tuple))
209
+
210
+ # No path found
211
+ return np.array([]).reshape(0, 2)
212
+
213
+ def visualize(
214
+ self,
215
+ ax: Any,
216
+ car_state: CarState,
217
+ plan: np.ndarray | None = None,
218
+ frame: str = "global",
219
+ **kwargs: Any,
220
+ ) -> None:
221
+ """Visualize planned path."""
222
+ if plan is None or len(plan) == 0:
223
+ return
224
+
225
+ # Transform plan to desired frame
226
+ if frame == "ego":
227
+ plan_plot = np.array([car_state.transform_to_car_frame(point) for point in plan])
228
+ else:
229
+ plan_plot = plan
230
+
231
+ # Extract visualization parameters
232
+ color = kwargs.pop("color", "blue")
233
+ linewidth = kwargs.pop("linewidth", DEFAULT_PLAN_LINEWIDTH)
234
+ kwargs.pop("linestyle", "-")
235
+ show_waypoints = kwargs.pop("show_waypoints", True)
236
+ alpha = kwargs.pop("alpha", 0.8)
237
+
238
+ # Plot plan
239
+ if show_waypoints:
240
+ ax.plot(
241
+ plan_plot[:, 0],
242
+ plan_plot[:, 1],
243
+ "o-",
244
+ color=color,
245
+ linewidth=linewidth,
246
+ markersize=4,
247
+ label="Plan",
248
+ alpha=alpha,
249
+ **kwargs,
250
+ )
251
+ else:
252
+ ax.plot(
253
+ plan_plot[:, 0],
254
+ plan_plot[:, 1],
255
+ "-",
256
+ color=color,
257
+ linewidth=linewidth,
258
+ label="Plan",
259
+ alpha=alpha,
260
+ **kwargs,
261
+ )