python-motion-planning 2.0.dev1__py3-none-any.whl → 2.0.1__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.
- python_motion_planning/__init__.py +1 -1
- python_motion_planning/common/env/map/base_map.py +2 -8
- python_motion_planning/common/env/map/grid.py +456 -198
- python_motion_planning/common/utils/__init__.py +2 -1
- python_motion_planning/common/utils/child_tree.py +22 -0
- python_motion_planning/common/utils/geometry.py +18 -29
- python_motion_planning/common/visualizer/__init__.py +3 -1
- python_motion_planning/common/visualizer/base_visualizer.py +165 -0
- python_motion_planning/common/visualizer/{visualizer.py → visualizer_2d.py} +97 -220
- python_motion_planning/common/visualizer/visualizer_3d.py +242 -0
- python_motion_planning/controller/base_controller.py +37 -4
- python_motion_planning/controller/path_tracker/__init__.py +2 -1
- python_motion_planning/controller/path_tracker/apf.py +22 -23
- python_motion_planning/controller/path_tracker/dwa.py +14 -17
- python_motion_planning/controller/path_tracker/path_tracker.py +4 -1
- python_motion_planning/controller/path_tracker/pid.py +7 -1
- python_motion_planning/controller/path_tracker/pure_pursuit.py +7 -1
- python_motion_planning/controller/path_tracker/rpp.py +111 -0
- python_motion_planning/path_planner/__init__.py +2 -1
- python_motion_planning/path_planner/base_path_planner.py +45 -11
- python_motion_planning/path_planner/graph_search/__init__.py +4 -1
- python_motion_planning/path_planner/graph_search/a_star.py +12 -14
- python_motion_planning/path_planner/graph_search/dijkstra.py +15 -15
- python_motion_planning/path_planner/graph_search/gbfs.py +100 -0
- python_motion_planning/path_planner/graph_search/jps.py +199 -0
- python_motion_planning/path_planner/graph_search/lazy_theta_star.py +113 -0
- python_motion_planning/path_planner/graph_search/theta_star.py +17 -19
- python_motion_planning/path_planner/hybrid_search/__init__.py +1 -0
- python_motion_planning/path_planner/hybrid_search/voronoi_planner.py +204 -0
- python_motion_planning/path_planner/sample_search/__init__.py +2 -1
- python_motion_planning/path_planner/sample_search/rrt.py +73 -31
- python_motion_planning/path_planner/sample_search/rrt_connect.py +237 -0
- python_motion_planning/path_planner/sample_search/rrt_star.py +220 -150
- python_motion_planning/traj_optimizer/__init__.py +2 -0
- python_motion_planning/traj_optimizer/base_curve_generator.py +53 -0
- python_motion_planning/traj_optimizer/curve_generator/__init__.py +2 -0
- python_motion_planning/traj_optimizer/curve_generator/point_based/__init__.py +2 -0
- python_motion_planning/traj_optimizer/curve_generator/point_based/bspline.py +256 -0
- python_motion_planning/traj_optimizer/curve_generator/point_based/cubic_spline.py +115 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/__init__.py +4 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/bezier.py +121 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/dubins.py +355 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/polynomial.py +197 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py +606 -0
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/METADATA +71 -29
- python_motion_planning-2.0.1.dist-info/RECORD +64 -0
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/WHEEL +1 -1
- python_motion_planning/common/env/robot/tmp.py +0 -404
- python_motion_planning/curve_generator/__init__.py +0 -9
- python_motion_planning/curve_generator/bezier_curve.py +0 -131
- python_motion_planning/curve_generator/bspline_curve.py +0 -271
- python_motion_planning/curve_generator/cubic_spline.py +0 -128
- python_motion_planning/curve_generator/curve.py +0 -64
- python_motion_planning/curve_generator/dubins_curve.py +0 -348
- python_motion_planning/curve_generator/fem_pos_smooth.py +0 -114
- python_motion_planning/curve_generator/polynomial_curve.py +0 -226
- python_motion_planning/curve_generator/reeds_shepp.py +0 -736
- python_motion_planning-2.0.dev1.dist-info/RECORD +0 -53
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: base_controller.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update: 2025.10.
|
|
4
|
+
@update: 2025.10.16
|
|
5
5
|
"""
|
|
6
6
|
from typing import List, Tuple
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
from gymnasium import spaces
|
|
10
10
|
|
|
11
|
+
from python_motion_planning.common.env import Grid, TYPES, BaseRobot
|
|
11
12
|
from python_motion_planning.common.utils.geometry import Geometry
|
|
12
13
|
|
|
13
14
|
class BaseController:
|
|
@@ -19,17 +20,20 @@ class BaseController:
|
|
|
19
20
|
observation_space: observation space ([pos, orient, lin_vel, ang_vel])
|
|
20
21
|
action_space: action space ([lin_acc, ang_acc])
|
|
21
22
|
dt: time step for control
|
|
22
|
-
path: pose path to follow
|
|
23
|
+
path: pose path to follow (world frame)
|
|
23
24
|
max_lin_speed: maximum linear speed of the robot
|
|
24
25
|
max_ang_speed: maximum angular speed of the robot
|
|
25
26
|
goal_dist_tol: goal distance tolerance
|
|
26
27
|
goal_orient_tol: goal orient tolerance
|
|
28
|
+
robot_model: robot model for kinematic parameters
|
|
29
|
+
obstacle_grid: occupancy grid map for collision checking
|
|
27
30
|
eps: epsilon (numerical precision)
|
|
28
31
|
"""
|
|
29
32
|
def __init__(self, observation_space: spaces.Space, action_space: spaces.Box,
|
|
30
33
|
dt: float, path: List[Tuple[float, ...]] = [],
|
|
31
34
|
max_lin_speed: float = np.inf, max_ang_speed: float = np.inf,
|
|
32
35
|
goal_dist_tol: float = 0.5, goal_orient_tol: float = np.deg2rad(5),
|
|
36
|
+
robot_model: BaseRobot = None, obstacle_grid: Grid = None,
|
|
33
37
|
eps: float = 1e-8):
|
|
34
38
|
self.observation_space = observation_space
|
|
35
39
|
self.action_space = action_space
|
|
@@ -57,9 +61,20 @@ class BaseController:
|
|
|
57
61
|
self.goal = self.path[-1]
|
|
58
62
|
else:
|
|
59
63
|
self.goal = None
|
|
60
|
-
self.path = False
|
|
64
|
+
self.path = False
|
|
61
65
|
|
|
62
66
|
self.pred_traj = np.array([])
|
|
67
|
+
|
|
68
|
+
if robot_model and robot_model.dim != self.dim:
|
|
69
|
+
raise ValueError("Dimension of robot model and controller must be the same")
|
|
70
|
+
self.robot_model = robot_model
|
|
71
|
+
|
|
72
|
+
if obstacle_grid and obstacle_grid.dim != self.dim:
|
|
73
|
+
raise ValueError("Dimension of obstacle grid and controller must be the same")
|
|
74
|
+
self.obstacle_grid = obstacle_grid
|
|
75
|
+
|
|
76
|
+
def __str__(self) -> str:
|
|
77
|
+
return "Base Controller"
|
|
63
78
|
|
|
64
79
|
def reset(self):
|
|
65
80
|
"""
|
|
@@ -155,4 +170,22 @@ class BaseController:
|
|
|
155
170
|
if np.abs(Geometry.regularize_orient(orient - goal_orient)) < self.goal_orient_tol:
|
|
156
171
|
desired_vel[self.dim:] = 0.0
|
|
157
172
|
|
|
158
|
-
return desired_vel
|
|
173
|
+
return desired_vel
|
|
174
|
+
|
|
175
|
+
def _get_dist_to_nearest_obstacle(self, pos: np.ndarray) -> float:
|
|
176
|
+
"""
|
|
177
|
+
Get distance to nearest obstacle.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
pos: position (length = self.dim)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
dist: distance to nearest obstacle
|
|
184
|
+
"""
|
|
185
|
+
grid_pt = self.obstacle_grid.world_to_map(tuple(pos))
|
|
186
|
+
if not self.obstacle_grid.within_bounds(grid_pt) or self.obstacle_grid.type_map[grid_pt] == TYPES.OBSTACLE:
|
|
187
|
+
dist = 0.0
|
|
188
|
+
else:
|
|
189
|
+
dist = self.obstacle_grid.esdf[grid_pt] * self.obstacle_grid.resolution # using ESDF to compute distance to nearest obstacle
|
|
190
|
+
|
|
191
|
+
return dist
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: apf.py
|
|
3
3
|
@author: Wu Maojia, Yang Haodong
|
|
4
|
-
@update: 2025.10.
|
|
4
|
+
@update: 2025.10.16
|
|
5
5
|
"""
|
|
6
6
|
from typing import List, Tuple
|
|
7
7
|
import math
|
|
@@ -18,39 +18,41 @@ from .path_tracker import PathTracker
|
|
|
18
18
|
|
|
19
19
|
class APF(PathTracker):
|
|
20
20
|
"""
|
|
21
|
-
Artificial Potential Field (APF) path-tracking controller.
|
|
21
|
+
Artificial Potential Field (APF) path-tracking controller. `robot_model` and `obstacle_grid` must be provided.
|
|
22
|
+
`
|
|
22
23
|
|
|
23
24
|
Args:
|
|
24
25
|
*args: see the parent class.
|
|
25
|
-
robot_model: robot model for kinematic parameters
|
|
26
|
-
obstacle_grid: occupancy grid map for collision checking
|
|
27
26
|
attr_weight: weight factor for attractive potential
|
|
28
27
|
rep_weight: weight factor for repulsive potential
|
|
29
28
|
rep_range: influence range for repulsive potential
|
|
30
29
|
**kwargs: see the parent class.
|
|
30
|
+
|
|
31
|
+
References:
|
|
32
|
+
[1] Real-time obstacle avoidance for manipulators and mobile robots
|
|
31
33
|
"""
|
|
32
34
|
def __init__(self,
|
|
33
35
|
*args,
|
|
34
|
-
robot_model: BaseRobot,
|
|
35
|
-
obstacle_grid: Grid = None,
|
|
36
36
|
attr_weight: float = 1.0,
|
|
37
37
|
rep_weight: float = 1.0,
|
|
38
38
|
rep_range: float = None,
|
|
39
39
|
**kwargs):
|
|
40
40
|
super().__init__(*args, **kwargs)
|
|
41
|
-
if robot_model.dim != self.dim:
|
|
42
|
-
raise ValueError("Dimension of robot model and controller must be the same")
|
|
43
|
-
self.robot_model = robot_model
|
|
44
41
|
|
|
45
|
-
if
|
|
46
|
-
raise ValueError("
|
|
47
|
-
|
|
42
|
+
if self.robot_model is None:
|
|
43
|
+
raise ValueError("Robot model is required.")
|
|
44
|
+
|
|
45
|
+
if self.obstacle_grid is None:
|
|
46
|
+
raise ValueError("Obstacle grid is required.")
|
|
48
47
|
|
|
49
48
|
# APF parameters
|
|
50
49
|
self.attr_weight = attr_weight # Attractive potential weight
|
|
51
50
|
self.rep_weight = rep_weight # Repulsive potential weight
|
|
52
51
|
self.rep_range = rep_range if rep_range is not None else self.robot_model.radius * 2.0 # Repulsive influence range
|
|
53
52
|
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
return "APF"
|
|
55
|
+
|
|
54
56
|
def get_action(self, obs: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
55
57
|
"""
|
|
56
58
|
Get action from observation using Artificial Potential Field method.
|
|
@@ -125,22 +127,19 @@ class APF(PathTracker):
|
|
|
125
127
|
if self.obstacle_grid is None:
|
|
126
128
|
return np.zeros(self.dim)
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
grid_pt = self.obstacle_grid.world_to_map(tuple(current_pos[:2]))
|
|
130
|
-
grid_x, grid_y = grid_pt
|
|
131
|
-
|
|
132
|
-
# Check if position is out of bounds or in an obstacle
|
|
133
|
-
if not self.obstacle_grid.within_bounds(grid_pt) or self.obstacle_grid.type_map[grid_pt] == TYPES.OBSTACLE:
|
|
134
|
-
# Large repulsive force if in collision
|
|
135
|
-
return np.full(self.dim, self.rep_weight * self.rep_range)
|
|
136
|
-
|
|
137
|
-
# Get distance to nearest obstacle from ESDF (in world units)
|
|
138
|
-
dist_to_obstacle = self.obstacle_grid.esdf[grid_pt] * self.obstacle_grid.resolution
|
|
130
|
+
dist_to_obstacle = self._get_dist_to_nearest_obstacle(current_pos[:self.dim])
|
|
139
131
|
|
|
140
132
|
# No repulsive force if outside influence range
|
|
141
133
|
if dist_to_obstacle >= self.rep_range:
|
|
142
134
|
return np.zeros(self.dim)
|
|
143
135
|
|
|
136
|
+
# avoid division by zero
|
|
137
|
+
dist_to_obstacle = max(dist_to_obstacle, self.eps)
|
|
138
|
+
|
|
139
|
+
# Convert world position to grid coordinates
|
|
140
|
+
grid_pt = self.obstacle_grid.world_to_map(tuple(current_pos[:2]))
|
|
141
|
+
grid_x, grid_y = grid_pt
|
|
142
|
+
|
|
144
143
|
# Calculate gradient of repulsive potential using numpy's gradient function
|
|
145
144
|
# Extract a small window around current grid point to compute gradient
|
|
146
145
|
window_size = 3 # 3x3 window for gradient calculation
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: dwa.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update: 2025.10.
|
|
4
|
+
@update: 2025.10.16
|
|
5
5
|
"""
|
|
6
6
|
from typing import List, Tuple, Optional
|
|
7
7
|
import math
|
|
@@ -18,23 +18,22 @@ from .path_tracker import PathTracker
|
|
|
18
18
|
|
|
19
19
|
class DWA(PathTracker):
|
|
20
20
|
"""
|
|
21
|
-
Dynamic Window Approach (DWA) path-tracking controller.
|
|
21
|
+
Dynamic Window Approach (DWA) path-tracking controller. `robot_model` and `obstacle_grid` must be provided.
|
|
22
22
|
|
|
23
23
|
Args:
|
|
24
24
|
*args: see the parent class.
|
|
25
|
-
robot_model: robot model for kinematic simulation
|
|
26
|
-
obstacle_grid: occupancy grid map for collision checking
|
|
27
25
|
vel_reso: resolution of velocity sampling
|
|
28
26
|
predict_time: forward simulation time horizon
|
|
29
27
|
heading_weight: weight for heading term
|
|
30
28
|
velocity_weight: weight for velocity term
|
|
31
29
|
clearance_weight: weight for obstacle clearance term
|
|
32
30
|
**kwargs: see the parent class.
|
|
31
|
+
|
|
32
|
+
References:
|
|
33
|
+
[1] The Dynamic Window Approach to Collision Avoidance
|
|
33
34
|
"""
|
|
34
35
|
def __init__(self,
|
|
35
36
|
*args,
|
|
36
|
-
robot_model: BaseRobot,
|
|
37
|
-
obstacle_grid: Grid = None,
|
|
38
37
|
vel_reso: float = np.array([0.2, 0.2, np.deg2rad(15)]),
|
|
39
38
|
predict_time: float = None,
|
|
40
39
|
heading_weight: float = 0.5,
|
|
@@ -42,13 +41,12 @@ class DWA(PathTracker):
|
|
|
42
41
|
clearance_weight: float = 0.3,
|
|
43
42
|
**kwargs):
|
|
44
43
|
super().__init__(*args, **kwargs)
|
|
45
|
-
if robot_model.dim != self.dim:
|
|
46
|
-
raise ValueError("Dimension of robot model and controller must be the same")
|
|
47
|
-
self.robot_model = robot_model
|
|
48
44
|
|
|
49
|
-
if
|
|
50
|
-
raise ValueError("
|
|
51
|
-
|
|
45
|
+
if self.robot_model is None:
|
|
46
|
+
raise ValueError("Robot model is required.")
|
|
47
|
+
|
|
48
|
+
if self.obstacle_grid is None:
|
|
49
|
+
raise ValueError("Obstacle grid is required.")
|
|
52
50
|
|
|
53
51
|
self.vel_reso = vel_reso
|
|
54
52
|
self.predict_time = predict_time if predict_time is not None else self.lookahead_distance / self.max_lin_speed
|
|
@@ -56,6 +54,9 @@ class DWA(PathTracker):
|
|
|
56
54
|
self.velocity_weight = velocity_weight
|
|
57
55
|
self.clearance_weight = clearance_weight
|
|
58
56
|
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return "DWA"
|
|
59
|
+
|
|
59
60
|
def get_action(self, obs: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
60
61
|
"""
|
|
61
62
|
Get action from observation using Dynamic Window Approach.
|
|
@@ -238,11 +239,7 @@ class DWA(PathTracker):
|
|
|
238
239
|
|
|
239
240
|
min_dist = float("inf")
|
|
240
241
|
for p in traj:
|
|
241
|
-
|
|
242
|
-
if not self.obstacle_grid.within_bounds(grid_pt) or self.obstacle_grid.type_map[grid_pt] == TYPES.OBSTACLE:
|
|
243
|
-
return -float("inf") # collision
|
|
244
|
-
# update min distance (Euclidean to occupied cells could be added here)
|
|
245
|
-
dist = self.obstacle_grid.esdf[grid_pt] * self.obstacle_grid.resolution # using ESDF to compute distance to nearest obstacle
|
|
242
|
+
dist = self._get_dist_to_nearest_obstacle(p[:self.dim])
|
|
246
243
|
min_dist = min(min_dist, dist)
|
|
247
244
|
|
|
248
245
|
normalized_min_dist = min_dist / self.robot_model.radius
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: path_tracker.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update: 2025.10.
|
|
4
|
+
@update: 2025.10.16
|
|
5
5
|
"""
|
|
6
6
|
from typing import List, Tuple
|
|
7
7
|
import math
|
|
@@ -35,6 +35,9 @@ class PathTracker(BaseController):
|
|
|
35
35
|
self.pose_interp = pose_interp
|
|
36
36
|
self.current_target_index = 0
|
|
37
37
|
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return "Path Tracker"
|
|
40
|
+
|
|
38
41
|
def reset(self):
|
|
39
42
|
"""
|
|
40
43
|
Reset the controller to initial state.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: pid.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update: 2025.10.
|
|
4
|
+
@update: 2025.10.16
|
|
5
5
|
"""
|
|
6
6
|
from typing import List, Tuple
|
|
7
7
|
import math
|
|
@@ -22,6 +22,9 @@ class PID(PathTracker):
|
|
|
22
22
|
Ki: integral gain
|
|
23
23
|
Kd: derivative gain
|
|
24
24
|
**kwargs: see the parent class.
|
|
25
|
+
|
|
26
|
+
References:
|
|
27
|
+
[1] Directional stability of automatically steered bodies
|
|
25
28
|
"""
|
|
26
29
|
def __init__(self,
|
|
27
30
|
*args,
|
|
@@ -38,6 +41,9 @@ class PID(PathTracker):
|
|
|
38
41
|
self.integral_error = np.zeros(self.action_space.shape[0])
|
|
39
42
|
self.prev_error = np.zeros(self.action_space.shape[0])
|
|
40
43
|
|
|
44
|
+
def __str__(self) -> str:
|
|
45
|
+
return "PID"
|
|
46
|
+
|
|
41
47
|
def reset(self):
|
|
42
48
|
"""
|
|
43
49
|
Reset the controller to initial state.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: pure_pursuit.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update: 2025.10.
|
|
4
|
+
@update: 2025.10.16
|
|
5
5
|
"""
|
|
6
6
|
from typing import List, Tuple
|
|
7
7
|
import math
|
|
@@ -19,12 +19,18 @@ class PurePursuit(PathTracker):
|
|
|
19
19
|
Args:
|
|
20
20
|
*args: see the parent class.
|
|
21
21
|
**kwargs: see the parent class.
|
|
22
|
+
|
|
23
|
+
References:
|
|
24
|
+
[1] Implementation of the Pure Pursuit Path Tracking Algorithm.
|
|
22
25
|
"""
|
|
23
26
|
def __init__(self,
|
|
24
27
|
*args,
|
|
25
28
|
**kwargs):
|
|
26
29
|
super().__init__(*args, **kwargs)
|
|
27
30
|
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return "Pure Pursuit"
|
|
33
|
+
|
|
28
34
|
def _get_desired_vel(self, target_pose: np.ndarray, cur_pose: np.ndarray) -> np.ndarray:
|
|
29
35
|
"""
|
|
30
36
|
Calculate the desired velocity in robot frame using pure pursuit.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: rpp.py
|
|
3
|
+
@author: Wu Maojia
|
|
4
|
+
@update: 2025.10.16
|
|
5
|
+
"""
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
import math
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from python_motion_planning.common.utils.geometry import Geometry
|
|
12
|
+
from python_motion_planning.common.utils.frame_transformer import FrameTransformer
|
|
13
|
+
from .pure_pursuit import PurePursuit
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RPP(PurePursuit):
|
|
17
|
+
"""
|
|
18
|
+
Regulated Pure Pursuit (RPP) path-tracking controller. `obstacle_grid` must be provided.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
*args: see the parent class.
|
|
22
|
+
curvature_thresh (float): minimum curvature threshold T_kappa to trigger speed scaling.
|
|
23
|
+
r_min (float): minimum radius (used in curvature heuristic).
|
|
24
|
+
d_prox (float): proximity distance threshold to start slowing down near obstacles.
|
|
25
|
+
alpha (float): proximity scaling gain (0 < alpha <= 1).
|
|
26
|
+
lookahead_gain (float): lookahead time gain for adaptive lookahead.
|
|
27
|
+
**kwargs: see the parent class.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self,
|
|
31
|
+
*args,
|
|
32
|
+
curvature_thresh: float = 0.1,
|
|
33
|
+
r_min: float = 1.0,
|
|
34
|
+
d_prox: float = None,
|
|
35
|
+
alpha: float = 0.8,
|
|
36
|
+
lookahead_gain: float = 1.0,
|
|
37
|
+
**kwargs):
|
|
38
|
+
super().__init__(*args, **kwargs)
|
|
39
|
+
|
|
40
|
+
if self.obstacle_grid is None:
|
|
41
|
+
raise ValueError("Obstacle grid is required.")
|
|
42
|
+
|
|
43
|
+
self.curvature_thresh = curvature_thresh
|
|
44
|
+
self.r_min = r_min
|
|
45
|
+
self.d_prox = d_prox if d_prox is not None else self.obstacle_grid.inflation_radius
|
|
46
|
+
self.alpha = alpha
|
|
47
|
+
self.lookahead_gain = lookahead_gain
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
return "RPP"
|
|
51
|
+
|
|
52
|
+
def _get_desired_vel(self, target_pose: np.ndarray, cur_pose: np.ndarray) -> np.ndarray:
|
|
53
|
+
"""
|
|
54
|
+
Calculate the desired velocity in robot frame using Regulated Pure Pursuit.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
target_pose: target pose in world frame (from lookahead)
|
|
58
|
+
cur_pose: current pose in world frame
|
|
59
|
+
obs_dist: distance to nearest obstacle (if available), defaults to infinity
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
desired_vel: desired velocity in robot frame [lin_x, lin_y, ang_z]
|
|
63
|
+
"""
|
|
64
|
+
# transform target pose to robot frame
|
|
65
|
+
rel_pose = FrameTransformer.pose_world_to_robot(self.dim, target_pose, cur_pose)
|
|
66
|
+
x = rel_pose[0]
|
|
67
|
+
y = rel_pose[1]
|
|
68
|
+
L = math.hypot(x, y)
|
|
69
|
+
|
|
70
|
+
# if lookahead distance is (nearly) zero, no movement
|
|
71
|
+
if L < self.eps:
|
|
72
|
+
desired_vel = np.zeros(self.action_space.shape[0])
|
|
73
|
+
return self.clip_velocity(desired_vel)
|
|
74
|
+
|
|
75
|
+
# Pure Pursuit curvature: kappa = 2*y / L^2
|
|
76
|
+
# Note: y is lateral offset in robot frame (positive left). For our coordinate,
|
|
77
|
+
# forward x, lateral y. Angular velocity = kappa * v.
|
|
78
|
+
kappa = (2.0 * y) / (L * L)
|
|
79
|
+
|
|
80
|
+
# Base linear speed (before regulation)
|
|
81
|
+
v_t = self.max_lin_speed
|
|
82
|
+
|
|
83
|
+
# Curvature heuristic
|
|
84
|
+
# Slow down in sharp turns: Eq. (5) in the paper
|
|
85
|
+
if abs(kappa) > self.curvature_thresh:
|
|
86
|
+
v_curv = v_t / (self.r_min * max(abs(kappa), self.eps))
|
|
87
|
+
else:
|
|
88
|
+
v_curv = v_t
|
|
89
|
+
|
|
90
|
+
# Proximity heuristic
|
|
91
|
+
# Slow down when close to obstacles: Eq. (6) in the paper
|
|
92
|
+
obs_dist = self._get_dist_to_nearest_obstacle(cur_pose[:self.dim])
|
|
93
|
+
if obs_dist <= self.d_prox:
|
|
94
|
+
v_prox = v_t * self.alpha * obs_dist / max(self.d_prox, self.eps)
|
|
95
|
+
else:
|
|
96
|
+
v_prox = v_t
|
|
97
|
+
v_prox = v_t
|
|
98
|
+
|
|
99
|
+
# Take minimum regulated velocity (for safety)
|
|
100
|
+
v_reg = min(v_curv, v_prox)
|
|
101
|
+
|
|
102
|
+
# Compute angular velocity using regulated linear speed
|
|
103
|
+
omega = np.clip(v_reg * kappa, -self.max_ang_speed, self.max_ang_speed)
|
|
104
|
+
|
|
105
|
+
# Assemble desired velocity vector
|
|
106
|
+
desired_lin_vel = np.array([v_reg * (1.0 if x >= 0 else -1.0), 0.0])
|
|
107
|
+
desired_ang_vel = np.array([omega])
|
|
108
|
+
desired_vel = np.concatenate([desired_lin_vel, desired_ang_vel])
|
|
109
|
+
|
|
110
|
+
desired_vel = self.clip_velocity(desired_vel)
|
|
111
|
+
return desired_vel
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
|
-
@file:
|
|
2
|
+
@file: base_path_planner.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update: 2025.
|
|
4
|
+
@update: 2025.12.19
|
|
5
5
|
"""
|
|
6
|
-
from typing import Union
|
|
6
|
+
from typing import Union, List, Tuple, Dict, Any, Iterable
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
|
|
9
9
|
from python_motion_planning.common import BaseMap
|
|
@@ -25,8 +25,31 @@ class BasePathPlanner(ABC):
|
|
|
25
25
|
self.goal = goal
|
|
26
26
|
self.failed_info = [], {"success": False, "start": None, "goal": None, "length": 0, "cost": 0, "expand": {}}
|
|
27
27
|
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return "Base Path Planner"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def dim(self) -> int:
|
|
33
|
+
"""
|
|
34
|
+
Get the dimension of the map.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dim (int): The dimension of the map.
|
|
38
|
+
"""
|
|
39
|
+
return self.map_.dim
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def bounds(self) -> Iterable:
|
|
43
|
+
"""
|
|
44
|
+
Get the bounds of the map.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
bounds (Iterable): The bounds of the map.
|
|
48
|
+
"""
|
|
49
|
+
return self.map_.bounds
|
|
50
|
+
|
|
28
51
|
@abstractmethod
|
|
29
|
-
def plan(self) -> Union[
|
|
52
|
+
def plan(self) -> Union[List[Tuple[float, ...]], Dict[str, Any]]:
|
|
30
53
|
"""
|
|
31
54
|
Interface for planning.
|
|
32
55
|
|
|
@@ -63,26 +86,37 @@ class BasePathPlanner(ABC):
|
|
|
63
86
|
return self.get_cost(point, self.goal)
|
|
64
87
|
|
|
65
88
|
|
|
66
|
-
def extract_path(self, closed_list: dict) ->
|
|
89
|
+
def extract_path(self, closed_list: dict, start: tuple = None, goal: tuple = None) -> Tuple[List[Tuple[float, ...]], float, float]:
|
|
67
90
|
"""
|
|
68
91
|
Extract the path based on the CLOSED list.
|
|
69
92
|
|
|
70
93
|
Args:
|
|
71
|
-
closed_list
|
|
94
|
+
closed_list: CLOSED list
|
|
95
|
+
start: Start point. (default: self.start)
|
|
96
|
+
goal: Goal point. (default: self.goal)
|
|
72
97
|
|
|
73
98
|
Returns:
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
path: A list containing the path waypoints
|
|
100
|
+
length: Length of the path
|
|
101
|
+
cost: Cost of the path
|
|
76
102
|
"""
|
|
77
103
|
length = 0
|
|
78
104
|
cost = 0
|
|
79
|
-
|
|
105
|
+
|
|
106
|
+
if start is None:
|
|
107
|
+
start = self.start
|
|
108
|
+
if goal is None:
|
|
109
|
+
goal = self.goal
|
|
110
|
+
|
|
111
|
+
node = closed_list.get(goal)
|
|
80
112
|
path = [node.current]
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
|
|
114
|
+
while node.current != start:
|
|
115
|
+
node_parent = closed_list.get(node.parent)
|
|
83
116
|
length += self.map_.get_distance(node.current, node_parent.current)
|
|
84
117
|
cost += self.get_cost(node.current, node_parent.current)
|
|
85
118
|
node = node_parent
|
|
86
119
|
path.append(node.current)
|
|
87
120
|
path = path[::-1] # make the order: start -> goal
|
|
121
|
+
|
|
88
122
|
return path, length, cost
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: a_star.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update: 2025.10.
|
|
4
|
+
@update: 2025.10.6
|
|
5
5
|
"""
|
|
6
|
-
from typing import Union
|
|
6
|
+
from typing import Union, List, Tuple, Dict, Any
|
|
7
7
|
import heapq
|
|
8
8
|
|
|
9
9
|
from python_motion_planning.common import BaseMap, Grid, Node, TYPES
|
|
@@ -23,12 +23,14 @@ class AStar(Dijkstra):
|
|
|
23
23
|
Examples:
|
|
24
24
|
>>> map_ = Grid(bounds=[[0, 15], [0, 15]])
|
|
25
25
|
>>> planner = AStar(map_=map_, start=(5, 5), goal=(10, 10))
|
|
26
|
-
>>> planner.plan()
|
|
27
|
-
([
|
|
28
|
-
|
|
26
|
+
>>> path, path_info = planner.plan()
|
|
27
|
+
>>> print(path_info['success'])
|
|
28
|
+
True
|
|
29
|
+
|
|
29
30
|
>>> planner.map_.type_map[3:10, 6] = TYPES.OBSTACLE
|
|
30
|
-
>>> planner.plan()
|
|
31
|
-
([
|
|
31
|
+
>>> path, path_info = planner.plan()
|
|
32
|
+
>>> print(path_info['success'])
|
|
33
|
+
True
|
|
32
34
|
"""
|
|
33
35
|
def __init__(self, *args, **kwargs) -> None:
|
|
34
36
|
super().__init__(*args, **kwargs)
|
|
@@ -36,13 +38,13 @@ class AStar(Dijkstra):
|
|
|
36
38
|
def __str__(self) -> str:
|
|
37
39
|
return "A*"
|
|
38
40
|
|
|
39
|
-
def plan(self) -> Union[
|
|
41
|
+
def plan(self) -> Union[List[Tuple[float, ...]], Dict[str, Any]]:
|
|
40
42
|
"""
|
|
41
43
|
Interface for planning.
|
|
42
44
|
|
|
43
45
|
Returns:
|
|
44
46
|
path: A list containing the path waypoints
|
|
45
|
-
path_info: A dictionary containing the path information
|
|
47
|
+
path_info: A dictionary containing the path information
|
|
46
48
|
"""
|
|
47
49
|
# OPEN list (priority queue) and CLOSED list (hash table)
|
|
48
50
|
OPEN = []
|
|
@@ -53,10 +55,6 @@ class AStar(Dijkstra):
|
|
|
53
55
|
while OPEN:
|
|
54
56
|
node = heapq.heappop(OPEN)
|
|
55
57
|
|
|
56
|
-
# obstacle found
|
|
57
|
-
if not self.map_.is_expandable(node.current, node.parent):
|
|
58
|
-
continue
|
|
59
|
-
|
|
60
58
|
# exists in CLOSED list
|
|
61
59
|
if node.current in CLOSED:
|
|
62
60
|
continue
|
|
@@ -74,7 +72,7 @@ class AStar(Dijkstra):
|
|
|
74
72
|
"expand": CLOSED
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
for node_n in self.map_.get_neighbors(node):
|
|
75
|
+
for node_n in self.map_.get_neighbors(node, diagonal=self.diagonal):
|
|
78
76
|
# exists in CLOSED list
|
|
79
77
|
if node_n.current in CLOSED:
|
|
80
78
|
continue
|