python-motion-planning 2.0.dev2__py3-none-any.whl → 2.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.
- python_motion_planning/__init__.py +1 -1
- python_motion_planning/common/env/map/grid.py +568 -147
- python_motion_planning/common/utils/geometry.py +21 -31
- python_motion_planning/path_planner/graph_search/lazy_theta_star.py +15 -7
- python_motion_planning/path_planner/graph_search/theta_star.py +4 -3
- python_motion_planning/path_planner/sample_search/rrt.py +6 -6
- python_motion_planning/path_planner/sample_search/rrt_connect.py +2 -2
- python_motion_planning/path_planner/sample_search/rrt_star.py +31 -11
- python_motion_planning/traj_optimizer/__init__.py +2 -0
- python_motion_planning/traj_optimizer/base_curve_generator.py +53 -0
- python_motion_planning/traj_optimizer/curve_generator/__init__.py +2 -0
- python_motion_planning/traj_optimizer/curve_generator/point_based/__init__.py +2 -0
- python_motion_planning/traj_optimizer/curve_generator/point_based/bspline.py +256 -0
- python_motion_planning/traj_optimizer/curve_generator/point_based/cubic_spline.py +115 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/__init__.py +4 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/bezier.py +121 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/dubins.py +355 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/polynomial.py +197 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py +606 -0
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.1.dist-info}/METADATA +23 -16
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.1.dist-info}/RECORD +24 -22
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.1.dist-info}/WHEEL +1 -1
- python_motion_planning/curve_generator/__init__.py +0 -9
- python_motion_planning/curve_generator/bezier_curve.py +0 -131
- python_motion_planning/curve_generator/bspline_curve.py +0 -271
- python_motion_planning/curve_generator/cubic_spline.py +0 -128
- python_motion_planning/curve_generator/curve.py +0 -64
- python_motion_planning/curve_generator/dubins_curve.py +0 -348
- python_motion_planning/curve_generator/fem_pos_smooth.py +0 -114
- python_motion_planning/curve_generator/polynomial_curve.py +0 -226
- python_motion_planning/curve_generator/reeds_shepp.py +0 -736
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.1.dist-info}/licenses/LICENSE +0 -0
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.1.dist-info}/top_level.txt +0 -0
|
@@ -1,20 +1,358 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: grid.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update:
|
|
4
|
+
@update: 2026.9.11
|
|
5
5
|
"""
|
|
6
6
|
from itertools import product
|
|
7
|
-
from typing import Iterable, Union, Tuple,
|
|
7
|
+
from typing import Iterable, Union, Tuple, List, Dict
|
|
8
8
|
import time
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
from scipy import ndimage
|
|
12
12
|
|
|
13
|
+
try:
|
|
14
|
+
from numba import njit as _numba_njit
|
|
15
|
+
except Exception: # pragma: no cover - keeps import compatibility without numba.
|
|
16
|
+
_numba_njit = None
|
|
17
|
+
|
|
13
18
|
from python_motion_planning.common.env.map.base_map import BaseMap
|
|
14
19
|
from python_motion_planning.common.env import Node, TYPES
|
|
15
20
|
from python_motion_planning.common.utils.geometry import Geometry
|
|
16
21
|
|
|
17
22
|
|
|
23
|
+
def _njit(*args, **kwargs):
|
|
24
|
+
if _numba_njit is None:
|
|
25
|
+
if args and callable(args[0]):
|
|
26
|
+
return args[0]
|
|
27
|
+
|
|
28
|
+
def decorator(func):
|
|
29
|
+
return func
|
|
30
|
+
|
|
31
|
+
return decorator
|
|
32
|
+
|
|
33
|
+
return _numba_njit(*args, **kwargs)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@_njit(cache=True)
|
|
37
|
+
def _grid_flat_index(point: np.ndarray, shape: np.ndarray) -> int:
|
|
38
|
+
idx = 0
|
|
39
|
+
for d in range(shape.size):
|
|
40
|
+
idx = idx * shape[d] + point[d]
|
|
41
|
+
return idx
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@_njit(cache=True)
|
|
45
|
+
def _grid_within_bounds(point: np.ndarray, shape: np.ndarray) -> bool:
|
|
46
|
+
for d in range(shape.size):
|
|
47
|
+
if point[d] < 0 or point[d] >= shape[d]:
|
|
48
|
+
return False
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@_njit(cache=True)
|
|
53
|
+
def _grid_is_expandable(
|
|
54
|
+
point: np.ndarray,
|
|
55
|
+
src_point: np.ndarray,
|
|
56
|
+
has_src_point: bool,
|
|
57
|
+
shape: np.ndarray,
|
|
58
|
+
type_map: np.ndarray,
|
|
59
|
+
esdf: np.ndarray,
|
|
60
|
+
obstacle_type: int,
|
|
61
|
+
inflation_type: int,
|
|
62
|
+
strict_collision: bool,
|
|
63
|
+
) -> bool:
|
|
64
|
+
if not _grid_within_bounds(point, shape):
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
point_idx = _grid_flat_index(point, shape)
|
|
68
|
+
src_idx = 0
|
|
69
|
+
src_is_inflation = False
|
|
70
|
+
if has_src_point:
|
|
71
|
+
src_idx = _grid_flat_index(src_point, shape)
|
|
72
|
+
src_is_inflation = type_map[src_idx] == inflation_type
|
|
73
|
+
|
|
74
|
+
point_type = type_map[point_idx]
|
|
75
|
+
if point_type == obstacle_type or (
|
|
76
|
+
point_type == inflation_type
|
|
77
|
+
and not (src_is_inflation and esdf[point_idx] >= esdf[src_idx])
|
|
78
|
+
):
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
if not has_src_point or not strict_collision:
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
dim = point.size
|
|
85
|
+
if dim == 2:
|
|
86
|
+
dx = point[0] - src_point[0]
|
|
87
|
+
dy = point[1] - src_point[1]
|
|
88
|
+
if abs(dx) > 1 or abs(dy) > 1 or dx == 0 or dy == 0:
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
side_idx = point[0] * shape[1] + src_point[1]
|
|
92
|
+
side_type = type_map[side_idx]
|
|
93
|
+
if side_type == obstacle_type or (
|
|
94
|
+
side_type == inflation_type
|
|
95
|
+
and not (src_is_inflation and esdf[side_idx] >= esdf[src_idx])
|
|
96
|
+
):
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
side_idx = src_point[0] * shape[1] + point[1]
|
|
100
|
+
side_type = type_map[side_idx]
|
|
101
|
+
return side_type != obstacle_type and (
|
|
102
|
+
side_type != inflation_type
|
|
103
|
+
or (src_is_inflation and esdf[side_idx] >= esdf[src_idx])
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
changed_mask = 0
|
|
107
|
+
for d in range(dim):
|
|
108
|
+
diff = point[d] - src_point[d]
|
|
109
|
+
if abs(diff) > 1:
|
|
110
|
+
return True
|
|
111
|
+
if diff != 0:
|
|
112
|
+
changed_mask |= 1 << d
|
|
113
|
+
|
|
114
|
+
if not changed_mask & (changed_mask - 1):
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
subset = (changed_mask - 1) & changed_mask
|
|
118
|
+
while subset:
|
|
119
|
+
side_idx = 0
|
|
120
|
+
for d in range(dim):
|
|
121
|
+
side = point[d] if subset & (1 << d) else src_point[d]
|
|
122
|
+
side_idx = side_idx * shape[d] + side
|
|
123
|
+
|
|
124
|
+
side_type = type_map[side_idx]
|
|
125
|
+
if side_type == obstacle_type or (
|
|
126
|
+
side_type == inflation_type
|
|
127
|
+
and not (src_is_inflation and esdf[side_idx] >= esdf[src_idx])
|
|
128
|
+
):
|
|
129
|
+
return False
|
|
130
|
+
subset = (subset - 1) & changed_mask
|
|
131
|
+
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@_njit(cache=True)
|
|
136
|
+
def _grid_world_to_map_int(
|
|
137
|
+
point: np.ndarray,
|
|
138
|
+
bounds: np.ndarray,
|
|
139
|
+
resolution: float,
|
|
140
|
+
shape: np.ndarray,
|
|
141
|
+
) -> np.ndarray:
|
|
142
|
+
point_map = np.empty(shape.size, dtype=np.int64)
|
|
143
|
+
inv_resolution = 1.0 / resolution
|
|
144
|
+
for d in range(shape.size):
|
|
145
|
+
value = int(round((point[d] - bounds[d, 0]) * inv_resolution - 0.5))
|
|
146
|
+
if value < 0:
|
|
147
|
+
value = 0
|
|
148
|
+
elif value >= shape[d]:
|
|
149
|
+
value = shape[d] - 1
|
|
150
|
+
point_map[d] = value
|
|
151
|
+
return point_map
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@_njit(cache=True)
|
|
155
|
+
def _grid_line_of_sight(p1: np.ndarray, p2: np.ndarray) -> np.ndarray:
|
|
156
|
+
dim = p1.size
|
|
157
|
+
delta = np.empty(dim, dtype=np.int64)
|
|
158
|
+
abs_delta = np.empty(dim, dtype=np.int64)
|
|
159
|
+
delta2 = np.empty(dim, dtype=np.int64)
|
|
160
|
+
|
|
161
|
+
primary_axis = 0
|
|
162
|
+
max_delta = 0
|
|
163
|
+
for d in range(dim):
|
|
164
|
+
delta[d] = p2[d] - p1[d]
|
|
165
|
+
abs_delta[d] = abs(delta[d])
|
|
166
|
+
delta2[d] = 2 * abs_delta[d]
|
|
167
|
+
if abs_delta[d] > max_delta:
|
|
168
|
+
max_delta = abs_delta[d]
|
|
169
|
+
primary_axis = d
|
|
170
|
+
|
|
171
|
+
primary_step = 1 if delta[primary_axis] > 0 else -1
|
|
172
|
+
steps = abs_delta[primary_axis]
|
|
173
|
+
result = np.empty((steps + 1, dim), dtype=np.int64)
|
|
174
|
+
current = p1.copy()
|
|
175
|
+
|
|
176
|
+
for d in range(dim):
|
|
177
|
+
result[0, d] = current[d]
|
|
178
|
+
|
|
179
|
+
error = np.zeros(dim, dtype=np.int64)
|
|
180
|
+
for i in range(1, steps + 1):
|
|
181
|
+
current[primary_axis] += primary_step
|
|
182
|
+
|
|
183
|
+
for d in range(dim):
|
|
184
|
+
if d == primary_axis:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
error[d] += delta2[d]
|
|
188
|
+
# Reverse the tie-break when traversing the primary axis backwards.
|
|
189
|
+
if error[d] > abs_delta[primary_axis] or (
|
|
190
|
+
error[d] == abs_delta[primary_axis] and primary_step < 0
|
|
191
|
+
):
|
|
192
|
+
current[d] += 1 if delta[d] > 0 else -1
|
|
193
|
+
error[d] -= delta2[primary_axis]
|
|
194
|
+
|
|
195
|
+
for d in range(dim):
|
|
196
|
+
result[i, d] = current[d]
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@_njit(cache=True)
|
|
202
|
+
def _grid_in_collision(
|
|
203
|
+
p1: np.ndarray,
|
|
204
|
+
p2: np.ndarray,
|
|
205
|
+
shape: np.ndarray,
|
|
206
|
+
type_map: np.ndarray,
|
|
207
|
+
esdf: np.ndarray,
|
|
208
|
+
obstacle_type: int,
|
|
209
|
+
inflation_type: int,
|
|
210
|
+
strict_collision: bool,
|
|
211
|
+
) -> bool:
|
|
212
|
+
if not _grid_is_expandable(p1, p1, False, shape, type_map, esdf, obstacle_type, inflation_type, False):
|
|
213
|
+
return True
|
|
214
|
+
if not _grid_is_expandable(p2, p1, True, shape, type_map, esdf, obstacle_type, inflation_type, False):
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
dim = p1.size
|
|
218
|
+
same_point = True
|
|
219
|
+
for d in range(dim):
|
|
220
|
+
if p1[d] != p2[d]:
|
|
221
|
+
same_point = False
|
|
222
|
+
break
|
|
223
|
+
if same_point:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
delta = np.empty(dim, dtype=np.int64)
|
|
227
|
+
abs_delta = np.empty(dim, dtype=np.int64)
|
|
228
|
+
delta2 = np.empty(dim, dtype=np.int64)
|
|
229
|
+
|
|
230
|
+
primary_axis = 0
|
|
231
|
+
max_delta = 0
|
|
232
|
+
for d in range(dim):
|
|
233
|
+
delta[d] = p2[d] - p1[d]
|
|
234
|
+
abs_delta[d] = abs(delta[d])
|
|
235
|
+
delta2[d] = 2 * abs_delta[d]
|
|
236
|
+
if abs_delta[d] > max_delta:
|
|
237
|
+
max_delta = abs_delta[d]
|
|
238
|
+
primary_axis = d
|
|
239
|
+
|
|
240
|
+
primary_step = 1 if delta[primary_axis] > 0 else -1
|
|
241
|
+
steps = abs_delta[primary_axis]
|
|
242
|
+
current = p1.copy()
|
|
243
|
+
last_point = np.empty(dim, dtype=np.int64)
|
|
244
|
+
error = np.zeros(dim, dtype=np.int64)
|
|
245
|
+
|
|
246
|
+
for _ in range(steps):
|
|
247
|
+
for d in range(dim):
|
|
248
|
+
last_point[d] = current[d]
|
|
249
|
+
|
|
250
|
+
current[primary_axis] += primary_step
|
|
251
|
+
|
|
252
|
+
for d in range(dim):
|
|
253
|
+
if d == primary_axis:
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
error[d] += delta2[d]
|
|
257
|
+
# Keep the same cells as _grid_line_of_sight in both directions.
|
|
258
|
+
if error[d] > abs_delta[primary_axis] or (
|
|
259
|
+
error[d] == abs_delta[primary_axis] and primary_step < 0
|
|
260
|
+
):
|
|
261
|
+
current[d] += 1 if delta[d] > 0 else -1
|
|
262
|
+
error[d] -= delta2[primary_axis]
|
|
263
|
+
|
|
264
|
+
if not _grid_is_expandable(
|
|
265
|
+
current,
|
|
266
|
+
last_point,
|
|
267
|
+
True,
|
|
268
|
+
shape,
|
|
269
|
+
type_map,
|
|
270
|
+
esdf,
|
|
271
|
+
obstacle_type,
|
|
272
|
+
inflation_type,
|
|
273
|
+
strict_collision,
|
|
274
|
+
):
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@_njit(cache=True)
|
|
281
|
+
def _grid_neighbor_positions_and_mask(
|
|
282
|
+
current: np.ndarray,
|
|
283
|
+
offsets: np.ndarray,
|
|
284
|
+
shape: np.ndarray,
|
|
285
|
+
type_map: np.ndarray,
|
|
286
|
+
esdf: np.ndarray,
|
|
287
|
+
obstacle_type: int,
|
|
288
|
+
inflation_type: int,
|
|
289
|
+
strict_collision: bool,
|
|
290
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
291
|
+
node_num = offsets.shape[0]
|
|
292
|
+
dim = offsets.shape[1]
|
|
293
|
+
positions = np.empty((node_num, dim), dtype=np.int64)
|
|
294
|
+
mask = np.zeros(node_num, dtype=np.bool_)
|
|
295
|
+
neighbor = np.empty(dim, dtype=np.int64)
|
|
296
|
+
|
|
297
|
+
for i in range(node_num):
|
|
298
|
+
for d in range(dim):
|
|
299
|
+
neighbor[d] = current[d] + offsets[i, d]
|
|
300
|
+
positions[i, d] = neighbor[d]
|
|
301
|
+
|
|
302
|
+
mask[i] = _grid_is_expandable(
|
|
303
|
+
neighbor,
|
|
304
|
+
current,
|
|
305
|
+
True,
|
|
306
|
+
shape,
|
|
307
|
+
type_map,
|
|
308
|
+
esdf,
|
|
309
|
+
obstacle_type,
|
|
310
|
+
inflation_type,
|
|
311
|
+
strict_collision,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return positions, mask
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@_njit(cache=True)
|
|
318
|
+
def _grid_path_map_to_world(points: np.ndarray, bounds: np.ndarray, resolution: float) -> np.ndarray:
|
|
319
|
+
path_world = np.empty((points.shape[0], points.shape[1]), dtype=np.float64)
|
|
320
|
+
for i in range(points.shape[0]):
|
|
321
|
+
for d in range(points.shape[1]):
|
|
322
|
+
path_world[i, d] = (points[i, d] + 0.5) * resolution + bounds[d, 0]
|
|
323
|
+
return path_world
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@_njit(cache=True)
|
|
327
|
+
def _grid_path_world_to_map_float(points: np.ndarray, bounds: np.ndarray, resolution: float) -> np.ndarray:
|
|
328
|
+
path_map = np.empty((points.shape[0], points.shape[1]), dtype=np.float64)
|
|
329
|
+
inv_resolution = 1.0 / resolution
|
|
330
|
+
for i in range(points.shape[0]):
|
|
331
|
+
for d in range(points.shape[1]):
|
|
332
|
+
path_map[i, d] = (points[i, d] - bounds[d, 0]) * inv_resolution - 0.5
|
|
333
|
+
return path_map
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@_njit(cache=True)
|
|
337
|
+
def _grid_path_world_to_map_int(
|
|
338
|
+
points: np.ndarray,
|
|
339
|
+
bounds: np.ndarray,
|
|
340
|
+
resolution: float,
|
|
341
|
+
shape: np.ndarray,
|
|
342
|
+
) -> np.ndarray:
|
|
343
|
+
path_map = np.empty((points.shape[0], shape.size), dtype=np.int64)
|
|
344
|
+
inv_resolution = 1.0 / resolution
|
|
345
|
+
for i in range(points.shape[0]):
|
|
346
|
+
for d in range(shape.size):
|
|
347
|
+
value = int(round((points[i, d] - bounds[d, 0]) * inv_resolution - 0.5))
|
|
348
|
+
if value < 0:
|
|
349
|
+
value = 0
|
|
350
|
+
elif value >= shape[d]:
|
|
351
|
+
value = shape[d] - 1
|
|
352
|
+
path_map[i, d] = value
|
|
353
|
+
return path_map
|
|
354
|
+
|
|
355
|
+
|
|
18
356
|
class GridTypeMap:
|
|
19
357
|
"""
|
|
20
358
|
Class for Grid Type Map. It is like a np.ndarray, except that its shape and dtype are fixed.
|
|
@@ -47,6 +385,7 @@ class GridTypeMap:
|
|
|
47
385
|
self._data = np.asarray(type_map)
|
|
48
386
|
self._shape = self._data.shape
|
|
49
387
|
self._dtype = self._data.dtype
|
|
388
|
+
self._lazy_flags_ = 3
|
|
50
389
|
|
|
51
390
|
self._dtype_options = [np.int8, np.int16, np.int32, np.int64]
|
|
52
391
|
if self._dtype not in self._dtype_options:
|
|
@@ -63,6 +402,7 @@ class GridTypeMap:
|
|
|
63
402
|
|
|
64
403
|
def __setitem__(self, idx, value):
|
|
65
404
|
self._data[idx] = value
|
|
405
|
+
self._lazy_flags_ = 3
|
|
66
406
|
|
|
67
407
|
@property
|
|
68
408
|
def data(self) -> np.ndarray:
|
|
@@ -89,6 +429,7 @@ class Grid(BaseMap):
|
|
|
89
429
|
resolution: resolution of the grid map
|
|
90
430
|
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)
|
|
91
431
|
inflation_radius: radius of the inflation
|
|
432
|
+
strict_collision: whether diagonal steps beside obstacles or inflation are collisions (default: True)
|
|
92
433
|
|
|
93
434
|
Examples:
|
|
94
435
|
>>> grid_map = Grid(bounds=[[0, 51], [0, 31]], resolution=0.5)
|
|
@@ -137,7 +478,7 @@ class Grid(BaseMap):
|
|
|
137
478
|
|
|
138
479
|
>>> grid_map[1, 0] = TYPES.OBSTACLE # place an obstacle
|
|
139
480
|
>>> grid_map.get_neighbors(Node((0, 0))) # limited within the bounds
|
|
140
|
-
[Node((0, 1), (0, 0), 0, 0)
|
|
481
|
+
[Node((0, 1), (0, 0), 0, 0)]
|
|
141
482
|
|
|
142
483
|
>>> grid_map.get_neighbors(Node((grid_map.shape[0] - 1, grid_map.shape[1] - 1)), diagonal=False) # limited within the boundss
|
|
143
484
|
[Node((100, 61), (101, 61), 0, 0), Node((101, 60), (101, 61), 0, 0)]
|
|
@@ -152,15 +493,31 @@ class Grid(BaseMap):
|
|
|
152
493
|
False
|
|
153
494
|
|
|
154
495
|
>>> grid_map[1, 3] = TYPES.OBSTACLE
|
|
155
|
-
>>> grid_map.update_esdf()
|
|
156
496
|
>>> grid_map.in_collision((1, 2), (3, 6))
|
|
157
497
|
True
|
|
498
|
+
|
|
499
|
+
>>> grid_map = Grid(bounds=[[0, 3], [0, 3]])
|
|
500
|
+
>>> grid_map[1, 0] = TYPES.OBSTACLE
|
|
501
|
+
>>> grid_map.in_collision((0, 0), (1, 1))
|
|
502
|
+
True
|
|
503
|
+
>>> grid_map.strict_collision = False
|
|
504
|
+
>>> grid_map.in_collision((0, 0), (1, 1))
|
|
505
|
+
False
|
|
506
|
+
|
|
507
|
+
>>> grid_map = Grid(bounds=[[0, 4], [0, 4]])
|
|
508
|
+
>>> grid_map[2, :] = TYPES.OBSTACLE
|
|
509
|
+
>>> grid_map.is_connected((0, 0), (3, 3))
|
|
510
|
+
False
|
|
511
|
+
>>> grid_map[2, 1] = TYPES.FREE
|
|
512
|
+
>>> grid_map.is_connected((0, 0), (3, 3))
|
|
513
|
+
True
|
|
158
514
|
"""
|
|
159
515
|
def __init__(self,
|
|
160
516
|
bounds: Iterable = [[0, 30], [0, 40]],
|
|
161
517
|
resolution: float = 1.0,
|
|
162
518
|
type_map: Union[GridTypeMap, np.ndarray] = None,
|
|
163
519
|
inflation_radius: float = 0.0,
|
|
520
|
+
strict_collision: bool = True,
|
|
164
521
|
) -> None:
|
|
165
522
|
super().__init__(bounds)
|
|
166
523
|
|
|
@@ -168,22 +525,24 @@ class Grid(BaseMap):
|
|
|
168
525
|
shape = tuple([int((self.bounds[i, 1] - self.bounds[i, 0]) / self.resolution) for i in range(self.dim)])
|
|
169
526
|
|
|
170
527
|
if type_map is None:
|
|
171
|
-
self.
|
|
528
|
+
self._type_map = GridTypeMap(np.zeros(shape, dtype=np.int8))
|
|
172
529
|
else:
|
|
173
530
|
if type_map.shape != shape:
|
|
174
531
|
raise ValueError("Shape must be {} instead of {} with given bounds={} and resolution={}".format(shape, type_map.shape, self.bounds, self.resolution))
|
|
175
532
|
|
|
176
533
|
if isinstance(type_map, GridTypeMap):
|
|
177
|
-
self.
|
|
534
|
+
self._type_map = type_map
|
|
178
535
|
elif isinstance(type_map, np.ndarray):
|
|
179
|
-
self.
|
|
536
|
+
self._type_map = GridTypeMap(type_map)
|
|
180
537
|
else:
|
|
181
538
|
raise ValueError("Type map must be GridTypeMap or numpy.ndarray instead of {}".format(type(type_map)))
|
|
182
539
|
|
|
540
|
+
self._shape_array = np.asarray(self.shape, dtype=np.int64)
|
|
183
541
|
self._precompute_offsets()
|
|
184
542
|
|
|
185
543
|
self._esdf = np.zeros(self.shape, dtype=np.float32)
|
|
186
|
-
|
|
544
|
+
self._connectivity_map = np.zeros(self.shape, dtype=np.int32)
|
|
545
|
+
self.strict_collision = strict_collision
|
|
187
546
|
|
|
188
547
|
self.inflation_radius = inflation_radius
|
|
189
548
|
if self.inflation_radius >= 1:
|
|
@@ -198,28 +557,79 @@ class Grid(BaseMap):
|
|
|
198
557
|
@property
|
|
199
558
|
def resolution(self) -> float:
|
|
200
559
|
return self._resolution
|
|
560
|
+
|
|
561
|
+
@property
|
|
562
|
+
def type_map(self) -> GridTypeMap:
|
|
563
|
+
return self._type_map
|
|
201
564
|
|
|
202
565
|
@property
|
|
203
566
|
def shape(self) -> tuple:
|
|
204
|
-
return self.
|
|
567
|
+
return self._type_map.shape
|
|
205
568
|
|
|
206
569
|
@property
|
|
207
570
|
def dtype(self) -> np.dtype:
|
|
208
|
-
return self.
|
|
571
|
+
return self._type_map.dtype
|
|
209
572
|
|
|
210
573
|
@property
|
|
211
574
|
def esdf(self) -> np.ndarray:
|
|
575
|
+
if self._esdf_lazy_flag_:
|
|
576
|
+
self.update_esdf()
|
|
577
|
+
self._esdf_lazy_flag_ = False
|
|
212
578
|
return self._esdf
|
|
579
|
+
|
|
580
|
+
@property
|
|
581
|
+
def connectivity_map(self) -> np.ndarray:
|
|
582
|
+
if self._connectivity_lazy_flag_:
|
|
583
|
+
self.update_connectivity()
|
|
584
|
+
self._connectivity_lazy_flag_ = False
|
|
585
|
+
return self._connectivity_map
|
|
213
586
|
|
|
214
587
|
@property
|
|
215
588
|
def data(self) -> np.ndarray:
|
|
216
|
-
return self.
|
|
589
|
+
return self._type_map.data
|
|
217
590
|
|
|
218
591
|
def __getitem__(self, idx):
|
|
219
|
-
return self.
|
|
592
|
+
return self._type_map[idx]
|
|
220
593
|
|
|
221
594
|
def __setitem__(self, idx, value):
|
|
222
|
-
self.
|
|
595
|
+
self._type_map[idx] = value
|
|
596
|
+
|
|
597
|
+
@property
|
|
598
|
+
def _esdf_lazy_flag_(self) -> bool:
|
|
599
|
+
return bool(self._type_map._lazy_flags_ & 1)
|
|
600
|
+
|
|
601
|
+
@_esdf_lazy_flag_.setter
|
|
602
|
+
def _esdf_lazy_flag_(self, value: bool) -> None:
|
|
603
|
+
if value:
|
|
604
|
+
self._type_map._lazy_flags_ |= 1
|
|
605
|
+
else:
|
|
606
|
+
self._type_map._lazy_flags_ &= ~1
|
|
607
|
+
|
|
608
|
+
@property
|
|
609
|
+
def _connectivity_lazy_flag_(self) -> bool:
|
|
610
|
+
return bool(self._type_map._lazy_flags_ & 2)
|
|
611
|
+
|
|
612
|
+
@_connectivity_lazy_flag_.setter
|
|
613
|
+
def _connectivity_lazy_flag_(self, value: bool) -> None:
|
|
614
|
+
if value:
|
|
615
|
+
self._type_map._lazy_flags_ |= 2
|
|
616
|
+
else:
|
|
617
|
+
self._type_map._lazy_flags_ &= ~2
|
|
618
|
+
|
|
619
|
+
@property
|
|
620
|
+
def strict_collision(self) -> bool:
|
|
621
|
+
return self._strict_collision
|
|
622
|
+
|
|
623
|
+
@strict_collision.setter
|
|
624
|
+
def strict_collision(self, value: bool) -> None:
|
|
625
|
+
self._strict_collision = value
|
|
626
|
+
self._connectivity_lazy_flag_ = True
|
|
627
|
+
|
|
628
|
+
def _type_map_flat(self) -> np.ndarray:
|
|
629
|
+
return np.ravel(self._type_map.data)
|
|
630
|
+
|
|
631
|
+
def _esdf_flat(self) -> np.ndarray:
|
|
632
|
+
return np.ravel(self.esdf)
|
|
223
633
|
|
|
224
634
|
def map_to_world(self, point: tuple) -> Tuple[float, ...]:
|
|
225
635
|
"""
|
|
@@ -234,7 +644,9 @@ class Grid(BaseMap):
|
|
|
234
644
|
if len(point) != self.dim:
|
|
235
645
|
raise ValueError("Point dimension does not match map dimension.")
|
|
236
646
|
|
|
237
|
-
|
|
647
|
+
bounds = self.bounds
|
|
648
|
+
resolution = self.resolution
|
|
649
|
+
return tuple(float((point[d] + 0.5) * resolution + bounds[d, 0]) for d in range(self.dim))
|
|
238
650
|
|
|
239
651
|
def world_to_map(self, point: Tuple[float, ...], discrete: bool = True) -> tuple:
|
|
240
652
|
"""
|
|
@@ -250,10 +662,13 @@ class Grid(BaseMap):
|
|
|
250
662
|
if len(point) != self.dim:
|
|
251
663
|
raise ValueError("Point dimension does not match map dimension.")
|
|
252
664
|
|
|
253
|
-
point_map = tuple((x - float(self.bounds[i, 0])) * (1.0 / self.resolution) - 0.5 for i, x in enumerate(point))
|
|
254
665
|
if discrete:
|
|
255
|
-
point_map =
|
|
256
|
-
|
|
666
|
+
point_map = _grid_world_to_map_int(np.asarray(point, dtype=np.float64), self.bounds, self.resolution, self._shape_array)
|
|
667
|
+
return tuple(int(x) for x in point_map)
|
|
668
|
+
else:
|
|
669
|
+
inv_resolution = 1.0 / self.resolution
|
|
670
|
+
bounds = self.bounds
|
|
671
|
+
return tuple(float((point[d] - bounds[d, 0]) * inv_resolution - 0.5) for d in range(self.dim))
|
|
257
672
|
|
|
258
673
|
def get_distance(self, p1: Tuple[int, int], p2: Tuple[int, int]) -> float:
|
|
259
674
|
"""
|
|
@@ -278,18 +693,32 @@ class Grid(BaseMap):
|
|
|
278
693
|
Returns:
|
|
279
694
|
bool: True if the point is within the bounds of the map, False otherwise.
|
|
280
695
|
"""
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
# return all(0 <= point[i] < self.shape[i] for i in range(self.dim))
|
|
285
|
-
dim = self.dim
|
|
696
|
+
if len(point) != self.dim:
|
|
697
|
+
return False
|
|
286
698
|
shape = self.shape
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if not (0 <= point[i] < shape[i]):
|
|
699
|
+
for d in range(self.dim):
|
|
700
|
+
if point[d] < 0 or point[d] >= shape[d]:
|
|
290
701
|
return False
|
|
291
702
|
return True
|
|
292
703
|
|
|
704
|
+
def is_connected(self, p1: Tuple[int, ...], p2: Tuple[int, ...]) -> bool:
|
|
705
|
+
"""
|
|
706
|
+
Check whether two points belong to the same free-space component.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
p1: First point.
|
|
710
|
+
p2: Second point.
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
connected: True if the two points are connected, False otherwise.
|
|
714
|
+
"""
|
|
715
|
+
if not self.within_bounds(p1) or not self.within_bounds(p2):
|
|
716
|
+
raise ValueError("Points are out of bounds or invalid.")
|
|
717
|
+
|
|
718
|
+
connectivity_map = self.connectivity_map
|
|
719
|
+
component = connectivity_map[p1]
|
|
720
|
+
return component != 0 and component == connectivity_map[p2]
|
|
721
|
+
|
|
293
722
|
def is_expandable(self, point: Tuple[int, ...], src_point: Tuple[int, ...] = None) -> bool:
|
|
294
723
|
"""
|
|
295
724
|
Check if a point is expandable.
|
|
@@ -301,13 +730,21 @@ class Grid(BaseMap):
|
|
|
301
730
|
Returns:
|
|
302
731
|
expandable: True if the point is expandable, False otherwise.
|
|
303
732
|
"""
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if src_point is
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
733
|
+
point_array = np.asarray(point, dtype=np.int64)
|
|
734
|
+
has_src_point = src_point is not None
|
|
735
|
+
src_array = point_array if src_point is None else np.asarray(src_point, dtype=np.int64)
|
|
736
|
+
|
|
737
|
+
return _grid_is_expandable(
|
|
738
|
+
point_array,
|
|
739
|
+
src_array,
|
|
740
|
+
has_src_point,
|
|
741
|
+
self._shape_array,
|
|
742
|
+
self._type_map_flat(),
|
|
743
|
+
self._esdf_flat(),
|
|
744
|
+
TYPES.OBSTACLE,
|
|
745
|
+
TYPES.INFLATION,
|
|
746
|
+
self.strict_collision,
|
|
747
|
+
)
|
|
311
748
|
|
|
312
749
|
def get_neighbors(self,
|
|
313
750
|
node: Node,
|
|
@@ -326,18 +763,28 @@ class Grid(BaseMap):
|
|
|
326
763
|
if node.dim != self.dim:
|
|
327
764
|
raise ValueError("Node dimension does not match map dimension.")
|
|
328
765
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
766
|
+
positions, mask = self._get_neighbor_arrays(node, diagonal)
|
|
767
|
+
|
|
768
|
+
return [
|
|
769
|
+
Node(tuple(positions[i].tolist()), node.current, node.g, node.h)
|
|
770
|
+
for i in range(positions.shape[0])
|
|
771
|
+
if mask[i]
|
|
772
|
+
]
|
|
773
|
+
|
|
774
|
+
def _get_neighbor_arrays(self, node: Node, diagonal: bool = True) -> Tuple[np.ndarray, np.ndarray]:
|
|
775
|
+
"""Get candidate neighbor positions and their expandable mask."""
|
|
776
|
+
offsets = self._diagonal_offsets_array if diagonal else self._orthogonal_offsets_array
|
|
777
|
+
positions, mask = _grid_neighbor_positions_and_mask(
|
|
778
|
+
np.asarray(node.current, dtype=np.int64),
|
|
779
|
+
offsets,
|
|
780
|
+
self._shape_array,
|
|
781
|
+
self._type_map_flat(),
|
|
782
|
+
self._esdf_flat(),
|
|
783
|
+
TYPES.OBSTACLE,
|
|
784
|
+
TYPES.INFLATION,
|
|
785
|
+
self.strict_collision,
|
|
786
|
+
)
|
|
787
|
+
return positions, mask
|
|
341
788
|
|
|
342
789
|
def line_of_sight(self, p1: Tuple[int, ...], p2: Tuple[int, ...]) -> List[Tuple[int, ...]]:
|
|
343
790
|
"""
|
|
@@ -350,45 +797,13 @@ class Grid(BaseMap):
|
|
|
350
797
|
Returns:
|
|
351
798
|
points: List of point on the line of sight.
|
|
352
799
|
"""
|
|
353
|
-
|
|
354
|
-
|
|
800
|
+
p1_array = np.asarray(p1, dtype=np.int64)
|
|
801
|
+
p2_array = np.asarray(p2, dtype=np.int64)
|
|
802
|
+
if p1_array.shape != p2_array.shape:
|
|
803
|
+
p2_array - p1_array
|
|
355
804
|
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
805
|
+
points = _grid_line_of_sight(p1_array, p2_array)
|
|
806
|
+
return [tuple(int(x) for x in points[i]) for i in range(points.shape[0])]
|
|
392
807
|
|
|
393
808
|
def in_collision(self, p1: Tuple[int, ...], p2: Tuple[int, ...]) -> bool:
|
|
394
809
|
"""
|
|
@@ -401,51 +816,21 @@ class Grid(BaseMap):
|
|
|
401
816
|
Returns:
|
|
402
817
|
in_collision: True if the line of sight is in collision, False otherwise.
|
|
403
818
|
"""
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
819
|
+
p1_array = np.asarray(p1, dtype=np.int64)
|
|
820
|
+
p2_array = np.asarray(p2, dtype=np.int64)
|
|
821
|
+
if p1_array.shape != p2_array.shape:
|
|
822
|
+
p2_array - p1_array
|
|
823
|
+
|
|
824
|
+
return _grid_in_collision(
|
|
825
|
+
p1_array,
|
|
826
|
+
p2_array,
|
|
827
|
+
self._shape_array,
|
|
828
|
+
self._type_map_flat(),
|
|
829
|
+
self._esdf_flat(),
|
|
830
|
+
TYPES.OBSTACLE,
|
|
831
|
+
TYPES.INFLATION,
|
|
832
|
+
self.strict_collision,
|
|
833
|
+
)
|
|
449
834
|
|
|
450
835
|
def fill_boundary_with_obstacles(self) -> None:
|
|
451
836
|
"""
|
|
@@ -456,12 +841,12 @@ class Grid(BaseMap):
|
|
|
456
841
|
# First boundary (start index)
|
|
457
842
|
slices_start = [slice(None)] * self.dim
|
|
458
843
|
slices_start[d] = 0
|
|
459
|
-
self.
|
|
844
|
+
self._type_map[tuple(slices_start)] = TYPES.OBSTACLE
|
|
460
845
|
|
|
461
846
|
# Last boundary (end index)
|
|
462
847
|
slices_end = [slice(None)] * self.dim
|
|
463
848
|
slices_end[d] = -1
|
|
464
|
-
self.
|
|
849
|
+
self._type_map[tuple(slices_end)] = TYPES.OBSTACLE
|
|
465
850
|
|
|
466
851
|
def inflate_obstacles(self, radius: float = 1.0) -> None:
|
|
467
852
|
"""
|
|
@@ -470,9 +855,8 @@ class Grid(BaseMap):
|
|
|
470
855
|
Args:
|
|
471
856
|
radius: Radius of the inflation.
|
|
472
857
|
"""
|
|
473
|
-
self.
|
|
474
|
-
mask =
|
|
475
|
-
self.type_map[mask] = TYPES.INFLATION
|
|
858
|
+
mask = (self.esdf <= radius) & (self._type_map.data == TYPES.FREE)
|
|
859
|
+
self._type_map[mask] = TYPES.INFLATION
|
|
476
860
|
self.inflation_radius = radius
|
|
477
861
|
|
|
478
862
|
def fill_expands(self, expands: Dict[Tuple[int, ...], Node]) -> None:
|
|
@@ -483,9 +867,9 @@ class Grid(BaseMap):
|
|
|
483
867
|
expands: List of expands.
|
|
484
868
|
"""
|
|
485
869
|
for expand in expands.keys():
|
|
486
|
-
if self.
|
|
870
|
+
if self._type_map[expand] != TYPES.FREE:
|
|
487
871
|
continue
|
|
488
|
-
self.
|
|
872
|
+
self._type_map[expand] = TYPES.EXPAND
|
|
489
873
|
|
|
490
874
|
def update_esdf(self) -> None:
|
|
491
875
|
"""
|
|
@@ -493,7 +877,7 @@ class Grid(BaseMap):
|
|
|
493
877
|
- Obstacle grid ESDF = 0
|
|
494
878
|
- Free grid ESDF > 0. The value is the di/stance to the nearest obstacle
|
|
495
879
|
"""
|
|
496
|
-
obstacle_mask = (self.
|
|
880
|
+
obstacle_mask = (self._type_map.data == TYPES.OBSTACLE)
|
|
497
881
|
free_mask = ~obstacle_mask
|
|
498
882
|
|
|
499
883
|
# distance to obstacles
|
|
@@ -503,6 +887,16 @@ class Grid(BaseMap):
|
|
|
503
887
|
|
|
504
888
|
self._esdf = dist_outside.astype(np.float32)
|
|
505
889
|
self._esdf[obstacle_mask] = -dist_inside[obstacle_mask]
|
|
890
|
+
self._esdf_lazy_flag_ = False
|
|
891
|
+
|
|
892
|
+
def update_connectivity(self) -> None:
|
|
893
|
+
"""Update the free-space connected component map."""
|
|
894
|
+
type_map = self._type_map.data
|
|
895
|
+
free_mask = (type_map != TYPES.OBSTACLE) & (type_map != TYPES.INFLATION)
|
|
896
|
+
connectivity = 1 if self.strict_collision else self.dim
|
|
897
|
+
structure = ndimage.generate_binary_structure(self.dim, connectivity)
|
|
898
|
+
ndimage.label(free_mask, structure=structure, output=self._connectivity_map)
|
|
899
|
+
self._connectivity_lazy_flag_ = False
|
|
506
900
|
|
|
507
901
|
def path_map_to_world(self, path: List[tuple]) -> List[Tuple[float, ...]]:
|
|
508
902
|
"""
|
|
@@ -514,7 +908,16 @@ class Grid(BaseMap):
|
|
|
514
908
|
Returns:
|
|
515
909
|
path: a list of world coordinates
|
|
516
910
|
"""
|
|
517
|
-
|
|
911
|
+
path = list(path)
|
|
912
|
+
if not path:
|
|
913
|
+
return []
|
|
914
|
+
|
|
915
|
+
points = np.asarray(path, dtype=np.float64)
|
|
916
|
+
if points.ndim != 2 or points.shape[1] != self.dim:
|
|
917
|
+
raise ValueError("Point dimension does not match map dimension.")
|
|
918
|
+
|
|
919
|
+
path_world = _grid_path_map_to_world(points, self.bounds, self.resolution)
|
|
920
|
+
return [tuple(float(x) for x in path_world[i]) for i in range(path_world.shape[0])]
|
|
518
921
|
|
|
519
922
|
def path_world_to_map(self, path: List[Tuple[float, ...]], discrete: bool = True) -> List[tuple]:
|
|
520
923
|
"""
|
|
@@ -527,7 +930,20 @@ class Grid(BaseMap):
|
|
|
527
930
|
Returns:
|
|
528
931
|
path: a list of map coordinates
|
|
529
932
|
"""
|
|
530
|
-
|
|
933
|
+
path = list(path)
|
|
934
|
+
if not path:
|
|
935
|
+
return []
|
|
936
|
+
|
|
937
|
+
points = np.asarray(path, dtype=np.float64)
|
|
938
|
+
if points.ndim != 2 or points.shape[1] != self.dim:
|
|
939
|
+
raise ValueError("Point dimension does not match map dimension.")
|
|
940
|
+
|
|
941
|
+
if discrete:
|
|
942
|
+
path_map = _grid_path_world_to_map_int(points, self.bounds, self.resolution, self._shape_array)
|
|
943
|
+
return [tuple(int(x) for x in path_map[i]) for i in range(path_map.shape[0])]
|
|
944
|
+
else:
|
|
945
|
+
path_map = _grid_path_world_to_map_float(points, self.bounds, self.resolution)
|
|
946
|
+
return [tuple(float(x) for x in path_map[i]) for i in range(path_map.shape[0])]
|
|
531
947
|
|
|
532
948
|
def point_float_to_int(self, point: Tuple[float, ...]) -> Tuple[int, ...]:
|
|
533
949
|
"""
|
|
@@ -539,24 +955,29 @@ class Grid(BaseMap):
|
|
|
539
955
|
Returns:
|
|
540
956
|
point: a point in integer coordinates
|
|
541
957
|
"""
|
|
958
|
+
shape = self.shape
|
|
542
959
|
point_int = []
|
|
543
960
|
for d in range(self.dim):
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
961
|
+
value = round(point[d])
|
|
962
|
+
if value < 0:
|
|
963
|
+
value = 0
|
|
964
|
+
elif value >= shape[d]:
|
|
965
|
+
value = shape[d] - 1
|
|
966
|
+
point_int.append(value)
|
|
967
|
+
return tuple(point_int)
|
|
547
968
|
|
|
548
969
|
def _precompute_offsets(self):
|
|
549
970
|
# Generate all possible offsets (-1, 0, +1) in each dimension
|
|
550
|
-
self.
|
|
971
|
+
self._diagonal_offsets_array = np.array(np.meshgrid(*[[-1, 0, 1]]*self.dim), dtype=np.int64).T.reshape(-1, self.dim)
|
|
551
972
|
# Remove the zero offset (current node itself)
|
|
552
|
-
self.
|
|
973
|
+
self._diagonal_offsets_array = self._diagonal_offsets_array[np.any(self._diagonal_offsets_array != 0, axis=1)]
|
|
553
974
|
# 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.
|
|
975
|
+
self._diagonal_offsets = [Node(tuple(offset.tolist())) for offset in self._diagonal_offsets_array]
|
|
555
976
|
|
|
556
977
|
# Generate only orthogonal offsets (one dimension changes by ±1)
|
|
557
|
-
self.
|
|
978
|
+
self._orthogonal_offsets_array = np.zeros((2*self.dim, self.dim), dtype=np.int64)
|
|
558
979
|
for d in range(self.dim):
|
|
559
|
-
self.
|
|
560
|
-
self.
|
|
980
|
+
self._orthogonal_offsets_array[2*d, d] = 1
|
|
981
|
+
self._orthogonal_offsets_array[2*d+1, d] = -1
|
|
561
982
|
# 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.
|
|
983
|
+
self._orthogonal_offsets = [Node(tuple(offset.tolist())) for offset in self._orthogonal_offsets_array]
|