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,20 +1,311 @@
1
1
  """
2
2
  @file: grid.py
3
3
  @author: Wu Maojia
4
- @update: 2025.10.3
4
+ @update: 2026.6.2
5
5
  """
6
6
  from itertools import product
7
7
  from typing import Iterable, Union, Tuple, Callable, List, Dict
8
+ import math
8
9
  import time
9
10
 
10
11
  import numpy as np
11
12
  from scipy import ndimage
12
13
 
14
+ try:
15
+ from numba import njit as _numba_njit
16
+ except Exception: # pragma: no cover - keeps import compatibility without numba.
17
+ _numba_njit = None
18
+
13
19
  from python_motion_planning.common.env.map.base_map import BaseMap
14
20
  from python_motion_planning.common.env import Node, TYPES
15
21
  from python_motion_planning.common.utils.geometry import Geometry
16
22
 
17
23
 
24
+ def _njit(*args, **kwargs):
25
+ if _numba_njit is None:
26
+ if args and callable(args[0]):
27
+ return args[0]
28
+
29
+ def decorator(func):
30
+ return func
31
+
32
+ return decorator
33
+
34
+ return _numba_njit(*args, **kwargs)
35
+
36
+
37
+ @_njit(cache=True)
38
+ def _grid_flat_index(point: np.ndarray, shape: np.ndarray) -> int:
39
+ idx = 0
40
+ for d in range(shape.size):
41
+ idx = idx * shape[d] + point[d]
42
+ return idx
43
+
44
+
45
+ @_njit(cache=True)
46
+ def _grid_within_bounds(point: np.ndarray, shape: np.ndarray) -> bool:
47
+ for d in range(shape.size):
48
+ if point[d] < 0 or point[d] >= shape[d]:
49
+ return False
50
+ return True
51
+
52
+
53
+ @_njit(cache=True)
54
+ def _grid_is_expandable(
55
+ point: np.ndarray,
56
+ src_point: np.ndarray,
57
+ has_src_point: bool,
58
+ shape: np.ndarray,
59
+ type_map: np.ndarray,
60
+ esdf: np.ndarray,
61
+ obstacle_type: int,
62
+ inflation_type: int,
63
+ ) -> bool:
64
+ if not _grid_within_bounds(point, shape):
65
+ return False
66
+
67
+ point_idx = _grid_flat_index(point, shape)
68
+ if has_src_point:
69
+ src_idx = _grid_flat_index(src_point, shape)
70
+ if type_map[src_idx] == inflation_type and esdf[point_idx] >= esdf[src_idx]:
71
+ return True
72
+
73
+ point_type = type_map[point_idx]
74
+ return point_type != obstacle_type and point_type != inflation_type
75
+
76
+
77
+ @_njit(cache=True)
78
+ def _grid_distance(p1: np.ndarray, p2: np.ndarray) -> float:
79
+ dist_square = 0.0
80
+ for d in range(p1.size):
81
+ diff = p1[d] - p2[d]
82
+ dist_square += diff * diff
83
+ return math.sqrt(dist_square)
84
+
85
+
86
+ @_njit(cache=True)
87
+ def _grid_map_to_world(point: np.ndarray, bounds: np.ndarray, resolution: float) -> np.ndarray:
88
+ point_world = np.empty(point.size, dtype=np.float64)
89
+ for d in range(point.size):
90
+ point_world[d] = (point[d] + 0.5) * resolution + bounds[d, 0]
91
+ return point_world
92
+
93
+
94
+ @_njit(cache=True)
95
+ def _grid_point_float_to_int(point: np.ndarray, shape: np.ndarray) -> np.ndarray:
96
+ point_int = np.empty(shape.size, dtype=np.int64)
97
+ for d in range(shape.size):
98
+ value = int(round(point[d]))
99
+ if value < 0:
100
+ value = 0
101
+ elif value >= shape[d]:
102
+ value = shape[d] - 1
103
+ point_int[d] = value
104
+ return point_int
105
+
106
+
107
+ @_njit(cache=True)
108
+ def _grid_world_to_map_float(point: np.ndarray, bounds: np.ndarray, resolution: float) -> np.ndarray:
109
+ point_map = np.empty(point.size, dtype=np.float64)
110
+ inv_resolution = 1.0 / resolution
111
+ for d in range(point.size):
112
+ point_map[d] = (point[d] - bounds[d, 0]) * inv_resolution - 0.5
113
+ return point_map
114
+
115
+
116
+ @_njit(cache=True)
117
+ def _grid_world_to_map_int(
118
+ point: np.ndarray,
119
+ bounds: np.ndarray,
120
+ resolution: float,
121
+ shape: np.ndarray,
122
+ ) -> np.ndarray:
123
+ point_map = np.empty(shape.size, dtype=np.int64)
124
+ inv_resolution = 1.0 / resolution
125
+ for d in range(shape.size):
126
+ value = int(round((point[d] - bounds[d, 0]) * inv_resolution - 0.5))
127
+ if value < 0:
128
+ value = 0
129
+ elif value >= shape[d]:
130
+ value = shape[d] - 1
131
+ point_map[d] = value
132
+ return point_map
133
+
134
+
135
+ @_njit(cache=True)
136
+ def _grid_line_of_sight(p1: np.ndarray, p2: np.ndarray) -> np.ndarray:
137
+ dim = p1.size
138
+ delta = np.empty(dim, dtype=np.int64)
139
+ abs_delta = np.empty(dim, dtype=np.int64)
140
+ delta2 = np.empty(dim, dtype=np.int64)
141
+
142
+ primary_axis = 0
143
+ max_delta = 0
144
+ for d in range(dim):
145
+ delta[d] = p2[d] - p1[d]
146
+ abs_delta[d] = abs(delta[d])
147
+ delta2[d] = 2 * abs_delta[d]
148
+ if abs_delta[d] > max_delta:
149
+ max_delta = abs_delta[d]
150
+ primary_axis = d
151
+
152
+ primary_step = 1 if delta[primary_axis] > 0 else -1
153
+ steps = abs_delta[primary_axis]
154
+ result = np.empty((steps + 1, dim), dtype=np.int64)
155
+ current = p1.copy()
156
+
157
+ for d in range(dim):
158
+ result[0, d] = current[d]
159
+
160
+ error = np.zeros(dim, dtype=np.int64)
161
+ for i in range(1, steps + 1):
162
+ current[primary_axis] += primary_step
163
+
164
+ for d in range(dim):
165
+ if d == primary_axis:
166
+ continue
167
+
168
+ error[d] += delta2[d]
169
+ if error[d] > abs_delta[primary_axis]:
170
+ current[d] += 1 if delta[d] > 0 else -1
171
+ error[d] -= delta2[primary_axis]
172
+
173
+ for d in range(dim):
174
+ result[i, d] = current[d]
175
+
176
+ return result
177
+
178
+
179
+ @_njit(cache=True)
180
+ def _grid_in_collision(
181
+ p1: np.ndarray,
182
+ p2: np.ndarray,
183
+ shape: np.ndarray,
184
+ type_map: np.ndarray,
185
+ esdf: np.ndarray,
186
+ obstacle_type: int,
187
+ inflation_type: int,
188
+ ) -> bool:
189
+ if not _grid_is_expandable(p1, p1, False, shape, type_map, esdf, obstacle_type, inflation_type):
190
+ return True
191
+ if not _grid_is_expandable(p2, p1, True, shape, type_map, esdf, obstacle_type, inflation_type):
192
+ return True
193
+
194
+ dim = p1.size
195
+ same_point = True
196
+ for d in range(dim):
197
+ if p1[d] != p2[d]:
198
+ same_point = False
199
+ break
200
+ if same_point:
201
+ return False
202
+
203
+ delta = np.empty(dim, dtype=np.int64)
204
+ abs_delta = np.empty(dim, dtype=np.int64)
205
+ delta2 = np.empty(dim, dtype=np.int64)
206
+
207
+ primary_axis = 0
208
+ max_delta = 0
209
+ for d in range(dim):
210
+ delta[d] = p2[d] - p1[d]
211
+ abs_delta[d] = abs(delta[d])
212
+ delta2[d] = 2 * abs_delta[d]
213
+ if abs_delta[d] > max_delta:
214
+ max_delta = abs_delta[d]
215
+ primary_axis = d
216
+
217
+ primary_step = 1 if delta[primary_axis] > 0 else -1
218
+ steps = abs_delta[primary_axis]
219
+ current = p1.copy()
220
+ last_point = np.empty(dim, dtype=np.int64)
221
+ error = np.zeros(dim, dtype=np.int64)
222
+
223
+ for _ in range(steps):
224
+ for d in range(dim):
225
+ last_point[d] = current[d]
226
+
227
+ current[primary_axis] += primary_step
228
+
229
+ for d in range(dim):
230
+ if d == primary_axis:
231
+ continue
232
+
233
+ error[d] += delta2[d]
234
+ if error[d] > abs_delta[primary_axis]:
235
+ current[d] += 1 if delta[d] > 0 else -1
236
+ error[d] -= delta2[primary_axis]
237
+
238
+ if not _grid_is_expandable(current, last_point, True, shape, type_map, esdf, obstacle_type, inflation_type):
239
+ return True
240
+
241
+ return False
242
+
243
+
244
+ @_njit(cache=True)
245
+ def _grid_neighbor_positions_and_mask(
246
+ current: np.ndarray,
247
+ offsets: np.ndarray,
248
+ shape: np.ndarray,
249
+ type_map: np.ndarray,
250
+ esdf: np.ndarray,
251
+ obstacle_type: int,
252
+ inflation_type: int,
253
+ ) -> Tuple[np.ndarray, np.ndarray]:
254
+ node_num = offsets.shape[0]
255
+ dim = offsets.shape[1]
256
+ positions = np.empty((node_num, dim), dtype=np.int64)
257
+ mask = np.zeros(node_num, dtype=np.bool_)
258
+ neighbor = np.empty(dim, dtype=np.int64)
259
+
260
+ for i in range(node_num):
261
+ for d in range(dim):
262
+ neighbor[d] = current[d] + offsets[i, d]
263
+ positions[i, d] = neighbor[d]
264
+
265
+ mask[i] = _grid_is_expandable(neighbor, current, True, shape, type_map, esdf, obstacle_type, inflation_type)
266
+
267
+ return positions, mask
268
+
269
+
270
+ @_njit(cache=True)
271
+ def _grid_path_map_to_world(points: np.ndarray, bounds: np.ndarray, resolution: float) -> np.ndarray:
272
+ path_world = np.empty((points.shape[0], points.shape[1]), dtype=np.float64)
273
+ for i in range(points.shape[0]):
274
+ for d in range(points.shape[1]):
275
+ path_world[i, d] = (points[i, d] + 0.5) * resolution + bounds[d, 0]
276
+ return path_world
277
+
278
+
279
+ @_njit(cache=True)
280
+ def _grid_path_world_to_map_float(points: np.ndarray, bounds: np.ndarray, resolution: float) -> np.ndarray:
281
+ path_map = np.empty((points.shape[0], points.shape[1]), dtype=np.float64)
282
+ inv_resolution = 1.0 / resolution
283
+ for i in range(points.shape[0]):
284
+ for d in range(points.shape[1]):
285
+ path_map[i, d] = (points[i, d] - bounds[d, 0]) * inv_resolution - 0.5
286
+ return path_map
287
+
288
+
289
+ @_njit(cache=True)
290
+ def _grid_path_world_to_map_int(
291
+ points: np.ndarray,
292
+ bounds: np.ndarray,
293
+ resolution: float,
294
+ shape: np.ndarray,
295
+ ) -> np.ndarray:
296
+ path_map = np.empty((points.shape[0], shape.size), dtype=np.int64)
297
+ inv_resolution = 1.0 / resolution
298
+ for i in range(points.shape[0]):
299
+ for d in range(shape.size):
300
+ value = int(round((points[i, d] - bounds[d, 0]) * inv_resolution - 0.5))
301
+ if value < 0:
302
+ value = 0
303
+ elif value >= shape[d]:
304
+ value = shape[d] - 1
305
+ path_map[i, d] = value
306
+ return path_map
307
+
308
+
18
309
  class GridTypeMap:
