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
@@ -1,29 +1,43 @@
1
1
  """
2
2
  @file: rrt_star.py
3
- @author: Wu Maojia, Yang Haodong
4
- @update: 2025.10.3
3
+ @author: Wu Maojia
4
+ @update: 2026.4.12
5
5
  """
6
6
  import math
7
7
  import random
8
- from typing import Union, Dict, List, Tuple
8
+ from typing import Union, Dict, List, Tuple, Any
9
+
10
+ import numpy as np
11
+ import faiss
12
+
13
+ from python_motion_planning.common import Node, Grid, TYPES
14
+ from python_motion_planning.path_planner.sample_search import RRT
15
+ from python_motion_planning.common.utils.child_tree import ChildTree
9
16
 
10
- from python_motion_planning.common import BaseMap, Node, Grid, TYPES
11
- from python_motion_planning.path_planner.sample_search.rrt import RRT
12
17
 
13
18
  class RRTStar(RRT):
14
19
  """
15
20
  Class for RRT* (Rapidly-exploring Random Tree Star) path planner.
16
-
17
- RRT* is an optimized version of RRT that provides asymptotically optimal paths
18
- by rewiring the tree to maintain lower-cost connections.
21
+
22
+ RRT* extends RRT by:
23
+ 1. Selecting the best parent (minimum cost) for a new node.
24
+ 2. Rewiring nearby nodes through the new node if it improves their cost.
19
25
 
20
26
  Args:
21
- *args: see the parent class.
22
- radius: Radius for finding nearby nodes during rewiring.
23
- *kwargs: see the parent class.
27
+ *args: see parent class.
28
+ rewire_radius: Neighborhood radius for rewiring (If None, adaptively calculated with gamma factor).
29
+ gamma: A factor for calculating rewire_radius. For details, see [1]. (Disabled when rewire_radius is not None)
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.
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.
37
+ *kwargs: see parent class.
24
38
 
25
39
  References:
26
- [1] Sampling-based algorithms for optimal motion planning
40
+ [1] Sampling-based Algorithms for Optimal Motion Planning
27
41
 
28
42
  Examples:
29
43
  >>> map_ = Grid(bounds=[[0, 15], [0, 15]])
@@ -32,178 +46,234 @@ class RRTStar(RRT):
32
46
  >>> print(path_info['success'])
33
47
  True
34
48
 
35
- >>> planner.map_.type_map[3:10, 6] = TYPES.OBSTACLE
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))
36
50
  >>> path, path_info = planner.plan()
37
51
  >>> print(path_info['success'])
38
52
  True
39
53
  """
40
- def __init__(self, *args,
41
- radius: float = 10.0,** kwargs) -> None:
54
+
55
+ def __init__(self, *args,
56
+ rewire_radius: float = None,
57
+ gamma: float = 50.0,
58
+ stop_func: callable = lambda current_step, first_success_step, max_step: (first_success_step is not None) or (current_step >= max_step),
59
+ propagate_cost_to_children: bool = True,
60
+ **kwargs) -> None:
42
61
  super().__init__(*args, **kwargs)
43
- # Radius for finding nearby nodes during rewiring phase
44
- self.radius = radius
62
+ self.rewire_radius = rewire_radius
63
+ self.gamma = gamma
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
+
70
+ self.best_results = self.failed_info
71
+ self.propagate_cost_to_children = propagate_cost_to_children
72
+
73
+ self._tree = None
74
+ self._child = None
45
75
 
46
76
  def __str__(self) -> str:
47
- return "Rapidly-exploring Random Tree Star (RRT*)"
77
+ return "RRT*"
48
78
 
49
- def plan(self) -> Union[list, dict]:
79
+ def plan(self) -> Union[List[Tuple[float, ...]], Dict[str, Any]]:
50
80
  """
51
- RRT* path planning algorithm implementation with optimality properties.
52
-
81
+ RRT* path planning algorithm implementation.
82
+
53
83
  Returns:
54
84
  path: A list containing the path waypoints
55
85
  path_info: A dictionary containing path information
56
86
  """
