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.
Files changed (31) hide show
  1. python_motion_planning/__init__.py +1 -1
  2. python_motion_planning/common/env/map/grid.py +394 -129
  3. python_motion_planning/common/utils/geometry.py +18 -29
  4. python_motion_planning/path_planner/sample_search/rrt.py +5 -5
  5. python_motion_planning/path_planner/sample_search/rrt_connect.py +2 -2
  6. python_motion_planning/path_planner/sample_search/rrt_star.py +31 -11
  7. python_motion_planning/traj_optimizer/__init__.py +2 -0
  8. python_motion_planning/traj_optimizer/base_curve_generator.py +53 -0
  9. python_motion_planning/traj_optimizer/curve_generator/__init__.py +2 -0
  10. python_motion_planning/traj_optimizer/curve_generator/point_based/__init__.py +2 -0
  11. python_motion_planning/traj_optimizer/curve_generator/point_based/bspline.py +256 -0
  12. python_motion_planning/traj_optimizer/curve_generator/point_based/cubic_spline.py +115 -0
  13. python_motion_planning/traj_optimizer/curve_generator/pose_based/__init__.py +4 -0
  14. python_motion_planning/traj_optimizer/curve_generator/pose_based/bezier.py +121 -0
  15. python_motion_planning/traj_optimizer/curve_generator/pose_based/dubins.py +355 -0
  16. python_motion_planning/traj_optimizer/curve_generator/pose_based/polynomial.py +197 -0
  17. python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py +606 -0
  18. {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/METADATA +22 -15
  19. {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/RECORD +22 -20
  20. {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/WHEEL +1 -1
  21. python_motion_planning/curve_generator/__init__.py +0 -9
  22. python_motion_planning/curve_generator/bezier_curve.py +0 -131
  23. python_motion_planning/curve_generator/bspline_curve.py +0 -271
  24. python_motion_planning/curve_generator/cubic_spline.py +0 -128
  25. python_motion_planning/curve_generator/curve.py +0 -64
  26. python_motion_planning/curve_generator/dubins_curve.py +0 -348
  27. python_motion_planning/curve_generator/fem_pos_smooth.py +0 -114
  28. python_motion_planning/curve_generator/polynomial_curve.py +0 -226
  29. python_motion_planning/curve_generator/reeds_shepp.py +0 -736
  30. {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/licenses/LICENSE +0 -0
  31. {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
- # @staticmethod
39
- # def angle(v1: tuple, v2: tuple) -> float:
40
- # """
41
- # Calculate the angle between two vectors
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
- # cos_theta = min(1.0, max(-1.0, cos_theta))
43
+ Args:
44
+ orient: the orientation angle
63
45
 
64
- # angle_rad = math.acos(cos_theta)
65
-
66
- # return angle_rad
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) - 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: 2025.12.19
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
- sample_num: Maximum number of samples to generate.
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, sample_num: int = 100000,
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.sample_num = sample_num
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.sample_num):
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: 2025.12.19
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.sample_num):
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: 2025.12.19
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
- stop_until_sample_num: Stop until sample number limitation is reached, otherwise stop when goal is found.
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), sample_num=1000, stop_until_sample_num=True)
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
- stop_until_sample_num: int = False,
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.stop_until_sample_num = stop_until_sample_num
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
- for i in range(self.sample_num):
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,2 @@
1
+ from .base_curve_generator import *
2
+ from .curve_generator import *
@@ -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,2 @@
1
+ from .point_based import *
2
+ from .pose_based import *
@@ -0,0 +1,2 @@
1
+ from .bspline import *
2
+ from .cubic_spline import *
@@ -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
@@ -0,0 +1,4 @@
1
+ from .bezier import *
2
+ from .dubins import *
3
+ from .polynomial import *
4
+ from .reeds_shepp import *