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.
Files changed (60) hide show
  1. python_motion_planning/__init__.py +1 -1
  2. python_motion_planning/common/env/map/base_map.py +2 -8
  3. python_motion_planning/common/env/map/grid.py +456 -198
  4. python_motion_planning/common/utils/__init__.py +2 -1
  5. python_motion_planning/common/utils/child_tree.py +22 -0
  6. python_motion_planning/common/utils/geometry.py +18 -29
  7. python_motion_planning/common/visualizer/__init__.py +3 -1
  8. python_motion_planning/common/visualizer/base_visualizer.py +165 -0
  9. python_motion_planning/common/visualizer/{visualizer.py → visualizer_2d.py} +97 -220
  10. python_motion_planning/common/visualizer/visualizer_3d.py +242 -0
  11. python_motion_planning/controller/base_controller.py +37 -4
  12. python_motion_planning/controller/path_tracker/__init__.py +2 -1
  13. python_motion_planning/controller/path_tracker/apf.py +22 -23
  14. python_motion_planning/controller/path_tracker/dwa.py +14 -17
  15. python_motion_planning/controller/path_tracker/path_tracker.py +4 -1
  16. python_motion_planning/controller/path_tracker/pid.py +7 -1
  17. python_motion_planning/controller/path_tracker/pure_pursuit.py +7 -1
  18. python_motion_planning/controller/path_tracker/rpp.py +111 -0
  19. python_motion_planning/path_planner/__init__.py +2 -1
  20. python_motion_planning/path_planner/base_path_planner.py +45 -11
  21. python_motion_planning/path_planner/graph_search/__init__.py +4 -1
  22. python_motion_planning/path_planner/graph_search/a_star.py +12 -14
  23. python_motion_planning/path_planner/graph_search/dijkstra.py +15 -15
  24. python_motion_planning/path_planner/graph_search/gbfs.py +100 -0
  25. python_motion_planning/path_planner/graph_search/jps.py +199 -0
  26. python_motion_planning/path_planner/graph_search/lazy_theta_star.py +113 -0
  27. python_motion_planning/path_planner/graph_search/theta_star.py +17 -19
  28. python_motion_planning/path_planner/hybrid_search/__init__.py +1 -0
  29. python_motion_planning/path_planner/hybrid_search/voronoi_planner.py +204 -0
  30. python_motion_planning/path_planner/sample_search/__init__.py +2 -1
  31. python_motion_planning/path_planner/sample_search/rrt.py +73 -31
  32. python_motion_planning/path_planner/sample_search/rrt_connect.py +237 -0
  33. python_motion_planning/path_planner/sample_search/rrt_star.py +220 -150
  34. python_motion_planning/traj_optimizer/__init__.py +2 -0
  35. python_motion_planning/traj_optimizer/base_curve_generator.py +53 -0
  36. python_motion_planning/traj_optimizer/curve_generator/__init__.py +2 -0
  37. python_motion_planning/traj_optimizer/curve_generator/point_based/__init__.py +2 -0
  38. python_motion_planning/traj_optimizer/curve_generator/point_based/bspline.py +256 -0
  39. python_motion_planning/traj_optimizer/curve_generator/point_based/cubic_spline.py +115 -0
  40. python_motion_planning/traj_optimizer/curve_generator/pose_based/__init__.py +4 -0
  41. python_motion_planning/traj_optimizer/curve_generator/pose_based/bezier.py +121 -0
  42. python_motion_planning/traj_optimizer/curve_generator/pose_based/dubins.py +355 -0
  43. python_motion_planning/traj_optimizer/curve_generator/pose_based/polynomial.py +197 -0
  44. python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py +606 -0
  45. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/METADATA +71 -29
  46. python_motion_planning-2.0.1.dist-info/RECORD +64 -0
  47. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/WHEEL +1 -1
  48. python_motion_planning/common/env/robot/tmp.py +0 -404
  49. python_motion_planning/curve_generator/__init__.py +0 -9
  50. python_motion_planning/curve_generator/bezier_curve.py +0 -131
  51. python_motion_planning/curve_generator/bspline_curve.py +0 -271
  52. python_motion_planning/curve_generator/cubic_spline.py +0 -128
  53. python_motion_planning/curve_generator/curve.py +0 -64
  54. python_motion_planning/curve_generator/dubins_curve.py +0 -348
  55. python_motion_planning/curve_generator/fem_pos_smooth.py +0 -114
  56. python_motion_planning/curve_generator/polynomial_curve.py +0 -226
  57. python_motion_planning/curve_generator/reeds_shepp.py +0 -736
  58. python_motion_planning-2.0.dev1.dist-info/RECORD +0 -53
  59. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/licenses/LICENSE +0 -0
  60. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/top_level.txt +0 -0
