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,112 @@
1
+ """Centralized constants for the Simple Autonomous Car SDK.
2
+
3
+ This module contains all magic numbers and default values used throughout the codebase.
4
+ Using constants makes the code more maintainable and allows easy tuning of parameters.
5
+ """
6
+
7
+ # ============================================================================
8
+ # Car Defaults
9
+ # ============================================================================
10
+
11
+ DEFAULT_WHEELBASE = 2.5 # meters
12
+ DEFAULT_INITIAL_VELOCITY = 10.0 # m/s
13
+ DEFAULT_MAX_VELOCITY = 25.0 # m/s
14
+ DEFAULT_MAX_STEERING_ANGLE = 0.7853981633974483 # π/4 radians (45 degrees)
15
+
16
+ # ============================================================================
17
+ # Controller Defaults
18
+ # ============================================================================
19
+
20
+ DEFAULT_LOOKAHEAD_DISTANCE = 10.0 # meters (controller default)
21
+ DEFAULT_LOOKAHEAD_GAIN = 2.0 # controller default (was 0.5, but controller uses 2.0)
22
+ DEFAULT_MAX_STEERING_RATE = 1.0 # rad/s
23
+ DEFAULT_TARGET_VELOCITY = 10.0 # m/s
24
+ DEFAULT_VELOCITY_GAIN = 0.5
25
+ DEFAULT_DT = 0.1 # seconds
26
+
27
+ # Controller Visualization
28
+ DEFAULT_ARC_LOOKAHEAD_DISTANCE = 20.0 # meters
29
+ DEFAULT_MIN_TURNING_RADIUS = 0.0 # meters
30
+ DEFAULT_MAX_TURNING_RADIUS = 1000.0 # meters
31
+ DEFAULT_ARC_LINEWIDTH = 3.5
32
+ DEFAULT_ARC_ALPHA = 0.95
33
+ DEFAULT_LOOKAHEAD_LINEWIDTH = 2.5
34
+
35
+ # Controller Thresholds
36
+ MIN_LOOKAHEAD_DISTANCE = 0.1 # meters
37
+ MIN_STEERING_ANGLE_THRESHOLD = 0.0 # radians
38
+ SMALL_STEERING_ANGLE_THRESHOLD = 1e-3 # radians
39
+ ADAPTIVE_GAIN_DENOMINATOR = 10.0 # used in adaptive gain calculation
40
+ DEFAULT_GOAL_TOLERANCE = 2.0 # meters - distance to goal to consider reached
41
+
42
+ # ============================================================================
43
+ # Planner Defaults
44
+ # ============================================================================
45
+
46
+ DEFAULT_PLANNER_LOOKAHEAD_DISTANCE = 50.0 # meters
47
+ DEFAULT_WAYPOINT_SPACING = 2.0 # meters
48
+ DEFAULT_PLAN_LINEWIDTH = 2.5
49
+
50
+ # Planner Thresholds
51
+ MIN_SEGMENT_LENGTH = 1e-6 # meters
52
+
53
+ # ============================================================================
54
+ # Costmap Defaults
55
+ # ============================================================================
56
+
57
+ DEFAULT_COSTMAP_WIDTH = 50.0 # meters
58
+ DEFAULT_COSTMAP_HEIGHT = 50.0 # meters
59
+ DEFAULT_COSTMAP_RESOLUTION = 0.5 # meters per cell
60
+ DEFAULT_INFLATION_RADIUS = 1.0 # meters
61
+
62
+ # Costmap Values
63
+ COST_FREE = 0.0
64
+ COST_OCCUPIED = 1.0
65
+ COST_THRESHOLD = 0.5 # threshold for occupied vs inflated
66
+
67
+ # Costmap Visualization
68
+ DEFAULT_COSTMAP_ALPHA = 0.6
69
+ DEFAULT_COSTMAP_ZORDER = 2 # Behind plan/controller but above track
70
+
71
+ # ============================================================================
72
+ # Sensor Defaults
73
+ # ============================================================================
74
+
75
+ DEFAULT_LIDAR_MAX_RANGE = 40.0 # meters
76
+ DEFAULT_LIDAR_ANGULAR_RESOLUTION = 0.1 # radians
77
+ DEFAULT_POINT_NOISE_STD = 0.1 # meters
78
+
79
+ # ============================================================================
80
+ # Track Defaults
81
+ # ============================================================================
82
+
83
+ DEFAULT_TRACK_LENGTH = 100.0 # meters
84
+ DEFAULT_TRACK_WIDTH = 50.0 # meters
85
+ DEFAULT_TRACK_WIDTH_INNER = 5.0 # meters
86
+ DEFAULT_TRACK_NUM_POINTS = 200
87
+
88
+ # ============================================================================
89
+ # Visualization Defaults
90
+ # ============================================================================
91
+
92
+ # Arrow visualization
93
+ DEFAULT_ARROW_HEAD_WIDTH = 1.0
94
+ DEFAULT_ARROW_HEAD_LENGTH = 1.0
95
+
96
+ # Perception visualization
97
+ DEFAULT_PERCEPTION_LINEWIDTH = 0.5
98
+
99
+ # ============================================================================
100
+ # Numerical Thresholds
101
+ # ============================================================================
102
+
103
+ EPSILON = 1e-6 # general small number threshold
104
+ EPSILON_SMALL = 1e-3 # slightly larger threshold
105
+ LARGE_NUMBER = 1e6 # used for "infinite" values
106
+
107
+ # ============================================================================
108
+ # Simulation Defaults
109
+ # ============================================================================
110
+
111
+ DEFAULT_SIMULATION_DT = 0.1 # seconds
112
+ DEFAULT_PLOT_PAUSE = 0.01 # seconds
@@ -0,0 +1,7 @@
1
+ """Control system for autonomous vehicles."""
2
+
3
+ from simple_autonomous_car.control.base_controller import BaseController
4
+ from simple_autonomous_car.control.pid_controller import PIDController
5
+ from simple_autonomous_car.control.pure_pursuit_controller import PurePursuitController
6
+
7
+ __all__ = ["BaseController", "PurePursuitController", "PIDController"]
@@ -0,0 +1,152 @@
1
+ """Base controller class for autonomous vehicle control."""
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
+ from simple_autonomous_car.costmap.base_costmap import BaseCostmap
11
+
12
+ from simple_autonomous_car.perception.perception import PerceptionPoints
13
+
14
+
15
+ class BaseController(ABC):
16
+ """
17
+ Base class for all controllers.
18
+
19
+ Controllers compute control commands (acceleration, steering_rate)
20
+ based on the current car state, perception data, and optionally a plan.
21
+
22
+ Attributes
23
+ ----------
24
+ name : str
25
+ Controller name/identifier.
26
+ enabled : bool
27
+ Whether the controller is enabled.
28
+ """
29
+
30
+ def __init__(self, name: str = "controller", enabled: bool = True):
31
+ """
32
+ Initialize base controller.
33
+
34
+ Parameters
35
+ ----------
36
+ name : str, default="controller"
37
+ Controller name/identifier.
38
+ enabled : bool, default=True
39
+ Whether the controller is enabled.
40
+ """
41
+ self.name = name
42
+ self.enabled = enabled
43
+
44
+ @abstractmethod
45
+ def compute_control(
46
+ self,
47
+ car_state: "CarState",
48
+ perception_data: dict[str, PerceptionPoints] | None = None,
49
+ costmap: Optional["BaseCostmap"] = None,
50
+ plan: np.ndarray | None = None,
51
+ goal: np.ndarray | None = None,
52
+ goal_tolerance: float | None = None,
53
+ dt: float = 0.1,
54
+ ) -> dict[str, float]:
55
+ """
56
+ Compute control commands.
57
+
58
+ Parameters
59
+ ----------
60
+ car_state : CarState
61
+ Current car state.
62
+ perception_data : dict, optional
63
+ Dictionary of perception data from sensors.
64
+ Keys are sensor names, values are PerceptionPoints.
65
+ costmap : BaseCostmap, optional
66
+ Costmap for obstacle avoidance.
67
+ plan : np.ndarray, optional
68
+ Planned path as array of shape (N, 2) with [x, y] waypoints.
69
+ goal : np.ndarray, optional
70
+ Goal position [x, y] for goal-based velocity adaptation.
71
+ dt : float, default=0.1
72
+ Time step in seconds.
73
+
74
+ Returns
75
+ -------
76
+ Dict[str, float]
77
+ Control commands with keys:
78
+ - "acceleration": Acceleration in m/s²
79
+ - "steering_rate": Steering rate in rad/s
80
+ """
81
+ pass
82
+
83
+ def is_enabled(self) -> bool:
84
+ """Check if controller is enabled."""
85
+ return self.enabled
86
+
87
+ def enable(self) -> None:
88
+ """Enable the controller."""
89
+ self.enabled = True
90
+
91
+ def disable(self) -> None:
92
+ """Disable the controller."""
93
+ self.enabled = False
94
+
95
+ def get_visualization_data(
96
+ self, car_state: "CarState", plan: np.ndarray | None = None, **kwargs: Any
97
+ ) -> dict[str, Any]:
98
+ """
99
+ Get visualization data for this controller.
100
+
101
+ This method should be overridden by subclasses to provide
102
+ controller-specific visualization data (e.g., lookahead points,
103
+ steering arcs, control commands).
104
+
105
+ Parameters
106
+ ----------
107
+ car_state : CarState
108
+ Current car state.
109
+ plan : np.ndarray, optional
110
+ Planned path.
111
+ **kwargs
112
+ Additional arguments.
113
+
114
+ Returns
115
+ -------
116
+ Dict
117
+ Dictionary containing visualization data. Default implementation
118
+ returns empty dict. Subclasses should override to provide specific data.
119
+ """
120
+ return {}
121
+
122
+ def visualize(
123
+ self,
124
+ ax: Any,
125
+ car_state: "CarState",
126
+ plan: np.ndarray | None = None,
127
+ frame: str = "global",
128
+ **kwargs: Any,
129
+ ) -> None:
130
+ """
131
+ Visualize controller state on the given axes.
132
+
133
+ This method should be overridden by subclasses to plot
134
+ controller-specific visualizations (e.g., lookahead points,
135
+ steering arcs, control commands).
136
+
137
+ Parameters
138
+ ----------
139
+ ax : matplotlib.axes.Axes
140
+ Axes to plot on.
141
+ car_state : CarState
142
+ Current car state.
143
+ plan : np.ndarray, optional
144
+ Planned path.
145
+ frame : str, default="global"
146
+ Frame to plot in: "global" or "ego".
147
+ **kwargs
148
+ Additional visualization arguments (colors, linewidths, etc.).
149
+ """
150
+ # Default implementation does nothing
151
+ # Subclasses should override to provide visualization
152
+ pass
@@ -0,0 +1,282 @@
1
+ """Controller-specific visualization utilities."""
2
+
3
+ import matplotlib.pyplot as plt
4
+ import numpy as np
5
+
6
+ from simple_autonomous_car.car.car import CarState
7
+ from simple_autonomous_car.control.pure_pursuit_controller import PurePursuitController
8
+
9
+
10
+ def plot_pure_pursuit_state(
11
+ controller: PurePursuitController,
12
+ car_state: CarState,
13
+ plan: np.ndarray,
14
+ ax: plt.Axes | None = None,
15
+ show_curvature: bool = True,
16
+ show_steering_circle: bool = True,
17
+ show_lookahead: bool = True,
18
+ ) -> plt.Axes:
19
+ """
20
+ Plot Pure Pursuit controller state with detailed visualization.
21
+
22
+ Shows:
23
+ - Planned path
24
+ - Car position and heading
25
+ - Lookahead point and line
26
+ - Steering circle (turning radius)
27
+ - Path curvature
28
+ - Control commands
29
+
30
+ Parameters
31
+ ----------
32
+ controller : PurePursuitController
33
+ Pure Pursuit controller instance.
34
+ car_state : CarState
35
+ Current car state.
36
+ plan : np.ndarray
37
+ Planned path as array of shape (N, 2).
38
+ ax : plt.Axes, optional
39
+ Axes to plot on. If None, creates new figure.
40
+ show_curvature : bool, default=True
41
+ Whether to show path curvature.
42
+ show_steering_circle : bool, default=True
43
+ Whether to show steering circle (turning radius).
44
+ show_lookahead : bool, default=True
45
+ Whether to show lookahead point and line.
46
+
47
+ Returns
48
+ -------
49
+ plt.Axes
50
+ Axes object with controller state plotted.
51
+
52
+ Examples
53
+ --------
54
+ >>> from simple_autonomous_car.control.controller_viz import plot_pure_pursuit_state
55
+ >>> ax = plot_pure_pursuit_state(controller, car.state, plan)
56
+ >>> plt.show()
57
+ """
58
+ if ax is None:
59
+ fig, ax = plt.subplots(figsize=(12, 10))
60
+
61
+ car_pos = car_state.position()
62
+
63
+ # Compute control
64
+ control = controller.compute_control(car_state, plan=plan)
65
+
66
+ # Plot plan
67
+ if len(plan) > 0:
68
+ ax.plot(plan[:, 0], plan[:, 1], "g-o", linewidth=2, markersize=4, alpha=0.6, label="Plan")
69
+
70
+ # Plot car
71
+ ax.plot(car_pos[0], car_pos[1], "bo", markersize=12, label="Car")
72
+ # Car heading arrow
73
+ heading_length = 3.0
74
+ dx = heading_length * np.cos(car_state.heading)
75
+ dy = heading_length * np.sin(car_state.heading)
76
+ ax.arrow(
77
+ car_pos[0],
78
+ car_pos[1],
79
+ dx,
80
+ dy,
81
+ head_width=1.0,
82
+ head_length=0.8,
83
+ fc="blue",
84
+ ec="blue",
85
+ label="Heading",
86
+ )
87
+
88
+ # Find lookahead point (simplified version of controller logic)
89
+ if show_lookahead and len(plan) > 0:
90
+ distances_to_path = np.linalg.norm(plan - car_pos, axis=1)
91
+ closest_idx = np.argmin(distances_to_path)
92
+
93
+ # Calculate adaptive lookahead
94
+ adaptive_lookahead = (
95
+ controller.lookahead_distance + controller.lookahead_gain * car_state.velocity
96
+ )
97
+
98
+ # Find point at lookahead distance ahead
99
+ path_distances = np.zeros(len(plan))
100
+ for i in range(closest_idx, len(plan) - 1):
101
+ path_distances[i + 1] = path_distances[i] + np.linalg.norm(plan[i + 1] - plan[i])
102
+ for i in range(closest_idx - 1, -1, -1):
103
+ path_distances[i] = path_distances[i + 1] - np.linalg.norm(plan[i + 1] - plan[i])
104
+
105
+ target_distances = path_distances - path_distances[closest_idx]
106
+ ahead_mask = target_distances >= 0
107
+ ahead_indices = np.where(ahead_mask)[0]
108
+
109
+ if len(ahead_indices) > 0:
110
+ ahead_distances = target_distances[ahead_indices]
111
+ lookahead_idx = np.argmin(np.abs(ahead_distances - adaptive_lookahead))
112
+ target_idx = ahead_indices[lookahead_idx]
113
+ if target_idx < len(plan):
114
+ target_point = plan[target_idx]
115
+
116
+ # Plot lookahead point
117
+ ax.plot(
118
+ target_point[0],
119
+ target_point[1],
120
+ "ro",
121
+ markersize=10,
122
+ label="Lookahead Point",
123
+ )
124
+
125
+ # Plot lookahead line
126
+ ax.plot(
127
+ [car_pos[0], target_point[0]],
128
+ [car_pos[1], target_point[1]],
129
+ "r--",
130
+ linewidth=2,
131
+ alpha=0.7,
132
+ label=f"Lookahead ({adaptive_lookahead:.1f}m)",
133
+ )
134
+
135
+ # Show steering circle (turning radius)
136
+ if show_steering_circle and abs(car_state.steering_angle) > 1e-6:
137
+ wheelbase = 2.5 # Standard wheelbase
138
+ if abs(car_state.steering_angle) > 1e-6:
139
+ turning_radius = wheelbase / np.tan(car_state.steering_angle)
140
+ else:
141
+ turning_radius = 1e6 # Straight line
142
+
143
+ # Calculate circle center (perpendicular to car heading)
144
+ perp_angle = car_state.heading + np.pi / 2
145
+ if turning_radius < 0:
146
+ perp_angle += np.pi
147
+ turning_radius = abs(turning_radius)
148
+
149
+ center_x = car_pos[0] + turning_radius * np.cos(perp_angle)
150
+ center_y = car_pos[1] + turning_radius * np.sin(perp_angle)
151
+
152
+ # Draw turning circle
153
+ circle = plt.Circle(
154
+ (center_x, center_y),
155
+ abs(turning_radius),
156
+ fill=False,
157
+ color="orange",
158
+ linestyle="--",
159
+ linewidth=2,
160
+ alpha=0.5,
161
+ label=f"Turning Radius ({abs(turning_radius):.1f}m)",
162
+ )
163
+ ax.add_patch(circle)
164
+
165
+ # Show curvature along path
166
+ if show_curvature and len(plan) > 2:
167
+ curvatures = []
168
+ for i in range(len(plan) - 2):
169
+ p1, p2, p3 = plan[i], plan[i + 1], plan[i + 2]
170
+ v1 = p2 - p1
171
+ v2 = p3 - p2
172
+ cross = v1[0] * v2[1] - v1[1] * v2[0]
173
+ norm1 = np.linalg.norm(v1)
174
+ norm2 = np.linalg.norm(v2)
175
+ if norm1 > 1e-6 and norm2 > 1e-6:
176
+ curvature = abs(cross) / (norm1 * norm2 * norm1)
177
+ else:
178
+ curvature = 0.0
179
+ curvatures.append(curvature)
180
+
181
+ if len(curvatures) > 0:
182
+ max_curvature = max(curvatures) if curvatures else 0.0
183
+ # Plot curvature as color-coded path segments
184
+ for i in range(len(plan) - 2):
185
+ if i < len(curvatures):
186
+ color_intensity = min(curvatures[i] / max(max_curvature, 1e-6), 1.0)
187
+ ax.plot(
188
+ [plan[i][0], plan[i + 1][0]],
189
+ [plan[i][1], plan[i + 1][1]],
190
+ "-",
191
+ color=plt.cm.RdYlGn(1.0 - color_intensity), # type: ignore[attr-defined]
192
+ linewidth=4,
193
+ alpha=0.6,
194
+ )
195
+
196
+ # Add text with control commands
197
+ info_text = (
198
+ f"Steering: {np.degrees(car_state.steering_angle):.1f}°\n"
199
+ f"Steering Rate: {control['steering_rate']:.2f} rad/s\n"
200
+ f"Acceleration: {control['acceleration']:.2f} m/s²\n"
201
+ f"Velocity: {car_state.velocity:.2f} m/s"
202
+ )
203
+ ax.text(
204
+ 0.02,
205
+ 0.98,
206
+ info_text,
207
+ transform=ax.transAxes,
208
+ fontsize=10,
209
+ verticalalignment="top",
210
+ bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
211
+ )
212
+
213
+ ax.set_aspect("equal")
214
+ ax.grid(True, alpha=0.3)
215
+ ax.set_xlabel("X (m)")
216
+ ax.set_ylabel("Y (m)")
217
+ ax.legend(loc="upper right")
218
+
219
+ return ax
220
+
221
+
222
+ def plot_control_history(
223
+ control_history: list[dict],
224
+ ax: plt.Axes | None = None,
225
+ show_steering: bool = True,
226
+ show_acceleration: bool = True,
227
+ show_velocity: bool = True,
228
+ ) -> plt.Axes:
229
+ """
230
+ Plot control command history over time.
231
+
232
+ Parameters
233
+ ----------
234
+ control_history : List[Dict]
235
+ List of control dictionaries with keys: 'steering_rate', 'acceleration', 'velocity', 'time'.
236
+ ax : plt.Axes, optional
237
+ Axes to plot on. If None, creates new figure.
238
+ show_steering : bool, default=True
239
+ Whether to show steering rate.
240
+ show_acceleration : bool, default=True
241
+ Whether to show acceleration.
242
+ show_velocity : bool, default=True
243
+ Whether to show velocity.
244
+
245
+ Returns
246
+ -------
247
+ plt.Axes
248
+ Axes object with control history plotted.
249
+
250
+ Examples
251
+ --------
252
+ >>> history = [{'steering_rate': 0.1, 'acceleration': 0.5, 'velocity': 10.0, 'time': i*0.1}
253
+ ... for i in range(100)]
254
+ >>> ax = plot_control_history(history)
255
+ >>> plt.show()
256
+ """
257
+ if ax is None:
258
+ fig, ax = plt.subplots(figsize=(12, 6))
259
+
260
+ if len(control_history) == 0:
261
+ return ax
262
+
263
+ times = [h.get("time", i) for i, h in enumerate(control_history)]
264
+
265
+ if show_steering:
266
+ steering_rates = [h.get("steering_rate", 0.0) for h in control_history]
267
+ ax.plot(times, steering_rates, "b-", linewidth=2, label="Steering Rate (rad/s)")
268
+
269
+ if show_acceleration:
270
+ accelerations = [h.get("acceleration", 0.0) for h in control_history]
271
+ ax.plot(times, accelerations, "r-", linewidth=2, label="Acceleration (m/s²)")
272
+
273
+ if show_velocity:
274
+ velocities = [h.get("velocity", 0.0) for h in control_history]
275
+ ax.plot(times, velocities, "g-", linewidth=2, label="Velocity (m/s)")
276
+
277
+ ax.set_xlabel("Time (s)")
278
+ ax.set_ylabel("Value")
279
+ ax.grid(True, alpha=0.3)
280
+ ax.legend()
281
+
282
+ return ax