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.
- python_motion_planning/__init__.py +1 -1
- python_motion_planning/common/env/map/base_map.py +2 -8
- python_motion_planning/common/env/map/grid.py +456 -198
- python_motion_planning/common/utils/__init__.py +2 -1
- python_motion_planning/common/utils/child_tree.py +22 -0
- python_motion_planning/common/utils/geometry.py +18 -29
- python_motion_planning/common/visualizer/__init__.py +3 -1
- python_motion_planning/common/visualizer/base_visualizer.py +165 -0
- python_motion_planning/common/visualizer/{visualizer.py → visualizer_2d.py} +97 -220
- python_motion_planning/common/visualizer/visualizer_3d.py +242 -0
- python_motion_planning/controller/base_controller.py +37 -4
- python_motion_planning/controller/path_tracker/__init__.py +2 -1
- python_motion_planning/controller/path_tracker/apf.py +22 -23
- python_motion_planning/controller/path_tracker/dwa.py +14 -17
- python_motion_planning/controller/path_tracker/path_tracker.py +4 -1
- python_motion_planning/controller/path_tracker/pid.py +7 -1
- python_motion_planning/controller/path_tracker/pure_pursuit.py +7 -1
- python_motion_planning/controller/path_tracker/rpp.py +111 -0
- python_motion_planning/path_planner/__init__.py +2 -1
- python_motion_planning/path_planner/base_path_planner.py +45 -11
- python_motion_planning/path_planner/graph_search/__init__.py +4 -1
- python_motion_planning/path_planner/graph_search/a_star.py +12 -14
- python_motion_planning/path_planner/graph_search/dijkstra.py +15 -15
- python_motion_planning/path_planner/graph_search/gbfs.py +100 -0
- python_motion_planning/path_planner/graph_search/jps.py +199 -0
- python_motion_planning/path_planner/graph_search/lazy_theta_star.py +113 -0
- python_motion_planning/path_planner/graph_search/theta_star.py +17 -19
- python_motion_planning/path_planner/hybrid_search/__init__.py +1 -0
- python_motion_planning/path_planner/hybrid_search/voronoi_planner.py +204 -0
- python_motion_planning/path_planner/sample_search/__init__.py +2 -1
- python_motion_planning/path_planner/sample_search/rrt.py +73 -31
- python_motion_planning/path_planner/sample_search/rrt_connect.py +237 -0
- python_motion_planning/path_planner/sample_search/rrt_star.py +220 -150
- 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.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/METADATA +71 -29
- python_motion_planning-2.0.1.dist-info/RECORD +64 -0
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/WHEEL +1 -1
- python_motion_planning/common/env/robot/tmp.py +0 -404
- 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.dev1.dist-info/RECORD +0 -53
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {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:
|
|
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(
|
|
320
|
+
GridTypeMap(data=
|
|
30
321
|
[[0 0 0]
|
|
31
322
|
[0 1 0]
|
|
32
323
|
[0 0 0]]
|
|
33
|
-
|
|
324
|
+
, shape=(3, 3), dtype=int8)
|
|
34
325
|
|
|
35
|
-
>>> grid_type_map.
|
|
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.
|
|
59
|
-
self._shape = self.
|
|
60
|
-
self._dtype = self.
|
|
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(
|
|
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.
|
|
353
|
+
return self._data[idx]
|
|
74
354
|
|
|
75
355
|
def __setitem__(self, idx, value):
|
|
76
|
-
self.
|
|
356
|
+
self._data[idx] = value
|
|
77
357
|
|
|
78
358
|
@property
|
|
79
|
-
def
|
|
80
|
-
return self.
|
|
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
|
-
|
|
404
|
+
dtype('int8')
|
|
134
405
|
|
|
135
406
|
>>> grid_map.type_map
|
|
136
|
-
GridTypeMap(
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
462
|
+
self.type_map = GridTypeMap(np.zeros(shape, dtype=np.int8))
|
|
197
463
|
else:
|
|
198
|
-
if type_map.shape !=
|
|
199
|
-
raise ValueError("Shape must be {} instead of {}".format(self.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
if src_point is
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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.
|
|
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,
|
|
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
|
|
740
|
+
- Free grid ESDF > 0. The value is the di/stance to the nearest obstacle
|
|
519
741
|
"""
|
|
520
|
-
obstacle_mask = (self.type_map.
|
|
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[
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
567
|
-
self.
|
|
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.
|
|
827
|
+
self._orthogonal_offsets = [Node(tuple(offset.tolist())) for offset in self._orthogonal_offsets_array]
|