python-motion-planning 2.0.dev2__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 (31) hide show
  1. python_motion_planning/__init__.py +1 -1
  2. python_motion_planning/common/env/map/grid.py +394 -129
  3. python_motion_planning/common/utils/geometry.py +18 -29
  4. python_motion_planning/path_planner/sample_search/rrt.py +5 -5
  5. python_motion_planning/path_planner/sample_search/rrt_connect.py +2 -2
  6. python_motion_planning/path_planner/sample_search/rrt_star.py +31 -11
  7. python_motion_planning/traj_optimizer/__init__.py +2 -0
  8. python_motion_planning/traj_optimizer/base_curve_generator.py +53 -0
  9. python_motion_planning/traj_optimizer/curve_generator/__init__.py +2 -0
  10. python_motion_planning/traj_optimizer/curve_generator/point_based/__init__.py +2 -0
  11. python_motion_planning/traj_optimizer/curve_generator/point_based/bspline.py +256 -0
  12. python_motion_planning/traj_optimizer/curve_generator/point_based/cubic_spline.py +115 -0
  13. python_motion_planning/traj_optimizer/curve_generator/pose_based/__init__.py +4 -0
  14. python_motion_planning/traj_optimizer/curve_generator/pose_based/bezier.py +121 -0
  15. python_motion_planning/traj_optimizer/curve_generator/pose_based/dubins.py +355 -0
  16. python_motion_planning/traj_optimizer/curve_generator/pose_based/polynomial.py +197 -0
  17. python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py +606 -0
  18. {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/METADATA +22 -15
  19. {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/RECORD +22 -20
  20. {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/WHEEL +1 -1
  21. python_motion_planning/curve_generator/__init__.py +0 -9
  22. python_motion_planning/curve_generator/bezier_curve.py +0 -131
  23. python_motion_planning/curve_generator/bspline_curve.py +0 -271
  24. python_motion_planning/curve_generator/cubic_spline.py +0 -128
  25. python_motion_planning/curve_generator/curve.py +0 -64
  26. python_motion_planning/curve_generator/dubins_curve.py +0 -348
  27. python_motion_planning/curve_generator/fem_pos_smooth.py +0 -114
  28. python_motion_planning/curve_generator/polynomial_curve.py +0 -226
  29. python_motion_planning/curve_generator/reeds_shepp.py +0 -736
  30. {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/licenses/LICENSE +0 -0
  31. {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
1
  from .common import *
2
2
  from .path_planner import *
3
3
  from .controller import *
4
- from .curve_generator import *
4
+ from .traj_optimizer import *
@@ -1,20 +1,311 @@
1
1
  """
2
2
  @file: grid.py
3
3
  @author: Wu Maojia
4
- @update: 2025.12.20
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.
@@ -180,6 +471,7 @@ class Grid(BaseMap):
180
471
  else:
181
472
  raise ValueError("Type map must be GridTypeMap or numpy.ndarray instead of {}".format(type(type_map)))
182
473
 
474
+ self._shape_array = np.asarray(self.shape, dtype=np.int64)
183
475
  self._precompute_offsets()
184
476
 
185
477
  self._esdf = np.zeros(self.shape, dtype=np.float32)
@@ -221,6 +513,12 @@ class Grid(BaseMap):
221
513
  def __setitem__(self, idx, value):
222
514
  self.type_map[idx] = value
223
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
+
224
522
  def map_to_world(self, point: tuple) -> Tuple[float, ...]:
225
523
  """
226
524
  Convert map coordinates to world coordinates.
@@ -234,7 +532,8 @@ class Grid(BaseMap):
234
532
  if len(point) != self.dim:
235
533
  raise ValueError("Point dimension does not match map dimension.")
236
534
 
237
- 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)
238
537
 
239
538
  def world_to_map(self, point: Tuple[float, ...], discrete: bool = True) -> tuple:
240
539
  """
@@ -250,10 +549,13 @@ class Grid(BaseMap):
250
549
  if len(point) != self.dim:
251
550
  raise ValueError("Point dimension does not match map dimension.")
252
551
 
253
- point_map = tuple((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)
254
553
  if discrete:
255
- point_map = self.point_float_to_int(point_map)
256
- return point_map
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)
257
559
 
258
560
  def get_distance(self, p1: Tuple[int, int], p2: Tuple[int, int]) -> float:
259
561
  """
@@ -266,7 +568,9 @@ class Grid(BaseMap):
266
568
  Returns:
267
569
  dist: Distance between two points.
268
570
  """
269
- 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))
270
574
 
271
575
  def within_bounds(self, point: Tuple[int, ...]) -> bool:
272
576
  """
@@ -282,13 +586,7 @@ class Grid(BaseMap):
282
586
  # raise ValueError("Point dimension does not match map dimension.")
283
587
 
284
588
  # return all(0 <= point[i] < self.shape[i] for i in range(self.dim))
285
- dim = self.dim
286
- shape = self.shape
287
-
288
- for i in range(dim):
289
- if not (0 <= point[i] < shape[i]):
290
- return False
291
- return True
589
+ return _grid_within_bounds(np.asarray(point, dtype=np.int64), self._shape_array)
292
590
 
293
591
  def is_expandable(self, point: Tuple[int, ...], src_point: Tuple[int, ...] = None) -> bool:
294
592
  """
@@ -301,13 +599,20 @@ class Grid(BaseMap):
301
599
  Returns:
302
600
  expandable: True if the point is expandable, False otherwise.
303
601
  """
304
- if not self.within_bounds(point):
305
- return False
306
- if src_point is not None:
307
- if self.type_map[src_point] == TYPES.INFLATION and self._esdf[point] >= self._esdf[src_point]:
308
- return True
309
-
310
- 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
+ )
311
616
 
312
617
  def get_neighbors(self,
313
618
  node: Node,
@@ -326,18 +631,22 @@ class Grid(BaseMap):
326
631
  if node.dim != self.dim:
327
632
  raise ValueError("Node dimension does not match map dimension.")
328
633
 
329
- offsets = self._diagonal_offsets if diagonal else self._orthogonal_offsets
330
-
331
- # Generate all neighbor positions
332
- # neighbor_positions = current_pos + offsets
333
- neighbors = [node + offset for offset in offsets]
334
- filtered_neighbors = []
335
-
336
- for neighbor in neighbors:
337
- if self.is_expandable(neighbor.current, node.current):
338
- filtered_neighbors.append(neighbor)
339
-
340
- 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
+ ]
341
650
 
342
651
  def line_of_sight(self, p1: Tuple[int, ...], p2: Tuple[int, ...]) -> List[Tuple[int, ...]]:
343
652
  """
@@ -350,45 +659,13 @@ class Grid(BaseMap):
350
659
  Returns:
351
660
  points: List of point on the line of sight.
352
661
  """
353
- p1 = np.array(p1)
354
- p2 = np.array(p2)
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
355
666
 
356
- dim = len(p1)
357
- delta = p2 - p1
358
- abs_delta = np.abs(delta)
359
-
360
- # Determine the main direction axis (the dimension with the greatest change)
361
- primary_axis = np.argmax(abs_delta)
362
- primary_step = 1 if delta[primary_axis] > 0 else -1
363
-
364
- # Initialize the error variable
365
- error = np.zeros(dim, dtype=int)
366
- delta2 = 2 * abs_delta
367
-
368
- # Calculate the number of steps and initialize the current point
369
- steps = abs_delta[primary_axis]
370
- current = p1
371
-
372
- # Allocate the result array
373
- result = []
374
- result.append(tuple(int(x) for x in current))
375
-
376
- for i in range(1, steps + 1):
377
- current[primary_axis] += primary_step
378
-
379
- # Update the error for the primary dimension
380
- for d in range(dim):
381
- if d == primary_axis:
382
- continue
383
-
384
- error[d] += delta2[d]
385
- if error[d] > abs_delta[primary_axis]:
386
- current[d] += 1 if delta[d] > 0 else -1
387
- error[d] -= delta2[primary_axis]
388
-
389
- result.append(tuple(int(x) for x in current))
390
-
391
- 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])]
392
669
 
