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.
Files changed (37) hide show
  1. python_motion_planning/common/env/map/base_map.py +2 -8
  2. python_motion_planning/common/env/map/grid.py +74 -81
  3. python_motion_planning/common/utils/__init__.py +2 -1
  4. python_motion_planning/common/utils/child_tree.py +22 -0
  5. python_motion_planning/common/visualizer/__init__.py +3 -1
  6. python_motion_planning/common/visualizer/base_visualizer.py +165 -0
  7. python_motion_planning/common/visualizer/{visualizer.py → visualizer_2d.py} +97 -220
  8. python_motion_planning/common/visualizer/visualizer_3d.py +242 -0
  9. python_motion_planning/controller/base_controller.py +37 -4
  10. python_motion_planning/controller/path_tracker/__init__.py +2 -1
  11. python_motion_planning/controller/path_tracker/apf.py +22 -23
  12. python_motion_planning/controller/path_tracker/dwa.py +14 -17
  13. python_motion_planning/controller/path_tracker/path_tracker.py +4 -1
  14. python_motion_planning/controller/path_tracker/pid.py +7 -1
  15. python_motion_planning/controller/path_tracker/pure_pursuit.py +7 -1
  16. python_motion_planning/controller/path_tracker/rpp.py +111 -0
  17. python_motion_planning/path_planner/__init__.py +2 -1
  18. python_motion_planning/path_planner/base_path_planner.py +45 -11
  19. python_motion_planning/path_planner/graph_search/__init__.py +4 -1
  20. python_motion_planning/path_planner/graph_search/a_star.py +12 -14
  21. python_motion_planning/path_planner/graph_search/dijkstra.py +15 -15
  22. python_motion_planning/path_planner/graph_search/gbfs.py +100 -0
  23. python_motion_planning/path_planner/graph_search/jps.py +199 -0
  24. python_motion_planning/path_planner/graph_search/lazy_theta_star.py +113 -0
  25. python_motion_planning/path_planner/graph_search/theta_star.py +17 -19
  26. python_motion_planning/path_planner/hybrid_search/__init__.py +1 -0
  27. python_motion_planning/path_planner/hybrid_search/voronoi_planner.py +204 -0
  28. python_motion_planning/path_planner/sample_search/__init__.py +2 -1
  29. python_motion_planning/path_planner/sample_search/rrt.py +70 -28
  30. python_motion_planning/path_planner/sample_search/rrt_connect.py +237 -0
  31. python_motion_planning/path_planner/sample_search/rrt_star.py +201 -151
  32. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.dev2.dist-info}/METADATA +54 -19
  33. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.dev2.dist-info}/RECORD +36 -27
  34. python_motion_planning/common/env/robot/tmp.py +0 -404
  35. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.dev2.dist-info}/WHEEL +0 -0
  36. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.dev2.dist-info}/licenses/LICENSE +0 -0
  37. {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.10.3
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, dtype: np.dtype) -> None:
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.10.3
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(array(
29
+ GridTypeMap(data=
30
30
  [[0 0 0]
31
31
  [0 1 0]
32
32
  [0 0 0]]
33
- ), shape=(3, 3), dtype=int8)
33
+ , shape=(3, 3), dtype=int8)
34
34
 
35
- >>> grid_type_map.array
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._array = np.asarray(type_map)
59
- self._shape = self._array.shape
60
- self._dtype = self._array.dtype
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(array(\n{}\n), shape={}, dtype={})".format(self._array, self._shape, self._dtype)
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._array[idx]
62
+ return self._data[idx]
74
63
 
75
64
  def __setitem__(self, idx, value):
76
- self._array[idx] = value
65
+ self._data[idx] = value
77
66
 
78
67
  @property
79
- def array(self) -> np.ndarray:
80
- return self._array.view()
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
- <class 'numpy.int32'>
113
+ dtype('int8')
134
114
 
135
115
  >>> grid_map.type_map
136
- GridTypeMap(array(
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
- ), shape=(102, 62), dtype=int8)
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.type_map[1, 0] = TYPES.OBSTACLE # place an obstacle
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, 0), (0, 0), 0, 0), Node((1, 1), (0, 0), 0, 0)]
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.type_map[1, 3] = TYPES.OBSTACLE
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, dtype)
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
- self._shape = tuple([int((self.bounds[i, 1] - self.bounds[i, 0]) / self.resolution) for i in range(self.dim)])
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(self._shape, dtype=np.int8))
171
+ self.type_map = GridTypeMap(np.zeros(shape, dtype=np.int8))
197
172
  else:
198
- if type_map.shape != self._shape:
199
- raise ValueError("Shape must be {} instead of {}".format(self._shape, type_map.shape))
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._shape, dtype=np.float32)
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._shape
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
- def map_to_world(self, point: Tuple[int, ...]) -> tuple:
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
- return tuple(round((x - float(self.bounds[i, 0])) * (1.0 / self.resolution) - 0.5) for i, x in enumerate(point))
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=self.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.array == TYPES.FREE)
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, int], Node]) -> None:
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 distance to the nearest obstacle
494
+ - Free grid ESDF > 0. The value is the di/stance to the nearest obstacle
519
495
  """
520
- obstacle_mask = (self.type_map.array == TYPES.OBSTACLE)
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[Tuple[int, int]]) -> List[Tuple[float, float]]:
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, float]]) -> List[Tuple[int, int]]:
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
@@ -1,2 +1,3 @@
1
1
  from .geometry import Geometry
2
- from .frame_transformer import FrameTransformer
2
+ from .frame_transformer import FrameTransformer
3
+ from .child_tree import ChildTree
@@ -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)
@@ -1 +1,3 @@
1
- from .visualizer import *
1
+ from .base_visualizer import *
2
+ from .visualizer_2d import *
3
+ from .visualizer_3d import *
@@ -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)