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,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,5 @@
1
+ """Alert system for localization and perception errors."""
2
+
3
+ from simple_autonomous_car.alerts.track_bounds_alert import TrackBoundsAlert
4
+
5
+ __all__ = ["TrackBoundsAlert"]
@@ -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,5 @@
1
+ """Car model and dynamics."""
2
+
3
+ from simple_autonomous_car.car.car import Car, CarState
4
+
5
+ __all__ = ["Car", "CarState"]
@@ -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