393
670
  def in_collision(self, p1: Tuple[int, ...], p2: Tuple[int, ...]) -> bool:
394
671
  """
@@ -401,51 +678,20 @@ class Grid(BaseMap):
401
678
  Returns:
402
679
  in_collision: True if the line of sight is in collision, False otherwise.
403
680
  """
404
- if not self.is_expandable(p1) or not self.is_expandable(p2, p1):
405
- return True
406
-
407
- # Corner Case: Start and end points are the same
408
- if p1 == p2:
409
- return False
410
-
411
- p1 = np.array(p1)
412
- p2 = np.array(p2)
413
-
414
- # Calculate delta and absolute delta
415
- delta = p2 - p1
416
- abs_delta = np.abs(delta)
417
-
418
- # Determine the primary axis (the dimension with the greatest change)
419
- primary_axis = np.argmax(abs_delta)
420
- primary_step = 1 if delta[primary_axis] > 0 else -1
421
-
422
- # Initialize the error variable
423
- error = np.zeros_like(delta, dtype=np.int32)
424
- delta2 = 2 * abs_delta
425
-
426
- # calculate the number of steps and initialize the current point
427
- steps = abs_delta[primary_axis]
428
- current = p1
429
-
430
- for _ in range(steps):
431
- last_point = current.copy()
432
- current[primary_axis] += primary_step
433
-
434
- # Update the error for the primary dimension
435
- for d in range(len(delta)):
436
- if d == primary_axis:
437
- continue
438
-
439
- error[d] += delta2[d]
440
- if error[d] > abs_delta[primary_axis]:
441
- current[d] += 1 if delta[d] > 0 else -1
442
- error[d] -= delta2[primary_axis]
443
-
444
- # Check the current point
445
- if not self.is_expandable(tuple(current), tuple(last_point)):
446
- return True
447
-
448
- 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
+ )
449
695
 