19
310
  """
20
311
  Class for Grid Type Map. It is like a np.ndarray, except that its shape and dtype are fixed.
@@ -26,13 +317,13 @@ class GridTypeMap:
26
317
  >>> type_map = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.int8)
27
318
  >>> grid_type_map = GridTypeMap(type_map)
28
319
  >>> grid_type_map
29
- GridTypeMap(array(
320
+ GridTypeMap(data=
30
321
  [[0 0 0]
31
322
  [0 1 0]
32
323
  [0 0 0]]
33
- ), shape=(3, 3), dtype=int8)
324
+ , shape=(3, 3), dtype=int8)
34
325
 
35
- >>> grid_type_map.array
326
+ >>> grid_type_map.data
36
327
  array([[0, 0, 0],
37
328
  [0, 1, 0],
38
329
  [0, 0, 0]], dtype=int8)
@@ -42,42 +333,31 @@ class GridTypeMap:
42
333
 
43
334
  >>> grid_type_map.dtype
44
335
  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
336
  """
57
337
  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
338
+ self._data = np.asarray(type_map)
339
+ self._shape = self._data.shape
340
+ self._dtype = self._data.dtype
61
341
 
62
342
  self._dtype_options = [np.int8, np.int16, np.int32, np.int64]
63
343
  if self._dtype not in self._dtype_options:
