python-motion-planning 2.0.dev2__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/grid.py +394 -129
- python_motion_planning/common/utils/geometry.py +18 -29
- python_motion_planning/path_planner/sample_search/rrt.py +5 -5
- 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.0.1.dist-info}/METADATA +22 -15
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/RECORD +22 -20
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.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.0.1.dist-info}/licenses/LICENSE +0 -0
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -35,42 +35,31 @@ class Geometry:
|
|
|
35
35
|
else:
|
|
36
36
|
raise ValueError("Invalid distance type")
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Args:
|
|
44
|
-
# v1: First vector
|
|
45
|
-
# v2: Second vector
|
|
46
|
-
|
|
47
|
-
# Returns:
|
|
48
|
-
# angle_rad: Angle in rad between the two vectors
|
|
49
|
-
# """
|
|
50
|
-
# if len(v1) != len(v2):
|
|
51
|
-
# raise ValueError("Dimension mismatch")
|
|
52
|
-
|
|
53
|
-
# dot_product = sum(a * b for a, b in zip(v1, v2))
|
|
54
|
-
# v1_norm = math.sqrt(sum(a**2 for a in v1))
|
|
55
|
-
# v2_norm = math.sqrt(sum(b**2 for b in v2))
|
|
56
|
-
|
|
57
|
-
# if v1_norm == 0 or v2_norm == 0:
|
|
58
|
-
# raise ValueError("Zero vector cannot calculate angle")
|
|
59
|
-
|
|
60
|
-
# cos_theta = dot_product / (v1_norm * v2_norm)
|
|
38
|
+
@staticmethod
|
|
39
|
+
def mod_to_2pi(orient: np.ndarray) -> np.ndarray:
|
|
40
|
+
"""
|
|
41
|
+
Regularize orientation to be within [0, 2*pi)
|
|
61
42
|
|
|
62
|
-
|
|
43
|
+
Args:
|
|
44
|
+
orient: the orientation angle
|
|
63
45
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
Returns:
|
|
47
|
+
new_orient: modded orientation
|
|
48
|
+
"""
|
|
49
|
+
return np.mod(orient, 2 * np.pi)
|
|
67
50
|
|
|
68
51
|
@staticmethod
|
|
69
52
|
def regularize_orient(orient: np.ndarray) -> np.ndarray:
|
|
70
53
|
"""
|
|
71
54
|
Regularize orientation to be within (-pi, pi]
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
orient: the orientation angle
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
new_orient: regularized orientation
|
|
72
61
|
"""
|
|
73
|
-
return np.mod(orient + np.pi, 2 * np.pi)
|
|
62
|
+
return -np.mod(-orient + np.pi, 2 * np.pi) + np.pi
|
|
74
63
|
|
|
75
64
|
@staticmethod
|
|
76
65
|
def add_orient_to_2d_path(path: List[Tuple[float, float]]) -> List[Tuple[float, float, float]]:
|
|
@@ -102,4 +91,4 @@ class Geometry:
|
|
|
102
91
|
last_x, last_y = path[-1]
|
|
103
92
|
path_with_orient.append((last_x, last_y, path_with_orient[-1][2]))
|
|
104
93
|
|
|
105
|
-
return path_with_orient
|
|
94
|
+
return path_with_orient
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: rrt.py
|
|
3
3
|
@author: Wu Maojia, Yang Haodong
|
|
4
|
-
@update:
|
|
4
|
+
@update: 2026.4.12
|
|
5
5
|
"""
|
|
6
6
|
import math
|
|
7
7
|
import random
|
|
@@ -20,7 +20,7 @@ class RRT(BasePathPlanner):
|
|
|
20
20
|
Args:
|
|
21
21
|
*args: see the parent class.
|
|
22
22
|
max_dist: Maximum expansion distance for each step.
|
|
23
|
-
|
|
23
|
+
max_sample_step: Maximum number of steps of samples to generate.
|
|
24
24
|
goal_sample_rate: Probability of sampling the goal directly.
|
|
25
25
|
discrete: Whether to use discrete or continuous space.
|
|
26
26
|
faiss: Whether to use Faiss to accelerate the search.
|
|
@@ -42,14 +42,14 @@ class RRT(BasePathPlanner):
|
|
|
42
42
|
True
|
|
43
43
|
"""
|
|
44
44
|
def __init__(self, *args,
|
|
45
|
-
max_dist: float = 5.0,
|
|
45
|
+
max_dist: float = 5.0, max_sample_step: int = 100000,
|
|
46
46
|
goal_sample_rate: float = 0.05,
|
|
47
47
|
discrete: bool = False,
|
|
48
48
|
use_faiss: bool = True,
|
|
49
49
|
**kwargs) -> None:
|
|
50
50
|
super().__init__(*args, **kwargs)
|
|
51
51
|
self.max_dist = max_dist
|
|
52
|
-
self.
|
|
52
|
+
self.max_sample_step = max_sample_step
|
|
53
53
|
self.goal_sample_rate = goal_sample_rate
|
|
54
54
|
self.discrete = discrete
|
|
55
55
|
self.use_faiss = use_faiss
|
|
@@ -77,7 +77,7 @@ class RRT(BasePathPlanner):
|
|
|
77
77
|
self._faiss_add_node(start_node, faiss_index, faiss_nodes)
|
|
78
78
|
|
|
79
79
|
# Main sampling loop
|
|
80
|
-
for _ in range(self.
|
|
80
|
+
for _ in range(self.max_sample_step):
|
|
81
81
|
# Generate random sample node
|
|
82
82
|
node_rand = self._generate_random_node()
|
|
83
83
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: rrt_connect.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update:
|
|
4
|
+
@update: 2026.4.12
|
|
5
5
|
"""
|
|
6
6
|
from typing import Union, Dict, List, Tuple, Any
|
|
7
7
|
|
|
@@ -73,7 +73,7 @@ class RRTConnect(RRT):
|
|
|
73
73
|
self._faiss_add_node(self.tree_b[self.goal], self.index_b, self.nodes_b)
|
|
74
74
|
|
|
75
75
|
# Main planning loop
|
|
76
|
-
for _ in range(self.
|
|
76
|
+
for _ in range(self.max_sample_step):
|
|
77
77
|
# Generate random sample node
|
|
78
78
|
node_rand = self._generate_random_node()
|
|
79
79
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: rrt_star.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update:
|
|
4
|
+
@update: 2026.4.12
|
|
5
5
|
"""
|
|
6
6
|
import math
|
|
7
7
|
import random
|
|
@@ -27,7 +27,12 @@ class RRTStar(RRT):
|
|
|
27
27
|
*args: see parent class.
|
|
28
28
|
rewire_radius: Neighborhood radius for rewiring (If None, adaptively calculated with gamma factor).
|
|
29
29
|
gamma: A factor for calculating rewire_radius. For details, see [1]. (Disabled when rewire_radius is not None)
|
|
30
|
-
|
|
30
|
+
stop_func: A callable(cur_step, first_success_step, max_sample_step) -> bool that controls when to stop sampling. Called at the beginning of each iteration.
|
|
31
|
+
Arguments:
|
|
32
|
+
- cur_step (int): Number of sampling iterations completed so far (starts from 0, incremented before each sampling, so the first completed sample has cur_step=1).
|
|
33
|
+
- first_success_step (int or None): The cur_step value at which a feasible path was first found, or None if no path has been found yet.
|
|
34
|
+
- max_sample_step (int): The planner's max_sample_step attribute, provided for convenience.
|
|
35
|
+
Returns True to stop, False to continue.
|
|
31
36
|
propagate_cost_to_children: Whether to propagate cost to children. This is a fix for ensuring the correctness of g-value of each node. But it may slow the algorithm a little.
|
|
32
37
|
*kwargs: see parent class.
|
|
33
38
|
|
|
@@ -41,7 +46,7 @@ class RRTStar(RRT):
|
|
|
41
46
|
>>> print(path_info['success'])
|
|
42
47
|
True
|
|
43
48
|
|
|
44
|
-
>>> planner = RRTStar(map_=map_, start=(5, 5), goal=(10, 10),
|
|
49
|
+
>>> planner = RRTStar(map_=map_, start=(5, 5), goal=(10, 10), max_sample_step=100000, stop_func=lambda current_step, first_success_step, max_step: (first_success_step is not None) or (current_step >= max_step))
|
|
45
50
|
>>> path, path_info = planner.plan()
|
|
46
51
|
>>> print(path_info['success'])
|
|
47
52
|
True
|
|
@@ -50,13 +55,18 @@ class RRTStar(RRT):
|
|
|
50
55
|
def __init__(self, *args,
|
|
51
56
|
rewire_radius: float = None,
|
|
52
57
|
gamma: float = 50.0,
|
|
53
|
-
|
|
58
|
+
stop_func: callable = lambda current_step, first_success_step, max_step: (first_success_step is not None) or (current_step >= max_step),
|
|
54
59
|
propagate_cost_to_children: bool = True,
|
|
55
60
|
**kwargs) -> None:
|
|
56
61
|
super().__init__(*args, **kwargs)
|
|
57
62
|
self.rewire_radius = rewire_radius
|
|
58
63
|
self.gamma = gamma
|
|
59
|
-
self.
|
|
64
|
+
self.stop_func = stop_func
|
|
65
|
+
|
|
66
|
+
self.failed_info[1]["first_success_step"] = None
|
|
67
|
+
self.failed_info[1]["best_step"] = None
|
|
68
|
+
self.failed_info[1]["total_step"] = None
|
|
69
|
+
|
|
60
70
|
self.best_results = self.failed_info
|
|
61
71
|
self.propagate_cost_to_children = propagate_cost_to_children
|
|
62
72
|
|
|
@@ -86,7 +96,14 @@ class RRTStar(RRT):
|
|
|
86
96
|
faiss_nodes = []
|
|
87
97
|
self._faiss_add_node(start_node, faiss_index, faiss_nodes)
|
|
88
98
|
|
|
89
|
-
|
|
99
|
+
i = 0
|
|
100
|
+
first_success_i = None
|
|
101
|
+
while True:
|
|
102
|
+
if self.stop_func(i, first_success_i, self.max_sample_step):
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
i += 1
|
|
106
|
+
|
|
90
107
|
# Generate random sample
|
|
91
108
|
node_rand = self._generate_random_node()
|
|
92
109
|
|
|
@@ -179,22 +196,25 @@ class RRTStar(RRT):
|
|
|
179
196
|
0
|
|
180
197
|
)
|
|
181
198
|
path, length, cost = self.extract_path(self._tree)
|
|
199
|
+
|
|
200
|
+
if first_success_i is None:
|
|
201
|
+
first_success_i = i
|
|
202
|
+
|
|
182
203
|
self.best_results = path, {
|
|
183
204
|
"success": True,
|
|
184
205
|
"start": self.start,
|
|
185
206
|
"goal": self.goal,
|
|
186
207
|
"length": length,
|
|
187
208
|
"cost": cost,
|
|
209
|
+
"first_success_step": first_success_i,
|
|
210
|
+
"best_step": i,
|
|
211
|
+
"total_step": i,
|
|
188
212
|
"expand": self._tree,
|
|
189
213
|
}
|
|
190
214
|
|
|
191
|
-
if not self.stop_until_sample_num:
|
|
192
|
-
return self.best_results
|
|
193
|
-
|
|
194
|
-
n = len(self._tree) + 1
|
|
195
|
-
radius = self.gamma * ((math.log(n) / n) ** (1 / self.dim))
|
|
196
215
|
|
|
197
216
|
# Planning stopped
|
|
217
|
+
self.best_results[1]["total_step"] = i
|
|
198
218
|
self.best_results[1]["expand"] = self._tree
|
|
199
219
|
return self.best_results
|
|
200
220
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: base_curve_generator.py
|
|
3
|
+
@author: Yang Haodong, Wu Maojia
|
|
4
|
+
@update: 2026.4.12
|
|
5
|
+
"""
|
|
6
|
+
from typing import List, Tuple, Dict, Any
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
import math
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseCurveGenerator(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Base class for curve generator (trajectory smoother).
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
step: Step size for interpolation or discretization along the curve.
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, step: float = 0.01) -> None:
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.step = step
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
return "Base Curve Generator"
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def generate(self, points: List[Tuple[float, ...]]) -> Tuple[List[Tuple[float, ...]], Dict[str, Any]]:
|
|
27
|
+
"""
|
|
28
|
+
Interface for curve generation.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
points: A list of waypoints in world frame. The exact format (2D position
|
|
32
|
+
or 2D pose with orientation) depends on the concrete generator.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
path: A list containing the smoothed path waypoints in world frame.
|
|
36
|
+
curve_info: A dictionary containing the curve information (success, length).
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
def length(self, path: List[Tuple[float, ...]]) -> float:
|
|
41
|
+
"""
|
|
42
|
+
Calculate the length of a path.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
path: A list containing the path waypoints.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
length: Length of the path.
|
|
49
|
+
"""
|
|
50
|
+
dist = 0.0
|
|
51
|
+
for i in range(len(path) - 1):
|
|
52
|
+
dist += math.hypot(path[i + 1][0] - path[i][0], path[i + 1][1] - path[i][1])
|
|
53
|
+
return dist
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: bspline_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 BSpline(BaseCurveGenerator):
|
|
15
|
+
"""
|
|
16
|
+
Class for B-Spline curve generator.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
*args: see the parent class.
|
|
20
|
+
k: Degree of the B-Spline curve.
|
|
21
|
+
param_mode: Parameter selection mode, one of {"centripetal", "chord_length", "uniform_spaced"}.
|
|
22
|
+
spline_mode: B-Spline construction mode, one of {"interpolation", "approximation"}.
|
|
23
|
+
*kwargs: see the parent class.
|
|
24
|
+
|
|
25
|
+
References:
|
|
26
|
+
[1] https://en.wikipedia.org/wiki/B-spline
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> generator = BSpline(step=0.1, k=3)
|
|
30
|
+
>>> points = [(0.0, 0.0), (10.0, 10.0), (20.0, 5.0)]
|
|
31
|
+
>>> path, curve_info = generator.generate(points)
|
|
32
|
+
>>> print(curve_info['success'])
|
|
33
|
+
True
|
|
34
|
+
"""
|
|
35
|
+
def __init__(self, *args,
|
|
36
|
+
k: int = 3,
|
|
37
|
+
param_mode: str = "centripetal",
|
|
38
|
+
spline_mode: str = "interpolation",
|
|
39
|
+
**kwargs) -> None:
|
|
40
|
+
super().__init__(*args, **kwargs)
|
|
41
|
+
self.k = k
|
|
42
|
+
|
|
43
|
+
if param_mode not in ("centripetal", "chord_length", "uniform_spaced"):
|
|
44
|
+
raise ValueError("Parameter selection mode error!")
|
|
45
|
+
self.param_mode = param_mode
|
|
46
|
+
|
|
47
|
+
if spline_mode not in ("interpolation", "approximation"):
|
|
48
|
+
raise ValueError("Spline mode selection error!")
|
|
49
|
+
self.spline_mode = spline_mode
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
return "B-Spline Curve"
|
|
53
|
+
|
|
54
|
+
def generate(self, points: List[Tuple[float, ...]]) -> Tuple[List[Tuple[float, float]], Dict[str, Any]]:
|
|
55
|
+
"""
|
|
56
|
+
Generate a B-Spline curve through a list of 2D points.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
points: A list of 2D points (x, y) in world frame. If the points contain
|
|
60
|
+
additional entries (e.g. yaw), only the first two values are used.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
path: A list of (x, y) waypoints of the generated curve in world frame.
|
|
64
|
+
curve_info: A dictionary containing the curve information (success, length).
|
|
65
|
+
"""
|
|
66
|
+
if len(points) < 2:
|
|
67
|
+
return [], {"success": False, "length": 0.0}
|
|
68
|
+
|
|
69
|
+
points_2d = [(float(p[0]), float(p[1])) for p in points]
|
|
70
|
+
|
|
71
|
+
t = np.linspace(0, 1, int(1 / self.step))
|
|
72
|
+
params = self._param_selection(points_2d)
|
|
73
|
+
knot = self._knot_generation(params, len(points_2d))
|
|
74
|
+
|
|
75
|
+
if self.spline_mode == "interpolation":
|
|
76
|
+
control_pts = self._interpolation(points_2d, params, knot)
|
|
77
|
+
else:
|
|
78
|
+
control_pts = self._approximation(points_2d, params, knot)
|
|
79
|
+
h = len(control_pts)
|
|
80
|
+
new_points = [(control_pts[i][0], control_pts[i][1]) for i in range(h)]
|
|
81
|
+
params = self._param_selection(new_points)
|
|
82
|
+
knot = self._knot_generation(params, h)
|
|
83
|
+
|
|
84
|
+
curve = self._generate_curve(t, self.k, knot, control_pts)
|
|
85
|
+
path = [(float(pt[0]), float(pt[1])) for pt in curve]
|
|
86
|
+
|
|
87
|
+
return path, {"success": True, "length": self.length(path)}
|
|
88
|
+
|
|
89
|
+
def _base_function(self, i: int, k: int, t: float, knot: List[float]) -> float:
|
|
90
|
+
"""
|
|
91
|
+
Calculate the base function using the Cox-de Boor recursion.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
i: The index of the base function.
|
|
95
|
+
k: The degree of the curve.
|
|
96
|
+
t: Parameter value.
|
|
97
|
+
knot: The knot vector.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Nik_t: The value of the base function Nik(t).
|
|
101
|
+
"""
|
|
102
|
+
Nik_t = 0.0
|
|
103
|
+
if k == 0:
|
|
104
|
+
Nik_t = 1.0 if knot[i] <= t < knot[i + 1] else 0.0
|
|
105
|
+
else:
|
|
106
|
+
length1 = knot[i + k] - knot[i]
|
|
107
|
+
length2 = knot[i + k + 1] - knot[i + 1]
|
|
108
|
+
if not length1 and not length2:
|
|
109
|
+
Nik_t = 0.0
|
|
110
|
+
elif not length1:
|
|
111
|
+
Nik_t = (knot[i + k + 1] - t) / length2 * self._base_function(i + 1, k - 1, t, knot)
|
|
112
|
+
elif not length2:
|
|
113
|
+
Nik_t = (t - knot[i]) / length1 * self._base_function(i, k - 1, t, knot)
|
|
114
|
+
else:
|
|
115
|
+
Nik_t = ((t - knot[i]) / length1 * self._base_function(i, k - 1, t, knot)
|
|
116
|
+
+ (knot[i + k + 1] - t) / length2 * self._base_function(i + 1, k - 1, t, knot))
|
|
117
|
+
return Nik_t
|
|
118
|
+
|
|
119
|
+
def _param_selection(self, points: List[Tuple[float, float]]) -> List[float]:
|
|
120
|
+
"""
|
|
121
|
+
Calculate the parameters of the given points using the selected method.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
points: A list of 2D points.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
parameters: The parameters of the given points.
|
|
128
|
+
"""
|
|
129
|
+
n = len(points)
|
|
130
|
+
x_list = [pt[0] for pt in points]
|
|
131
|
+
y_list = [pt[1] for pt in points]
|
|
132
|
+
dx, dy = np.diff(x_list), np.diff(y_list)
|
|
133
|
+
|
|
134
|
+
if self.param_mode == "uniform_spaced":
|
|
135
|
+
return np.linspace(0, 1, n).tolist()
|
|
136
|
+
|
|
137
|
+
if self.param_mode == "chord_length":
|
|
138
|
+
parameters = np.zeros(n)
|
|
139
|
+
s = np.cumsum([math.hypot(idx, idy) for (idx, idy) in zip(dx, dy)])
|
|
140
|
+
for i in range(1, n):
|
|
141
|
+
parameters[i] = s[i - 1] / s[-1]
|
|
142
|
+
return parameters.tolist()
|
|
143
|
+
|
|
144
|
+
# centripetal
|
|
145
|
+
alpha = 0.5
|
|
146
|
+
s = np.cumsum([math.pow(math.hypot(idx, idy), alpha) for (idx, idy) in zip(dx, dy)])
|
|
147
|
+
parameters = np.zeros(n)
|
|
148
|
+
for i in range(1, n):
|
|
149
|
+
parameters[i] = s[i - 1] / s[-1]
|
|
150
|
+
return parameters.tolist()
|
|
151
|
+
|
|
152
|
+
def _knot_generation(self, param: List[float], n: int) -> List[float]:
|
|
153
|
+
"""
|
|
154
|
+
Generate the knot vector.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
param: The parameters of the given points.
|
|
158
|
+
n: The number of data points.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
knot: The knot vector.
|
|
162
|
+
"""
|
|
163
|
+
m = n + self.k + 1
|
|
164
|
+
knot = np.zeros(m)
|
|
165
|
+
for i in range(n, m):
|
|
166
|
+
knot[i] = 1
|
|
167
|
+
for i in range(self.k + 1, n):
|
|
168
|
+
for j in range(i - self.k, i):
|
|
169
|
+
knot[i] = knot[i] + param[j]
|
|
170
|
+
knot[i] = knot[i] / self.k
|
|
171
|
+
return knot.tolist()
|
|
172
|
+
|
|
173
|
+
def _interpolation(self, points: List[Tuple[float, float]], param: List[float],
|
|
174
|
+
knot: List[float]) -> np.ndarray:
|
|
175
|
+
"""
|
|
176
|
+
Build a B-Spline curve of degree k defined by N control points that passes
|
|
177
|
+
through all N data points.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
points: A list of 2D points.
|
|
181
|
+
param: The parameters of the given points.
|
|
182
|
+
knot: The knot vector.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
control_points: The control points of the resulting B-Spline.
|
|
186
|
+
"""
|
|
187
|
+
n = len(points)
|
|
188
|
+
N = np.zeros((n, n))
|
|
189
|
+
|
|
190
|
+
for i in range(n):
|
|
191
|
+
for j in range(n):
|
|
192
|
+
N[i][j] = self._base_function(j, self.k, param[i], knot)
|
|
193
|
+
N[n - 1][n - 1] = 1
|
|
194
|
+
N_inv = np.linalg.inv(N)
|
|
195
|
+
|
|
196
|
+
D = np.array(points)
|
|
197
|
+
return N_inv @ D
|
|
198
|
+
|
|
199
|
+
def _approximation(self, points: List[Tuple[float, float]], param: List[float],
|
|
200
|
+
knot: List[float]) -> np.ndarray:
|
|
201
|
+
"""
|
|
202
|
+
Build a B-Spline curve of degree k defined by H control points (H < N) that
|
|
203
|
+
passes through the first and last data points and approximates the rest in
|
|
204
|
+
the least-square sense.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
points: A list of 2D points.
|
|
208
|
+
param: The parameters of the given points.
|
|
209
|
+
knot: The knot vector.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
control_points: The control points of the resulting B-Spline.
|
|
213
|
+
"""
|
|
214
|
+
n = len(points)
|
|
215
|
+
D = np.array(points)
|
|
216
|
+
|
|
217
|
+
h = n - 1
|
|
218
|
+
|
|
219
|
+
N = np.zeros((n, h))
|
|
220
|
+
for i in range(n):
|
|
221
|
+
for j in range(h):
|
|
222
|
+
N[i][j] = self._base_function(j, self.k, param[i], knot)
|
|
223
|
+
N_ = N[1: n - 1, 1: h - 1]
|
|
224
|
+
|
|
225
|
+
qk = np.zeros((n - 2, 2))
|
|
226
|
+
for i in range(1, n - 1):
|
|
227
|
+
qk[i - 1] = D[i, :] - N[i][0] * D[0, :] - N[i][h - 1] * D[-1, :]
|
|
228
|
+
Q = N_.T @ qk
|
|
229
|
+
|
|
230
|
+
P = np.linalg.inv(N_.T @ N_) @ Q
|
|
231
|
+
P = np.insert(P, 0, D[0, :], axis=0)
|
|
232
|
+
P = np.insert(P, len(P), D[-1, :], axis=0)
|
|
233
|
+
return P
|
|
234
|
+
|
|
235
|
+
def _generate_curve(self, t: np.ndarray, k: int, knot: List[float],
|
|
236
|
+
control_pts: np.ndarray) -> np.ndarray:
|
|
237
|
+
"""
|
|
238
|
+
Sample points on the B-Spline curve.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
t: The parameter values to sample at.
|
|
242
|
+
k: The degree of the B-Spline curve.
|
|
243
|
+
knot: The knot vector.
|
|
244
|
+
control_pts: The control points.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
curve: Sampled points along the B-Spline curve.
|
|
248
|
+
"""
|
|
249
|
+
N = np.zeros((len(t), len(control_pts)))
|
|
250
|
+
|
|
251
|
+
for i in range(len(t)):
|
|
252
|
+
for j in range(len(control_pts)):
|
|
253
|
+
N[i][j] = self._base_function(j, k, t[i], knot)
|
|
254
|
+
N[len(t) - 1][len(control_pts) - 1] = 1
|
|
255
|
+
|
|
256
|
+
return N @ control_pts
|
|
@@ -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
|