python-motion-planning 2.0.dev2__py3-none-any.whl → 2.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/grid.py +568 -147
- python_motion_planning/common/utils/geometry.py +21 -31
- python_motion_planning/path_planner/graph_search/lazy_theta_star.py +15 -7
- python_motion_planning/path_planner/graph_search/theta_star.py +4 -3
- python_motion_planning/path_planner/sample_search/rrt.py +6 -6
- python_motion_planning/path_planner/sample_search/rrt_connect.py +2 -2
- python_motion_planning/path_planner/sample_search/rrt_star.py +31 -11
- 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.dev2.dist-info → python_motion_planning-2.1.dist-info}/METADATA +23 -16
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.1.dist-info}/RECORD +24 -22
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.1.dist-info}/WHEEL +1 -1
- 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.dev2.dist-info → python_motion_planning-2.1.dist-info}/licenses/LICENSE +0 -0
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: cubic_spline.py
|
|
3
|
+
@author: Yang Haodong, Wu Maojia
|
|
4
|
+
@update: 2026.4.12
|
|
5
|
+
"""
|
|
6
|
+
from typing import List, Tuple, Dict, Any
|
|
7
|
+
import math
|
|
8
|
+
import bisect
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from python_motion_planning.traj_optimizer.base_curve_generator import BaseCurveGenerator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CubicSpline(BaseCurveGenerator):
|
|
16
|
+
"""
|
|
17
|
+
Class for cubic spline curve generator.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
*args: see the parent class.
|
|
21
|
+
*kwargs: see the parent class.
|
|
22
|
+
|
|
23
|
+
References:
|
|
24
|
+
[1] https://en.wikipedia.org/wiki/Spline_(mathematics)#Algorithm_for_computing_natural_cubic_splines
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
>>> generator = CubicSpline(step=0.1)
|
|
28
|
+
>>> points = [(0.0, 0.0), (10.0, 10.0), (20.0, 5.0)]
|
|
29
|
+
>>> path, curve_info = generator.generate(points)
|
|
30
|
+
>>> print(curve_info['success'])
|
|
31
|
+
True
|
|
32
|
+
"""
|
|
33
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
34
|
+
super().__init__(*args, **kwargs)
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
return "Cubic Spline"
|
|
38
|
+
|
|
39
|
+
def generate(self, points: List[Tuple[float, ...]]) -> Tuple[List[Tuple[float, float]], Dict[str, Any]]:
|
|
40
|
+
"""
|
|
41
|
+
Generate a cubic spline curve through a list of 2D points.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
points: A list of 2D points (x, y) in world frame. If the points contain
|
|
45
|
+
additional entries (e.g. yaw), only the first two values are used.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
path: A list of (x, y) waypoints of the generated curve in world frame.
|
|
49
|
+
curve_info: A dictionary containing the curve information (success, length).
|
|
50
|
+
"""
|
|
51
|
+
if len(points) < 2:
|
|
52
|
+
return [], {"success": False, "length": 0.0}
|
|
53
|
+
|
|
54
|
+
x_list = [float(p[0]) for p in points]
|
|
55
|
+
y_list = [float(p[1]) for p in points]
|
|
56
|
+
|
|
57
|
+
dx, dy = np.diff(x_list), np.diff(y_list)
|
|
58
|
+
ds = [math.hypot(idx, idy) for (idx, idy) in zip(dx, dy)]
|
|
59
|
+
s = [0.0]
|
|
60
|
+
s.extend(np.cumsum(ds))
|
|
61
|
+
t = np.arange(0, s[-1], self.step)
|
|
62
|
+
|
|
63
|
+
path_x, _ = self._spline(s, x_list, t)
|
|
64
|
+
path_y, _ = self._spline(s, y_list, t)
|
|
65
|
+
|
|
66
|
+
path = [(float(ix), float(iy)) for ix, iy in zip(path_x, path_y)]
|
|
67
|
+
return path, {"success": True, "length": self.length(path)}
|
|
68
|
+
|
|
69
|
+
def _spline(self, x_list: List[float], y_list: List[float],
|
|
70
|
+
t: np.ndarray) -> Tuple[List[float], List[float]]:
|
|
71
|
+
"""
|
|
72
|
+
Build and evaluate a 1D natural cubic spline y = f(x).
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
x_list: Monotonically increasing x-coordinates of the control points.
|
|
76
|
+
y_list: y-coordinates of the control points.
|
|
77
|
+
t: Values of x at which to evaluate the spline.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
p: Values of the spline evaluated at t.
|
|
81
|
+
dp: Values of the spline derivative evaluated at t.
|
|
82
|
+
"""
|
|
83
|
+
a, b, c, d = y_list, [], [], []
|
|
84
|
+
h = np.diff(x_list)
|
|
85
|
+
num = len(x_list)
|
|
86
|
+
|
|
87
|
+
A = np.zeros((num, num))
|
|
88
|
+
for i in range(1, num - 1):
|
|
89
|
+
A[i, i - 1] = h[i - 1]
|
|
90
|
+
A[i, i] = 2.0 * (h[i - 1] + h[i])
|
|
91
|
+
A[i, i + 1] = h[i]
|
|
92
|
+
A[0, 0] = 1.0
|
|
93
|
+
A[num - 1, num - 1] = 1.0
|
|
94
|
+
|
|
95
|
+
B = np.zeros(num)
|
|
96
|
+
for i in range(1, num - 1):
|
|
97
|
+
B[i] = (3.0 * (a[i + 1] - a[i]) / h[i]
|
|
98
|
+
- 3.0 * (a[i] - a[i - 1]) / h[i - 1])
|
|
99
|
+
|
|
100
|
+
c = np.linalg.solve(A, B)
|
|
101
|
+
for i in range(num - 1):
|
|
102
|
+
d.append((c[i + 1] - c[i]) / (3.0 * h[i]))
|
|
103
|
+
b.append((a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2.0 * c[i]) / 3.0)
|
|
104
|
+
|
|
105
|
+
p, dp = [], []
|
|
106
|
+
for it in t:
|
|
107
|
+
if it < x_list[0] or it > x_list[-1]:
|
|
108
|
+
continue
|
|
109
|
+
i = bisect.bisect(x_list, it) - 1
|
|
110
|
+
i = min(max(i, 0), num - 2)
|
|
111
|
+
dx = it - x_list[i]
|
|
112
|
+
p.append(a[i] + b[i] * dx + c[i] * dx ** 2 + d[i] * dx ** 3)
|
|
113
|
+
dp.append(b[i] + 2.0 * c[i] * dx + 3.0 * d[i] * dx ** 2)
|
|
114
|
+
|
|
115
|
+
return p, dp
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: bezier_curve.py
|
|
3
|
+
@author: Yang Haodong, Wu Maojia
|
|
4
|
+
@update: 2026.4.12
|
|
5
|
+
"""
|
|
6
|
+
from typing import List, Tuple, Dict, Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy.special import comb
|
|
10
|
+
|
|
11
|
+
from python_motion_planning.traj_optimizer.base_curve_generator import BaseCurveGenerator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Bezier(BaseCurveGenerator):
|
|
15
|
+
"""
|
|
16
|
+
Class for Bezier curve generator.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
*args: see the parent class.
|
|
20
|
+
offset: The offset of control points (larger value yields sharper curvature).
|
|
21
|
+
*kwargs: see the parent class.
|
|
22
|
+
|
|
23
|
+
References:
|
|
24
|
+
[1] https://en.wikipedia.org/wiki/B%C3%A9zier_curve
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
>>> import math
|
|
28
|
+
>>> generator = Bezier(step=0.1, offset=3.0)
|
|
29
|
+
>>> points = [(0.0, 0.0, 0.0), (10.0, 10.0, -math.pi/2), (20.0, 5.0, math.pi/3)]
|
|
30
|
+
>>> path, curve_info = generator.generate(points)
|
|
31
|
+
>>> print(curve_info['success'])
|
|
32
|
+
True
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self, *args,
|
|
35
|
+
offset: float = 3.0,
|
|
36
|
+
**kwargs) -> None:
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
self.offset = offset
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return "Bezier Curve"
|
|
42
|
+
|
|
43
|
+
def generate(self, points: List[Tuple[float, float, float]]) -> Tuple[List[Tuple[float, float]], Dict[str, Any]]:
|
|
44
|
+
"""
|
|
45
|
+
Generate a concatenated Bezier curve through a list of poses.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
points: A list of poses (x, y, yaw) in world frame.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
path: A list of (x, y) waypoints of the generated curve in world frame.
|
|
52
|
+
curve_info: A dictionary containing the curve information (success, length).
|
|
53
|
+
"""
|
|
54
|
+
if len(points) < 2:
|
|
55
|
+
return [], {"success": False, "length": 0.0}
|
|
56
|
+
|
|
57
|
+
path: List[Tuple[float, float]] = []
|
|
58
|
+
for i in range(len(points) - 1):
|
|
59
|
+
segment, _ = self._generate_segment(points[i], points[i + 1])
|
|
60
|
+
path.extend([(float(pt[0]), float(pt[1])) for pt in segment])
|
|
61
|
+
|
|
62
|
+
return path, {"success": True, "length": self.length(path)}
|
|
63
|
+
|
|
64
|
+
def _generate_segment(self, start_pose: Tuple[float, float, float],
|
|
65
|
+
goal_pose: Tuple[float, float, float]
|
|
66
|
+
) -> Tuple[List[np.ndarray], List[Tuple[float, float]]]:
|
|
67
|
+
"""
|
|
68
|
+
Generate a single Bezier curve segment between two poses.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
start_pose: Initial pose (x, y, yaw).
|
|
72
|
+
goal_pose: Target pose (x, y, yaw).
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
segment: A list of points sampled from the Bezier curve.
|
|
76
|
+
control_points: The control points of the Bezier curve.
|
|
77
|
+
"""
|
|
78
|
+
sx, sy, _ = start_pose
|
|
79
|
+
gx, gy, _ = goal_pose
|
|
80
|
+
n_points = max(int(np.hypot(sx - gx, sy - gy) / self.step), 2)
|
|
81
|
+
control_points = self._get_control_points(start_pose, goal_pose)
|
|
82
|
+
|
|
83
|
+
segment = [self._bezier(t, control_points) for t in np.linspace(0, 1, n_points)]
|
|
84
|
+
return segment, control_points
|
|
85
|
+
|
|
86
|
+
def _bezier(self, t: float, control_points: List[Tuple[float, float]]) -> np.ndarray:
|
|
87
|
+
"""
|
|
88
|
+
Calculate the Bezier curve point.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
t: Scale factor in [0, 1].
|
|
92
|
+
control_points: Control points of the Bezier curve.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
point: Point on the Bezier curve at the given t.
|
|
96
|
+
"""
|
|
97
|
+
n = len(control_points) - 1
|
|
98
|
+
control_points = np.array(control_points)
|
|
99
|
+
return np.sum([comb(n, i) * t ** i * (1 - t) ** (n - i) * control_points[i]
|
|
100
|
+
for i in range(n + 1)], axis=0)
|
|
101
|
+
|
|
102
|
+
def _get_control_points(self, start_pose: Tuple[float, float, float],
|
|
103
|
+
goal_pose: Tuple[float, float, float]) -> List[Tuple[float, float]]:
|
|
104
|
+
"""
|
|
105
|
+
Calculate the control points heuristically from start and goal poses.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
start_pose: Initial pose (x, y, yaw).
|
|
109
|
+
goal_pose: Target pose (x, y, yaw).
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
control_points: Control points of the Bezier curve.
|
|
113
|
+
"""
|
|
114
|
+
sx, sy, syaw = start_pose
|
|
115
|
+
gx, gy, gyaw = goal_pose
|
|
116
|
+
|
|
117
|
+
dist = np.hypot(sx - gx, sy - gy) / self.offset
|
|
118
|
+
return [(sx, sy),
|
|
119
|
+
(sx + dist * np.cos(syaw), sy + dist * np.sin(syaw)),
|
|
120
|
+
(gx - dist * np.cos(gyaw), gy - dist * np.sin(gyaw)),
|
|
121
|
+
(gx, gy)]
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: dubins_curve.py
|
|
3
|
+
@author: Yang Haodong, Wu Maojia
|
|
4
|
+
@update: 2026.4.12
|
|
5
|
+
"""
|
|
6
|
+
from typing import List, Tuple, Dict, Any
|
|
7
|
+
import math
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy.spatial.transform import Rotation as Rot
|
|
11
|
+
|
|
12
|
+
from python_motion_planning.traj_optimizer.base_curve_generator import BaseCurveGenerator
|
|
13
|
+
from python_motion_planning.common.utils.geometry import Geometry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Dubins(BaseCurveGenerator):
|
|
17
|
+
"""
|
|
18
|
+
Class for Dubins curve generator.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
*args: see the parent class.
|
|
22
|
+
max_curv: The maximum curvature of the curve.
|
|
23
|
+
*kwargs: see the parent class.
|
|
24
|
+
|
|
25
|
+
References:
|
|
26
|
+
[1] On curves of minimal length with a constraint on average curvature, and with prescribed initial and terminal positions and tangents
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> import math
|
|
30
|
+
>>> generator = Dubins(step=0.1, max_curv=1.0)
|
|
31
|
+
>>> points = [(0.0, 0.0, 0.0), (10.0, 10.0, -math.pi/2), (20.0, 5.0, math.pi/3)]
|
|
32
|
+
>>> path, curve_info = generator.generate(points)
|
|
33
|
+
>>> print(curve_info['success'])
|
|
34
|
+
True
|
|
35
|
+
"""
|
|
36
|
+
def __init__(self, *args, max_curv: float = 1.0, **kwargs) -> None:
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
self.max_curv = max_curv
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return "Dubins Curve"
|
|
42
|
+
|
|
43
|
+
def generate(self, points: List[Tuple[float, float, float]]) -> Tuple[List[Tuple[float, float, float]], Dict[str, Any]]:
|
|
44
|
+
"""
|
|
45
|
+
Generate a concatenated Dubins curve through a list of poses.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
points: A list of poses (x, y, yaw) in world frame.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
path: A list of (x, y, yaw) waypoints of the generated curve in world frame.
|
|
52
|
+
curve_info: A dictionary containing the curve information (success, length).
|
|
53
|
+
"""
|
|
54
|
+
if len(points) < 2:
|
|
55
|
+
return [], {"success": False, "length": 0.0}
|
|
56
|
+
|
|
57
|
+
path: List[Tuple[float, float, float]] = []
|
|
58
|
+
total_cost = 0.0
|
|
59
|
+
for i in range(len(points) - 1):
|
|
60
|
+
best_cost, _, x_list, y_list, yaw_list = self._generate_segment(
|
|
61
|
+
points[i], points[i + 1])
|
|
62
|
+
if best_cost is None:
|
|
63
|
+
return [], {"success": False, "length": 0.0}
|
|
64
|
+
total_cost += best_cost / self.max_curv
|
|
65
|
+
|
|
66
|
+
start = 1 if i > 0 else 0
|
|
67
|
+
for x, y, yaw in zip(x_list[start:], y_list[start:], yaw_list[start:]):
|
|
68
|
+
path.append((float(x), float(y), float(yaw)))
|
|
69
|
+
|
|
70
|
+
total_cost = float(total_cost)
|
|
71
|
+
|
|
72
|
+
return path, {"success": True, "length": total_cost}
|
|
73
|
+
|
|
74
|
+
def _lsl(self, alpha: float, beta: float, dist: float):
|
|
75
|
+
"""
|
|
76
|
+
Left-Straight-Left generation mode.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
alpha: Initial heading of pose (0, 0, alpha).
|
|
80
|
+
beta: Goal heading of pose (dist, 0, beta).
|
|
81
|
+
dist: The distance between the initial and goal poses.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
t, p, q: Moving length of segments.
|
|
85
|
+
mode: Motion mode.
|
|
86
|
+
"""
|
|
87
|
+
sin_a, sin_b, cos_a, cos_b, _, cos_a_b = self.trigonometric(alpha, beta)
|
|
88
|
+
|
|
89
|
+
p_lsl = 2 + dist ** 2 - 2 * cos_a_b + 2 * dist * (sin_a - sin_b)
|
|
90
|
+
if p_lsl < 0:
|
|
91
|
+
return None, None, None, ["L", "S", "L"]
|
|
92
|
+
p_lsl = math.sqrt(p_lsl)
|
|
93
|
+
|
|
94
|
+
t_lsl = Geometry.mod_to_2pi(-alpha + math.atan2(cos_b - cos_a, dist + sin_a - sin_b))
|
|
95
|
+
q_lsl = Geometry.mod_to_2pi(beta - math.atan2(cos_b - cos_a, dist + sin_a - sin_b))
|
|
96
|
+
return t_lsl, p_lsl, q_lsl, ["L", "S", "L"]
|
|
97
|
+
|
|
98
|
+
def _rsr(self, alpha: float, beta: float, dist: float):
|
|
99
|
+
"""
|
|
100
|
+
Right-Straight-Right generation mode.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
alpha: Initial heading of pose (0, 0, alpha).
|
|
104
|
+
beta: Goal heading of pose (dist, 0, beta).
|
|
105
|
+
dist: The distance between the initial and goal poses.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
t, p, q: Moving length of segments.
|
|
109
|
+
mode: Motion mode.
|
|
110
|
+
"""
|
|
111
|
+
sin_a, sin_b, cos_a, cos_b, _, cos_a_b = self.trigonometric(alpha, beta)
|
|
112
|
+
|
|
113
|
+
p_rsr = 2 + dist ** 2 - 2 * cos_a_b + 2 * dist * (sin_b - sin_a)
|
|
114
|
+
if p_rsr < 0:
|
|
115
|
+
return None, None, None, ["R", "S", "R"]
|
|
116
|
+
p_rsr = math.sqrt(p_rsr)
|
|
117
|
+
|
|
118
|
+
t_rsr = Geometry.mod_to_2pi(alpha - math.atan2(cos_a - cos_b, dist - sin_a + sin_b))
|
|
119
|
+
q_rsr = Geometry.mod_to_2pi(-beta + math.atan2(cos_a - cos_b, dist - sin_a + sin_b))
|
|
120
|
+
return t_rsr, p_rsr, q_rsr, ["R", "S", "R"]
|
|
121
|
+
|
|
122
|
+
def _lsr(self, alpha: float, beta: float, dist: float):
|
|
123
|
+
"""
|
|
124
|
+
Left-Straight-Right generation mode.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
alpha: Initial heading of pose (0, 0, alpha).
|
|
128
|
+
beta: Goal heading of pose (dist, 0, beta).
|
|
129
|
+
dist: The distance between the initial and goal poses.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
t, p, q: Moving length of segments.
|
|
133
|
+
mode: Motion mode.
|
|
134
|
+
"""
|
|
135
|
+
sin_a, sin_b, cos_a, cos_b, _, cos_a_b = self.trigonometric(alpha, beta)
|
|
136
|
+
|
|
137
|
+
p_lsr = -2 + dist ** 2 + 2 * cos_a_b + 2 * dist * (sin_a + sin_b)
|
|
138
|
+
if p_lsr < 0:
|
|
139
|
+
return None, None, None, ["L", "S", "R"]
|
|
140
|
+
p_lsr = math.sqrt(p_lsr)
|
|
141
|
+
|
|
142
|
+
t_lsr = Geometry.mod_to_2pi(-alpha + math.atan2(-cos_a - cos_b, dist + sin_a + sin_b) - math.atan2(-2.0, p_lsr))
|
|
143
|
+
q_lsr = Geometry.mod_to_2pi(-beta + math.atan2(-cos_a - cos_b, dist + sin_a + sin_b) - math.atan2(-2.0, p_lsr))
|
|
144
|
+
return t_lsr, p_lsr, q_lsr, ["L", "S", "R"]
|
|
145
|
+
|
|
146
|
+
def _rsl(self, alpha: float, beta: float, dist: float):
|
|
147
|
+
"""
|
|
148
|
+
Right-Straight-Left generation mode.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
alpha: Initial heading of pose (0, 0, alpha).
|
|
152
|
+
beta: Goal heading of pose (dist, 0, beta).
|
|
153
|
+
dist: The distance between the initial and goal poses.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
t, p, q: Moving length of segments.
|
|
157
|
+
mode: Motion mode.
|
|
158
|
+
"""
|
|
159
|
+
sin_a, sin_b, cos_a, cos_b, _, cos_a_b = self.trigonometric(alpha, beta)
|
|
160
|
+
|
|
161
|
+
p_rsl = -2 + dist ** 2 + 2 * cos_a_b - 2 * dist * (sin_a + sin_b)
|
|
162
|
+
if p_rsl < 0:
|
|
163
|
+
return None, None, None, ["R", "S", "L"]
|
|
164
|
+
p_rsl = math.sqrt(p_rsl)
|
|
165
|
+
|
|
166
|
+
t_rsl = Geometry.mod_to_2pi(alpha - math.atan2(cos_a + cos_b, dist - sin_a - sin_b) + math.atan2(2.0, p_rsl))
|
|
167
|
+
q_rsl = Geometry.mod_to_2pi(beta - math.atan2(cos_a + cos_b, dist - sin_a - sin_b) + math.atan2(2.0, p_rsl))
|
|
168
|
+
return t_rsl, p_rsl, q_rsl, ["R", "S", "L"]
|
|
169
|
+
|
|
170
|
+
def _rlr(self, alpha: float, beta: float, dist: float):
|
|
171
|
+
"""
|
|
172
|
+
Right-Left-Right generation mode.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
alpha: Initial heading of pose (0, 0, alpha).
|
|
176
|
+
beta: Goal heading of pose (dist, 0, beta).
|
|
177
|
+
dist: The distance between the initial and goal poses.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
t, p, q: Moving length of segments.
|
|
181
|
+
mode: Motion mode.
|
|
182
|
+
"""
|
|
183
|
+
sin_a, sin_b, cos_a, cos_b, _, cos_a_b = self.trigonometric(alpha, beta)
|
|
184
|
+
|
|
185
|
+
p_rlr = (6.0 - dist ** 2 + 2.0 * cos_a_b + 2.0 * dist * (sin_a - sin_b)) / 8.0
|
|
186
|
+
if abs(p_rlr) > 1.0:
|
|
187
|
+
return None, None, None, ["R", "L", "R"]
|
|
188
|
+
p_rlr = Geometry.mod_to_2pi(2 * math.pi - math.acos(p_rlr))
|
|
189
|
+
|
|
190
|
+
t_rlr = Geometry.mod_to_2pi(alpha - math.atan2(cos_a - cos_b, dist - sin_a + sin_b) + p_rlr / 2.0)
|
|
191
|
+
q_rlr = Geometry.mod_to_2pi(alpha - beta - t_rlr + p_rlr)
|
|
192
|
+
return t_rlr, p_rlr, q_rlr, ["R", "L", "R"]
|
|
193
|
+
|
|
194
|
+
def _lrl(self, alpha: float, beta: float, dist: float):
|
|
195
|
+
"""
|
|
196
|
+
Left-Right-Left generation mode.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
alpha: Initial heading of pose (0, 0, alpha).
|
|
200
|
+
beta: Goal heading of pose (dist, 0, beta).
|
|
201
|
+
dist: The distance between the initial and goal poses.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
t, p, q: Moving length of segments.
|
|
205
|
+
mode: Motion mode.
|
|
206
|
+
"""
|
|
207
|
+
sin_a, sin_b, cos_a, cos_b, _, cos_a_b = self.trigonometric(alpha, beta)
|
|
208
|
+
|
|
209
|
+
p_lrl = (6.0 - dist ** 2 + 2.0 * cos_a_b + 2.0 * dist * (sin_b - sin_a)) / 8.0
|
|
210
|
+
if abs(p_lrl) > 1.0:
|
|
211
|
+
return None, None, None, ["L", "R", "L"]
|
|
212
|
+
p_lrl = Geometry.mod_to_2pi(2 * math.pi - math.acos(p_lrl) )
|
|
213
|
+
|
|
214
|
+
t_lrl = Geometry.mod_to_2pi(-alpha + math.atan2(-cos_a + cos_b, dist + sin_a - sin_b) + p_lrl / 2.0)
|
|
215
|
+
q_lrl = Geometry.mod_to_2pi(beta - alpha - t_lrl + p_lrl)
|
|
216
|
+
return t_lrl, p_lrl, q_lrl, ["L", "R", "L"]
|
|
217
|
+
|
|
218
|
+
def _interpolate(self, mode: str, length: float,
|
|
219
|
+
init_pose: Tuple[float, float, float]) -> Tuple[float, float, float]:
|
|
220
|
+
"""
|
|
221
|
+
Planning path interpolation.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
mode: Motion type, one of {"L", "S", "R"}.
|
|
225
|
+
length: Single step motion path length.
|
|
226
|
+
init_pose: Initial pose (x, y, yaw).
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
new_pose: New pose (new_x, new_y, new_yaw) after moving.
|
|
230
|
+
"""
|
|
231
|
+
x, y, yaw = init_pose
|
|
232
|
+
|
|
233
|
+
if mode == "S":
|
|
234
|
+
new_x = x + length / self.max_curv * math.cos(yaw)
|
|
235
|
+
new_y = y + length / self.max_curv * math.sin(yaw)
|
|
236
|
+
new_yaw = yaw
|
|
237
|
+
elif mode == "L":
|
|
238
|
+
new_x = x + (math.sin(yaw + length) - math.sin(yaw)) / self.max_curv
|
|
239
|
+
new_y = y - (math.cos(yaw + length) - math.cos(yaw)) / self.max_curv
|
|
240
|
+
new_yaw = yaw + length
|
|
241
|
+
elif mode == "R":
|
|
242
|
+
new_x = x - (math.sin(yaw - length) - math.sin(yaw)) / self.max_curv
|
|
243
|
+
new_y = y + (math.cos(yaw - length) - math.cos(yaw)) / self.max_curv
|
|
244
|
+
new_yaw = yaw - length
|
|
245
|
+
else:
|
|
246
|
+
raise NotImplementedError
|
|
247
|
+
|
|
248
|
+
return new_x, new_y, new_yaw
|
|
249
|
+
|
|
250
|
+
def _generate_segment(self, start_pose: Tuple[float, float, float],
|
|
251
|
+
goal_pose: Tuple[float, float, float]):
|
|
252
|
+
"""
|
|
253
|
+
Generate a single Dubins curve segment between two poses.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
start_pose: Initial pose (x, y, yaw).
|
|
257
|
+
goal_pose: Target pose (x, y, yaw).
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
best_cost: Best planning path length.
|
|
261
|
+
best_mode: Best motion modes.
|
|
262
|
+
x_list: Trajectory of x.
|
|
263
|
+
y_list: Trajectory of y.
|
|
264
|
+
yaw_list: Trajectory of yaw.
|
|
265
|
+
"""
|
|
266
|
+
sx, sy, syaw = start_pose
|
|
267
|
+
gx, gy, gyaw = goal_pose
|
|
268
|
+
|
|
269
|
+
dx = gx - sx
|
|
270
|
+
dy = gy - sy
|
|
271
|
+
|
|
272
|
+
cos_s = math.cos(syaw)
|
|
273
|
+
sin_s = math.sin(syaw)
|
|
274
|
+
local_gx = dx * cos_s + dy * sin_s
|
|
275
|
+
local_gy = -dx * sin_s + dy * cos_s
|
|
276
|
+
local_gyaw = gyaw - syaw
|
|
277
|
+
|
|
278
|
+
dist = math.hypot(local_gx, local_gy) * self.max_curv
|
|
279
|
+
theta = Geometry.mod_to_2pi(math.atan2(local_gy, local_gx))
|
|
280
|
+
|
|
281
|
+
alpha = Geometry.mod_to_2pi(-theta)
|
|
282
|
+
beta = Geometry.mod_to_2pi(local_gyaw - theta)
|
|
283
|
+
|
|
284
|
+
planners = [self._lsl, self._rsr, self._lsr, self._rsl, self._rlr, self._lrl]
|
|
285
|
+
best_t, best_p, best_q, best_mode, best_cost = None, None, None, None, float("inf")
|
|
286
|
+
|
|
287
|
+
for planner in planners:
|
|
288
|
+
t, p, q, mode = planner(alpha, beta, dist)
|
|
289
|
+
if t is None:
|
|
290
|
+
continue
|
|
291
|
+
cost = abs(t) + abs(p) + abs(q)
|
|
292
|
+
if best_cost > cost:
|
|
293
|
+
best_t, best_p, best_q, best_mode, best_cost = t, p, q, mode, cost
|
|
294
|
+
|
|
295
|
+
if best_mode is None:
|
|
296
|
+
return None, None, [], [], []
|
|
297
|
+
|
|
298
|
+
segments = [best_t, best_p, best_q]
|
|
299
|
+
points_num = int(sum(segments) / self.step) + len(segments) + 3
|
|
300
|
+
x_list = [0.0 for _ in range(points_num)]
|
|
301
|
+
y_list = [0.0 for _ in range(points_num)]
|
|
302
|
+
yaw_list = [0.0 for _ in range(points_num)]
|
|
303
|
+
|
|
304
|
+
idx = 0
|
|
305
|
+
for mode_, seg_length in zip(best_mode, segments):
|
|
306
|
+
d_length = self.step if seg_length > 0.0 else -self.step
|
|
307
|
+
current_x, current_y, current_yaw = x_list[idx], y_list[idx], yaw_list[idx]
|
|
308
|
+
length = d_length
|
|
309
|
+
while abs(length) <= abs(seg_length):
|
|
310
|
+
idx += 1
|
|
311
|
+
current_x, current_y, current_yaw = self._interpolate(
|
|
312
|
+
mode_, d_length, (current_x, current_y, current_yaw)
|
|
313
|
+
)
|
|
314
|
+
x_list[idx], y_list[idx], yaw_list[idx] = current_x, current_y, current_yaw
|
|
315
|
+
length += d_length
|
|
316
|
+
|
|
317
|
+
idx += 1
|
|
318
|
+
remainder = seg_length - (length - d_length)
|
|
319
|
+
x_list[idx], y_list[idx], yaw_list[idx] = self._interpolate(
|
|
320
|
+
mode_, remainder, (x_list[idx-1], y_list[idx-1], y_list[idx-1])
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
x_list = x_list[:idx + 1]
|
|
324
|
+
y_list = y_list[:idx + 1]
|
|
325
|
+
yaw_list = yaw_list[:idx + 1]
|
|
326
|
+
|
|
327
|
+
if len(x_list) <= 1:
|
|
328
|
+
return None, None, [], [], []
|
|
329
|
+
|
|
330
|
+
rot = Rot.from_euler('z', syaw).as_matrix()[0:2, 0:2]
|
|
331
|
+
converted_xy = rot @ np.stack([x_list, y_list])
|
|
332
|
+
x_list = (converted_xy[0, :] + sx).tolist()
|
|
333
|
+
y_list = (converted_xy[1, :] + sy).tolist()
|
|
334
|
+
yaw_list = [Geometry.regularize_orient(i_yaw + syaw) for i_yaw in yaw_list]
|
|
335
|
+
|
|
336
|
+
return best_cost, best_mode, x_list, y_list, yaw_list
|
|
337
|
+
|
|
338
|
+
def trigonometric(self, alpha: float, beta: float) -> Tuple[float, float, float, float, float, float]:
|
|
339
|
+
"""
|
|
340
|
+
Calculate trigonometric values for alpha and beta.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
alpha: Initial heading angle.
|
|
344
|
+
beta: Goal heading angle.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
sin_a, sin_b, cos_a, cos_b, cos_ab, cos_a_b: Trigonometric values.
|
|
348
|
+
"""
|
|
349
|
+
sin_a = math.sin(alpha)
|
|
350
|
+
sin_b = math.sin(beta)
|
|
351
|
+
cos_a = math.cos(alpha)
|
|
352
|
+
cos_b = math.cos(beta)
|
|
353
|
+
cos_ab = math.cos(alpha - beta)
|
|
354
|
+
cos_a_b = math.cos(alpha + beta) if hasattr(self, '_use_sum') else cos_ab
|
|
355
|
+
return sin_a, sin_b, cos_a, cos_b, cos_ab, cos_ab
|