64
- raise ValueError("Dtype must be one of {} instead of {}".format(self._dtype_options, self._dtype))
344
+ 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
345
 
66
346
  def __str__(self) -> str:
67
- return "GridTypeMap(array(\n{}\n), shape={}, dtype={})".format(self._array, self._shape, self._dtype)
347
+ return "GridTypeMap(data=\n{}\n, shape={}, dtype={})".format(self._data, self._shape, self._dtype)
68
348
 
69
349
  def __repr__(self) -> str:
70
350
  return self.__str__()
71
351
 
72
352
  def __getitem__(self, idx):
73
- return self._array[idx]
353
+ return self._data[idx]
74
354
 
75
355
  def __setitem__(self, idx, value):
76
- self._array[idx] = value
356
+ self._data[idx] = value
77
357
 
78
358
  @property
79
- def array(self) -> np.ndarray:
80
- return self._array.view()
359
+ def data(self) -> np.ndarray:
360
+ return self._data.view()
81
361
 
82
362
  @property
83
363
  def shape(self) -> Tuple:
@@ -87,14 +367,6 @@ class GridTypeMap:
87
367
  def dtype(self) -> np.dtype:
88
368
  return self._dtype
89
369
 
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
370
 
99
371
  class Grid(BaseMap):
100
372
  """
@@ -107,7 +379,6 @@ class Grid(BaseMap):
107
379
  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
380
  resolution: resolution of the grid map
109
381
  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
382
  inflation_radius: radius of the inflation
112
383
 
113
384
  Examples:
@@ -130,10 +401,10 @@ class Grid(BaseMap):
130
401
  (102, 62)
131
402
 
132
403
  >>> grid_map.dtype
133
- <class 'numpy.int32'>
404
+ dtype('int8')
134
405
 
135
406
  >>> grid_map.type_map
136
- GridTypeMap(array(
407
+ GridTypeMap(data=
137
408
  [[0 0 0 ... 0 0 0]
138
409
  [0 0 0 ... 0 0 0]
139
410
  [0 0 0 ... 0 0 0]
@@ -141,7 +412,7 @@ class Grid(BaseMap):
141
412
  [0 0 0 ... 0 0 0]
142
413
  [0 0 0 ... 0 0 0]
143
414
  [0 0 0 ... 0 0 0]]
144
- ), shape=(102, 62), dtype=int8)
415
+ , shape=(102, 62), dtype=int8)
145
416
 
146
417
  >>> grid_map.map_to_world((1, 2))
147
418
  (0.75, 1.25)
@@ -155,9 +426,9 @@ class Grid(BaseMap):
155
426
  >>> grid_map.get_neighbors(Node((1, 2)), diagonal=False)
156
427
  [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
428
 
158
- >>> grid_map.type_map[1, 0] = TYPES.OBSTACLE # place an obstacle
429
+ >>> grid_map[1, 0] = TYPES.OBSTACLE # place an obstacle
159
430
  >>> 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)]
431
+ [Node((0, 1), (0, 0), 0, 0), Node((1, 1), (0, 0), 0, 0)]
161
432
 
162
433
  >>> grid_map.get_neighbors(Node((grid_map.shape[0] - 1, grid_map.shape[1] - 1)), diagonal=False) # limited within the boundss
163
434
  [Node((100, 61), (101, 61), 0, 0), Node((101, 60), (101, 61), 0, 0)]
@@ -171,7 +442,7 @@ class Grid(BaseMap):
171
442
  >>> grid_map.in_collision((1, 2), (3, 6))
172
443
  False
173
444
 
174
- >>> grid_map.type_map[1, 3] = TYPES.OBSTACLE
445
+ >>> grid_map[1, 3] = TYPES.OBSTACLE
175
446
  >>> grid_map.update_esdf()
176
447
  >>> grid_map.in_collision((1, 2), (3, 6))
177
448
  True
@@ -179,26 +450,19 @@ class Grid(BaseMap):
179
450
  def __init__(self,
180
451
  bounds: Iterable = [[0, 30], [0, 40]],
181
452
  resolution: float = 1.0,
182
- type_map: Union[GridTypeMap, np.ndarray] = None,
183
- dtype: np.dtype = np.int32,
453
+ type_map: Union[GridTypeMap, np.ndarray] = None,
184
454
  inflation_radius: float = 0.0,
185
455
  ) -> 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))
456
+ super().__init__(bounds)
191
457
 
192
458
  self._resolution = resolution
193
- self._shape = tuple([int((self.bounds[i, 1] - self.bounds[i, 0]) / self.resolution) for i in range(self.dim)])
459
+ shape = tuple([int((self.bounds[i, 1] - self.bounds[i, 0]) / self.resolution) for i in range(self.dim)])
194
460
 
195
461
  if type_map is None:
196
- self.type_map = GridTypeMap(np.zeros(self._shape, dtype=np.int8))
462
+ self.type_map = GridTypeMap(np.zeros(shape, dtype=np.int8))
197
463
  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))
464
+ if type_map.shape != shape:
465
+ raise ValueError("Shape must be {} instead of {} with given bounds={} and resolution={}".format(shape, type_map.shape, self.bounds, self.resolution))
202
466
 
203
467
  if isinstance(type_map, GridTypeMap):
204
468
  self.type_map = type_map
@@ -207,9 +471,10 @@ class Grid(BaseMap):
207
471
  else:
208
472
  raise ValueError("Type map must be GridTypeMap or numpy.ndarray instead of {}".format(type(type_map)))
209
473
 
474
+ self._shape_array = np.asarray(self.shape, dtype=np.int64)
210
475
  self._precompute_offsets()
211
476
 
212
- self._esdf = np.zeros(self._shape, dtype=np.float32)
477
+ self._esdf = np.zeros(self.shape, dtype=np.float32)
213
478
  # self.update_esdf() # updated in self.inflate_obstacles()
214
479
 
215
480
  self.inflation_radius = inflation_radius
@@ -228,13 +493,33 @@ class Grid(BaseMap):
228
493
 
229
494
  @property
230
495
  def shape(self) -> tuple:
231
- return self._shape
496
+ return self.type_map.shape
497
+
498
+ @property
499
+ def dtype(self) -> np.dtype:
500
+ return self.type_map.dtype
232
501
 
233
502
  @property
234
503
  def esdf(self) -> np.ndarray:
235
504
  return self._esdf
236
505
 
237
- def map_to_world(self, point: Tuple[int, ...]) -> tuple:
506
+ @property
507
+ def data(self) -> np.ndarray:
508
+ return self.type_map.data
509
+
510
+ def __getitem__(self, idx):
511
+ return self.type_map[idx]
512
+
513
+ def __setitem__(self, idx, value):
514
+ self.type_map[idx] = value
515
+
516
+ def _type_map_flat(self) -> np.ndarray:
517
+ return np.ravel(self.type_map.data)
518
+
519
+ def _esdf_flat(self) -> np.ndarray:
520
+ return np.ravel(self._esdf)
521
+
522
+ def map_to_world(self, point: tuple) -> Tuple[float, ...]:
238
523
  """