57
87
  # Initialize tree with start node
58
- tree = {}
88
+ self._tree = {}
89
+ self._child = ChildTree()
59
90
  start_node = Node(self.start, None, 0, 0)
60
- tree[self.start] = start_node
91
+ self._tree[self.start] = start_node
92
+
93
+ # Initialize FAISS index
94
+ if self.use_faiss:
95
+ faiss_index = faiss.IndexFlatL2(self.dim)
96
+ faiss_nodes = []
97
+ self._faiss_add_node(start_node, faiss_index, faiss_nodes)
98
+
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
61
106
 
62
- # Main sampling loop
63
- for _ in range(self.sample_num):
64
- # Generate random sample node
107
+ # Generate random sample
65
108
  node_rand = self._generate_random_node()
66
-
67
- # Skip if node already exists
68
- if node_rand.current in tree:
109
+
110
+ if node_rand.current in self._tree:
69
111
  continue
70
-
71
- # Find nearest node in tree
72
- node_near = self._get_nearest_node(tree, node_rand)
73
-
74
- # Create new node towards random sample
75
- node_new = self._steer(node_near, node_rand)
112
+
113
+ # Find nearest node
114
+ node_nearest = self._get_nearest_node(self._tree, node_rand, faiss_index, faiss_nodes)
115
+
116
+ # Steer towards random sample
117
+ node_new = self._steer(node_nearest, node_rand)
76
118
  if node_new is None:
77
119
  continue
78
-
79
- # Check if edge is collision-free
80
- if self.map_.in_collision(node_new.current, node_near.current):
81
- continue
82
120
 
83
- # Find all nearby nodes within radius
84
- near_nodes = self._find_near_nodes(tree, node_new)
85
-
86
- # Select optimal parent from nearby nodes
87
- node_new, node_near = self._choose_parent(node_new, node_near, near_nodes)
88
- if node_new is None:
121
+ # Collision check
122
+ if self.map_.in_collision(
123
+ self.map_.point_float_to_int(node_nearest.current),
124
+ self.map_.point_float_to_int(node_new.current)
125
+ ):
89
126
  continue
90
-
91
- # Add new node to tree
92
- tree[node_new.current] = node_new
93
-
94
- # Rewire tree to potentially improve existing paths
95
- self._rewire(tree, node_new, near_nodes)
96
127
 
97
- # Check if goal is reachable
128
+ # Find nearby nodes for choosing best parent
129
+ near_nodes = self._get_near_nodes(node_new, faiss_index, faiss_nodes)
130
+
131
+ # Choose parent with minimum cost
132
+ min_parent = node_nearest
133
+ min_cost = node_nearest.g + self.get_cost(node_nearest.current, node_new.current)
134
+
135
+ for node in near_nodes:
136
+ if self.map_.in_collision(
137
+ self.map_.point_float_to_int(node.current),
138
+ self.map_.point_float_to_int(node_new.current)
139
+ ):
140
+ continue
141
+ cost = node.g + self.get_cost(node.current, node_new.current)
142
+ if cost < min_cost:
143
+ min_parent = node
144
+ min_cost = cost
145
+
146
+ # Add new node
147
+ if self.propagate_cost_to_children:
148
+ self._child.remove(node_new.parent, node_new.current)
149
+ self._child.add(min_parent.current, node_new.current)
150
+
151
+ node_new.parent = min_parent.current
152
+ node_new.g = min_cost
153
+ self._tree[node_new.current] = node_new
154
+ if self.use_faiss:
155
+ self._faiss_add_node(node_new, faiss_index, faiss_nodes)
156
+
157
+ # Rewire nearby nodes through new node
158
+ for node in near_nodes:
159
+ if node.current == min_parent.current:
160
+ continue
161
+ if self.map_.in_collision(
162
+ self.map_.point_float_to_int(node.current),
163
+ self.map_.point_float_to_int(node_new.current)
164
+ ):
165
+ continue
166
+
167
+ new_cost = node_new.g + self.get_cost(node_new.current, node.current)
168
+ if new_cost < node.g:
169
+ if self.propagate_cost_to_children:
170
+ self._child.remove(node.parent, node.current)
171
+ self._child.add(node_new.current, node.current)
172
+
173
+ node.parent = node_new.current
174
+ node.g = new_cost
175
+ self._tree[node.current] = node
176
+
177
+ if self.propagate_cost_to_children:
178
+ self._propagate_cost_to_children(node)
179
+
180
+ # Check goal connection
98
181
  dist_to_goal = self.get_cost(node_new.current, self.goal)