@@ -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
@@ -0,0 +1,197 @@
1
+ """
2
+ @file: polynomial_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
+
11
+ from python_motion_planning.traj_optimizer.base_curve_generator import BaseCurveGenerator
12
+
13
+
14
+ class Polynomial(BaseCurveGenerator):
15
+ """
16
+ Class for quintic polynomial curve generator.
17
+
18
+ Args:
19
+ *args: see the parent class.
20
+ max_acc: Maximum allowed acceleration magnitude.
21
+ max_jerk: Maximum allowed jerk magnitude.
22
+ *args: see the parent class.
23
+
24
+ References:
25
+ [1] https://en.wikipedia.org/wiki/Polynomial_trajectory
26
+
27
+ Examples:
28
+ >>> import math
29
+ >>> generator = Polynomial(step=0.1, max_acc=2.82, max_jerk=10.0)
30
+ >>> points = [(0.0, 0.0, 0.0), (10.0, 10.0, -math.pi/2), (20.0, 5.0, math.pi/3)]
31
+ >>> path, curve_info = generator.generate(points)
32
+ >>> print(curve_info['success'])
33
+ True
34
+ """
35
+ def __init__(self, *args,
36
+ max_acc: float = 2.82,
37
+ max_jerk: float = 10.0,
38
+ **kwargs) -> None:
39
+ super().__init__(*args, **kwargs)
40
+ self.max_acc = max_acc
41
+ self.max_jerk = max_jerk
42
+ self.dt = 0.1
43
+ self.t_min = 1
44
+ self.t_max = 30
45
+
46
+ def __str__(self) -> str:
47
+ return "Quintic Polynomial Curve"
48
+
49
+ class _Poly:
50
+ """
51
+ Quintic polynomial solver for a single dimension.
52
+ """
53
+ def __init__(self, state0: tuple, state1: tuple, t: float) -> None:
54
+ x0, v0, a0 = state0
55
+ xt, vt, at = state1
56
+
57
+ A = np.array([[t ** 3, t ** 4, t ** 5],
58
+ [3 * t ** 2, 4 * t ** 3, 5 * t ** 4],
59
+ [6 * t, 12 * t ** 2, 20 * t ** 3]])
60
+ b = np.array([xt - x0 - v0 * t - a0 * t ** 2 / 2,
61
+ vt - v0 - a0 * t,
62
+ at - a0])
63
+ X = np.linalg.solve(A, b)
64
+
65
+ self.p0 = x0
66
+ self.p1 = v0
67
+ self.p2 = a0 / 2.0
68
+ self.p3 = X[0]
69
+ self.p4 = X[1]
70
+ self.p5 = X[2]
71
+
72
+ def x(self, t: float) -> float:
73
+ return (self.p0 + self.p1 * t + self.p2 * t ** 2
74
+ + self.p3 * t ** 3 + self.p4 * t ** 4 + self.p5 * t ** 5)
75
+
76
+ def dx(self, t: float) -> float:
77
+ return (self.p1 + 2 * self.p2 * t + 3 * self.p3 * t ** 2
78
+ + 4 * self.p4 * t ** 3 + 5 * self.p5 * t ** 4)
79
+
80
+ def ddx(self, t: float) -> float:
81
+ return 2 * self.p2 + 6 * self.p3 * t + 12 * self.p4 * t ** 2 + 20 * self.p5 * t ** 3
82
+
83
+ def dddx(self, t: float) -> float:
84
+ return 6 * self.p3 + 24 * self.p4 * t + 60 * self.p5 * t ** 2
85
+
86
+ class _Trajectory:
87
+ """
88
+ Container for a polynomial trajectory.
89
+ """
90
+ def __init__(self):
91
+ self.clear()
92
+
93
+ def clear(self):
94
+ self.time = []
95
+ self.x = []
96
+ self.y = []
97
+ self.yaw = []
98
+ self.v = []
99
+ self.a = []
100
+ self.jerk = []
101
+
102
+ @property
103
+ def size(self) -> int:
104
+ assert (len(self.time) == len(self.x) == len(self.y) == len(self.yaw)
105
+ == len(self.v) == len(self.a) == len(self.jerk)), \
106
+ "Unequal dimensions of each attribute, this should not happen."
107
+ return len(self.time)
108
+
109
+ def generate(self, points: List[Tuple[float, ...]]) -> Tuple[List[Tuple[float, float, float]], Dict[str, Any]]:
110
+ """
111
+ Generate a concatenated quintic polynomial curve through a list of poses.
112
+
113
+ Args:
114
+ points: A list of poses (x, y, yaw) in world frame. Optionally the
115
+ points may also carry velocity and acceleration, i.e.
116
+ (x, y, yaw, v, a). Missing values are filled heuristically.
117
+
118
+ Returns:
119
+ path: A list of (x, y, yaw) waypoints of the generated curve in world frame.
120
+ curve_info: A dictionary containing the curve information (success, length).
121
+ """
122
+ if len(points) < 2:
123
+ return [], {"success": False, "length": 0.0}
124
+
125
+ states: List[Tuple[float, float, float, float, float]] = []
126
+ for i, pt in enumerate(points):
127
+ if len(pt) >= 5:
128
+ states.append((float(pt[0]), float(pt[1]), float(pt[2]), float(pt[3]), float(pt[4])))
129
+ elif len(pt) == 3:
130
+ v = 0.0 if (i == 0 or i == len(points) - 1) else 1.0
131
+ states.append((float(pt[0]), float(pt[1]), float(pt[2]), v, 0.0))
132
+ else:
133
+ raise ValueError("Points must be (x, y, yaw) or (x, y, yaw, v, a).")
134
+
135
+ path: List[Tuple[float, float, float]] = []
136
+ for i in range(len(states) - 1):
137
+ traj = self._generate_segment(states[i], states[i + 1])
138
+ for j in range(traj.size):
139
+ path.append((float(traj.x[j]), float(traj.y[j]), float(traj.yaw[j])))
140
+
141
+ success = len(path) > 0
142
+ return path, {"success": success, "length": self.length(path) if success else 0.0}
143
+
144
+ def _generate_segment(self, start_state: Tuple[float, float, float, float, float],
145
+ goal_state: Tuple[float, float, float, float, float]) -> "_Trajectory":
146
+ """
147
+ Generate a single quintic polynomial segment between two states.
148
+
149
+ Args:
150
+ start_state: Initial state (x, y, yaw, v, a).
151
+ goal_state: Target state (x, y, yaw, v, a).
152
+
153
+ Returns:
154
+ traj: The first trajectory that satisfies the acceleration and jerk constraints.
155
+ """
156
+ sx, sy, syaw, sv, sa = start_state
157
+ gx, gy, gyaw, gv, ga = goal_state
158
+
159
+ sv_x, sv_y = sv * math.cos(syaw), sv * math.sin(syaw)
160
+ gv_x, gv_y = gv * math.cos(gyaw), gv * math.sin(gyaw)
161
+
162
+ sa_x, sa_y = sa * math.cos(syaw), sa * math.sin(syaw)
163
+ ga_x, ga_y = ga * math.cos(gyaw), ga * math.sin(gyaw)
164
+
165
+ traj = self._Trajectory()
166
+
167
+ for T in np.arange(self.t_min, self.t_max, self.step):
168
+ x_psolver = self._Poly((sx, sv_x, sa_x), (gx, gv_x, ga_x), T)
169
+ y_psolver = self._Poly((sy, sv_y, sa_y), (gy, gv_y, ga_y), T)
170
+
171
+ for t in np.arange(0.0, T + self.dt, self.dt):
172
+ traj.time.append(t)
173
+ traj.x.append(x_psolver.x(t))
174
+ traj.y.append(y_psolver.x(t))
175
+
176
+ vx, vy = x_psolver.dx(t), y_psolver.dx(t)
177
+ traj.v.append(math.hypot(vx, vy))
178
+ traj.yaw.append(math.atan2(vy, vx))
179
+
180
+ ax, ay = x_psolver.ddx(t), y_psolver.ddx(t)
181
+ a = math.hypot(ax, ay)
182
+ if len(traj.v) >= 2 and traj.v[-1] - traj.v[-2] < 0.0:
183
+ a *= -1
184
+ traj.a.append(a)
185
+
186
+ jx, jy = x_psolver.dddx(t), y_psolver.dddx(t)
187
+ j = math.hypot(jx, jy)
188
+ if len(traj.a) >= 2 and traj.a[-1] - traj.a[-2] < 0.0:
189
+ j *= -1
190
+ traj.jerk.append(j)
191
+
192
+ if (max(np.abs(traj.a)) <= self.max_acc
193
+ and max(np.abs(traj.jerk)) <= self.max_jerk):
194
+ return traj
195
+ traj.clear()
196
+
197
+ return traj