239
524
  Convert map coordinates to world coordinates.
240
525
 
@@ -247,14 +532,16 @@ class Grid(BaseMap):
247
532
  if len(point) != self.dim:
248
533
  raise ValueError("Point dimension does not match map dimension.")
249
534
 
250
- return tuple((x + 0.5) * self.resolution + float(self.bounds[i, 0]) for i, x in enumerate(point))
535
+ point_world = _grid_map_to_world(np.asarray(point, dtype=np.float64), self.bounds, self.resolution)
536
+ return tuple(float(x) for x in point_world)
251
537
 
252
- def world_to_map(self, point: Tuple[float, ...]) -> tuple:
538
+ def world_to_map(self, point: Tuple[float, ...], discrete: bool = True) -> tuple:
253
539
  """
254
540
  Convert world coordinates to map coordinates.
255
541
 
256
542
  Args:
257
543
  point: Point in world coordinates.
544
+ discrete: Whether to round the coordinates to the nearest integer.
258
545
 
259
546
  Returns:
260
547
  point: Point in map coordinates.
@@ -262,7 +549,13 @@ class Grid(BaseMap):
262
549
  if len(point) != self.dim:
263
550
  raise ValueError("Point dimension does not match map dimension.")
264
551
 
265
- return tuple(round((x - float(self.bounds[i, 0])) * (1.0 / self.resolution) - 0.5) for i, x in enumerate(point))
552
+ point_array = np.asarray(point, dtype=np.float64)
553
+ if discrete:
554
+ point_map = _grid_world_to_map_int(point_array, self.bounds, self.resolution, self._shape_array)
555
+ return tuple(int(x) for x in point_map)
556
+ else:
557
+ point_map = _grid_world_to_map_float(point_array, self.bounds, self.resolution)
558
+ return tuple(float(x) for x in point_map)
266
559
 
267
560
  def get_distance(self, p1: Tuple[int, int], p2: Tuple[int, int]) -> float:
268
561
  """
