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