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.
- python_motion_planning/__init__.py +1 -1
- python_motion_planning/common/env/map/grid.py +394 -129
- python_motion_planning/common/utils/geometry.py +18 -29
- python_motion_planning/path_planner/sample_search/rrt.py +5 -5
- 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.0.1.dist-info}/METADATA +22 -15
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.1.dist-info}/RECORD +22 -20
- {python_motion_planning-2.0.dev2.dist-info → python_motion_planning-2.0.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.0.1.dist-info}/licenses/LICENSE +0 -0
- {python_motion_planning-2.0.dev2.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:
|
|
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
|
-
|
|
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
|
-
|
|
552
|
+
point_array = np.asarray(point, dtype=np.float64)
|
|
254
553
|
if discrete:
|
|
255
|
-
point_map = self.
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
if src_point is
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
560
|
-
self.
|
|
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.
|
|
827
|
+
self._orthogonal_offsets = [Node(tuple(offset.tolist())) for offset in self._orthogonal_offsets_array]
|