99
182
  if dist_to_goal <= self.max_dist:
100
- # Check final edge to goal
101
- if not self.map_.in_collision(node_new.current, self.goal):
183
+ if not self.map_.in_collision(
184
+ self.map_.point_float_to_int(node_new.current),
185
+ self.map_.point_float_to_int(self.goal)
186
+ ):
102
187
  goal_cost = node_new.g + dist_to_goal
103
- # Update goal node if it already exists with a lower cost path
104
- if self.goal in tree:
105
- if goal_cost < tree[self.goal].g:
106
- tree[self.goal].parent = node_new.current
107
- tree[self.goal].g = goal_cost
108
- else:
109
- goal_node = Node(self.goal, node_new.current, goal_cost, 0)
110
- tree[self.goal] = goal_node
111
-
112
- # Extract and return the path
113
- path, length, cost = self.extract_path(tree)
114
- return path, {
115
- "success": True,
116
- "start": self.start,
117
- "goal": self.goal,
118
- "length": length,
119
- "cost": cost,
120
- "expand": tree,
121
- }
122
-
123
- # Planning failed
124
- self.failed_info[1]["expand"] = tree
125
- return self.failed_info
126
-
127
- def _find_near_nodes(self, tree: Dict[tuple, Node], node_new: Node) -> List[Node]:
188
+ if self.goal not in self._tree or goal_cost < self._tree[self.goal].g:
189
+ if node_new.current == self.goal:
190
+ self._tree[self.goal] = node_new
191
+ else:
192
+ self._tree[self.goal] = Node(
193
+ self.goal,
194
+ node_new.current,
195
+ goal_cost,
196
+ 0
197
+ )
198
+ path, length, cost = self.extract_path(self._tree)
199
+
200
+ if first_success_i is None:
201
+ first_success_i = i
202
+
203
+ self.best_results = path, {
204
+ "success": True,
205
+ "start": self.start,
206
+ "goal": self.goal,
207
+ "length": length,
208
+ "cost": cost,
209
+ "first_success_step": first_success_i,
210
+ "best_step": i,
211
+ "total_step": i,
212
+ "expand": self._tree,
213
+ }
214
+
215
+
216
+ # Planning stopped
217
+ self.best_results[1]["total_step"] = i
218
+ self.best_results[1]["expand"] = self._tree
219
+ return self.best_results
220
+
221
+ def _get_near_nodes(self, node_new: Node, index=None, nodes=None) -> List[Node]:
128
222
  """
129
- Find all nodes in the tree within a specified radius of the new node.
130
-
223
+ Get nearby nodes within rewiring radius.
224
+
131
225
  Args:
132
- tree: Current tree of nodes
133
- node_new: Newly created node
134
-
226
+ node_new: Newly added node
227
+ index: FAISS index (required when `use_faiss`=True)
228
+ nodes: List of nodes in FAISS index (required when `use_faiss`=True)
229
+
135
230
  Returns:
136
- near_nodes: List of nodes within the specified radius
231
+ near_nodes: List of nearby nodes
137
232
  """
233
+ # Adaptive radius from RRT* theory
234
+ if self.rewire_radius is None:
235
+ n = len(self._tree) + 1
236
+ radius = self.gamma * ((math.log(n) / n) ** (1 / self.dim))
237
+ if self.discrete:
238
+ radius = max(1, radius)
239
+ else:
240
+ radius = self.rewire_radius
241
+
138
242
  near_nodes = []
