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,96 @@
|
|
|
1
|
+
"""Simple Autonomous Car SDK.
|
|
2
|
+
|
|
3
|
+
A comprehensive SDK for building autonomous vehicle systems with modular sensors,
|
|
4
|
+
controllers, planners, and alert systems.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.3.0"
|
|
8
|
+
|
|
9
|
+
# Core modules
|
|
10
|
+
# Constants (available as simple_autonomous_car.constants)
|
|
11
|
+
import simple_autonomous_car.constants
|
|
12
|
+
from simple_autonomous_car.alerts import TrackBoundsAlert
|
|
13
|
+
from simple_autonomous_car.car import Car, CarState
|
|
14
|
+
from simple_autonomous_car.control import BaseController, PIDController, PurePursuitController
|
|
15
|
+
from simple_autonomous_car.costmap import BaseCostmap, GridCostmap, inflate_obstacles
|
|
16
|
+
from simple_autonomous_car.detection import LocalizationErrorDetector
|
|
17
|
+
from simple_autonomous_car.filters import BaseFilter, KalmanFilter, ParticleFilter
|
|
18
|
+
from simple_autonomous_car.frames import (
|
|
19
|
+
FrenetFrame,
|
|
20
|
+
ego_to_frenet,
|
|
21
|
+
ego_to_sensor,
|
|
22
|
+
frenet_to_ego,
|
|
23
|
+
frenet_to_global,
|
|
24
|
+
global_to_frenet,
|
|
25
|
+
sensor_to_ego,
|
|
26
|
+
)
|
|
27
|
+
from simple_autonomous_car.maps import FrenetMap, GridMap, GroundTruthMap, PerceivedMap
|
|
28
|
+
from simple_autonomous_car.perception import PerceptionPoints
|
|
29
|
+
from simple_autonomous_car.planning import BasePlanner, GoalPlanner, TrackPlanner
|
|
30
|
+
from simple_autonomous_car.sensors import BaseSensor, LiDARSensor
|
|
31
|
+
from simple_autonomous_car.track import Track
|
|
32
|
+
from simple_autonomous_car.visualization import (
|
|
33
|
+
AlertVisualizer,
|
|
34
|
+
plot_car,
|
|
35
|
+
plot_control_history,
|
|
36
|
+
plot_perception,
|
|
37
|
+
plot_pure_pursuit_state,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Version
|
|
42
|
+
"__version__",
|
|
43
|
+
# Track
|
|
44
|
+
"Track",
|
|
45
|
+
# Car
|
|
46
|
+
"Car",
|
|
47
|
+
"CarState",
|
|
48
|
+
# Maps
|
|
49
|
+
"GroundTruthMap",
|
|
50
|
+
"PerceivedMap",
|
|
51
|
+
"FrenetMap",
|
|
52
|
+
"GridMap",
|
|
53
|
+
# Perception
|
|
54
|
+
"PerceptionPoints",
|
|
55
|
+
# Frames
|
|
56
|
+
"FrenetFrame",
|
|
57
|
+
"global_to_frenet",
|
|
58
|
+
"frenet_to_global",
|
|
59
|
+
"ego_to_frenet",
|
|
60
|
+
"frenet_to_ego",
|
|
61
|
+
"sensor_to_ego",
|
|
62
|
+
"ego_to_sensor",
|
|
63
|
+
# Sensors
|
|
64
|
+
"BaseSensor",
|
|
65
|
+
"LiDARSensor",
|
|
66
|
+
# Control
|
|
67
|
+
"BaseController",
|
|
68
|
+
"PurePursuitController",
|
|
69
|
+
"PIDController",
|
|
70
|
+
# Planning
|
|
71
|
+
"BasePlanner",
|
|
72
|
+
"TrackPlanner",
|
|
73
|
+
"GoalPlanner",
|
|
74
|
+
# Costmap
|
|
75
|
+
"BaseCostmap",
|
|
76
|
+
"GridCostmap",
|
|
77
|
+
"inflate_obstacles",
|
|
78
|
+
# Detection
|
|
79
|
+
"LocalizationErrorDetector",
|
|
80
|
+
# Alerts
|
|
81
|
+
"TrackBoundsAlert",
|
|
82
|
+
# Filters
|
|
83
|
+
"BaseFilter",
|
|
84
|
+
"KalmanFilter",
|
|
85
|
+
"ParticleFilter",
|
|
86
|
+
# Footprints
|
|
87
|
+
"BaseFootprint",
|
|
88
|
+
"RectangularFootprint",
|
|
89
|
+
"CircularFootprint",
|
|
90
|
+
# Visualization
|
|
91
|
+
"AlertVisualizer",
|
|
92
|
+
"plot_perception",
|
|
93
|
+
"plot_car",
|
|
94
|
+
"plot_pure_pursuit_state",
|
|
95
|
+
"plot_control_history",
|
|
96
|
+
]
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Track bounds alert system - detects when perceived lines are too far from map lines."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from simple_autonomous_car.car.car import CarState
|
|
6
|
+
from simple_autonomous_car.maps.frenet_map import FrenetMap
|
|
7
|
+
from simple_autonomous_car.perception.perception import PerceptionPoints
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TrackBoundsAlert:
|
|
11
|
+
"""
|
|
12
|
+
Alert system for detecting when perceived track boundaries deviate significantly
|
|
13
|
+
from the ground truth map boundaries.
|
|
14
|
+
|
|
15
|
+
This is the primary alert system for track bounds violations. It converts
|
|
16
|
+
perception data to Frenet coordinates and compares against ground truth bounds
|
|
17
|
+
to detect when the car is approaching or leaving the track.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
frenet_map : FrenetMap
|
|
22
|
+
Frenet map with ground truth boundaries.
|
|
23
|
+
warning_threshold : float, default=1.0
|
|
24
|
+
Distance threshold for warnings in meters. Deviations above this trigger warnings.
|
|
25
|
+
critical_threshold : float, default=2.0
|
|
26
|
+
Distance threshold for critical alerts in meters. Deviations above this trigger critical alerts.
|
|
27
|
+
Must be greater than warning_threshold.
|
|
28
|
+
lookahead_distance : float, default=20.0
|
|
29
|
+
Distance ahead to check in meters. Only points within this distance are evaluated.
|
|
30
|
+
|
|
31
|
+
Attributes
|
|
32
|
+
----------
|
|
33
|
+
frenet_map : FrenetMap
|
|
34
|
+
Frenet map with ground truth boundaries.
|
|
35
|
+
warning_threshold : float
|
|
36
|
+
Warning threshold in meters.
|
|
37
|
+
critical_threshold : float
|
|
38
|
+
Critical threshold in meters.
|
|
39
|
+
lookahead_distance : float
|
|
40
|
+
Lookahead distance in meters.
|
|
41
|
+
alert_history : List[Dict]
|
|
42
|
+
History of alert checks.
|
|
43
|
+
|
|
44
|
+
Examples
|
|
45
|
+
--------
|
|
46
|
+
>>> from simple_autonomous_car import Track, FrenetMap, TrackBoundsAlert
|
|
47
|
+
>>> track = Track.create_simple_track()
|
|
48
|
+
>>> frenet_map = FrenetMap(track)
|
|
49
|
+
>>> alert_system = TrackBoundsAlert(
|
|
50
|
+
... frenet_map,
|
|
51
|
+
... warning_threshold=1.0,
|
|
52
|
+
... critical_threshold=2.0,
|
|
53
|
+
... lookahead_distance=20.0
|
|
54
|
+
... )
|
|
55
|
+
>>> result = alert_system.check(perception_points, car_state)
|
|
56
|
+
>>> if result["has_critical"]:
|
|
57
|
+
... print(f"CRITICAL: Max deviation = {result['max_deviation']:.2f}m")
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
frenet_map: FrenetMap,
|
|
63
|
+
warning_threshold: float = 1.0,
|
|
64
|
+
critical_threshold: float = 2.0,
|
|
65
|
+
lookahead_distance: float = 20.0,
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Initialize track bounds alert system.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
frenet_map : FrenetMap
|
|
73
|
+
Frenet map with ground truth boundaries.
|
|
74
|
+
warning_threshold : float, default=1.0
|
|
75
|
+
Distance threshold for warnings (meters).
|
|
76
|
+
critical_threshold : float, default=2.0
|
|
77
|
+
Distance threshold for critical alerts (meters).
|
|
78
|
+
Must be >= warning_threshold.
|
|
79
|
+
lookahead_distance : float, default=20.0
|
|
80
|
+
Distance ahead to check (meters). Must be positive.
|
|
81
|
+
"""
|
|
82
|
+
if warning_threshold < 0:
|
|
83
|
+
raise ValueError("warning_threshold must be non-negative")
|
|
84
|
+
if critical_threshold < warning_threshold:
|
|
85
|
+
raise ValueError("critical_threshold must be >= warning_threshold")
|
|
86
|
+
if lookahead_distance <= 0:
|
|
87
|
+
raise ValueError("lookahead_distance must be positive")
|
|
88
|
+
|
|
89
|
+
self.frenet_map = frenet_map
|
|
90
|
+
self.warning_threshold = warning_threshold
|
|
91
|
+
self.critical_threshold = critical_threshold
|
|
92
|
+
self.lookahead_distance = lookahead_distance
|
|
93
|
+
self.alert_history: list[dict] = []
|
|
94
|
+
|
|
95
|
+
def check(
|
|
96
|
+
self,
|
|
97
|
+
perception_points: PerceptionPoints,
|
|
98
|
+
car_state: CarState,
|
|
99
|
+
) -> dict:
|
|
100
|
+
"""
|
|
101
|
+
Check for track bounds violations.
|
|
102
|
+
|
|
103
|
+
This method converts perception points to Frenet coordinates, filters them
|
|
104
|
+
by lookahead distance, and compares against ground truth bounds to detect
|
|
105
|
+
deviations.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
perception_points : PerceptionPoints
|
|
110
|
+
Perceived track boundaries. Can be in any frame (will be converted to ego).
|
|
111
|
+
car_state : CarState
|
|
112
|
+
Current car state for coordinate transformations.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
Dict
|
|
117
|
+
Dictionary with alert information containing:
|
|
118
|
+
- has_warning : bool
|
|
119
|
+
True if any deviation exceeds warning_threshold.
|
|
120
|
+
- has_critical : bool
|
|
121
|
+
True if any deviation exceeds critical_threshold.
|
|
122
|
+
- max_deviation : float
|
|
123
|
+
Maximum deviation from bounds in meters.
|
|
124
|
+
- mean_deviation : float
|
|
125
|
+
Mean deviation from bounds in meters.
|
|
126
|
+
- deviations : np.ndarray
|
|
127
|
+
Array of deviations for each point (meters).
|
|
128
|
+
- alert_points : List[Dict]
|
|
129
|
+
List of points with alerts, each containing:
|
|
130
|
+
- s : float - Longitudinal position
|
|
131
|
+
- d : float - Lateral position
|
|
132
|
+
- deviation : float - Deviation from bounds
|
|
133
|
+
|
|
134
|
+
Examples
|
|
135
|
+
--------
|
|
136
|
+
>>> result = alert_system.check(perception_points, car_state)
|
|
137
|
+
>>> if result["has_critical"]:
|
|
138
|
+
... print(f"CRITICAL: {result['max_deviation']:.2f}m deviation")
|
|
139
|
+
>>> print(f"Found {len(result['alert_points'])} alert points")
|
|
140
|
+
"""
|
|
141
|
+
# Convert perception points to Frenet frame
|
|
142
|
+
# First ensure points are in ego frame
|
|
143
|
+
if perception_points.frame != "ego":
|
|
144
|
+
perception_points = perception_points.to_ego_frame(car_state)
|
|
145
|
+
|
|
146
|
+
if len(perception_points.points) == 0:
|
|
147
|
+
return self._empty_result()
|
|
148
|
+
|
|
149
|
+
# Convert points to Frenet frame (vectorized where possible)
|
|
150
|
+
frenet_points = []
|
|
151
|
+
for point in perception_points.points:
|
|
152
|
+
try:
|
|
153
|
+
# Convert ego to global, then global to Frenet
|
|
154
|
+
point_global = car_state.transform_to_world_frame(point)
|
|
155
|
+
s, d = self.frenet_map.frenet_frame.global_to_frenet(point_global)
|
|
156
|
+
# Only include valid points (s >= 0)
|
|
157
|
+
if s >= 0:
|
|
158
|
+
frenet_points.append([s, d])
|
|
159
|
+
except (ValueError, RuntimeError):
|
|
160
|
+
# Skip points that can't be converted (e.g., outside track)
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
if len(frenet_points) == 0:
|
|
164
|
+
return self._empty_result()
|
|
165
|
+
|
|
166
|
+
frenet_points = np.asarray(frenet_points, dtype=np.float64) # type: ignore[assignment]
|
|
167
|
+
|
|
168
|
+
# Get current position in Frenet frame
|
|
169
|
+
car_pos_global = car_state.position()
|
|
170
|
+
try:
|
|
171
|
+
s_car, d_car = self.frenet_map.frenet_frame.global_to_frenet(car_pos_global)
|
|
172
|
+
except (ValueError, RuntimeError):
|
|
173
|
+
# Car position can't be converted (shouldn't happen, but handle gracefully)
|
|
174
|
+
return self._empty_result()
|
|
175
|
+
|
|
176
|
+
# Check points within lookahead distance (ahead of car)
|
|
177
|
+
s_end = s_car + self.lookahead_distance
|
|
178
|
+
mask = (frenet_points[:, 0] >= s_car) & (frenet_points[:, 0] <= s_end) # type: ignore[call-overload]
|
|
179
|
+
relevant_points = np.asarray(frenet_points[mask], dtype=np.float64) # type: ignore[call-overload]
|
|
180
|
+
|
|
181
|
+
if len(relevant_points) == 0:
|
|
182
|
+
return self._empty_result()
|
|
183
|
+
|
|
184
|
+
# Get ground truth bounds for each point and calculate deviations
|
|
185
|
+
deviations = []
|
|
186
|
+
alert_points = []
|
|
187
|
+
|
|
188
|
+
for s, d_perceived in relevant_points:
|
|
189
|
+
try:
|
|
190
|
+
d_inner, d_outer = self.frenet_map.get_bounds_at_s(s)
|
|
191
|
+
except (ValueError, IndexError):
|
|
192
|
+
# Skip if bounds can't be retrieved for this s
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
# Check if perceived point is within bounds
|
|
196
|
+
if d_perceived < d_inner:
|
|
197
|
+
# Too far left (inside track) - negative d means left of centerline
|
|
198
|
+
deviation = abs(d_perceived - d_inner)
|
|
199
|
+
elif d_perceived > d_outer:
|
|
200
|
+
# Too far right (outside track) - positive d means right of centerline
|
|
201
|
+
deviation = abs(d_perceived - d_outer)
|
|
202
|
+
else:
|
|
203
|
+
# Within bounds
|
|
204
|
+
deviation = 0.0
|
|
205
|
+
|
|
206
|
+
deviations.append(deviation)
|
|
207
|
+
|
|
208
|
+
# Store alert points that exceed warning threshold
|
|
209
|
+
if deviation > self.warning_threshold:
|
|
210
|
+
alert_points.append({"s": s, "d": d_perceived, "deviation": deviation})
|
|
211
|
+
|
|
212
|
+
deviations = np.asarray(deviations, dtype=np.float64) # type: ignore[assignment]
|
|
213
|
+
|
|
214
|
+
max_deviation = np.max(deviations) if len(deviations) > 0 else 0.0
|
|
215
|
+
mean_deviation = np.mean(deviations) if len(deviations) > 0 else 0.0
|
|
216
|
+
|
|
217
|
+
has_warning = max_deviation > self.warning_threshold
|
|
218
|
+
has_critical = max_deviation > self.critical_threshold
|
|
219
|
+
|
|
220
|
+
result = {
|
|
221
|
+
"has_warning": has_warning,
|
|
222
|
+
"has_critical": has_critical,
|
|
223
|
+
"max_deviation": max_deviation,
|
|
224
|
+
"mean_deviation": mean_deviation,
|
|
225
|
+
"deviations": deviations,
|
|
226
|
+
"alert_points": alert_points,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Store in history
|
|
230
|
+
self.alert_history.append(
|
|
231
|
+
{
|
|
232
|
+
"step": len(self.alert_history),
|
|
233
|
+
"car_state": car_state,
|
|
234
|
+
"result": result,
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
def _empty_result(self) -> dict:
|
|
241
|
+
"""
|
|
242
|
+
Return empty result dictionary.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
Dict
|
|
247
|
+
Empty result with no alerts.
|
|
248
|
+
"""
|
|
249
|
+
return {
|
|
250
|
+
"has_warning": False,
|
|
251
|
+
"has_critical": False,
|
|
252
|
+
"max_deviation": 0.0,
|
|
253
|
+
"mean_deviation": 0.0,
|
|
254
|
+
"deviations": np.array([]),
|
|
255
|
+
"alert_points": [],
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
def get_recent_alerts(self, num_recent: int = 10) -> list[dict]:
|
|
259
|
+
"""
|
|
260
|
+
Get recent alert history.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
num_recent : int, default=10
|
|
265
|
+
Number of recent alerts to return.
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
List[Dict]
|
|
270
|
+
List of recent alert history entries.
|
|
271
|
+
"""
|
|
272
|
+
return self.alert_history[-num_recent:] if num_recent > 0 else []
|
|
273
|
+
|
|
274
|
+
def reset_history(self) -> None:
|
|
275
|
+
"""Clear alert history."""
|
|
276
|
+
self.alert_history = []
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Car model with state and reference frame handling."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from simple_autonomous_car.perception.perception import PerceptionPoints
|
|
10
|
+
from simple_autonomous_car.sensors.base_sensor import BaseSensor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CarState:
|
|
15
|
+
"""State of the car."""
|
|
16
|
+
|
|
17
|
+
x: float # x position in world frame
|
|
18
|
+
y: float # y position in world frame
|
|
19
|
+
heading: float # heading angle in radians (0 = east, counterclockwise)
|
|
20
|
+
velocity: float = 0.0 # forward velocity
|
|
21
|
+
steering_angle: float = 0.0 # steering angle in radians
|
|
22
|
+
|
|
23
|
+
def position(self) -> np.ndarray:
|
|
24
|
+
"""Get position as numpy array."""
|
|
25
|
+
return np.array([self.x, self.y])
|
|
26
|
+
|
|
27
|
+
def rotation_matrix(self) -> np.ndarray:
|
|
28
|
+
"""Get rotation matrix from world to car frame."""
|
|
29
|
+
cos_h = np.cos(self.heading)
|
|
30
|
+
sin_h = np.sin(self.heading)
|
|
31
|
+
return np.array([[cos_h, sin_h], [-sin_h, cos_h]])
|
|
32
|
+
|
|
33
|
+
def transform_to_car_frame(self, point: np.ndarray) -> np.ndarray:
|
|
34
|
+
"""Transform point from world frame to car frame."""
|
|
35
|
+
rotation = self.rotation_matrix()
|
|
36
|
+
translated = point - self.position()
|
|
37
|
+
result = rotation @ translated
|
|
38
|
+
return np.asarray(result, dtype=np.float64)
|
|
39
|
+
|
|
40
|
+
def transform_to_world_frame(self, point: np.ndarray) -> np.ndarray:
|
|
41
|
+
"""Transform point from car frame to world frame."""
|
|
42
|
+
rotation = self.rotation_matrix().T # Inverse rotation
|
|
43
|
+
rotated = rotation @ point
|
|
44
|
+
result = rotated + self.position()
|
|
45
|
+
return np.asarray(result, dtype=np.float64)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Car:
|
|
49
|
+
"""
|
|
50
|
+
Autonomous car model with sensor support.
|
|
51
|
+
|
|
52
|
+
This class represents an autonomous vehicle that can:
|
|
53
|
+
- Move using bicycle model dynamics
|
|
54
|
+
- Have multiple sensors attached
|
|
55
|
+
- Be controlled by controllers
|
|
56
|
+
- Follow plans from planners
|
|
57
|
+
|
|
58
|
+
Attributes
|
|
59
|
+
----------
|
|
60
|
+
state : CarState
|
|
61
|
+
Current state of the car.
|
|
62
|
+
wheelbase : float
|
|
63
|
+
Distance between front and rear axles (meters).
|
|
64
|
+
max_velocity : float
|
|
65
|
+
Maximum velocity (m/s).
|
|
66
|
+
max_steering_angle : float
|
|
67
|
+
Maximum steering angle (radians).
|
|
68
|
+
sensors : List[BaseSensor]
|
|
69
|
+
List of sensors attached to the car.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
initial_state: CarState | None = None,
|
|
75
|
+
wheelbase: float = 2.5,
|
|
76
|
+
max_velocity: float = 30.0,
|
|
77
|
+
max_steering_angle: float = np.pi / 6,
|
|
78
|
+
):
|
|
79
|
+
"""
|
|
80
|
+
Initialize car.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
initial_state : CarState, optional
|
|
85
|
+
Initial state of the car. If None, creates car at origin.
|
|
86
|
+
wheelbase : float, default=2.5
|
|
87
|
+
Distance between front and rear axles in meters.
|
|
88
|
+
max_velocity : float, default=30.0
|
|
89
|
+
Maximum velocity in m/s.
|
|
90
|
+
max_steering_angle : float, default=π/6
|
|
91
|
+
Maximum steering angle in radians (30 degrees).
|
|
92
|
+
"""
|
|
93
|
+
self.state = (
|
|
94
|
+
initial_state if initial_state is not None else CarState(x=0.0, y=0.0, heading=0.0)
|
|
95
|
+
)
|
|
96
|
+
self.wheelbase = wheelbase
|
|
97
|
+
self.max_velocity = max_velocity
|
|
98
|
+
self.max_steering_angle = max_steering_angle
|
|
99
|
+
self.sensors: list[BaseSensor] = []
|
|
100
|
+
|
|
101
|
+
def add_sensor(self, sensor: "BaseSensor") -> None:
|
|
102
|
+
"""
|
|
103
|
+
Add a sensor to the car.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
sensor : BaseSensor
|
|
108
|
+
Sensor instance to add. Can be LiDAR, Camera, Radar, etc.
|
|
109
|
+
"""
|
|
110
|
+
self.sensors.append(sensor)
|
|
111
|
+
|
|
112
|
+
def remove_sensor(self, sensor_name: str) -> bool:
|
|
113
|
+
"""
|
|
114
|
+
Remove a sensor by name.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
sensor_name : str
|
|
119
|
+
Name of the sensor to remove.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
bool
|
|
124
|
+
True if sensor was found and removed, False otherwise.
|
|
125
|
+
"""
|
|
126
|
+
for i, sensor in enumerate(self.sensors):
|
|
127
|
+
if sensor.name == sensor_name:
|
|
128
|
+
self.sensors.pop(i)
|
|
129
|
+
return True
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def get_sensor(self, sensor_name: str) -> Optional["BaseSensor"]:
|
|
133
|
+
"""
|
|
134
|
+
Get a sensor by name.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
sensor_name : str
|
|
139
|
+
Name of the sensor.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
BaseSensor, optional
|
|
144
|
+
Sensor instance if found, None otherwise.
|
|
145
|
+
"""
|
|
146
|
+
for sensor in self.sensors:
|
|
147
|
+
if sensor.name == sensor_name:
|
|
148
|
+
return sensor
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def sense_all(self, environment_data: dict | None = None) -> dict[str, "PerceptionPoints"]:
|
|
152
|
+
"""
|
|
153
|
+
Get perception data from all enabled sensors.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
environment_data : dict, optional
|
|
158
|
+
Environment data to pass to sensors.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
Dict[str, PerceptionPoints]
|
|
163
|
+
Dictionary mapping sensor names to their perception data.
|
|
164
|
+
"""
|
|
165
|
+
# Import here to avoid circular import
|
|
166
|
+
|
|
167
|
+
perception_data = {}
|
|
168
|
+
for sensor in self.sensors:
|
|
169
|
+
if sensor.is_enabled():
|
|
170
|
+
env_data = environment_data if environment_data is not None else {}
|
|
171
|
+
perception_data[sensor.name] = sensor.sense(self.state, env_data)
|
|
172
|
+
return perception_data
|
|
173
|
+
|
|
174
|
+
def update(self, dt: float, acceleration: float = 0.0, steering_rate: float = 0.0) -> None:
|
|
175
|
+
"""
|
|
176
|
+
Update car state using bicycle model.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
dt: Time step
|
|
180
|
+
acceleration: Acceleration (can be negative for braking)
|
|
181
|
+
steering_rate: Rate of change of steering angle
|
|
182
|
+
"""
|
|
183
|
+
# Update steering angle
|
|
184
|
+
self.state.steering_angle += steering_rate * dt
|
|
185
|
+
self.state.steering_angle = np.clip(
|
|
186
|
+
self.state.steering_angle, -self.max_steering_angle, self.max_steering_angle
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Update velocity
|
|
190
|
+
self.state.velocity += acceleration * dt
|
|
191
|
+
self.state.velocity = np.clip(self.state.velocity, 0.0, self.max_velocity)
|
|
192
|
+
|
|
193
|
+
# Bicycle model dynamics
|
|
194
|
+
if abs(self.state.steering_angle) > 1e-6:
|
|
195
|
+
turning_radius = self.wheelbase / np.tan(self.state.steering_angle)
|
|
196
|
+
angular_velocity = self.state.velocity / turning_radius
|
|
197
|
+
else:
|
|
198
|
+
angular_velocity = 0.0
|
|
199
|
+
|
|
200
|
+
# Update position and heading
|
|
201
|
+
self.state.heading += angular_velocity * dt
|
|
202
|
+
self.state.x += self.state.velocity * np.cos(self.state.heading) * dt
|
|
203
|
+
self.state.y += self.state.velocity * np.sin(self.state.heading) * dt
|
|
204
|
+
|
|
205
|
+
def get_corners(self, length: float = 4.0, width: float = 1.8) -> np.ndarray:
|
|
206
|
+
"""
|
|
207
|
+
Get car corner points in world frame.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
length: Car length
|
|
211
|
+
width: Car width
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Array of shape (4, 2) with corner coordinates
|
|
215
|
+
"""
|
|
216
|
+
half_length = length / 2.0
|
|
217
|
+
half_width = width / 2.0
|
|
218
|
+
|
|
219
|
+
# Corners in car frame (rear-left, rear-right, front-right, front-left)
|
|
220
|
+
corners_car = np.array(
|
|
221
|
+
[
|
|
222
|
+
[-half_length, -half_width],
|
|
223
|
+
[-half_length, half_width],
|
|
224
|
+
[half_length, half_width],
|
|
225
|
+
[half_length, -half_width],
|
|
226
|
+
]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Transform to world frame
|
|
230
|
+
corners_world = np.array(
|
|
231
|
+
[self.state.transform_to_world_frame(corner) for corner in corners_car]
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return corners_world
|