@@ -275,7 +568,9 @@ class Grid(BaseMap):
275
568
  Returns:
276
569
  dist: Distance between two points.
277
570
  """
278
- return Geometry.dist(p1, p2, type='Euclidean')
571
+ if len(p1) != len(p2):
572
+ raise ValueError("Dimension mismatch")
573
+ return _grid_distance(np.asarray(p1, dtype=np.float64), np.asarray(p2, dtype=np.float64))
279
574
 
280
575
  def within_bounds(self, point: Tuple[int, ...]) -> bool:
281
576
  """
@@ -291,13 +586,7 @@ class Grid(BaseMap):
291
586
  # raise ValueError("Point dimension does not match map dimension.")
292
587
 
293
588
  # return all(0 <= point[i] < self.shape[i] for i in range(self.dim))
294
- dim = self.dim
295
- shape = self.shape
296
-
297
- for i in range(dim):
298
- if not (0 <= point[i] < shape[i]):
299
- return False
300
- return True
589
+ return _grid_within_bounds(np.asarray(point, dtype=np.int64), self._shape_array)
301
590
 
302
591
  def is_expandable(self, point: Tuple[int, ...], src_point: Tuple[int, ...] = None) -> bool:
303
592
  """
@@ -310,13 +599,20 @@ class Grid(BaseMap):
310
599
  Returns:
311
600
  expandable: True if the point is expandable, False otherwise.
312
601
  """
