python-motion-planning 2.0.1__tar.gz → 2.1__tar.gz
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-2.0.1 → python_motion_planning-2.1}/PKG-INFO +2 -2
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/pyproject.toml +2 -2
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/map/grid.py +241 -85
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/utils/geometry.py +3 -2
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/graph_search/lazy_theta_star.py +15 -7
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/graph_search/theta_star.py +4 -3
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/sample_search/rrt.py +1 -1
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning.egg-info/PKG-INFO +2 -2
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning.egg-info/SOURCES.txt +3 -1
- python_motion_planning-2.1/tests/test_anya.py +67 -0
- python_motion_planning-2.1/tests/test_grid_line_symmetry.py +81 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/LICENSE +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/README.md +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/setup.cfg +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/map/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/map/base_map.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/node.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/robot/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/robot/base_robot.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/robot/circular_robot.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/robot/diff_drive_robot.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/types.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/world/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/world/base_world.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/env/world/toy_simulator.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/utils/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/utils/child_tree.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/utils/frame_transformer.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/visualizer/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/visualizer/base_visualizer.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/visualizer/visualizer_2d.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/common/visualizer/visualizer_3d.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/controller/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/controller/base_controller.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/controller/path_tracker/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/controller/path_tracker/apf.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/controller/path_tracker/dwa.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/controller/path_tracker/path_tracker.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/controller/path_tracker/pid.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/controller/path_tracker/pure_pursuit.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/controller/path_tracker/rpp.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/controller/random_controller.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/base_path_planner.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/graph_search/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/graph_search/a_star.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/graph_search/dijkstra.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/graph_search/gbfs.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/graph_search/jps.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/hybrid_search/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/hybrid_search/voronoi_planner.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/sample_search/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/sample_search/rrt_connect.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/path_planner/sample_search/rrt_star.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/base_curve_generator.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/curve_generator/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/curve_generator/point_based/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/curve_generator/point_based/bspline.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/curve_generator/point_based/cubic_spline.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/curve_generator/pose_based/__init__.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/curve_generator/pose_based/bezier.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/curve_generator/pose_based/dubins.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/curve_generator/pose_based/polynomial.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning.egg-info/dependency_links.txt +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning.egg-info/requires.txt +0 -0
- {python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-motion-planning
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1
|
|
4
4
|
Summary: Motion planning algorithms for Python
|
|
5
5
|
Maintainer-email: Wu Maojia <omige@mail.nwpu.edu.cn>, Yang Haodong <913982779@qq.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -681,7 +681,7 @@ Project-URL: Repository, https://github.com/ai-winter/python_motion_planning
|
|
|
681
681
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
682
682
|
Classifier: Programming Language :: Python :: 3
|
|
683
683
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
684
|
-
Requires-Python: >=3.
|
|
684
|
+
Requires-Python: >=3.8
|
|
685
685
|
Description-Content-Type: text/markdown
|
|
686
686
|
License-File: LICENSE
|
|
687
687
|
Requires-Dist: numpy
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-motion-planning"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.1"
|
|
8
8
|
description = "Motion planning algorithms for Python"
|
|
9
9
|
maintainers = [
|
|
10
10
|
{name = "Wu Maojia", email = "omige@mail.nwpu.edu.cn"},
|
|
@@ -12,7 +12,7 @@ maintainers = [
|
|
|
12
12
|
]
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
license = {file = "LICENSE"}
|
|
15
|
-
requires-python = ">=3.
|
|
15
|
+
requires-python = ">=3.8"
|
|
16
16
|
dependencies = [
|
|
17
17
|
"numpy",
|
|
18
18
|
"numba",
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: grid.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update: 2026.
|
|
4
|
+
@update: 2026.9.11
|
|
5
5
|
"""
|
|
6
6
|
from itertools import product
|
|
7
|
-
from typing import Iterable, Union, Tuple,
|
|
8
|
-
import math
|
|
7
|
+
from typing import Iterable, Union, Tuple, List, Dict
|
|
9
8
|
import time
|
|
10
9
|
|
|
11
10
|
import numpy as np
|
|
@@ -60,57 +59,77 @@ def _grid_is_expandable(
|
|
|
60
59
|
esdf: np.ndarray,
|
|
61
60
|
obstacle_type: int,
|
|
62
61
|
inflation_type: int,
|
|
62
|
+
strict_collision: bool,
|
|
63
63
|
) -> bool:
|
|
64
64
|
if not _grid_within_bounds(point, shape):
|
|
65
65
|
return False
|
|
66
66
|
|
|
67
67
|
point_idx = _grid_flat_index(point, shape)
|
|
68
|
+
src_idx = 0
|
|
69
|
+
src_is_inflation = False
|
|
68
70
|
if has_src_point:
|
|
69
71
|
src_idx = _grid_flat_index(src_point, shape)
|
|
70
|
-
|
|
71
|
-
return True
|
|
72
|
+
src_is_inflation = type_map[src_idx] == inflation_type
|
|
72
73
|
|
|
73
74
|
point_type = type_map[point_idx]
|
|
74
|
-
|
|
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
|
|
75
80
|
|
|
81
|
+
if not has_src_point or not strict_collision:
|
|
82
|
+
return True
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return math.sqrt(dist_square)
|
|
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
|
|
84
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
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
)
|
|
92
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
|
|
93
113
|
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
114
|
+
if not changed_mask & (changed_mask - 1):
|
|
115
|
+
return True
|
|
105
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
|
|
106
131
|
|
|
107
|
-
|
|
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
|
|
132
|
+
return True
|
|
114
133
|
|
|
115
134
|
|
|
116
135
|
@_njit(cache=True)
|
|
@@ -166,7 +185,10 @@ def _grid_line_of_sight(p1: np.ndarray, p2: np.ndarray) -> np.ndarray:
|
|
|
166
185
|
continue
|
|
167
186
|
|
|
168
187
|
error[d] += delta2[d]
|
|
169
|
-
|
|
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
|
+
):
|
|
170
192
|
current[d] += 1 if delta[d] > 0 else -1
|
|
171
193
|
error[d] -= delta2[primary_axis]
|
|
172
194
|
|
|
@@ -185,10 +207,11 @@ def _grid_in_collision(
|
|
|
185
207
|
esdf: np.ndarray,
|
|
186
208
|
obstacle_type: int,
|
|
187
209
|
inflation_type: int,
|
|
210
|
+
strict_collision: bool,
|
|
188
211
|
) -> bool:
|
|
189
|
-
if not _grid_is_expandable(p1, p1, False, shape, type_map, esdf, obstacle_type, inflation_type):
|
|
212
|
+
if not _grid_is_expandable(p1, p1, False, shape, type_map, esdf, obstacle_type, inflation_type, False):
|
|
190
213
|
return True
|
|
191
|
-
if not _grid_is_expandable(p2, p1, True, shape, type_map, esdf, obstacle_type, inflation_type):
|
|
214
|
+
if not _grid_is_expandable(p2, p1, True, shape, type_map, esdf, obstacle_type, inflation_type, False):
|
|
192
215
|
return True
|
|
193
216
|
|
|
194
217
|
dim = p1.size
|
|
@@ -231,11 +254,24 @@ def _grid_in_collision(
|
|
|
231
254
|
continue
|
|
232
255
|
|
|
233
256
|
error[d] += delta2[d]
|
|
234
|
-
|
|
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
|
+
):
|
|
235
261
|
current[d] += 1 if delta[d] > 0 else -1
|
|
236
262
|
error[d] -= delta2[primary_axis]
|
|
237
263
|
|
|
238
|
-
if not _grid_is_expandable(
|
|
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
|
+
):
|
|
239
275
|
return True
|
|
240
276
|
|
|
241
277
|
return False
|
|
@@ -250,6 +286,7 @@ def _grid_neighbor_positions_and_mask(
|
|
|
250
286
|
esdf: np.ndarray,
|
|
251
287
|
obstacle_type: int,
|
|
252
288
|
inflation_type: int,
|
|
289
|
+
strict_collision: bool,
|
|
253
290
|
) -> Tuple[np.ndarray, np.ndarray]:
|
|
254
291
|
node_num = offsets.shape[0]
|
|
255
292
|
dim = offsets.shape[1]
|
|
@@ -262,7 +299,17 @@ def _grid_neighbor_positions_and_mask(
|
|
|
262
299
|
neighbor[d] = current[d] + offsets[i, d]
|
|
263
300
|
positions[i, d] = neighbor[d]
|
|
264
301
|
|
|
265
|
-
mask[i] = _grid_is_expandable(
|
|
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
|
+
)
|
|
266
313
|
|
|
267
314
|
return positions, mask
|
|
268
315
|
|
|
@@ -338,6 +385,7 @@ class GridTypeMap:
|
|
|
338
385
|
self._data = np.asarray(type_map)
|
|
339
386
|
self._shape = self._data.shape
|
|
340
387
|
self._dtype = self._data.dtype
|
|
388
|
+
self._lazy_flags_ = 3
|
|
341
389
|
|
|
342
390
|
self._dtype_options = [np.int8, np.int16, np.int32, np.int64]
|
|
343
391
|
if self._dtype not in self._dtype_options:
|
|
@@ -354,6 +402,7 @@ class GridTypeMap:
|
|
|
354
402
|
|
|
355
403
|
def __setitem__(self, idx, value):
|
|
356
404
|
self._data[idx] = value
|
|
405
|
+
self._lazy_flags_ = 3
|
|
357
406
|
|
|
358
407
|
@property
|
|
359
408
|
def data(self) -> np.ndarray:
|
|
@@ -380,6 +429,7 @@ class Grid(BaseMap):
|
|
|
380
429
|
resolution: resolution of the grid map
|
|
381
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)
|
|
382
431
|
inflation_radius: radius of the inflation
|
|
432
|
+
strict_collision: whether diagonal steps beside obstacles or inflation are collisions (default: True)
|
|
383
433
|
|
|
384
434
|
Examples:
|
|
385
435
|
>>> grid_map = Grid(bounds=[[0, 51], [0, 31]], resolution=0.5)
|
|
@@ -428,7 +478,7 @@ class Grid(BaseMap):
|
|
|
428
478
|
|
|
429
479
|
>>> grid_map[1, 0] = TYPES.OBSTACLE # place an obstacle
|
|
430
480
|
>>> grid_map.get_neighbors(Node((0, 0))) # limited within the bounds
|
|
431
|
-
[Node((0, 1), (0, 0), 0, 0)
|
|
481
|
+
[Node((0, 1), (0, 0), 0, 0)]
|
|
432
482
|
|
|
433
483
|
>>> grid_map.get_neighbors(Node((grid_map.shape[0] - 1, grid_map.shape[1] - 1)), diagonal=False) # limited within the boundss
|
|
434
484
|
[Node((100, 61), (101, 61), 0, 0), Node((101, 60), (101, 61), 0, 0)]
|
|
@@ -443,15 +493,31 @@ class Grid(BaseMap):
|
|
|
443
493
|
False
|
|
444
494
|
|
|
445
495
|
>>> grid_map[1, 3] = TYPES.OBSTACLE
|
|
446
|
-
>>> grid_map.update_esdf()
|
|
447
496
|
>>> grid_map.in_collision((1, 2), (3, 6))
|
|
448
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
|
|
449
514
|
"""
|
|
450
515
|
def __init__(self,
|
|
451
516
|
bounds: Iterable = [[0, 30], [0, 40]],
|
|
452
517
|
resolution: float = 1.0,
|
|
453
518
|
type_map: Union[GridTypeMap, np.ndarray] = None,
|
|
454
519
|
inflation_radius: float = 0.0,
|
|
520
|
+
strict_collision: bool = True,
|
|
455
521
|
) -> None:
|
|
456
522
|
super().__init__(bounds)
|
|
457
523
|
|
|
@@ -459,15 +525,15 @@ class Grid(BaseMap):
|
|
|
459
525
|
shape = tuple([int((self.bounds[i, 1] - self.bounds[i, 0]) / self.resolution) for i in range(self.dim)])
|
|
460
526
|
|
|
461
527
|
if type_map is None:
|
|
462
|
-
self.
|
|
528
|
+
self._type_map = GridTypeMap(np.zeros(shape, dtype=np.int8))
|
|
463
529
|
else:
|
|
464
530
|
if type_map.shape != shape:
|
|
465
531
|
raise ValueError("Shape must be {} instead of {} with given bounds={} and resolution={}".format(shape, type_map.shape, self.bounds, self.resolution))
|
|
466
532
|
|
|
467
533
|
if isinstance(type_map, GridTypeMap):
|
|
468
|
-
self.
|
|
534
|
+
self._type_map = type_map
|
|
469
535
|
elif isinstance(type_map, np.ndarray):
|
|
470
|
-
self.
|
|
536
|
+
self._type_map = GridTypeMap(type_map)
|
|
471
537
|
else:
|
|
472
538
|
raise ValueError("Type map must be GridTypeMap or numpy.ndarray instead of {}".format(type(type_map)))
|
|
473
539
|
|
|
@@ -475,7 +541,8 @@ class Grid(BaseMap):
|
|
|
475
541
|
self._precompute_offsets()
|
|
476
542
|
|
|
477
543
|
self._esdf = np.zeros(self.shape, dtype=np.float32)
|
|
478
|
-
|
|
544
|
+
self._connectivity_map = np.zeros(self.shape, dtype=np.int32)
|
|
545
|
+
self.strict_collision = strict_collision
|
|
479
546
|
|
|
480
547
|
self.inflation_radius = inflation_radius
|
|
481
548
|
if self.inflation_radius >= 1:
|
|
@@ -490,34 +557,79 @@ class Grid(BaseMap):
|
|
|
490
557
|
@property
|
|
491
558
|
def resolution(self) -> float:
|
|
492
559
|
return self._resolution
|
|
560
|
+
|
|
561
|
+
@property
|
|
562
|
+
def type_map(self) -> GridTypeMap:
|
|
563
|
+
return self._type_map
|
|
493
564
|
|
|
494
565
|
@property
|
|
495
566
|
def shape(self) -> tuple:
|
|
496
|
-
return self.
|
|
567
|
+
return self._type_map.shape
|
|
497
568
|
|
|
498
569
|
@property
|
|
499
570
|
def dtype(self) -> np.dtype:
|
|
500
|
-
return self.
|
|
571
|
+
return self._type_map.dtype
|
|
501
572
|
|
|
502
573
|
@property
|
|
503
574
|
def esdf(self) -> np.ndarray:
|
|
575
|
+
if self._esdf_lazy_flag_:
|
|
576
|
+
self.update_esdf()
|
|
577
|
+
self._esdf_lazy_flag_ = False
|
|
504
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
|
|
505
586
|
|
|
506
587
|
@property
|
|
507
588
|
def data(self) -> np.ndarray:
|
|
508
|
-
return self.
|
|
589
|
+
return self._type_map.data
|
|
509
590
|
|
|
510
591
|
def __getitem__(self, idx):
|
|
511
|
-
return self.
|
|
592
|
+
return self._type_map[idx]
|
|
512
593
|
|
|
513
594
|
def __setitem__(self, idx, value):
|
|
514
|
-
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
|
|
515
627
|
|
|
516
628
|
def _type_map_flat(self) -> np.ndarray:
|
|
517
|
-
return np.ravel(self.
|
|
629
|
+
return np.ravel(self._type_map.data)
|
|
518
630
|
|
|
519
631
|
def _esdf_flat(self) -> np.ndarray:
|
|
520
|
-
return np.ravel(self.
|
|
632
|
+
return np.ravel(self.esdf)
|
|
521
633
|
|
|
522
634
|
def map_to_world(self, point: tuple) -> Tuple[float, ...]:
|
|
523
635
|
"""
|
|
@@ -532,8 +644,9 @@ class Grid(BaseMap):
|
|
|
532
644
|
if len(point) != self.dim:
|
|
533
645
|
raise ValueError("Point dimension does not match map dimension.")
|
|
534
646
|
|
|
535
|
-
|
|
536
|
-
|
|
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))
|
|
537
650
|
|
|
538
651
|
def world_to_map(self, point: Tuple[float, ...], discrete: bool = True) -> tuple:
|
|
539
652
|
"""
|
|
@@ -549,13 +662,13 @@ class Grid(BaseMap):
|
|
|
549
662
|
if len(point) != self.dim:
|
|
550
663
|
raise ValueError("Point dimension does not match map dimension.")
|
|
551
664
|
|
|
552
|
-
point_array = np.asarray(point, dtype=np.float64)
|
|
553
665
|
if discrete:
|
|
554
|
-
point_map = _grid_world_to_map_int(
|
|
666
|
+
point_map = _grid_world_to_map_int(np.asarray(point, dtype=np.float64), self.bounds, self.resolution, self._shape_array)
|
|
555
667
|
return tuple(int(x) for x in point_map)
|
|
556
668
|
else:
|
|
557
|
-
|
|
558
|
-
|
|
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))
|
|
559
672
|
|
|
560
673
|
def get_distance(self, p1: Tuple[int, int], p2: Tuple[int, int]) -> float:
|
|
561
674
|
"""
|
|
@@ -568,9 +681,7 @@ class Grid(BaseMap):
|
|
|
568
681
|
Returns:
|
|
569
682
|
dist: Distance between two points.
|
|
570
683
|
"""
|
|
571
|
-
|
|
572
|
-
raise ValueError("Dimension mismatch")
|
|
573
|
-
return _grid_distance(np.asarray(p1, dtype=np.float64), np.asarray(p2, dtype=np.float64))
|
|
684
|
+
return Geometry.dist(p1, p2, type='Euclidean')
|
|
574
685
|
|
|
575
686
|
def within_bounds(self, point: Tuple[int, ...]) -> bool:
|
|
576
687
|
"""
|
|
@@ -582,11 +693,31 @@ class Grid(BaseMap):
|
|
|
582
693
|
Returns:
|
|
583
694
|
bool: True if the point is within the bounds of the map, False otherwise.
|
|
584
695
|
"""
|
|
585
|
-
|
|
586
|
-
|
|
696
|
+
if len(point) != self.dim:
|
|
697
|
+
return False
|
|
698
|
+
shape = self.shape
|
|
699
|
+
for d in range(self.dim):
|
|
700
|
+
if point[d] < 0 or point[d] >= shape[d]:
|
|
701
|
+
return False
|
|
702
|
+
return True
|
|
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.")
|
|
587
717
|
|
|
588
|
-
|
|
589
|
-
|
|
718
|
+
connectivity_map = self.connectivity_map
|
|
719
|
+
component = connectivity_map[p1]
|
|
720
|
+
return component != 0 and component == connectivity_map[p2]
|
|
590
721
|
|
|
591
722
|
def is_expandable(self, point: Tuple[int, ...], src_point: Tuple[int, ...] = None) -> bool:
|
|
592
723
|
"""
|
|
@@ -612,6 +743,7 @@ class Grid(BaseMap):
|
|
|
612
743
|
self._esdf_flat(),
|
|
613
744
|
TYPES.OBSTACLE,
|
|
614
745
|
TYPES.INFLATION,
|
|
746
|
+
self.strict_collision,
|
|
615
747
|
)
|
|
616
748
|
|
|
617
749
|
def get_neighbors(self,
|
|
@@ -631,6 +763,16 @@ class Grid(BaseMap):
|
|
|
631
763
|
if node.dim != self.dim:
|
|
632
764
|
raise ValueError("Node dimension does not match map dimension.")
|
|
633
765
|
|
|
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."""
|
|
634
776
|
offsets = self._diagonal_offsets_array if diagonal else self._orthogonal_offsets_array
|
|
635
777
|
positions, mask = _grid_neighbor_positions_and_mask(
|
|
636
778
|
np.asarray(node.current, dtype=np.int64),
|
|
@@ -640,13 +782,9 @@ class Grid(BaseMap):
|
|
|
640
782
|
self._esdf_flat(),
|
|
641
783
|
TYPES.OBSTACLE,
|
|
642
784
|
TYPES.INFLATION,
|
|
785
|
+
self.strict_collision,
|
|
643
786
|
)
|
|
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
|
-
]
|
|
787
|
+
return positions, mask
|
|
650
788
|
|
|
651
789
|
def line_of_sight(self, p1: Tuple[int, ...], p2: Tuple[int, ...]) -> List[Tuple[int, ...]]:
|
|
652
790
|
"""
|
|
@@ -691,6 +829,7 @@ class Grid(BaseMap):
|
|
|
691
829
|
self._esdf_flat(),
|
|
692
830
|
TYPES.OBSTACLE,
|
|
693
831
|
TYPES.INFLATION,
|
|
832
|
+
self.strict_collision,
|
|
694
833
|
)
|
|
695
834
|
|
|
696
835
|
def fill_boundary_with_obstacles(self) -> None:
|
|
@@ -702,12 +841,12 @@ class Grid(BaseMap):
|
|
|
702
841
|
# First boundary (start index)
|
|
703
842
|
slices_start = [slice(None)] * self.dim
|
|
704
843
|
slices_start[d] = 0
|
|
705
|
-
self.
|
|
844
|
+
self._type_map[tuple(slices_start)] = TYPES.OBSTACLE
|
|
706
845
|
|
|
707
846
|
# Last boundary (end index)
|
|
708
847
|
slices_end = [slice(None)] * self.dim
|
|
709
848
|
slices_end[d] = -1
|
|
710
|
-
self.
|
|
849
|
+
self._type_map[tuple(slices_end)] = TYPES.OBSTACLE
|
|
711
850
|
|
|
712
851
|
def inflate_obstacles(self, radius: float = 1.0) -> None:
|
|
713
852
|
"""
|
|
@@ -716,9 +855,8 @@ class Grid(BaseMap):
|
|
|
716
855
|
Args:
|
|
717
856
|
radius: Radius of the inflation.
|
|
718
857
|
"""
|
|
719
|
-
self.
|
|
720
|
-
mask =
|
|
721
|
-
self.type_map[mask] = TYPES.INFLATION
|
|
858
|
+
mask = (self.esdf <= radius) & (self._type_map.data == TYPES.FREE)
|
|
859
|
+
self._type_map[mask] = TYPES.INFLATION
|
|
722
860
|
self.inflation_radius = radius
|
|
723
861
|
|
|
724
862
|
def fill_expands(self, expands: Dict[Tuple[int, ...], Node]) -> None:
|
|
@@ -729,9 +867,9 @@ class Grid(BaseMap):
|
|
|
729
867
|
expands: List of expands.
|
|
730
868
|
"""
|
|
731
869
|
for expand in expands.keys():
|
|
732
|
-
if self.
|
|
870
|
+
if self._type_map[expand] != TYPES.FREE:
|
|
733
871
|
continue
|
|
734
|
-
self.
|
|
872
|
+
self._type_map[expand] = TYPES.EXPAND
|
|
735
873
|
|
|
736
874
|
def update_esdf(self) -> None:
|
|
737
875
|
"""
|
|
@@ -739,7 +877,7 @@ class Grid(BaseMap):
|
|
|
739
877
|
- Obstacle grid ESDF = 0
|
|
740
878
|
- Free grid ESDF > 0. The value is the di/stance to the nearest obstacle
|
|
741
879
|
"""
|
|
742
|
-
obstacle_mask = (self.
|
|
880
|
+
obstacle_mask = (self._type_map.data == TYPES.OBSTACLE)
|
|
743
881
|
free_mask = ~obstacle_mask
|
|
744
882
|
|
|
745
883
|
# distance to obstacles
|
|
@@ -749,6 +887,16 @@ class Grid(BaseMap):
|
|
|
749
887
|
|
|
750
888
|
self._esdf = dist_outside.astype(np.float32)
|
|
751
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
|
|
752
900
|
|
|
753
901
|
def path_map_to_world(self, path: List[tuple]) -> List[Tuple[float, ...]]:
|
|
754
902
|
"""
|
|
@@ -807,8 +955,16 @@ class Grid(BaseMap):
|
|
|
807
955
|
Returns:
|
|
808
956
|
point: a point in integer coordinates
|
|
809
957
|
"""
|
|
810
|
-
|
|
811
|
-
|
|
958
|
+
shape = self.shape
|
|
959
|
+
point_int = []
|
|
960
|
+
for d in range(self.dim):
|
|
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)
|
|
812
968
|
|
|
813
969
|
def _precompute_offsets(self):
|
|
814
970
|
# Generate all possible offsets (-1, 0, +1) in each dimension
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: geometry.py
|
|
3
3
|
@author: Wu Maojia
|
|
4
|
-
@update:
|
|
4
|
+
@update: 2026.9.12
|
|
5
5
|
"""
|
|
6
6
|
from typing import List, Tuple
|
|
7
7
|
import math
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
class Geometry:
|
|
12
13
|
"""
|
|
13
14
|
Geometry tools class
|
|
@@ -29,7 +30,7 @@ class Geometry:
|
|
|
29
30
|
if len(p1) != len(p2):
|
|
30
31
|
raise ValueError("Dimension mismatch")
|
|
31
32
|
if type == 'Euclidean':
|
|
32
|
-
return math.
|
|
33
|
+
return math.dist(p1, p2)
|
|
33
34
|
elif type == 'Manhattan':
|
|
34
35
|
return sum(abs(a - b) for a, b in zip(p1, p2))
|
|
35
36
|
else:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: lazy_theta_star.py
|
|
3
3
|
@author: Wu Maojia, Yang Haodong
|
|
4
|
-
@update:
|
|
4
|
+
@update: 2026.9.12
|
|
5
5
|
"""
|
|
6
6
|
from typing import Union, List, Tuple, Dict, Any
|
|
7
7
|
import heapq
|
|
@@ -61,12 +61,20 @@ class LazyThetaStar(ThetaStar):
|
|
|
61
61
|
if node_p:
|
|
62
62
|
if self.map_.in_collision(node_p.current, node.current):
|
|
63
63
|
node.g = float("inf")
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
get_neighbor_arrays = getattr(self.map_, "_get_neighbor_arrays", None)
|
|
65
|
+
if get_neighbor_arrays:
|
|
66
|
+
positions, mask = get_neighbor_arrays(node, diagonal=self.diagonal)
|
|
67
|
+
neighbors = (tuple(positions[i].tolist())
|
|
68
|
+
for i in range(positions.shape[0]) if mask[i])
|
|
69
|
+
else:
|
|
70
|
+
neighbors = (node_n.current for node_n in self.map_.get_neighbors(node, diagonal=self.diagonal))
|
|
71
|
+
for point in neighbors:
|
|
72
|
+
node_n = CLOSED.get(point)
|
|
73
|
+
if node_n:
|
|
74
|
+
cost = node_n.g + self.get_cost(point, node.current)
|
|
75
|
+
if node.g > cost:
|
|
76
|
+
node.g = cost
|
|
77
|
+
node.parent = point
|
|
70
78
|
|
|
71
79
|
# exists in CLOSED list
|
|
72
80
|
if node.current in CLOSED:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
@file: theta_star.py
|
|
3
3
|
@author: Wu Maojia, Yang Haodong
|
|
4
|
-
@update:
|
|
4
|
+
@update: 2026.9.12
|
|
5
5
|
"""
|
|
6
6
|
from typing import Union, List, Tuple, Dict, Any
|
|
7
7
|
import heapq
|
|
@@ -111,6 +111,7 @@ class ThetaStar(AStar):
|
|
|
111
111
|
node_p (Node): parent node
|
|
112
112
|
node_n (Node): next node
|
|
113
113
|
"""
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
cost = node_p.g + self.get_cost(node_p.current, node_n.current)
|
|
115
|
+
if cost <= node_n.g:
|
|
116
|
+
node_n.g = cost
|
|
116
117
|
node_n.parent = node_p.current
|
|
@@ -23,7 +23,7 @@ class RRT(BasePathPlanner):
|
|
|
23
23
|
max_sample_step: Maximum number of steps of samples to generate.
|
|
24
24
|
goal_sample_rate: Probability of sampling the goal directly.
|
|
25
25
|
discrete: Whether to use discrete or continuous space.
|
|
26
|
-
|
|
26
|
+
use_faiss: Whether to use Faiss to accelerate the search.
|
|
27
27
|
*kwargs: see the parent class.
|
|
28
28
|
|
|
29
29
|
References:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-motion-planning
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1
|
|
4
4
|
Summary: Motion planning algorithms for Python
|
|
5
5
|
Maintainer-email: Wu Maojia <omige@mail.nwpu.edu.cn>, Yang Haodong <913982779@qq.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -681,7 +681,7 @@ Project-URL: Repository, https://github.com/ai-winter/python_motion_planning
|
|
|
681
681
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
682
682
|
Classifier: Programming Language :: Python :: 3
|
|
683
683
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
684
|
-
Requires-Python: >=3.
|
|
684
|
+
Requires-Python: >=3.8
|
|
685
685
|
Description-Content-Type: text/markdown
|
|
686
686
|
License-File: LICENSE
|
|
687
687
|
Requires-Dist: numpy
|
|
@@ -64,4 +64,6 @@ src/python_motion_planning/traj_optimizer/curve_generator/pose_based/__init__.py
|
|
|
64
64
|
src/python_motion_planning/traj_optimizer/curve_generator/pose_based/bezier.py
|
|
65
65
|
src/python_motion_planning/traj_optimizer/curve_generator/pose_based/dubins.py
|
|
66
66
|
src/python_motion_planning/traj_optimizer/curve_generator/pose_based/polynomial.py
|
|
67
|
-
src/python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py
|
|
67
|
+
src/python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py
|
|
68
|
+
tests/test_anya.py
|
|
69
|
+
tests/test_grid_line_symmetry.py
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from python_motion_planning.common import Grid, TYPES
|
|
4
|
+
from python_motion_planning.path_planner import Anya
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_anya_empty_map_returns_straight_path():
|
|
8
|
+
map_ = Grid(bounds=[[0, 12], [0, 12]])
|
|
9
|
+
path, info = Anya(map_=map_, start=(1, 2), goal=(10, 9)).plan()
|
|
10
|
+
|
|
11
|
+
assert info["success"]
|
|
12
|
+
assert path == [(1, 2), (10, 9)]
|
|
13
|
+
assert math.isclose(info["length"], math.hypot(9, 7))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_anya_routes_around_obstacle():
|
|
17
|
+
map_ = Grid(bounds=[[0, 12], [0, 12]])
|
|
18
|
+
map_[5, 2:10] = TYPES.OBSTACLE
|
|
19
|
+
|
|
20
|
+
path, info = Anya(map_=map_, start=(2, 5), goal=(9, 5)).plan()
|
|
21
|
+
|
|
22
|
+
assert info["success"]
|
|
23
|
+
assert path[0] == (2, 5)
|
|
24
|
+
assert path[-1] == (9, 5)
|
|
25
|
+
assert len(path) > 2
|
|
26
|
+
assert all(isinstance(value, int) for point in path for value in point)
|
|
27
|
+
assert all(not map_.in_collision(path[i - 1], path[i]) for i in range(1, len(path)))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_anya_reports_unreachable_goal():
|
|
31
|
+
map_ = Grid(bounds=[[0, 10], [0, 10]])
|
|
32
|
+
map_[5, :] = TYPES.OBSTACLE
|
|
33
|
+
|
|
34
|
+
path, info = Anya(map_=map_, start=(2, 5), goal=(8, 5)).plan()
|
|
35
|
+
|
|
36
|
+
assert path == []
|
|
37
|
+
assert not info["success"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_anya_respects_strict_collision():
|
|
41
|
+
map_ = Grid(bounds=[[0, 5], [0, 5]])
|
|
42
|
+
map_[1, 0] = TYPES.OBSTACLE
|
|
43
|
+
|
|
44
|
+
path, info = Anya(map_=map_, start=(0, 0), goal=(1, 1)).plan()
|
|
45
|
+
assert info["success"]
|
|
46
|
+
assert len(path) > 2
|
|
47
|
+
assert all(not map_.in_collision(path[i - 1], path[i]) for i in range(1, len(path)))
|
|
48
|
+
|
|
49
|
+
map_.strict_collision = False
|
|
50
|
+
path, info = Anya(map_=map_, start=(0, 0), goal=(1, 1)).plan()
|
|
51
|
+
assert info["success"]
|
|
52
|
+
assert path == [(0, 0), (1, 1)]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_anya_uses_grid_rule_for_double_corner():
|
|
56
|
+
map_ = Grid(bounds=[[0, 3], [0, 3]])
|
|
57
|
+
map_[1, 0] = TYPES.OBSTACLE
|
|
58
|
+
map_[0, 1] = TYPES.OBSTACLE
|
|
59
|
+
|
|
60
|
+
path, info = Anya(map_=map_, start=(0, 0), goal=(1, 1)).plan()
|
|
61
|
+
assert path == []
|
|
62
|
+
assert not info["success"]
|
|
63
|
+
|
|
64
|
+
map_.strict_collision = False
|
|
65
|
+
path, info = Anya(map_=map_, start=(0, 0), goal=(1, 1)).plan()
|
|
66
|
+
assert info["success"]
|
|
67
|
+
assert path == [(0, 0), (1, 1)]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from python_motion_planning.common import Grid, TYPES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GridLineSymmetryTest(unittest.TestCase):
|
|
10
|
+
def test_half_cell_ties(self):
|
|
11
|
+
for dim in (2, 3, 4):
|
|
12
|
+
grid = Grid(bounds=[[0, 13]] * dim)
|
|
13
|
+
start = (6,) * dim
|
|
14
|
+
for delta in itertools.product(range(-3, 4), repeat=dim):
|
|
15
|
+
end = tuple(a + b for a, b in zip(start, delta))
|
|
16
|
+
path = grid.line_of_sight(start, end)
|
|
17
|
+
self.assertEqual(path, grid.line_of_sight(end, start)[::-1])
|
|
18
|
+
self.assertEqual(path[0], start)
|
|
19
|
+
self.assertEqual(path[-1], end)
|
|
20
|
+
# Independent rational reference: ties follow the increasing
|
|
21
|
+
# primary-axis traversal, including when requested in reverse.
|
|
22
|
+
axis = max(range(dim), key=lambda d: abs(delta[d]))
|
|
23
|
+
a, b = (start, end) if delta[axis] >= 0 else (end, start)
|
|
24
|
+
steps = abs(delta[axis])
|
|
25
|
+
expected = [a]
|
|
26
|
+
for i in range(1, steps + 1):
|
|
27
|
+
expected.append(tuple(
|
|
28
|
+
a[d] + (1 if b[d] >= a[d] else -1)
|
|
29
|
+
* ((2 * i * abs(b[d] - a[d]) + steps - 1) // (2 * steps))
|
|
30
|
+
for d in range(dim)
|
|
31
|
+
))
|
|
32
|
+
if delta[axis] < 0:
|
|
33
|
+
expected.reverse()
|
|
34
|
+
self.assertEqual(path, expected)
|
|
35
|
+
|
|
36
|
+
def test_inflated_corner_regression(self):
|
|
37
|
+
grid = Grid(bounds=[[0, 51], [0, 31]])
|
|
38
|
+
grid.fill_boundary_with_obstacles()
|
|
39
|
+
grid[10:21, 15] = TYPES.OBSTACLE
|
|
40
|
+
grid[20, :15] = TYPES.OBSTACLE
|
|
41
|
+
grid[30, 15:] = TYPES.OBSTACLE
|
|
42
|
+
grid[40, :16] = TYPES.OBSTACLE
|
|
43
|
+
grid.inflate_obstacles(radius=3)
|
|
44
|
+
a, b = (27, 12), (31, 11)
|
|
45
|
+
self.assertEqual(grid[30, 12], TYPES.INFLATION)
|
|
46
|
+
self.assertTrue(grid.in_collision(a, b))
|
|
47
|
+
self.assertTrue(grid.in_collision(b, a))
|
|
48
|
+
grid.strict_collision = False
|
|
49
|
+
self.assertFalse(grid.in_collision(a, b))
|
|
50
|
+
self.assertFalse(grid.in_collision(b, a))
|
|
51
|
+
grid.strict_collision = True
|
|
52
|
+
grid[30, 12] = TYPES.FREE
|
|
53
|
+
self.assertFalse(grid.in_collision(a, b))
|
|
54
|
+
self.assertFalse(grid.in_collision(b, a))
|
|
55
|
+
|
|
56
|
+
def test_random_collision_matches_steps(self):
|
|
57
|
+
rng = np.random.default_rng(0)
|
|
58
|
+
for dim in (2, 3, 4):
|
|
59
|
+
shape = (9,) * dim
|
|
60
|
+
data = np.zeros(shape, dtype=np.int8)
|
|
61
|
+
values = rng.random(shape)
|
|
62
|
+
data[values < 0.08] = TYPES.OBSTACLE
|
|
63
|
+
data[(values >= 0.08) & (values < 0.16)] = TYPES.INFLATION
|
|
64
|
+
grid = Grid(bounds=[[0, 9]] * dim, type_map=data)
|
|
65
|
+
for strict in (False, True):
|
|
66
|
+
grid.strict_collision = strict
|
|
67
|
+
for _ in range(500):
|
|
68
|
+
a = tuple(int(x) for x in rng.integers(0, 9, dim))
|
|
69
|
+
b = tuple(int(x) for x in rng.integers(0, 9, dim))
|
|
70
|
+
path = grid.line_of_sight(a, b)
|
|
71
|
+
expected = not grid.is_expandable(a) or any(
|
|
72
|
+
not grid.is_expandable(v, u)
|
|
73
|
+
for u, v in zip(path, path[1:])
|
|
74
|
+
)
|
|
75
|
+
actual = grid.in_collision(a, b)
|
|
76
|
+
self.assertEqual(actual, expected)
|
|
77
|
+
self.assertEqual(actual, grid.in_collision(b, a))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == '__main__':
|
|
81
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_motion_planning-2.0.1 → python_motion_planning-2.1}/src/python_motion_planning/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|