python-motion-planning 2.0.dev1__py3-none-any.whl → 2.0.dev2__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/common/env/map/base_map.py +2 -8
- python_motion_planning/common/env/map/grid.py +74 -81
- python_motion_planning/common/utils/__init__.py +2 -1
- python_motion_planning/common/utils/child_tree.py +22 -0
- python_motion_planning/common/visualizer/__init__.py +3 -1
- python_motion_planning/common/visualizer/base_visualizer.py +165 -0
- python_motion_planning/common/visualizer/{visualizer.py → visualizer_2d.py} +97 -220
- python_motion_planning/common/visualizer/visualizer_3d.py +242 -0
- python_motion_planning/controller/base_controller.py +37 -4
- python_motion_planning/controller/path_tracker/__init__.py +2 -1
- python_motion_planning/controller/path_tracker/apf.py +22 -23
- python_motion_planning/controller/path_tracker/dwa.py +14 -17
- python_motion_planning/controller/path_tracker/path_tracker.py +4 -1
- python_motion_planning/controller/path_tracker/pid.py +7 -1
- python_motion_planning/controller/path_tracker/pure_pursuit.py +7 -1
- python_motion_planning/controller/path_tracker/rpp.py +111 -0
- python_motion_planning/path_planner/__init__.py +2 -1
- python_motion_planning/path_planner/base_path_planner.py +45 -11
- python_motion_planning/path_planner/graph_search/__init__.py +4 -1
- python_motion_planning/path_planner/graph_search/a_star.py +12 -14
- python_motion_planning/path_planner/graph_search/dijkstra.py +15 -15
- python_motion_planning/path_planner/graph_search/gbfs.py +100 -0
- python_motion_planning/path_planner/graph_search/jps.py +199 -0
- python_motion_planning/path_planner/graph_search/lazy_theta_star.py +113 -0
- python_motion_planning/path_planner/graph_search/theta_star.py +17 -19
- python_motion_planning/path_planner/hybrid_search/__init__.py +1 -0
- python_motion_planning/path_planner/hybrid_search/voronoi_planner.py +204 -0
- python_motion_planning/path_planner/sample_search/__init__.py +2 -1
- python_motion_planning/path_planner/sample_search/rrt.py +70 -28
- python_motion_planning/path_planner/sample_search/rrt_connect.py +237 -0
- python_motion_planning/path_planner/sample_search/rrt_star.py +201 -151
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.dev2.dist-info}/METADATA +54 -19
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.dev2.dist-info}/RECORD +36 -27
- python_motion_planning/common/env/robot/tmp.py +0 -404
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.dev2.dist-info}/WHEEL +0 -0
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.dev2.dist-info}/licenses/LICENSE +0 -0
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.dev2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: voronoi.py
|
|
3
|
+
@author: Wu Maojia
|
|
4
|
+
@update: 2025.12.20
|
|
5
|
+
"""
|
|
6
|
+
import copy
|
|
7
|
+
from typing import Union, List, Tuple, Dict, Any
|
|
8
|
+
import heapq
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from scipy.spatial import Voronoi
|
|
12
|
+
|
|
13
|
+
from python_motion_planning.common import Node, TYPES, Grid
|
|
14
|
+
from python_motion_planning.path_planner.base_path_planner import BasePathPlanner
|
|
15
|
+
from python_motion_planning.path_planner.graph_search.a_star import AStar
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VoronoiPlanner(BasePathPlanner):
|
|
19
|
+
"""
|
|
20
|
+
Path planner based on Voronoi diagram.
|
|
21
|
+
Core idea: find the nearest points on the Voronoi diagram to the start and goal,
|
|
22
|
+
plan the path on the Voronoi graph using a base planner, and then concatenate the full path.
|
|
23
|
+
Note that the boundary of grid map passed in Voronoi planner should be filled with obstacles using Grid.fill_boundary_with_obstacles() method. If not, the Voronoi planner will automatically fill.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
*args: see the parent class.
|
|
27
|
+
base_planner: base planner class for path planning.
|
|
28
|
+
base_planner_kwargs: keyword arguments for the base planner.
|
|
29
|
+
cover_inflation: determine whether the voronoi candidates cover the inflation region.
|
|
30
|
+
*kwargs: see the parent class.
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
>>> map_ = Grid(bounds=[[0, 15], [0, 15]])
|
|
34
|
+
>>> planner = VoronoiPlanner(map_=map_, start=(5, 5), goal=(10, 10))
|
|
35
|
+
>>> path, path_info = planner.plan()
|
|
36
|
+
>>> print(path_info['success'])
|
|
37
|
+
True
|
|
38
|
+
|
|
39
|
+
>>> planner.map_.type_map[3:10, 6] = TYPES.OBSTACLE
|
|
40
|
+
>>> path, path_info = planner.plan()
|
|
41
|
+
>>> print(path_info['success'])
|
|
42
|
+
True
|
|
43
|
+
"""
|
|
44
|
+
def __init__(self,
|
|
45
|
+
*args,
|
|
46
|
+
base_planner: BasePathPlanner = AStar,
|
|
47
|
+
base_planner_kwargs: dict = {},
|
|
48
|
+
cover_inflation: bool = False,
|
|
49
|
+
**kwargs
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
self.base_planner = base_planner
|
|
54
|
+
self.base_planner_kwargs = base_planner_kwargs
|
|
55
|
+
self.base_planner_kwargs["map_"] = self.map_
|
|
56
|
+
self.base_planner_kwargs["start"] = self.start
|
|
57
|
+
self.base_planner_kwargs["goal"] = self.goal
|
|
58
|
+
|
|
59
|
+
self.cover_inflation = cover_inflation
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
return "Voronoi Planner"
|
|
63
|
+
|
|
64
|
+
def find_voronoi_candidates(self, map_: Grid) -> np.ndarray:
|
|
65
|
+
"""
|
|
66
|
+
Find Voronoi candidate points for the grid map.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
map_: grid map.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
candidates: Voronoi candidate points matrix.
|
|
73
|
+
"""
|
|
74
|
+
generators = np.argwhere(map_.data == 1)
|
|
75
|
+
candidates = np.zeros_like(map_.data, dtype=np.bool_)
|
|
76
|
+
if len(generators) < 2:
|
|
77
|
+
return candidates
|
|
78
|
+
|
|
79
|
+
vor = Voronoi(generators)
|
|
80
|
+
|
|
81
|
+
for ridge in vor.ridge_vertices:
|
|
82
|
+
v1_idx, v2_idx = ridge[:2]
|
|
83
|
+
if v1_idx == -1 or v2_idx == -1:
|
|
84
|
+
continue # skip infinite ridges
|
|
85
|
+
|
|
86
|
+
# continuous coordinates
|
|
87
|
+
v1 = vor.vertices[v1_idx]
|
|
88
|
+
v2 = vor.vertices[v2_idx]
|
|
89
|
+
|
|
90
|
+
v1 = map_.point_float_to_int(v1)
|
|
91
|
+
v2 = map_.point_float_to_int(v2)
|
|
92
|
+
|
|
93
|
+
line = map_.line_of_sight(v1, v2)
|
|
94
|
+
|
|
95
|
+
for point in line:
|
|
96
|
+
candidates[point] = True
|
|
97
|
+
|
|
98
|
+
candidates[map_.data == TYPES.OBSTACLE] = False
|
|
99
|
+
if not self.cover_inflation:
|
|
100
|
+
candidates[map_.data == TYPES.INFLATION] = False
|
|
101
|
+
|
|
102
|
+
return candidates
|
|
103
|
+
|
|
104
|
+
def find_nearest_voronoi_point(self,
|
|
105
|
+
point: Tuple[float, ...],
|
|
106
|
+
voronoi_candidates: np.ndarray,
|
|
107
|
+
) -> Union[Tuple[float, ...], None]:
|
|
108
|
+
"""
|
|
109
|
+
Find the nearest Voronoi candidate point to the target point using brute force search.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
point: target point.
|
|
113
|
+
voronoi_candidates: Voronoi candidate points matrix.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
nearest_point: nearest Voronoi point.
|
|
117
|
+
"""
|
|
118
|
+
min_dist = float('inf')
|
|
119
|
+
nearest_point = None
|
|
120
|
+
|
|
121
|
+
# Iterate through all Voronoi candidate points to find the nearest one
|
|
122
|
+
for indices in np.ndindex(voronoi_candidates.shape):
|
|
123
|
+
if voronoi_candidates[indices]:
|
|
124
|
+
candidate_point = indices
|
|
125
|
+
dist = self.map_.get_distance(point, candidate_point)
|
|
126
|
+
if dist < min_dist:
|
|
127
|
+
min_dist = dist
|
|
128
|
+
nearest_point = candidate_point
|
|
129
|
+
|
|
130
|
+
return nearest_point
|
|
131
|
+
|
|
132
|
+
def plan(self) -> Union[List[Tuple[float, ...]], Dict[str, Any]]:
|
|
133
|
+
"""
|
|
134
|
+
Execute the path planning:
|
|
135
|
+
1. Compute Voronoi candidate points
|
|
136
|
+
2. Find the nearest Voronoi points for start and goal
|
|
137
|
+
3. Plan the path on the Voronoi graph
|
|
138
|
+
4. Concatenate the full path (start -> Voronoi start -> ... -> Voronoi goal -> goal)
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
path: A list containing the path waypoints
|
|
142
|
+
path_info: A dictionary containing the path information
|
|
143
|
+
"""
|
|
144
|
+
voronoi_map = copy.deepcopy(self.map_)
|
|
145
|
+
voronoi_map.fill_boundary_with_obstacles()
|
|
146
|
+
|
|
147
|
+
# Compute Voronoi candidate points
|
|
148
|
+
voronoi_candidates = self.find_voronoi_candidates(voronoi_map)
|
|
149
|
+
|
|
150
|
+
# If no Voronoi candidates are found, fall back to normal base planner
|
|
151
|
+
if not np.any(voronoi_candidates):
|
|
152
|
+
return self.base_planner(**self.base_planner_kwargs).plan()
|
|
153
|
+
|
|
154
|
+
# Find the nearest Voronoi points for start and goal
|
|
155
|
+
start_voronoi = self.find_nearest_voronoi_point(self.start, voronoi_candidates)
|
|
156
|
+
goal_voronoi = self.find_nearest_voronoi_point(self.goal, voronoi_candidates)
|
|
157
|
+
|
|
158
|
+
# If no valid Voronoi points found, fall back to normal base planner
|
|
159
|
+
if start_voronoi is None or goal_voronoi is None:
|
|
160
|
+
return self.base_planner(**self.base_planner_kwargs).plan()
|
|
161
|
+
|
|
162
|
+
voronoi_map.type_map[voronoi_candidates] = TYPES.FREE
|
|
163
|
+
voronoi_map.type_map[~voronoi_candidates] = TYPES.OBSTACLE
|
|
164
|
+
|
|
165
|
+
self.base_planner_kwargs["map_"] = voronoi_map
|
|
166
|
+
self.base_planner_kwargs["start"] = start_voronoi
|
|
167
|
+
self.base_planner_kwargs["goal"] = goal_voronoi
|
|
168
|
+
|
|
169
|
+
voronoi_path, voronoi_path_info = self.base_planner(**self.base_planner_kwargs).plan()
|
|
170
|
+
|
|
171
|
+
# If Voronoi path planning fails, fall back to normal base planner
|
|
172
|
+
if not voronoi_path_info["success"]:
|
|
173
|
+
self.base_planner_kwargs["map_"] = self.map_
|
|
174
|
+
self.base_planner_kwargs["start"] = self.start
|
|
175
|
+
self.base_planner_kwargs["goal"] = self.goal
|
|
176
|
+
return self.base_planner(**self.base_planner_kwargs).plan()
|
|
177
|
+
|
|
178
|
+
# Compute total path length and cost
|
|
179
|
+
start_segment_len = self.map_.get_distance(self.start, start_voronoi)
|
|
180
|
+
end_segment_len = self.map_.get_distance(goal_voronoi, self.goal)
|
|
181
|
+
total_length = voronoi_path_info["length"] + start_segment_len + end_segment_len
|
|
182
|
+
|
|
183
|
+
start_segment_cost = self.get_cost(self.start, start_voronoi)
|
|
184
|
+
end_segment_cost = self.get_cost(goal_voronoi, self.goal)
|
|
185
|
+
total_cost = voronoi_path_info["cost"] + start_segment_cost + end_segment_cost
|
|
186
|
+
|
|
187
|
+
# Concatenate the final path
|
|
188
|
+
final_path = [self.start] + voronoi_path + [self.goal]
|
|
189
|
+
|
|
190
|
+
# Collect path information
|
|
191
|
+
path_info = {
|
|
192
|
+
"success": True,
|
|
193
|
+
"start": self.start,
|
|
194
|
+
"goal": self.goal,
|
|
195
|
+
"length": total_length,
|
|
196
|
+
"cost": total_cost,
|
|
197
|
+
"expand": voronoi_path_info["expand"],
|
|
198
|
+
"voronoi_candidates": voronoi_candidates,
|
|
199
|
+
"voronoi_start": start_voronoi,
|
|
200
|
+
"voronoi_goal": goal_voronoi,
|
|
201
|
+
"voronoi_path": voronoi_path
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return final_path, path_info
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: rrt.py
|
|
3
3
|
@author: Wu Maojia, Yang Haodong
|
|
4
|
-
@update: 2025.
|
|
4
|
+
@update: 2025.12.19
|
|
5
5
|
"""
|
|
6
6
|
import math
|
|
7
7
|
import random
|
|
8
|
+
from typing import Union, Dict, List, Tuple, Any
|
|
9
|
+
|
|
8
10
|
import numpy as np
|
|
9
|
-
|
|
11
|
+
import faiss
|
|
10
12
|
|
|
11
13
|
from python_motion_planning.common import BaseMap, Node, TYPES, Grid
|
|
12
14
|
from python_motion_planning.path_planner import BasePathPlanner
|
|
@@ -17,9 +19,11 @@ class RRT(BasePathPlanner):
|
|
|
17
19
|
|
|
18
20
|
Args:
|
|
19
21
|
*args: see the parent class.
|
|
20
|
-
max_dist: Maximum expansion distance for each step
|
|
21
|
-
sample_num: Maximum number of samples to generate
|
|
22
|
-
goal_sample_rate: Probability of sampling the goal directly
|
|
22
|
+
max_dist: Maximum expansion distance for each step.
|
|
23
|
+
sample_num: Maximum number of samples to generate.
|
|
24
|
+
goal_sample_rate: Probability of sampling the goal directly.
|
|
25
|
+
discrete: Whether to use discrete or continuous space.
|
|
26
|
+
faiss: Whether to use Faiss to accelerate the search.
|
|
23
27
|
*kwargs: see the parent class.
|
|
24
28
|
|
|
25
29
|
References:
|
|
@@ -40,21 +44,20 @@ class RRT(BasePathPlanner):
|
|
|
40
44
|
def __init__(self, *args,
|
|
41
45
|
max_dist: float = 5.0, sample_num: int = 100000,
|
|
42
46
|
goal_sample_rate: float = 0.05,
|
|
47
|
+
discrete: bool = False,
|
|
48
|
+
use_faiss: bool = True,
|
|
43
49
|
**kwargs) -> None:
|
|
44
50
|
super().__init__(*args, **kwargs)
|
|
45
|
-
# Maximum expansion distance per step
|
|
46
51
|
self.max_dist = max_dist
|
|
47
|
-
# Maximum number of samples
|
|
48
52
|
self.sample_num = sample_num
|
|
49
|
-
# Goal sampling probability
|
|
50
53
|
self.goal_sample_rate = goal_sample_rate
|
|
51
|
-
|
|
52
|
-
self.
|
|
54
|
+
self.discrete = discrete
|
|
55
|
+
self.use_faiss = use_faiss
|
|
53
56
|
|
|
54
57
|
def __str__(self) -> str:
|
|
55
|
-
return "
|
|
58
|
+
return "RRT"
|
|
56
59
|
|
|
57
|
-
def plan(self) -> Union[
|
|
60
|
+
def plan(self) -> Union[List[Tuple[float, ...]], Dict[str, Any]]:
|
|
58
61
|
"""
|
|
59
62
|
RRT path planning algorithm implementation.
|
|
60
63
|
|
|
@@ -67,6 +70,12 @@ class RRT(BasePathPlanner):
|
|
|
67
70
|
start_node = Node(self.start, None, 0, 0)
|
|
68
71
|
tree[self.start] = start_node
|
|
69
72
|
|
|
73
|
+
# Initialize FAISS index
|
|
74
|
+
if self.use_faiss:
|
|
75
|
+
faiss_index = faiss.IndexFlatL2(self.dim)
|
|
76
|
+
faiss_nodes = []
|
|
77
|
+
self._faiss_add_node(start_node, faiss_index, faiss_nodes)
|
|
78
|
+
|
|
70
79
|
# Main sampling loop
|
|
71
80
|
for _ in range(self.sample_num):
|
|
72
81
|
# Generate random sample node
|
|
@@ -77,7 +86,7 @@ class RRT(BasePathPlanner):
|
|
|
77
86
|
continue
|
|
78
87
|
|
|
79
88
|
# Find nearest node in tree
|
|
80
|
-
node_near = self._get_nearest_node(tree, node_rand)
|
|
89
|
+
node_near = self._get_nearest_node(tree, node_rand, faiss_index, faiss_nodes)
|
|
81
90
|
|
|
82
91
|
# Create new node towards random sample
|
|
83
92
|
node_new = self._steer(node_near, node_rand)
|
|
@@ -85,19 +94,27 @@ class RRT(BasePathPlanner):
|
|
|
85
94
|
continue
|
|
86
95
|
|
|
87
96
|
# Check if edge is collision-free
|
|
88
|
-
if self.map_.in_collision(
|
|
97
|
+
if self.map_.in_collision(
|
|
98
|
+
self.map_.point_float_to_int(node_new.current),
|
|
99
|
+
self.map_.point_float_to_int(node_near.current)
|
|
100
|
+
):
|
|
89
101
|
continue
|
|
90
102
|
|
|
91
103
|
# Add new node to tree
|
|
92
104
|
node_new.parent = node_near.current
|
|
93
105
|
node_new.g = node_near.g + self.get_cost(node_near.current, node_new.current)
|
|
94
106
|
tree[node_new.current] = node_new
|
|
107
|
+
if self.use_faiss:
|
|
108
|
+
self._faiss_add_node(node_new, faiss_index, faiss_nodes)
|
|
95
109
|
|
|
96
110
|
# Check if goal is reachable
|
|
97
111
|
dist_to_goal = self.get_cost(node_new.current, self.goal)
|
|
98
112
|
if dist_to_goal <= self.max_dist:
|
|
99
113
|
# Check final edge to goal
|
|
100
|
-
if not self.map_.in_collision(
|
|
114
|
+
if not self.map_.in_collision(
|
|
115
|
+
self.map_.point_float_to_int(node_new.current),
|
|
116
|
+
self.map_.point_float_to_int(self.goal)
|
|
117
|
+
):
|
|
101
118
|
if node_new.current == self.goal:
|
|
102
119
|
goal_node = node_new
|
|
103
120
|
else:
|
|
@@ -131,25 +148,37 @@ class RRT(BasePathPlanner):
|
|
|
131
148
|
|
|
132
149
|
point = []
|
|
133
150
|
# Generate random integer point within grid bounds
|
|
134
|
-
for d in range(self.
|
|
135
|
-
d_min, d_max = self.
|
|
136
|
-
point.append(random.
|
|
151
|
+
for d in range(self.dim):
|
|
152
|
+
d_min, d_max = -0.5, self.map_.shape[d] - 0.5
|
|
153
|
+
point.append(random.uniform(d_min, d_max))
|
|
137
154
|
point = tuple(point)
|
|
138
155
|
|
|
156
|
+
if self.discrete:
|
|
157
|
+
point = self.map_.point_float_to_int(point)
|
|
158
|
+
|
|
139
159
|
return Node(point, None, 0, 0)
|
|
140
160
|
|
|
141
161
|
def _get_nearest_node(self, tree: Dict[Tuple[int, ...], Node],
|
|
142
|
-
node_rand: Node) -> Node:
|
|
162
|
+
node_rand: Node, index=None, nodes=None) -> Node:
|
|
143
163
|
"""
|
|
144
164
|
Find the nearest node in the tree to a random sample.
|
|
145
165
|
|
|
146
166
|
Args:
|
|
147
167
|
tree: Current tree of nodes
|
|
148
168
|
node_rand: Random sample node
|
|
169
|
+
index: FAISS index (required when `use_faiss`=True)
|
|
170
|
+
nodes: List of nodes in FAISS index (required when `use_faiss`=True)
|
|
149
171
|
|
|
150
172
|
Returns:
|
|
151
173
|
node: Nearest node in the tree
|
|
152
174
|
"""
|
|
175
|
+
# knn search using faiss
|
|
176
|
+
if self.use_faiss:
|
|
177
|
+
query = np.array(node_rand.current, dtype=np.float32).reshape(1, -1)
|
|
178
|
+
_, indices = index.search(query, 1)
|
|
179
|
+
return nodes[indices[0][0]]
|
|
180
|
+
|
|
181
|
+
# brute force search
|
|
153
182
|
min_dist = float('inf')
|
|
154
183
|
nearest_node = None
|
|
155
184
|
|
|
@@ -174,7 +203,7 @@ class RRT(BasePathPlanner):
|
|
|
174
203
|
node: New node in direction of random sample
|
|
175
204
|
"""
|
|
176
205
|
# Calculate differences for each dimension
|
|
177
|
-
diffs = [node_rand.current[i] - node_near.current[i] for i in range(self.
|
|
206
|
+
diffs = [node_rand.current[i] - node_near.current[i] for i in range(self.dim)]
|
|
178
207
|
|
|
179
208
|
# Calculate Euclidean distance in n-dimensional space
|
|
180
209
|
dist = math.sqrt(sum(diff**2 for diff in diffs))
|
|
@@ -189,13 +218,26 @@ class RRT(BasePathPlanner):
|
|
|
189
218
|
|
|
190
219
|
# Otherwise scale to maximum distance
|
|
191
220
|
scale = self.max_dist / dist
|
|
192
|
-
|
|
193
|
-
node_near.current[i] + scale * diffs[i]
|
|
194
|
-
for i in range(self.
|
|
221
|
+
new_point = [
|
|
222
|
+
node_near.current[i] + scale * diffs[i]
|
|
223
|
+
for i in range(self.dim)
|
|
195
224
|
]
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if
|
|
199
|
-
|
|
225
|
+
new_point = tuple(new_point)
|
|
226
|
+
|
|
227
|
+
if self.discrete:
|
|
228
|
+
new_point = self.map_.point_float_to_int(new_point)
|
|
200
229
|
|
|
201
|
-
return Node(
|
|
230
|
+
return Node(new_point, None, 0, 0)
|
|
231
|
+
|
|
232
|
+
def _faiss_add_node(self, node: Node, index, nodes):
|
|
233
|
+
"""
|
|
234
|
+
Add a node to the FAISS index.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
node: Node to add
|
|
238
|
+
index: FAISS index
|
|
239
|
+
nodes: List of nodes in FAISS index
|
|
240
|
+
"""
|
|
241
|
+
vec = np.array(node.current, dtype=np.float32).reshape(1, -1)
|
|
242
|
+
index.add(vec)
|
|
243
|
+
nodes.append(node)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: rrt_connect.py
|
|
3
|
+
@author: Wu Maojia
|
|
4
|
+
@update: 2025.12.19
|
|
5
|
+
"""
|
|
6
|
+
from typing import Union, Dict, List, Tuple, Any
|
|
7
|
+
|
|
8
|
+
import faiss
|
|
9
|
+
|
|
10
|
+
from python_motion_planning.common import Node, Grid, TYPES
|
|
11
|
+
from python_motion_planning.path_planner.sample_search.rrt import RRT
|
|
12
|
+
|
|
13
|
+
class RRTConnect(RRT):
|
|
14
|
+
"""
|
|
15
|
+
Class for RRT-Connect path planner, an improved version of RRT.
|
|
16
|
+
|
|
17
|
+
RRT-Connect uses two trees (one from start, one from goal) that grow towards
|
|
18
|
+
each other, which typically results in faster convergence than standard RRT.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
*args: see the parent class.
|
|
22
|
+
*kwargs: see the parent class.
|
|
23
|
+
|
|
24
|
+
References:
|
|
25
|
+
[1] RRT-connect: An efficient approach to single-query path planning.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> map_ = Grid(bounds=[[0, 15], [0, 15]])
|
|
29
|
+
>>> planner = RRTConnect(map_=map_, start=(5, 5), goal=(10, 10))
|
|
30
|
+
>>> path, path_info = planner.plan()
|
|
31
|
+
>>> print(path_info['success'])
|
|
32
|
+
True
|
|
33
|
+
|
|
34
|
+
>>> planner.map_.type_map[3:10, 6] = TYPES.OBSTACLE
|
|
35
|
+
>>> path, path_info = planner.plan()
|
|
36
|
+
>>> print(path_info['success'])
|
|
37
|
+
True
|
|
38
|
+
"""
|
|
39
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
40
|
+
super().__init__(*args, **kwargs)
|
|
41
|
+
# Two trees for bidirectional search: one from start, one from goal
|
|
42
|
+
self.tree_a = None # Tree originating from start point
|
|
43
|
+
self.tree_b = None # Tree originating from goal point
|
|
44
|
+
|
|
45
|
+
if self.use_faiss:
|
|
46
|
+
self.index_a = None
|
|
47
|
+
self.index_b = None
|
|
48
|
+
self.nodes_a = []
|
|
49
|
+
self.nodes_b = []
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
return "RRT-Connect"
|
|
53
|
+
|
|
54
|
+
def plan(self) -> Union[List[Tuple[float, ...]], Dict[str, Any]]:
|
|
55
|
+
"""
|
|
56
|
+
RRT-Connect path planning algorithm implementation.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
path: A list containing the path waypoints
|
|
60
|
+
path_info: A dictionary containing path information
|
|
61
|
+
"""
|
|
62
|
+
# Initialize both trees with start and goal nodes respectively
|
|
63
|
+
self.tree_a = {self.start: Node(self.start, None, 0, 0)}
|
|
64
|
+
self.tree_b = {self.goal: Node(self.goal, None, 0, 0)}
|
|
65
|
+
|
|
66
|
+
# Initialize FAISS index
|
|
67
|
+
if self.use_faiss:
|
|
68
|
+
self.index_a = faiss.IndexFlatL2(self.dim)
|
|
69
|
+
self.index_b = faiss.IndexFlatL2(self.dim)
|
|
70
|
+
self.nodes_a = []
|
|
71
|
+
self.nodes_b = []
|
|
72
|
+
self._faiss_add_node(self.tree_a[self.start], self.index_a, self.nodes_a)
|
|
73
|
+
self._faiss_add_node(self.tree_b[self.goal], self.index_b, self.nodes_b)
|
|
74
|
+
|
|
75
|
+
# Main planning loop
|
|
76
|
+
for _ in range(self.sample_num):
|
|
77
|
+
# Generate random sample node
|
|
78
|
+
node_rand = self._generate_random_node()
|
|
79
|
+
|
|
80
|
+
# Extend tree A towards random sample
|
|
81
|
+
node_new_a = self._extend(self.tree_a, node_rand, self.index_a, self.nodes_a)
|
|
82
|
+
if node_new_a:
|
|
83
|
+
# Try to connect tree B to the new node from tree A
|
|
84
|
+
if self._connect(self.tree_b, node_new_a.current, self.index_b, self.nodes_b):
|
|
85
|
+
tree_a_goal = self.tree_a.get(self.goal)
|
|
86
|
+
if tree_a_goal is not None and tree_a_goal.parent is None:
|
|
87
|
+
self.tree_a, self.tree_b = self.tree_b, self.tree_a
|
|
88
|
+
# Path found - combine paths from both trees
|
|
89
|
+
path_a, length_a, cost_a = self._extract_subpath(self.tree_a, node_new_a.current, self.start)
|
|
90
|
+
path_b, length_b, cost_b = self._extract_subpath(self.tree_b, node_new_a.current, self.goal)
|
|
91
|
+
path_a = path_a[::-1]
|
|
92
|
+
# Combine paths (remove duplicate meeting point)
|
|
93
|
+
full_path = path_a + path_b[1:]
|
|
94
|
+
total_length = length_a + length_b
|
|
95
|
+
total_cost = cost_a + cost_b
|
|
96
|
+
|
|
97
|
+
return full_path, {
|
|
98
|
+
"success": True,
|
|
99
|
+
"start": self.start,
|
|
100
|
+
"goal": self.goal,
|
|
101
|
+
"length": total_length,
|
|
102
|
+
"cost": total_cost,
|
|
103
|
+
"expand": [self.tree_a, self.tree_b],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Swap trees to maintain bidirectional growth
|
|
107
|
+
self.tree_a, self.tree_b = self.tree_b, self.tree_a
|
|
108
|
+
|
|
109
|
+
if self.use_faiss:
|
|
110
|
+
self.index_a, self.index_b = self.index_b, self.index_a
|
|
111
|
+
self.nodes_a, self.nodes_b = self.nodes_b, self.nodes_a
|
|
112
|
+
|
|
113
|
+
# If loop exits without return, planning failed
|
|
114
|
+
self.failed_info[1]["expand"] = [self.tree_a, self.tree_b]
|
|
115
|
+
return self.failed_info
|
|
116
|
+
|
|
117
|
+
def _extend(self, tree: Dict[Tuple[int, ...], Node], node_rand: Node, index=None, nodes=None) -> Union[Node, None]:
|
|
118
|
+
"""
|
|
119
|
+
Extend the tree towards a random node, adding at most one new node.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
tree: The tree to extend
|
|
123
|
+
node_rand: The target node to extend towards
|
|
124
|
+
index: FAISS index (required when `use_faiss`=True)
|
|
125
|
+
nodes: List of nodes in FAISS index (required when `use_faiss`=True)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The new node added to the tree, or None if no node was added
|
|
129
|
+
"""
|
|
130
|
+
# Find nearest node in the tree
|
|
131
|
+
node_near = self._get_nearest_node(tree, node_rand, index, nodes)
|
|
132
|
+
|
|
133
|
+
# Steer towards the random node
|
|
134
|
+
node_new = self._steer(node_near, node_rand)
|
|
135
|
+
if node_new is None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
# Check if path is collision-free
|
|
139
|
+
if not self.map_.in_collision(
|
|
140
|
+
self.map_.point_float_to_int(node_near.current),
|
|
141
|
+
self.map_.point_float_to_int(node_new.current)
|
|
142
|
+
):
|
|
143
|
+
# Add new node to the tree
|
|
144
|
+
node_new.parent = node_near.current
|
|
145
|
+
node_new.g = node_near.g + self.get_cost(node_near.current, node_new.current)
|
|
146
|
+
tree[node_new.current] = node_new
|
|
147
|
+
if self.use_faiss:
|
|
148
|
+
self._faiss_add_node(node_new, index, nodes)
|
|
149
|
+
return node_new
|
|
150
|
+
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def _connect(self, tree: Dict[Tuple[int, ...], Node], target: Tuple, index=None, nodes=None) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
Connect the tree to a target point by repeatedly extending towards it.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
tree: The tree to connect
|
|
159
|
+
target: The target point to connect to
|
|
160
|
+
index: FAISS index (required when `use_faiss`=True)
|
|
161
|
+
nodes: List of nodes in FAISS index (required when `use_faiss`=True)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if connection successful, False otherwise
|
|
165
|
+
"""
|
|
166
|
+
while True:
|
|
167
|
+
# Create node for the target
|
|
168
|
+
node_target = Node(target, None, 0, 0)
|
|
169
|
+
|
|
170
|
+
# Find nearest node in the tree
|
|
171
|
+
node_near = self._get_nearest_node(tree, node_target, index, nodes)
|
|
172
|
+
|
|
173
|
+
# Check distance to target
|
|
174
|
+
dist = self.get_cost(node_near.current, target)
|
|
175
|
+
|
|
176
|
+
# If close enough, check final connection
|
|
177
|
+
if dist <= self.max_dist:
|
|
178
|
+
if not self.map_.in_collision(
|
|
179
|
+
self.map_.point_float_to_int(node_near.current),
|
|
180
|
+
self.map_.point_float_to_int(target)
|
|
181
|
+
):
|
|
182
|
+
# Add target to tree
|
|
183
|
+
node_new = Node(target, node_near.current,
|
|
184
|
+
node_near.g + dist, 0)
|
|
185
|
+
tree[target] = node_new
|
|
186
|
+
if self.use_faiss:
|
|
187
|
+
self._faiss_add_node(node_new, index, nodes)
|
|
188
|
+
return True
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
# Otherwise, extend towards target
|
|
192
|
+
node_new = self._steer(node_near, node_target)
|
|
193
|
+
if node_new is None:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
# Check collision
|
|
197
|
+
if self.map_.in_collision(
|
|
198
|
+
self.map_.point_float_to_int(node_near.current),
|
|
199
|
+
self.map_.point_float_to_int(node_new.current)
|
|
200
|
+
):
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
# Add new node to tree
|
|
204
|
+
node_new.parent = node_near.current
|
|
205
|
+
node_new.g = node_near.g + self.get_cost(node_near.current, node_new.current)
|
|
206
|
+
tree[node_new.current] = node_new
|
|
207
|
+
if self.use_faiss:
|
|
208
|
+
self._faiss_add_node(node_new, index, nodes)
|
|
209
|
+
|
|
210
|
+
def _extract_subpath(self, tree: dict, end_point: tuple, root: tuple) -> Tuple[List[Tuple[float, ...]], float, float]:
|
|
211
|
+
"""
|
|
212
|
+
Extract a subpath from the root of the tree to the end_point.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
tree: Tree to extract path from
|
|
216
|
+
end_point: End point of the subpath
|
|
217
|
+
root: Root point of the tree
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
path: the subpath
|
|
221
|
+
length: length of the subpath
|
|
222
|
+
cost: cost of the subpath
|
|
223
|
+
"""
|
|
224
|
+
length = 0
|
|
225
|
+
cost = 0
|
|
226
|
+
node = tree.get(end_point)
|
|
227
|
+
path = [node.current]
|
|
228
|
+
|
|
229
|
+
while node.current != root:
|
|
230
|
+
node_parent = tree.get(node.parent)
|
|
231
|
+
length += self.map_.get_distance(node.current, node_parent.current)
|
|
232
|
+
cost += self.get_cost(node.current, node_parent.current)
|
|
233
|
+
node = node_parent
|
|
234
|
+
path.append(node.current)
|
|
235
|
+
|
|
236
|
+
return path, length, cost
|
|
237
|
+
|