313
- if not self.within_bounds(point):
314
- return False
315
- if src_point is not None:
316
- if self._esdf[point] >= self._esdf[src_point]:
317
- return True
318
-
319
- return not self.type_map[point] == TYPES.OBSTACLE and not self.type_map[point] == TYPES.INFLATION
602
+ point_array = np.asarray(point, dtype=np.int64)
603
+ has_src_point = src_point is not None
604
+ src_array = point_array if src_point is None else np.asarray(src_point, dtype=np.int64)
605
+
606
+ return _grid_is_expandable(
607
+ point_array,
608
+ src_array,
609
+ has_src_point,
610
+ self._shape_array,
611
+ self._type_map_flat(),
612
+ self._esdf_flat(),
613
+ TYPES.OBSTACLE,
614
+ TYPES.INFLATION,
615
+ )
320
616
 
321
617
  def get_neighbors(self,
322
618
  node: Node,
@@ -334,34 +630,23 @@ class Grid(BaseMap):
334
630
  """
335
631
  if node.dim != self.dim:
336
632
  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
-
342
- offsets = self._diagonal_offsets if diagonal else self._orthogonal_offsets
343
633
 
344
- # Generate all neighbor positions
345
- # neighbor_positions = current_pos + offsets
346
- neighbors = [node + offset for offset in offsets]
347
- filtered_neighbors = []
348
-
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
- for neighbor in neighbors:
359
- if self.is_expandable(neighbor.current, node.current):
360
- filtered_neighbors.append(neighbor)
361
-
362
- # print(filtered_neighbors)
363
-
364
- return filtered_neighbors
634
+ offsets = self._diagonal_offsets_array if diagonal else self._orthogonal_offsets_array
635
+ positions, mask = _grid_neighbor_positions_and_mask(
636
+ np.asarray(node.current, dtype=np.int64),
637
+ offsets,
638
+ self._shape_array,
639
+ self._type_map_flat(),
640
+ self._esdf_flat(),
641
+ TYPES.OBSTACLE,
642
+ TYPES.INFLATION,
643
+ )
644
+
645
+ return [
646
+ Node(tuple(int(x) for x in positions[i]), node.current, node.g, node.h)
647
+ for i in range(positions.shape[0])
648
+ if mask[i]
649
+ ]
365
650
 
366
651
  def line_of_sight(self, p1: Tuple[int, ...], p2: Tuple[int, ...]) -> List[Tuple[int, ...]]:
367
652
  """
@@ -374,45 +659,13 @@ class Grid(BaseMap):
374
659
  Returns:
375
660
  points: List of point on the line of sight.
376
661
  """
377
- p1 = np.array(p1)
378
- p2 = np.array(p2)
379
-
380
- dim = len(p1)
381
- delta = p2 - p1
382
- abs_delta = np.abs(delta)
383
-
384
- # Determine the main direction axis (the dimension with the greatest change)
385
- primary_axis = np.argmax(abs_delta)
386
- primary_step = 1 if delta[primary_axis] > 0 else -1
387
-
388
- # Initialize the error variable
389
- error = np.zeros(dim, dtype=self.dtype)
390
- delta2 = 2 * abs_delta
391
-
392
- # Calculate the number of steps and initialize the current point
393
- steps = abs_delta[primary_axis]
394
- current = p1
395
-
396
- # Allocate the result array
397
- result = []
398
- result.append(tuple(int(x) for x in current))
399
-
400
- for i in range(1, steps + 1):
401
- current[primary_axis] += primary_step
402
-
403
- # Update the error for the primary dimension
404
- for d in range(dim):
405
- if d == primary_axis:
406
- continue
407
-
408
- error[d] += delta2[d]
409
- if error[d] > abs_delta[primary_axis]:
410
- current[d] += 1 if delta[d] > 0 else -1
411
- error[d] -= delta2[primary_axis]
412
-
413
- result.append(tuple(int(x) for x in current))
662
+ p1_array = np.asarray(p1, dtype=np.int64)
663
+ p2_array = np.asarray(p2, dtype=np.int64)
664
+ if p1_array.shape != p2_array.shape:
665
+ p2_array - p1_array
414
666
 
415
- return result
667
+ points = _grid_line_of_sight(p1_array, p2_array)
668
+ return [tuple(int(x) for x in points[i]) for i in range(points.shape[0])]
416
669
 
417
670
  def in_collision(self, p1: Tuple[int, ...], p2: Tuple[int, ...]) -> bool:
418
671
  """
@@ -425,51 +678,20 @@ class Grid(BaseMap):
425
678
  Returns:
426
679
  in_collision: True if the line of sight is in collision, False otherwise.
427
680
  """
428
- if not self.is_expandable(p1) or not self.is_expandable(p2, p1):
429
- return True
430
-
431
- # Corner Case: Start and end points are the same
432
- if p1 == p2:
433
- return False
434
-
435
- p1 = np.array(p1)
436
- p2 = np.array(p2)
437
-
438
- # Calculate delta and absolute delta
439
- delta = p2 - p1
440
- abs_delta = np.abs(delta)
441
-
442
- # Determine the primary axis (the dimension with the greatest change)
443
- primary_axis = np.argmax(abs_delta)
444
- primary_step = 1 if delta[primary_axis] > 0 else -1
445
-
446
- # Initialize the error variable
447
- error = np.zeros_like(delta, dtype=np.int32)
448
- delta2 = 2 * abs_delta
449
-
450
- # calculate the number of steps and initialize the current point
451
- steps = abs_delta[primary_axis]
452
- current = p1
453
-
454
- for _ in range(steps):
455
- last_point = current.copy()
456
- current[primary_axis] += primary_step
457
-
458
- # Update the error for the primary dimension
459
- for d in range(len(delta)):
460
- if d == primary_axis:
461
- continue
462
-
463
- error[d] += delta2[d]
464
- if error[d] > abs_delta[primary_axis]:
465
- current[d] += 1 if delta[d] > 0 else -1
466
- error[d] -= delta2[primary_axis]
467
-
468
- # Check the current point
469
- if not self.is_expandable(tuple(current), tuple(last_point)):
470
- return True
471
-
472
- return False
681
+ p1_array = np.asarray(p1, dtype=np.int64)
682
+ p2_array = np.asarray(p2, dtype=np.int64)
683
+ if p1_array.shape != p2_array.shape:
684
+ p2_array - p1_array
685
+
686
+ return _grid_in_collision(
687
+ p1_array,
688
+ p2_array,
689
+ self._shape_array,
690
+ self._type_map_flat(),
691
+ self._esdf_flat(),
692
+ TYPES.OBSTACLE,
693
+ TYPES.INFLATION,
694
+ )
473
695
 
474
696
  def fill_boundary_with_obstacles(self) -> None:
475
697
  """
@@ -495,11 +717,11 @@ class Grid(BaseMap):
495
717
  radius: Radius of the inflation.
496
718
  """
497
719
  self.update_esdf()
498
- mask = (self.esdf <= radius) & (self.type_map.array == TYPES.FREE)
720
+ mask = (self.esdf <= radius) & (self.type_map.data == TYPES.FREE)
499
721
  self.type_map[mask] = TYPES.INFLATION
500
722
  self.inflation_radius = radius
501
723
 
502
- def fill_expands(self, expands: Dict[Tuple[int, int], Node]) -> None:
724
+ def fill_expands(self, expands: Dict[Tuple[int, ...], Node]) -> None:
503
725
  """
504
726
  Fill the expands in the map.
505
727
 
@@ -515,9 +737,9 @@ class Grid(BaseMap):
515
737
  """
516
738
  Update the ESDF (signed Euclidean Distance Field) based on the obstacles in the map.
517
739
  - Obstacle grid ESDF = 0
518
- - Free grid ESDF > 0. The value is the distance to the nearest obstacle
740
+ - Free grid ESDF > 0. The value is the di/stance to the nearest obstacle
519
741
  """
520
- obstacle_mask = (self.type_map.array == TYPES.OBSTACLE)
742
+ obstacle_mask = (self.type_map.data == TYPES.OBSTACLE)
521
743
  free_mask = ~obstacle_mask
522
744
 
523
745
  # distance to obstacles
@@ -528,7 +750,7 @@ class Grid(BaseMap):
528
750
  self._esdf = dist_outside.astype(np.float32)
529
751
  self._esdf[obstacle_mask] = -dist_inside[obstacle_mask]
530
752
 
531
- def path_map_to_world(self, path: List[Tuple[int, int]]) -> List[Tuple[float, float]]:
753
+ def path_map_to_world(self, path: List[tuple]) -> List[Tuple[float, ...]]:
532
754
  """
533
755
  Convert path from map coordinates to world coordinates
534
756
 
@@ -538,32 +760,68 @@ class Grid(BaseMap):
538
760
  Returns:
539
761
  path: a list of world coordinates
540
762
  """
541
- return [self.map_to_world(p) for p in path]
763
+ path = list(path)
764
+ if not path:
765
+ return []
766
+
767
+ points = np.asarray(path, dtype=np.float64)
768
+ if points.ndim != 2 or points.shape[1] != self.dim:
769
+ raise ValueError("Point dimension does not match map dimension.")
770
+
771
+ path_world = _grid_path_map_to_world(points, self.bounds, self.resolution)
772
+ return [tuple(float(x) for x in path_world[i]) for i in range(path_world.shape[0])]
542
773
 
543
- def path_world_to_map(self, path: List[Tuple[float, float]]) -> List[Tuple[int, int]]:
774
+ def path_world_to_map(self, path: List[Tuple[float, ...]], discrete: bool = True) -> List[tuple]:
544
775
  """
545
776
  Convert path from world coordinates to map coordinates
546
777
 
547
778
  Args:
548
779
  path: a list of world coordinates
780
+ discrete: whether to round the coordinates to the nearest integer
549
781
 
550
782
  Returns:
551
783
  path: a list of map coordinates
552
784
  """
553
- return [self.world_to_map(p) for p in path]
785
+ path = list(path)
786
+ if not path:
787
+ return []
788
+
789
+ points = np.asarray(path, dtype=np.float64)
790
+ if points.ndim != 2 or points.shape[1] != self.dim:
791
+ raise ValueError("Point dimension does not match map dimension.")
792
+
793
+ if discrete:
794
+ path_map = _grid_path_world_to_map_int(points, self.bounds, self.resolution, self._shape_array)
795
+ return [tuple(int(x) for x in path_map[i]) for i in range(path_map.shape[0])]
796
+ else:
797
+ path_map = _grid_path_world_to_map_float(points, self.bounds, self.resolution)
798
+ return [tuple(float(x) for x in path_map[i]) for i in range(path_map.shape[0])]
799
+
800
+ def point_float_to_int(self, point: Tuple[float, ...]) -> Tuple[int, ...]:
801
+ """
802
+ Convert a point from float to integer coordinates.
803
+
804
+ Args:
805
+ point: a point in float coordinates
806
+
807
+ Returns:
808
+ point: a point in integer coordinates
809
+ """
810
+ point_int = _grid_point_float_to_int(np.asarray(point, dtype=np.float64), self._shape_array)
811
+ return tuple(int(x) for x in point_int)
554
812
 
555
813
  def _precompute_offsets(self):
556
814
  # Generate all possible offsets (-1, 0, +1) in each dimension
557
- self._diagonal_offsets = np.array(np.meshgrid(*[[-1, 0, 1]]*self.dim), dtype=self.dtype).T.reshape(-1, self.dim)
815
+ self._diagonal_offsets_array = np.array(np.meshgrid(*[[-1, 0, 1]]*self.dim), dtype=np.int64).T.reshape(-1, self.dim)
558
816
  # Remove the zero offset (current node itself)
559
- self._diagonal_offsets = self._diagonal_offsets[np.any(self._diagonal_offsets != 0, axis=1)]
817
+ self._diagonal_offsets_array = self._diagonal_offsets_array[np.any(self._diagonal_offsets_array != 0, axis=1)]
560
818
  # self._diagonal_offsets = [Node((offset.tolist(), dtype=self.dtype)) for offset in self._diagonal_offsets]
561
- self._diagonal_offsets = [Node(tuple(offset.tolist())) for offset in self._diagonal_offsets]
819
+ self._diagonal_offsets = [Node(tuple(offset.tolist())) for offset in self._diagonal_offsets_array]
562
820
 
563
821
  # Generate only orthogonal offsets (one dimension changes by ±1)
564
- self._orthogonal_offsets = np.zeros((2*self.dim, self.dim), dtype=self.dtype)
822
+ self._orthogonal_offsets_array = np.zeros((2*self.dim, self.dim), dtype=np.int64)
565
823
  for d in range(self.dim):
566
- self._orthogonal_offsets[2*d, d] = 1
567
- self._orthogonal_offsets[2*d+1, d] = -1
824
+ self._orthogonal_offsets_array[2*d, d] = 1
825
+ self._orthogonal_offsets_array[2*d+1, d] = -1
568
826
  # self._orthogonal_offsets = [Node((offset.tolist(), dtype=self.dtype)) for offset in self._orthogonal_offsets]
569
- self._orthogonal_offsets = [Node(tuple(offset.tolist())) for offset in self._orthogonal_offsets]
827
+ self._orthogonal_offsets = [Node(tuple(offset.tolist())) for offset in self._orthogonal_offsets_array]