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,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
|