450
696
  def fill_boundary_with_obstacles(self) -> None:
451
697
  """
@@ -514,7 +760,16 @@ class Grid(BaseMap):
514
760
  Returns:
515
761
  path: a list of world coordinates
516
762
  """
517
- 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])]
518
773
 
519
774
  def path_world_to_map(self, path: List[Tuple[float, ...]], discrete: bool = True) -> List[tuple]:
520
775
  """
@@ -527,7 +782,20 @@ class Grid(BaseMap):
527
782
  Returns:
528
783
  path: a list of map coordinates
529
784
  """
530
- return [self.world_to_map(p, discrete) 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])]
531
799
 
532
800
  def point_float_to_int(self, point: Tuple[float, ...]) -> Tuple[int, ...]:
533
801
  """
@@ -539,24 +807,21 @@ class Grid(BaseMap):
539
807
  Returns:
540
808
  point: a point in integer coordinates
541
809
  """
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
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)
547
812
 
548
813
  def _precompute_offsets(self):
549
814
  # Generate all possible offsets (-1, 0, +1) in each dimension
550
- 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)
551
816
  # Remove the zero offset (current node itself)
552
- 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)]
553
818
  # self._diagonal_offsets = [Node((offset.tolist(), dtype=self.dtype)) for offset in self._diagonal_offsets]
554
- 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]
555
820
 
556
821
  # Generate only orthogonal offsets (one dimension changes by ±1)
557
- 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)
558
823
  for d in range(self.dim):
559
- self._orthogonal_offsets[2*d, d] = 1
560
- 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
561
826
  # self._orthogonal_offsets = [Node((offset.tolist(), dtype=self.dtype)) for offset in self._orthogonal_offsets]
562
- 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]