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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: map.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update: 2025.
|
|
4
|
+
@update: 2025.11.25
|
|
5
5
|
"""
|
|
6
6
|
from typing import Iterable, Union
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
@@ -17,12 +17,10 @@ class BaseMap(ABC):
|
|
|
17
17
|
|
|
18
18
|
Args:
|
|
19
19
|
bounds: The size of map in the world (shape: (n, 2) (n>=2)). bounds[i, 0] means the lower bound of the world in the i-th dimension. bounds[i, 1] means the upper bound of the world in the i-th dimension.
|
|
20
|
-
dtype: data type of coordinates
|
|
21
20
|
"""
|
|
22
|
-
def __init__(self, bounds: Iterable
|
|
21
|
+
def __init__(self, bounds: Iterable) -> None:
|
|
23
22
|
super().__init__()
|
|
24
23
|
self._bounds = np.asarray(bounds, dtype=float)
|
|
25
|
-
self._dtype = dtype
|
|
26
24
|
|
|
27
25
|
if len(self._bounds.shape) != 2 or self._bounds.shape[0] <= 1 or self._bounds.shape[1] != 2:
|
|
28
26
|
raise ValueError(f"The shape of bounds must be (n, 2) (n>=2) instead of {self._bounds.shape}")
|
|
@@ -39,10 +37,6 @@ class BaseMap(ABC):
|
|
|
39
37
|
def dim(self) -> int:
|
|
40
38
|
return self._bounds.shape[0]
|
|
41
39
|
|
|
42
|
-
@property
|
|
43
|
-
def dtype(self) -> np.dtype:
|
|
44
|
-
return self._dtype
|
|
45
|
-
|
|
46
40
|
@abstractmethod
|
|
47
41
|
def map_to_world(self, point: tuple) -> tuple:
|
|
48
42
|
"""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: grid.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update: 2025.
|
|
4
|
+
@update: 2025.12.20
|
|
5
5
|
"""
|
|
6
6
|
from itertools import product
|
|
7
7
|
from typing import Iterable, Union, Tuple, Callable, List, Dict
|
|
@@ -26,13 +26,13 @@ class GridTypeMap:
|
|
|
26
26
|
>>> type_map = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.int8)
|
|
27
27
|
>>> grid_type_map = GridTypeMap(type_map)
|
|
28
28
|
>>> grid_type_map
|
|
29
|
-
GridTypeMap(
|
|
29
|
+
GridTypeMap(data=
|
|
30
30
|
[[0 0 0]
|
|
31
31
|
[0 1 0]
|
|
32
32
|
[0 0 0]]
|
|
33
|
-
|
|
33
|
+
, shape=(3, 3), dtype=int8)
|
|
34
34
|
|
|
35
|
-
>>> grid_type_map.
|
|
35
|
+
>>> grid_type_map.data
|
|
36
36
|
array([[0, 0, 0],
|
|
37
37
|
[0, 1, 0],
|
|
38
38
|
[0, 0, 0]], dtype=int8)
|
|
@@ -42,42 +42,31 @@ class GridTypeMap:
|
|
|
42
42
|
|
|
43
43
|
>>> grid_type_map.dtype
|
|
44
44
|
dtype('int8')
|
|
45
|
-
|
|
46
|
-
>>> new_array = np.array([[1, 1, 1], [0, 0, 0], [0, 0, 0]], dtype=np.int8)
|
|
47
|
-
|
|
48
|
-
>>> grid_type_map.update(new_array)
|
|
49
|
-
|
|
50
|
-
>>> grid_type_map
|
|
51
|
-
GridTypeMap(array(
|
|
52
|
-
[[1 1 1]
|
|
53
|
-
[0 0 0]
|
|
54
|
-
[0 0 0]]
|
|
55
|
-
), shape=(3, 3), dtype=int8)
|
|
56
45
|
"""
|
|
57
46
|
def __init__(self, type_map: np.ndarray):
|
|
58
|
-
self.
|
|
59
|
-
self._shape = self.
|
|
60
|
-
self._dtype = self.
|
|
47
|
+
self._data = np.asarray(type_map)
|
|
48
|
+
self._shape = self._data.shape
|
|
49
|
+
self._dtype = self._data.dtype
|
|
61
50
|
|
|
62
51
|
self._dtype_options = [np.int8, np.int16, np.int32, np.int64]
|
|
63
52
|
if self._dtype not in self._dtype_options:
|
|
64
|
-
raise ValueError("Dtype must be one of {} instead of {}".format(self._dtype_options, self._dtype))
|
|
53
|
+
raise ValueError("Dtype must be one of {} instead of {}. If you are not sure, set it to `np.int8`.".format(self._dtype_options, self._dtype))
|
|
65
54
|
|
|
66
55
|
def __str__(self) -> str:
|
|
67
|
-
return "GridTypeMap(
|
|
56
|
+
return "GridTypeMap(data=\n{}\n, shape={}, dtype={})".format(self._data, self._shape, self._dtype)
|
|
68
57
|
|
|
69
58
|
def __repr__(self) -> str:
|
|
70
59
|
return self.__str__()
|
|
71
60
|
|
|
72
61
|
def __getitem__(self, idx):
|
|
73
|
-
return self.
|
|
62
|
+
return self._data[idx]
|
|
74
63
|
|
|
75
64
|
def __setitem__(self, idx, value):
|
|
76
|
-
self.
|
|
65
|
+
self._data[idx] = value
|
|
77
66
|
|
|
78
67
|
@property
|
|
79
|
-
def
|
|
80
|
-
return self.
|
|
68
|
+
def data(self) -> np.ndarray:
|
|
69
|
+
return self._data.view()
|
|
81
70
|
|
|
82
71
|
@property
|
|
83
72
|
def shape(self) -> Tuple:
|
|
@@ -87,14 +76,6 @@ class GridTypeMap:
|
|
|
87
76
|
def dtype(self) -> np.dtype:
|
|
88
77
|
return self._dtype
|
|
89
78
|
|
|
90
|
-
def update(self, new_array):
|
|
91
|
-
new_array = np.asarray(new_array)
|
|
92
|
-
if new_array.shape != self._shape:
|
|
93
|
-
raise ValueError(f"Shape must be {self._shape}")
|
|
94
|
-
if new_array.dtype != self.dtype:
|
|
95
|
-
raise ValueError(f"New values dtype must be {self.dtype}")
|
|
96
|
-
np.copyto(self._array, new_array)
|
|
97
|
-
|
|
98
79
|
|
|
99
80
|
class Grid(BaseMap):
|
|
100
81
|
"""
|
|
@@ -107,7 +88,6 @@ class Grid(BaseMap):
|
|
|
107
88
|
bounds: The size of map in the world (shape: (n, 2) (n>=2)). bounds[i, 0] means the lower bound of the world in the i-th dimension. bounds[i, 1] means the upper bound of the world in the i-th dimension.
|
|
108
89
|
resolution: resolution of the grid map
|
|
109
90
|
type_map: initial type map of the grid map (its shape must be the same as the converted grid map shape, and its dtype must be int)
|
|
110
|
-
dtype: data type of coordinates (must be int)
|
|
111
91
|
inflation_radius: radius of the inflation
|
|
112
92
|
|
|
113
93
|
Examples:
|
|
@@ -130,10 +110,10 @@ class Grid(BaseMap):
|
|
|
130
110
|
(102, 62)
|
|
131
111
|
|
|
132
112
|
>>> grid_map.dtype
|
|
133
|
-
|
|
113
|
+
dtype('int8')
|
|
134
114
|
|
|
135
115
|
>>> grid_map.type_map
|
|
136
|
-
GridTypeMap(
|
|
116
|
+
GridTypeMap(data=
|
|
137
117
|
[[0 0 0 ... 0 0 0]
|
|
138
118
|
[0 0 0 ... 0 0 0]
|
|
139
119
|
[0 0 0 ... 0 0 0]
|
|
@@ -141,7 +121,7 @@ class Grid(BaseMap):
|
|
|
141
121
|
[0 0 0 ... 0 0 0]
|
|
142
122
|
[0 0 0 ... 0 0 0]
|
|
143
123
|
[0 0 0 ... 0 0 0]]
|
|
144
|
-
|
|
124
|
+
, shape=(102, 62), dtype=int8)
|
|
145
125
|
|
|
146
126
|
>>> grid_map.map_to_world((1, 2))
|
|
147
127
|
(0.75, 1.25)
|
|
@@ -155,9 +135,9 @@ class Grid(BaseMap):
|
|
|
155
135
|
>>> grid_map.get_neighbors(Node((1, 2)), diagonal=False)
|
|
156
136
|
[Node((2, 2), (1, 2), 0, 0), Node((0, 2), (1, 2), 0, 0), Node((1, 3), (1, 2), 0, 0), Node((1, 1), (1, 2), 0, 0)]
|
|
157
137
|
|
|
158
|
-
>>> grid_map
|
|
138
|
+
>>> grid_map[1, 0] = TYPES.OBSTACLE # place an obstacle
|
|
159
139
|
>>> grid_map.get_neighbors(Node((0, 0))) # limited within the bounds
|
|
160
|
-
[Node((0, 1), (0, 0), 0, 0), Node((1,
|
|
140
|
+
[Node((0, 1), (0, 0), 0, 0), Node((1, 1), (0, 0), 0, 0)]
|
|
161
141
|
|
|
162
142
|
>>> grid_map.get_neighbors(Node((grid_map.shape[0] - 1, grid_map.shape[1] - 1)), diagonal=False) # limited within the boundss
|
|
163
143
|
[Node((100, 61), (101, 61), 0, 0), Node((101, 60), (101, 61), 0, 0)]
|
|
@@ -171,7 +151,7 @@ class Grid(BaseMap):
|
|
|
171
151
|
>>> grid_map.in_collision((1, 2), (3, 6))
|
|
172
152
|
False
|
|
173
153
|
|
|
174
|
-
>>> grid_map
|
|
154
|
+
>>> grid_map[1, 3] = TYPES.OBSTACLE
|
|
175
155
|
>>> grid_map.update_esdf()
|
|
176
156
|
>>> grid_map.in_collision((1, 2), (3, 6))
|
|
177
157
|
True
|
|
@@ -179,26 +159,19 @@ class Grid(BaseMap):
|
|
|
179
159
|
def __init__(self,
|
|
180
160
|
bounds: Iterable = [[0, 30], [0, 40]],
|
|
181
161
|
resolution: float = 1.0,
|
|
182
|
-
type_map: Union[GridTypeMap, np.ndarray] = None,
|
|
183
|
-
dtype: np.dtype = np.int32,
|
|
162
|
+
type_map: Union[GridTypeMap, np.ndarray] = None,
|
|
184
163
|
inflation_radius: float = 0.0,
|
|
185
164
|
) -> None:
|
|
186
|
-
super().__init__(bounds
|
|
187
|
-
|
|
188
|
-
self._dtype_options = [np.int8, np.int16, np.int32, np.int64]
|
|
189
|
-
if self._dtype not in self._dtype_options:
|
|
190
|
-
raise ValueError("Dtype must be one of {} instead of {}".format(self._dtype_options, self._dtype))
|
|
165
|
+
super().__init__(bounds)
|
|
191
166
|
|
|
192
167
|
self._resolution = resolution
|
|
193
|
-
|
|
168
|
+
shape = tuple([int((self.bounds[i, 1] - self.bounds[i, 0]) / self.resolution) for i in range(self.dim)])
|
|
194
169
|
|
|
195
170
|
if type_map is None:
|
|
196
|
-
self.type_map = GridTypeMap(np.zeros(
|
|
171
|
+
self.type_map = GridTypeMap(np.zeros(shape, dtype=np.int8))
|
|
197
172
|
else:
|
|
198
|
-
if type_map.shape !=
|
|
199
|
-
raise ValueError("Shape must be {} instead of {}".format(self.
|
|
200
|
-
if type_map.dtype not in self._dtype_options:
|
|
201
|
-
raise ValueError("Dtype must be one of {} instead of {}".format(self._dtype_options, type_map.dtype))
|
|
173
|
+
if type_map.shape != shape:
|
|
174
|
+
raise ValueError("Shape must be {} instead of {} with given bounds={} and resolution={}".format(shape, type_map.shape, self.bounds, self.resolution))
|
|
202
175
|
|
|
203
176
|
if isinstance(type_map, GridTypeMap):
|
|
204
177
|
self.type_map = type_map
|
|
@@ -209,7 +182,7 @@ class Grid(BaseMap):
|
|
|
209
182
|
|
|
210
183
|
self._precompute_offsets()
|
|
211
184
|
|
|
212
|
-
self._esdf = np.zeros(self.
|
|
185
|
+
self._esdf = np.zeros(self.shape, dtype=np.float32)
|
|
213
186
|
# self.update_esdf() # updated in self.inflate_obstacles()
|
|
214
187
|
|
|
215
188
|
self.inflation_radius = inflation_radius
|
|
@@ -228,13 +201,27 @@ class Grid(BaseMap):
|
|
|
228
201
|
|
|
229
202
|
@property
|
|
230
203
|
def shape(self) -> tuple:
|
|
231
|
-
return self.
|
|
204
|
+
return self.type_map.shape
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def dtype(self) -> np.dtype:
|
|
208
|
+
return self.type_map.dtype
|
|
232
209
|
|
|
233
210
|
@property
|
|
234
211
|
def esdf(self) -> np.ndarray:
|
|
235
212
|
return self._esdf
|
|
236
213
|
|
|
237
|
-
|
|
214
|
+
@property
|
|
215
|
+
def data(self) -> np.ndarray:
|
|
216
|
+
return self.type_map.data
|
|
217
|
+
|
|
218
|
+
def __getitem__(self, idx):
|
|
219
|
+
return self.type_map[idx]
|
|
220
|
+
|
|
221
|
+
def __setitem__(self, idx, value):
|
|
222
|
+
self.type_map[idx] = value
|
|
223
|
+
|
|
224
|
+
def map_to_world(self, point: tuple) -> Tuple[float, ...]:
|
|
238
225
|
"""
|
|
239
226
|
Convert map coordinates to world coordinates.
|
|
240
227
|
|
|
@@ -249,12 +236,13 @@ class Grid(BaseMap):
|
|
|
249
236
|
|
|
250
237
|
return tuple((x + 0.5) * self.resolution + float(self.bounds[i, 0]) for i, x in enumerate(point))
|
|
251
238
|
|
|
252
|
-
def world_to_map(self, point: Tuple[float, ...]) -> tuple:
|
|
239
|
+
def world_to_map(self, point: Tuple[float, ...], discrete: bool = True) -> tuple:
|
|
253
240
|
"""
|
|
254
241
|
Convert world coordinates to map coordinates.
|
|
255
242
|
|
|
256
243
|
Args:
|
|
257
244
|
point: Point in world coordinates.
|
|
245
|
+
discrete: Whether to round the coordinates to the nearest integer.
|
|
258
246
|
|
|
259
247
|
Returns:
|
|
260
248
|
point: Point in map coordinates.
|
|
@@ -262,7 +250,10 @@ class Grid(BaseMap):
|
|
|
262
250
|
if len(point) != self.dim:
|
|
263
251
|
raise ValueError("Point dimension does not match map dimension.")
|
|
264
252
|
|
|
265
|
-
|
|
253
|
+
point_map = tuple((x - float(self.bounds[i, 0])) * (1.0 / self.resolution) - 0.5 for i, x in enumerate(point))
|
|
254
|
+
if discrete:
|
|
255
|
+
point_map = self.point_float_to_int(point_map)
|
|
256
|
+
return point_map
|
|
266
257
|
|
|
267
258
|
def get_distance(self, p1: Tuple[int, int], p2: Tuple[int, int]) -> float:
|
|
268
259
|
"""
|
|
@@ -313,7 +304,7 @@ class Grid(BaseMap):
|
|
|
313
304
|
if not self.within_bounds(point):
|
|
314
305
|
return False
|
|
315
306
|
if src_point is not None:
|
|
316
|
-
if self._esdf[point] >= self._esdf[src_point]:
|
|
307
|
+
if self.type_map[src_point] == TYPES.INFLATION and self._esdf[point] >= self._esdf[src_point]:
|
|
317
308
|
return True
|
|
318
309
|
|
|
319
310
|
return not self.type_map[point] == TYPES.OBSTACLE and not self.type_map[point] == TYPES.INFLATION
|
|
@@ -334,10 +325,6 @@ class Grid(BaseMap):
|
|
|
334
325
|
"""
|
|
335
326
|
if node.dim != self.dim:
|
|
336
327
|
raise ValueError("Node dimension does not match map dimension.")
|
|
337
|
-
|
|
338
|
-
# current_point = node.current.astype(self.dtype)
|
|
339
|
-
# current_pos = current_point.numpy()
|
|
340
|
-
# neighbors = []
|
|
341
328
|
|
|
342
329
|
offsets = self._diagonal_offsets if diagonal else self._orthogonal_offsets
|
|
343
330
|
|
|
@@ -346,20 +333,9 @@ class Grid(BaseMap):
|
|
|
346
333
|
neighbors = [node + offset for offset in offsets]
|
|
347
334
|
filtered_neighbors = []
|
|
348
335
|
|
|
349
|
-
# print(neighbors)
|
|
350
|
-
|
|
351
|
-
# Filter out positions outside map bounds
|
|
352
|
-
# for pos in neighbor_positions:
|
|
353
|
-
# point = (pos, dtype=self.dtype)
|
|
354
|
-
# if self.within_bounds(point):
|
|
355
|
-
# if self.type_map[tuple(point)] != TYPES.OBSTACLE:
|
|
356
|
-
# neighbor_node = Node(point, parent=current_point)
|
|
357
|
-
# neighbors.append(neighbor_node)
|
|
358
336
|
for neighbor in neighbors:
|
|
359
337
|
if self.is_expandable(neighbor.current, node.current):
|
|
360
338
|
filtered_neighbors.append(neighbor)
|
|
361
|
-
|
|
362
|
-
# print(filtered_neighbors)
|
|
363
339
|
|
|
364
340
|
return filtered_neighbors
|
|
365
341
|
|
|
@@ -386,7 +362,7 @@ class Grid(BaseMap):
|
|
|
386
362
|
primary_step = 1 if delta[primary_axis] > 0 else -1
|
|
387
363
|
|
|
388
364
|
# Initialize the error variable
|
|
389
|
-
error = np.zeros(dim, dtype=
|
|
365
|
+
error = np.zeros(dim, dtype=int)
|
|
390
366
|
delta2 = 2 * abs_delta
|
|
391
367
|
|
|
392
368
|
# Calculate the number of steps and initialize the current point
|
|
@@ -495,11 +471,11 @@ class Grid(BaseMap):
|
|
|
495
471
|
radius: Radius of the inflation.
|
|
496
472
|
"""
|
|
497
473
|
self.update_esdf()
|
|
498
|
-
mask = (self.esdf <= radius) & (self.type_map.
|
|
474
|
+
mask = (self.esdf <= radius) & (self.type_map.data == TYPES.FREE)
|
|
499
475
|
self.type_map[mask] = TYPES.INFLATION
|
|
500
476
|
self.inflation_radius = radius
|
|
501
477
|
|
|
502
|
-
def fill_expands(self, expands: Dict[Tuple[int,
|
|
478
|
+
def fill_expands(self, expands: Dict[Tuple[int, ...], Node]) -> None:
|
|
503
479
|
"""
|
|
504
480
|
Fill the expands in the map.
|
|
505
481
|
|
|
@@ -515,9 +491,9 @@ class Grid(BaseMap):
|
|
|
515
491
|
"""
|
|
516
492
|
Update the ESDF (signed Euclidean Distance Field) based on the obstacles in the map.
|
|
517
493
|
- Obstacle grid ESDF = 0
|
|
518
|
-
- Free grid ESDF > 0. The value is the
|
|
494
|
+
- Free grid ESDF > 0. The value is the di/stance to the nearest obstacle
|
|
519
495
|
"""
|
|
520
|
-
obstacle_mask = (self.type_map.
|
|
496
|
+
obstacle_mask = (self.type_map.data == TYPES.OBSTACLE)
|
|
521
497
|
free_mask = ~obstacle_mask
|
|
522
498
|
|
|
523
499
|
# distance to obstacles
|
|
@@ -528,7 +504,7 @@ class Grid(BaseMap):
|
|
|
528
504
|
self._esdf = dist_outside.astype(np.float32)
|
|
529
505
|
self._esdf[obstacle_mask] = -dist_inside[obstacle_mask]
|
|
530
506
|
|
|
531
|
-
def path_map_to_world(self, path: List[
|
|
507
|
+
def path_map_to_world(self, path: List[tuple]) -> List[Tuple[float, ...]]:
|
|
532
508
|
"""
|
|
533
509
|
Convert path from map coordinates to world coordinates
|
|
534
510
|
|
|
@@ -540,17 +516,34 @@ class Grid(BaseMap):
|
|
|
540
516
|
"""
|
|
541
517
|
return [self.map_to_world(p) for p in path]
|
|
542
518
|
|
|
543
|
-
def path_world_to_map(self, path: List[Tuple[float,
|
|
519
|
+
def path_world_to_map(self, path: List[Tuple[float, ...]], discrete: bool = True) -> List[tuple]:
|
|
544
520
|
"""
|
|
545
521
|
Convert path from world coordinates to map coordinates
|
|
546
522
|
|
|
547
523
|
Args:
|
|
548
524
|
path: a list of world coordinates
|
|
525
|
+
discrete: whether to round the coordinates to the nearest integer
|
|
549
526
|
|
|
550
527
|
Returns:
|
|
551
528
|
path: a list of map coordinates
|
|
552
529
|
"""
|
|
553
|
-
return [self.world_to_map(p) for p in path]
|
|
530
|
+
return [self.world_to_map(p, discrete) for p in path]
|
|
531
|
+
|
|
532
|
+
def point_float_to_int(self, point: Tuple[float, ...]) -> Tuple[int, ...]:
|
|
533
|
+
"""
|
|
534
|
+
Convert a point from float to integer coordinates.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
point: a point in float coordinates
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
point: a point in integer coordinates
|
|
541
|
+
"""
|
|
542
|
+
point_int = []
|
|
543
|
+
for d in range(self.dim):
|
|
544
|
+
point_int.append(max(0, min(self.shape[d] - 1, int(round(point[d])))))
|
|
545
|
+
point_int = tuple(point_int)
|
|
546
|
+
return point_int
|
|
554
547
|
|
|
555
548
|
def _precompute_offsets(self):
|
|
556
549
|
# Generate all possible offsets (-1, 0, +1) in each dimension
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: frame_transformer.py
|
|
3
|
+
@author: Wu Maojia
|
|
4
|
+
@update: 2025.12.19
|
|
5
|
+
"""
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
class ChildTree:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.tree = {}
|
|
11
|
+
|
|
12
|
+
def __getitem__(self, point: tuple) -> Union[set, None]:
|
|
13
|
+
return self.tree.get(point)
|
|
14
|
+
|
|
15
|
+
def add(self, point: tuple, child_point: tuple) -> None:
|
|
16
|
+
if self.tree.get(point) is None:
|
|
17
|
+
self.tree[point] = set()
|
|
18
|
+
self.tree[point].add(child_point)
|
|
19
|
+
|
|
20
|
+
def remove(self, point: tuple, child_point: tuple) -> None:
|
|
21
|
+
if self.tree.get(point) is not None:
|
|
22
|
+
self.tree[point].discard(child_point)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: base_visualizer.py
|
|
3
|
+
@author: Wu Maojia
|
|
4
|
+
@update: 2025.12.20
|
|
5
|
+
"""
|
|
6
|
+
from typing import Union, Dict, List, Tuple, Any
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from python_motion_planning.common.utils import Geometry
|
|
12
|
+
|
|
13
|
+
class BaseVisualizer(ABC):
|
|
14
|
+
"""
|
|
15
|
+
Base visualizer for motion planning.
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.dim = None
|
|
19
|
+
self.trajs = {}
|
|
20
|
+
|
|
21
|
+
def __del__(self):
|
|
22
|
+
self.close()
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def show(self):
|
|
26
|
+
"""
|
|
27
|
+
Show plot.
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def close(self):
|
|
33
|
+
"""
|
|
34
|
+
Close plot.
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def get_traj_info(self,
|
|
39
|
+
rid: int,
|
|
40
|
+
ref_path: List[Tuple[float, ...]],
|
|
41
|
+
goal_pose: np.ndarray,
|
|
42
|
+
goal_dist_tol: float,
|
|
43
|
+
goal_orient_tol: float
|
|
44
|
+
) -> Dict[str, Any]:
|
|
45
|
+
"""
|
|
46
|
+
Get trajectory information.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
rid: Robot ID.
|
|
50
|
+
ref_path: Reference pose path (world frame).
|
|
51
|
+
goal_pose: Goal pose.
|
|
52
|
+
goal_dist_tol: Distance tolerance for goal.
|
|
53
|
+
goal_orient_tol: Orientation tolerance for goal.
|
|
54
|
+
"""
|
|
55
|
+
traj = self.trajs[rid]
|
|
56
|
+
|
|
57
|
+
info = {
|
|
58
|
+
"traj_length": 0.0,
|
|
59
|
+
"navigation_error": None,
|
|
60
|
+
"DTW": None,
|
|
61
|
+
"nDTW": None,
|
|
62
|
+
"success": False,
|
|
63
|
+
"dist_success": False,
|
|
64
|
+
"oracle_success": False,
|
|
65
|
+
"oracle_dist_success": False,
|
|
66
|
+
"success_time": None,
|
|
67
|
+
"dist_success_time": None,
|
|
68
|
+
"oracle_success_time": None,
|
|
69
|
+
"oracle_dist_success_time": None,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
goal_pos = goal_pose[:self.dim]
|
|
73
|
+
goal_orient = goal_pose[self.dim:]
|
|
74
|
+
|
|
75
|
+
for i in range(len(traj["poses"])):
|
|
76
|
+
pose = traj["poses"][i]
|
|
77
|
+
time = traj["time"][i]
|
|
78
|
+
|
|
79
|
+
pos = pose[:self.dim]
|
|
80
|
+
orient = pose[self.dim:]
|
|
81
|
+
|
|
82
|
+
if i > 0:
|
|
83
|
+
info["traj_length"] += np.linalg.norm(pos - traj["poses"][i-1][:self.dim])
|
|
84
|
+
|
|
85
|
+
if np.linalg.norm(pos - goal_pos) <= goal_dist_tol:
|
|
86
|
+
if not info["oracle_dist_success"]:
|
|
87
|
+
info["oracle_dist_success"] = True
|
|
88
|
+
info["oracle_dist_success_time"] = time
|
|
89
|
+
|
|
90
|
+
if not info["dist_success"]:
|
|
91
|
+
info["dist_success"] = True
|
|
92
|
+
info["dist_success_time"] = time
|
|
93
|
+
|
|
94
|
+
if np.abs(Geometry.regularize_orient(orient - goal_orient)) <= goal_orient_tol:
|
|
95
|
+
if not info["oracle_success"]:
|
|
96
|
+
info["oracle_success"] = True
|
|
97
|
+
info["oracle_success_time"] = time
|
|
98
|
+
|
|
99
|
+
if not info["success"]:
|
|
100
|
+
info["success"] = True
|
|
101
|
+
info["success_time"] = time
|
|
102
|
+
|
|
103
|
+
else:
|
|
104
|
+
info["success"] = False
|
|
105
|
+
info["success_time"] = None
|
|
106
|
+
|
|
107
|
+
else:
|
|
108
|
+
info["success"] = False
|
|
109
|
+
info["success_time"] = None
|
|
110
|
+
info["dist_success"] = False
|
|
111
|
+
info["dist_success_time"] = None
|
|
112
|
+
|
|
113
|
+
info["navigation_error"] = float(np.linalg.norm(traj["poses"][-1][:self.dim] - goal_pos))
|
|
114
|
+
info["traj_length"] = float(info["traj_length"])
|
|
115
|
+
info["DTW"], info["nDTW"] = self.calc_dtw_ndtw(np.array(traj["poses"])[:, :self.dim], np.array(ref_path)[:, :self.dim])
|
|
116
|
+
return info
|
|
117
|
+
|
|
118
|
+
def calc_dtw_ndtw(self, path1: np.ndarray, path2: np.ndarray) -> Tuple[float, float]:
|
|
119
|
+
"""
|
|
120
|
+
Compute the Dynamic Time Warping (DTW) and normalized DTW (nDTW)
|
|
121
|
+
between two N-dimensional paths.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
path1 (np.ndarray): Path 1, shape (N, D)
|
|
125
|
+
path2 (np.ndarray): Path 2, shape (M, D)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
dtw: accumulated dynamic time warping distance
|
|
129
|
+
ndtw: normalized DTW in [0, 1], higher means more similar
|
|
130
|
+
|
|
131
|
+
Reference:
|
|
132
|
+
[1] General Evaluation for Instruction Conditioned Navigation using Dynamic Time Warping
|
|
133
|
+
"""
|
|
134
|
+
# Input validation
|
|
135
|
+
if path1.ndim != 2 or path2.ndim != 2:
|
|
136
|
+
raise ValueError("Both paths must be 2D arrays with shape (T, D).")
|
|
137
|
+
if path1.shape[1] != path2.shape[1]:
|
|
138
|
+
raise ValueError("Paths must have the same dimensionality.")
|
|
139
|
+
|
|
140
|
+
N, M = len(path1), len(path2)
|
|
141
|
+
|
|
142
|
+
# Initialize DTW cost matrix
|
|
143
|
+
dtw_matrix = np.full((N + 1, M + 1), np.inf)
|
|
144
|
+
dtw_matrix[0, 0] = 0.0
|
|
145
|
+
|
|
146
|
+
# Fill the DTW matrix
|
|
147
|
+
for i in range(1, N + 1):
|
|
148
|
+
for j in range(1, M + 1):
|
|
149
|
+
# Euclidean distance between points
|
|
150
|
+
cost = np.linalg.norm(path1[i - 1] - path2[j - 1])
|
|
151
|
+
# Accumulate cost with the best previous step
|
|
152
|
+
dtw_matrix[i, j] = cost + min(
|
|
153
|
+
dtw_matrix[i - 1, j], # insertion
|
|
154
|
+
dtw_matrix[i, j - 1], # deletion
|
|
155
|
+
dtw_matrix[i - 1, j - 1] # match
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Final DTW distance
|
|
159
|
+
dtw = dtw_matrix[N, M]
|
|
160
|
+
|
|
161
|
+
# Normalized DTW: exponential decay with respect to path length
|
|
162
|
+
max_len = max(N, M)
|
|
163
|
+
ndtw = np.exp(-dtw / (max_len + 1e-8)) # nDTW ∈ (0, 1]
|
|
164
|
+
|
|
165
|
+
return float(dtw), float(ndtw)
|