139
- for node in tree.values():
140
- if self.get_cost(node.current, node_new.current) <= self.radius:
141
- near_nodes.append(node)
243
+
244
+ if self.use_faiss:
245
+ # range search using faiss
246
+ query = np.asarray(node_new.current, dtype=np.float32).reshape(1, -1)
247
+ lims, D, I = index.range_search(query, radius * radius)
248
+ for idx in I:
249
+ near_nodes.append(nodes[idx])
250
+
251
+ else:
252
+ # brute force radius search
253
+ for node in self._tree.values():
254
+ if self.get_cost(node.current, node_new.current) <= radius:
255
+ near_nodes.append(node)
256
+
142
257
  return near_nodes
143
258
 
144
- def _choose_parent(self, node_new: Node, node_near: Node, near_nodes: List[Node]) -> Tuple[Union[Node, None], Node]:
145
- """
146
- Select the optimal parent for the new node from nearby nodes to minimize cost.
147
-
148
- Args:
149
- node_new: Newly created node
150
- node_near: Nearest node in the tree
151
- near_nodes: List of nearby nodes
152
-
153
- Returns:
154
- node_new: Updated new node with optimal parent
155
- node_near: The chosen parent node
259
+ def _propagate_cost_to_children(self, node: Node):
156
260
  """
157
- # Initialize with nearest node as potential parent
158
- node_new.g = node_near.g + self.get_cost(node_near.current, node_new.current)
159
- best_parent = node_near
160
-
161
- # Check all nearby nodes for potentially lower-cost paths
162
- for node_near_candidate in near_nodes:
163
- # Skip if candidate is the same as initial nearest node
164
- if node_near_candidate.current == best_parent.current:
165
- continue
166
-
167
- # Check if path from candidate to new node is collision-free
168
- if self.map_.in_collision(node_near_candidate.current, node_new.current):
169
- continue
170
-
171
- # Calculate cost through this candidate parent
172
- new_cost = node_near_candidate.g + self.get_cost(node_near_candidate.current, node_new.current)
173
-
174
- # Update parent if this path is cheaper
175
- if new_cost < node_new.g:
176
- node_new.g = new_cost
177
- best_parent = node_near_candidate
178
-
179
- # Set the best parent for the new node
180
- node_new.parent = best_parent.current
181
- return node_new, best_parent
261
+ Propagate cost update to children of a node.
182
262
 
183
- def _rewire(self, tree: Dict[tuple, Node], node_new: Node, near_nodes: List[Node]) -> None:
184
- """
185
- Rewire the tree to potentially improve paths for existing nodes using the new node.
186
-
187
263
  Args:
188
- tree: Current tree of nodes
189
- node_new: Newly added node
190
- near_nodes: List of nearby nodes
264
+ node: Node to propagate cost to children
191
265
  """
192
- for node_near in near_nodes:
193
- # Skip if node is the parent of the new node
194
- if node_near.current == node_new.parent:
195
- continue
196
-
197
- # Check if path from new node to existing node is collision-free
198
- if self.map_.in_collision(node_new.current, node_near.current):
199
- continue
200
-
201
- # Calculate potential new cost for existing node through new node
202
- new_cost = node_new.g + self.get_cost(node_new.current, node_near.current)
203
-
204
- # Update parent if new path is cheaper
205
- if new_cost < node_near.g:
206
- node_near.parent = node_new.current
207
- node_near.g = new_cost
208
- # Update the node in the tree
209
- tree[node_near.current] = node_near
266
+ child_set = self._child[node.current]
267
+ if child_set is None:
268
+ return
269
+
270
+ for child in child_set:
271
+ node_child = self._tree.get(child)
272
+
273
+ if node_child is not None:
274
+ old_g = node_child.g
275
+ node_child.g = node.g + self.get_cost(
276
+ node.current, node_child.current
277
+ )
278
+ if not math.isclose(node_child.g, old_g):
279
+ self._propagate_cost_to_children(node_child)
@@ -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 *