python-motion-planning 1.1__py3-none-any.whl → 2.0__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 +4 -4
- python_motion_planning/common/__init__.py +3 -0
- python_motion_planning/common/env/__init__.py +6 -0
- python_motion_planning/common/env/map/__init__.py +2 -0
- python_motion_planning/common/env/map/base_map.py +119 -0
- python_motion_planning/common/env/map/grid.py +562 -0
- python_motion_planning/common/env/node.py +111 -0
- python_motion_planning/common/env/robot/__init__.py +3 -0
- python_motion_planning/common/env/robot/base_robot.py +214 -0
- python_motion_planning/common/env/robot/circular_robot.py +47 -0
- python_motion_planning/common/env/robot/diff_drive_robot.py +109 -0
- python_motion_planning/common/env/types.py +19 -0
- python_motion_planning/common/env/world/__init__.py +1 -0
- python_motion_planning/common/env/world/base_world.py +120 -0
- python_motion_planning/common/env/world/toy_simulator.py +206 -0
- python_motion_planning/common/utils/__init__.py +3 -0
- python_motion_planning/common/utils/child_tree.py +22 -0
- python_motion_planning/common/utils/frame_transformer.py +218 -0
- python_motion_planning/common/utils/geometry.py +94 -0
- python_motion_planning/common/visualizer/__init__.py +3 -0
- python_motion_planning/common/visualizer/base_visualizer.py +165 -0
- python_motion_planning/common/visualizer/visualizer_2d.py +406 -0
- python_motion_planning/common/visualizer/visualizer_3d.py +242 -0
- python_motion_planning/controller/__init__.py +3 -0
- python_motion_planning/controller/base_controller.py +191 -0
- python_motion_planning/controller/path_tracker/__init__.py +6 -0
- python_motion_planning/controller/path_tracker/apf.py +231 -0
- python_motion_planning/controller/path_tracker/dwa.py +250 -0
- python_motion_planning/controller/path_tracker/path_tracker.py +260 -0
- python_motion_planning/controller/path_tracker/pid.py +82 -0
- python_motion_planning/controller/path_tracker/pure_pursuit.py +71 -0
- python_motion_planning/controller/path_tracker/rpp.py +111 -0
- python_motion_planning/controller/random_controller.py +27 -0
- python_motion_planning/path_planner/__init__.py +4 -0
- python_motion_planning/path_planner/base_path_planner.py +122 -0
- python_motion_planning/path_planner/graph_search/__init__.py +6 -0
- python_motion_planning/path_planner/graph_search/a_star.py +94 -0
- python_motion_planning/path_planner/graph_search/dijkstra.py +97 -0
- 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 +116 -0
- 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 +3 -0
- python_motion_planning/path_planner/sample_search/rrt.py +243 -0
- python_motion_planning/path_planner/sample_search/rrt_connect.py +237 -0
- python_motion_planning/path_planner/sample_search/rrt_star.py +279 -0
- 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-1.1.dist-info → python_motion_planning-2.0.dist-info}/METADATA +115 -146
- python_motion_planning-2.0.dist-info/RECORD +64 -0
- {python_motion_planning-1.1.dist-info → python_motion_planning-2.0.dist-info}/WHEEL +1 -1
- python_motion_planning/curve_generation/__init__.py +0 -9
- python_motion_planning/curve_generation/bezier_curve.py +0 -131
- python_motion_planning/curve_generation/bspline_curve.py +0 -271
- python_motion_planning/curve_generation/cubic_spline.py +0 -128
- python_motion_planning/curve_generation/curve.py +0 -64
- python_motion_planning/curve_generation/dubins_curve.py +0 -348
- python_motion_planning/curve_generation/fem_pos_smooth.py +0 -114
- python_motion_planning/curve_generation/polynomial_curve.py +0 -226
- python_motion_planning/curve_generation/reeds_shepp.py +0 -736
- python_motion_planning/global_planner/__init__.py +0 -3
- python_motion_planning/global_planner/evolutionary_search/__init__.py +0 -4
- python_motion_planning/global_planner/evolutionary_search/aco.py +0 -186
- python_motion_planning/global_planner/evolutionary_search/evolutionary_search.py +0 -87
- python_motion_planning/global_planner/evolutionary_search/pso.py +0 -356
- python_motion_planning/global_planner/graph_search/__init__.py +0 -28
- python_motion_planning/global_planner/graph_search/a_star.py +0 -124
- python_motion_planning/global_planner/graph_search/d_star.py +0 -291
- python_motion_planning/global_planner/graph_search/d_star_lite.py +0 -188
- python_motion_planning/global_planner/graph_search/dijkstra.py +0 -77
- python_motion_planning/global_planner/graph_search/gbfs.py +0 -78
- python_motion_planning/global_planner/graph_search/graph_search.py +0 -87
- python_motion_planning/global_planner/graph_search/jps.py +0 -165
- python_motion_planning/global_planner/graph_search/lazy_theta_star.py +0 -114
- python_motion_planning/global_planner/graph_search/lpa_star.py +0 -230
- python_motion_planning/global_planner/graph_search/s_theta_star.py +0 -133
- python_motion_planning/global_planner/graph_search/theta_star.py +0 -171
- python_motion_planning/global_planner/graph_search/voronoi.py +0 -200
- python_motion_planning/global_planner/sample_search/__init__.py +0 -6
- python_motion_planning/global_planner/sample_search/informed_rrt.py +0 -152
- python_motion_planning/global_planner/sample_search/rrt.py +0 -151
- python_motion_planning/global_planner/sample_search/rrt_connect.py +0 -147
- python_motion_planning/global_planner/sample_search/rrt_star.py +0 -77
- python_motion_planning/global_planner/sample_search/sample_search.py +0 -135
- python_motion_planning/local_planner/__init__.py +0 -15
- python_motion_planning/local_planner/apf.py +0 -144
- python_motion_planning/local_planner/dwa.py +0 -212
- python_motion_planning/local_planner/local_planner.py +0 -262
- python_motion_planning/local_planner/lqr.py +0 -146
- python_motion_planning/local_planner/mpc.py +0 -214
- python_motion_planning/local_planner/pid.py +0 -158
- python_motion_planning/local_planner/rpp.py +0 -147
- python_motion_planning/utils/__init__.py +0 -19
- python_motion_planning/utils/agent/__init__.py +0 -0
- python_motion_planning/utils/agent/agent.py +0 -135
- python_motion_planning/utils/environment/__init__.py +0 -0
- python_motion_planning/utils/environment/env.py +0 -134
- python_motion_planning/utils/environment/node.py +0 -85
- python_motion_planning/utils/environment/point2d.py +0 -96
- python_motion_planning/utils/environment/pose2d.py +0 -91
- python_motion_planning/utils/helper/__init__.py +0 -3
- python_motion_planning/utils/helper/math_helper.py +0 -65
- python_motion_planning/utils/planner/__init__.py +0 -0
- python_motion_planning/utils/planner/control_factory.py +0 -27
- python_motion_planning/utils/planner/curve_factory.py +0 -29
- python_motion_planning/utils/planner/planner.py +0 -40
- python_motion_planning/utils/planner/search_factory.py +0 -51
- python_motion_planning/utils/plot/__init__.py +0 -0
- python_motion_planning/utils/plot/plot.py +0 -274
- python_motion_planning-1.1.dist-info/RECORD +0 -64
- {python_motion_planning-1.1.dist-info → python_motion_planning-2.0.dist-info/licenses}/LICENSE +0 -0
- {python_motion_planning-1.1.dist-info → python_motion_planning-2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
3
|
-
from .
|
|
4
|
-
from .
|
|
1
|
+
from .common import *
|
|
2
|
+
from .path_planner import *
|
|
3
|
+
from .controller import *
|
|
4
|
+
from .traj_optimizer import *
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: map.py
|
|
3
|
+
@author: Wu Maojia
|
|
4
|
+
@update: 2025.11.25
|
|
5
|
+
"""
|
|
6
|
+
from typing import Iterable, Union
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from python_motion_planning.common.env import Node
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseMap(ABC):
|
|
15
|
+
"""
|
|
16
|
+
Base class for Path Planning Map.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
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.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, bounds: Iterable) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._bounds = np.asarray(bounds, dtype=float)
|
|
24
|
+
|
|
25
|
+
if len(self._bounds.shape) != 2 or self._bounds.shape[0] <= 1 or self._bounds.shape[1] != 2:
|
|
26
|
+
raise ValueError(f"The shape of bounds must be (n, 2) (n>=2) instead of {self._bounds.shape}")
|
|
27
|
+
|
|
28
|
+
for d in range(self._bounds.shape[0]):
|
|
29
|
+
if self._bounds[d, 0] >= self._bounds[d, 1]:
|
|
30
|
+
raise ValueError(f"The lower bound of the world in the {d}-th dimension must be smaller than the upper bound of the world in the {d}-th dimension.")
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def bounds(self) -> np.ndarray:
|
|
34
|
+
return self._bounds
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def dim(self) -> int:
|
|
38
|
+
return self._bounds.shape[0]
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def map_to_world(self, point: tuple) -> tuple:
|
|
42
|
+
"""
|
|
43
|
+
Convert map coordinates to world coordinates.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
point: Point in map coordinates.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
point: Point in world coordinates.
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def world_to_map(self, point: tuple) -> tuple:
|
|
55
|
+
"""
|
|
56
|
+
Convert world coordinates to map coordinates.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
point: Point in world coordinates.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
point: Point in map coordinates.
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def get_distance(self, p1: tuple, p2: tuple) -> float:
|
|
68
|
+
"""
|
|
69
|
+
Get the distance between two points.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
p1: First point.
|
|
73
|
+
p2: Second point.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
dist: Distance between two points.
|
|
77
|
+
"""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def get_neighbors(self, node: Node) -> list:
|
|
82
|
+
"""
|
|
83
|
+
Get neighbor nodes of a given node.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
node: Node to get neighbor nodes.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
nodes: List of neighbor nodes.
|
|
90
|
+
"""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def is_expandable(self, point: tuple) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Check if a point is expandable.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
point: Point to check.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
expandable: True if the point is expandable, False otherwise.
|
|
103
|
+
"""
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def in_collision(self, p1: tuple, p2: tuple) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Check if the line of sight between two points is in collision.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
p1: Start point of the line.
|
|
113
|
+
p2: End point of the line.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
in_collision: True if the line of sight is in collision, False otherwise.
|
|
117
|
+
"""
|
|
118
|
+
pass
|
|
119
|
+
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: grid.py
|
|
3
|
+
@author: Wu Maojia
|
|
4
|
+
@update: 2025.12.20
|
|
5
|
+
"""
|
|
6
|
+
from itertools import product
|
|
7
|
+
from typing import Iterable, Union, Tuple, Callable, List, Dict
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from scipy import ndimage
|
|
12
|
+
|
|
13
|
+
from python_motion_planning.common.env.map.base_map import BaseMap
|
|
14
|
+
from python_motion_planning.common.env import Node, TYPES
|
|
15
|
+
from python_motion_planning.common.utils.geometry import Geometry
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GridTypeMap:
|
|
19
|
+
"""
|
|
20
|
+
Class for Grid Type Map. It is like a np.ndarray, except that its shape and dtype are fixed.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
type_map: The np.ndarray type map.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
>>> type_map = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.int8)
|
|
27
|
+
>>> grid_type_map = GridTypeMap(type_map)
|
|
28
|
+
>>> grid_type_map
|
|
29
|
+
GridTypeMap(data=
|
|
30
|
+
[[0 0 0]
|
|
31
|
+
[0 1 0]
|
|
32
|
+
[0 0 0]]
|
|
33
|
+
, shape=(3, 3), dtype=int8)
|
|
34
|
+
|
|
35
|
+
>>> grid_type_map.data
|
|
36
|
+
array([[0, 0, 0],
|
|
37
|
+
[0, 1, 0],
|
|
38
|
+
[0, 0, 0]], dtype=int8)
|
|
39
|
+
|
|
40
|
+
>>> grid_type_map.shape
|
|
41
|
+
(3, 3)
|
|
42
|
+
|
|
43
|
+
>>> grid_type_map.dtype
|
|
44
|
+
dtype('int8')
|
|
45
|
+
"""
|
|
46
|
+
def __init__(self, type_map: np.ndarray):
|
|
47
|
+
self._data = np.asarray(type_map)
|
|
48
|
+
self._shape = self._data.shape
|
|
49
|
+
self._dtype = self._data.dtype
|
|
50
|
+
|
|
51
|
+
self._dtype_options = [np.int8, np.int16, np.int32, np.int64]
|
|
52
|
+
if self._dtype not in self._dtype_options:
|
|
53
|
+
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))
|
|
54
|
+
|
|
55
|
+
def __str__(self) -> str:
|
|
56
|
+
return "GridTypeMap(data=\n{}\n, shape={}, dtype={})".format(self._data, self._shape, self._dtype)
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
return self.__str__()
|
|
60
|
+
|
|
61
|
+
def __getitem__(self, idx):
|
|
62
|
+
return self._data[idx]
|
|
63
|
+
|
|
64
|
+
def __setitem__(self, idx, value):
|
|
65
|
+
self._data[idx] = value
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def data(self) -> np.ndarray:
|
|
69
|
+
return self._data.view()
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def shape(self) -> Tuple:
|
|
73
|
+
return self._shape
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def dtype(self) -> np.dtype:
|
|
77
|
+
return self._dtype
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Grid(BaseMap):
|
|
81
|
+
"""
|
|
82
|
+
Class for Grid Map.
|
|
83
|
+
The shape of each dimension of the grid map is determined by the base world and resolution.
|
|
84
|
+
For each dimension, the conversion equation is: shape_grid = shape_world * resolution + 1
|
|
85
|
+
For example, if the base world is (30, 40) and the resolution is 0.5, the grid map will be (30 * 0.5 + 1, 40 * 0.5 + 1) = (61, 81).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
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.
|
|
89
|
+
resolution: resolution of the grid map
|
|
90
|
+
type_map: initial type map of the grid map (its shape must be the same as the converted grid map shape, and its dtype must be int)
|
|
91
|
+
inflation_radius: radius of the inflation
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
>>> grid_map = Grid(bounds=[[0, 51], [0, 31]], resolution=0.5)
|
|
95
|
+
>>> grid_map
|
|
96
|
+
Grid(bounds=[[ 0. 51.]
|
|
97
|
+
[ 0. 31.]], resolution=0.5)
|
|
98
|
+
|
|
99
|
+
>>> grid_map.bounds # bounds of the base world
|
|
100
|
+
array([[ 0., 51.],
|
|
101
|
+
[ 0., 31.]])
|
|
102
|
+
|
|
103
|
+
>>> grid_map.dim
|
|
104
|
+
2
|
|
105
|
+
|
|
106
|
+
>>> grid_map.resolution
|
|
107
|
+
0.5
|
|
108
|
+
|
|
109
|
+
>>> grid_map.shape # shape of the grid map
|
|
110
|
+
(102, 62)
|
|
111
|
+
|
|
112
|
+
>>> grid_map.dtype
|
|
113
|
+
dtype('int8')
|
|
114
|
+
|
|
115
|
+
>>> grid_map.type_map
|
|
116
|
+
GridTypeMap(data=
|
|
117
|
+
[[0 0 0 ... 0 0 0]
|
|
118
|
+
[0 0 0 ... 0 0 0]
|
|
119
|
+
[0 0 0 ... 0 0 0]
|
|
120
|
+
...
|
|
121
|
+
[0 0 0 ... 0 0 0]
|
|
122
|
+
[0 0 0 ... 0 0 0]
|
|
123
|
+
[0 0 0 ... 0 0 0]]
|
|
124
|
+
, shape=(102, 62), dtype=int8)
|
|
125
|
+
|
|
126
|
+
>>> grid_map.map_to_world((1, 2))
|
|
127
|
+
(0.75, 1.25)
|
|
128
|
+
|
|
129
|
+
>>> grid_map.world_to_map((0.5, 1.0))
|
|
130
|
+
(0, 2)
|
|
131
|
+
|
|
132
|
+
>>> grid_map.get_neighbors(Node((1, 2)))
|
|
133
|
+
[Node((0, 1), (1, 2), 0, 0), Node((0, 2), (1, 2), 0, 0), Node((0, 3), (1, 2), 0, 0), Node((1, 1), (1, 2), 0, 0), Node((1, 3), (1, 2), 0, 0), Node((2, 1), (1, 2), 0, 0), Node((2, 2), (1, 2), 0, 0), Node((2, 3), (1, 2), 0, 0)]
|
|
134
|
+
|
|
135
|
+
>>> grid_map.get_neighbors(Node((1, 2)), diagonal=False)
|
|
136
|
+
[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)]
|
|
137
|
+
|
|
138
|
+
>>> grid_map[1, 0] = TYPES.OBSTACLE # place an obstacle
|
|
139
|
+
>>> grid_map.get_neighbors(Node((0, 0))) # limited within the bounds
|
|
140
|
+
[Node((0, 1), (0, 0), 0, 0), Node((1, 1), (0, 0), 0, 0)]
|
|
141
|
+
|
|
142
|
+
>>> grid_map.get_neighbors(Node((grid_map.shape[0] - 1, grid_map.shape[1] - 1)), diagonal=False) # limited within the boundss
|
|
143
|
+
[Node((100, 61), (101, 61), 0, 0), Node((101, 60), (101, 61), 0, 0)]
|
|
144
|
+
|
|
145
|
+
>>> grid_map.line_of_sight((1, 2), (3, 6))
|
|
146
|
+
[(1, 2), (1, 3), (2, 4), (2, 5), (3, 6)]
|
|
147
|
+
|
|
148
|
+
>>> grid_map.line_of_sight((1, 2), (1, 2))
|
|
149
|
+
[(1, 2)]
|
|
150
|
+
|
|
151
|
+
>>> grid_map.in_collision((1, 2), (3, 6))
|
|
152
|
+
False
|
|
153
|
+
|
|
154
|
+
>>> grid_map[1, 3] = TYPES.OBSTACLE
|
|
155
|
+
>>> grid_map.update_esdf()
|
|
156
|
+
>>> grid_map.in_collision((1, 2), (3, 6))
|
|
157
|
+
True
|
|
158
|
+
"""
|
|
159
|
+
def __init__(self,
|
|
160
|
+
bounds: Iterable = [[0, 30], [0, 40]],
|
|
161
|
+
resolution: float = 1.0,
|
|
162
|
+
type_map: Union[GridTypeMap, np.ndarray] = None,
|
|
163
|
+
inflation_radius: float = 0.0,
|
|
164
|
+
) -> None:
|
|
165
|
+
super().__init__(bounds)
|
|
166
|
+
|
|
167
|
+
self._resolution = resolution
|
|
168
|
+
shape = tuple([int((self.bounds[i, 1] - self.bounds[i, 0]) / self.resolution) for i in range(self.dim)])
|
|
169
|
+
|
|
170
|
+
if type_map is None:
|
|
171
|
+
self.type_map = GridTypeMap(np.zeros(shape, dtype=np.int8))
|
|
172
|
+
else:
|
|
173
|
+
if type_map.shape != shape:
|
|
174
|
+
raise ValueError("Shape must be {} instead of {} with given bounds={} and resolution={}".format(shape, type_map.shape, self.bounds, self.resolution))
|
|
175
|
+
|
|
176
|
+
if isinstance(type_map, GridTypeMap):
|
|
177
|
+
self.type_map = type_map
|
|
178
|
+
elif isinstance(type_map, np.ndarray):
|
|
179
|
+
self.type_map = GridTypeMap(type_map)
|
|
180
|
+
else:
|
|
181
|
+
raise ValueError("Type map must be GridTypeMap or numpy.ndarray instead of {}".format(type(type_map)))
|
|
182
|
+
|
|
183
|
+
self._precompute_offsets()
|
|
184
|
+
|
|
185
|
+
self._esdf = np.zeros(self.shape, dtype=np.float32)
|
|
186
|
+
# self.update_esdf() # updated in self.inflate_obstacles()
|
|
187
|
+
|
|
188
|
+
self.inflation_radius = inflation_radius
|
|
189
|
+
if self.inflation_radius >= 1:
|
|
190
|
+
self.inflate_obstacles(self.inflation_radius)
|
|
191
|
+
|
|
192
|
+
def __str__(self) -> str:
|
|
193
|
+
return "Grid(bounds={}, resolution={})".format(self.bounds, self.resolution)
|
|
194
|
+
|
|
195
|
+
def __repr__(self) -> str:
|
|
196
|
+
return self.__str__()
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def resolution(self) -> float:
|
|
200
|
+
return self._resolution
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def shape(self) -> tuple:
|
|
204
|
+
return self.type_map.shape
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def dtype(self) -> np.dtype:
|
|
208
|
+
return self.type_map.dtype
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def esdf(self) -> np.ndarray:
|
|
212
|
+
return self._esdf
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def data(self) -> np.ndarray:
|
|
216
|
+
return self.type_map.data
|
|
217
|
+
|
|
218
|
+
def __getitem__(self, idx):
|
|
219
|
+
return self.type_map[idx]
|
|
220
|
+
|
|
221
|
+
def __setitem__(self, idx, value):
|
|
222
|
+
self.type_map[idx] = value
|
|
223
|
+
|
|
224
|
+
def map_to_world(self, point: tuple) -> Tuple[float, ...]:
|
|
225
|
+
"""
|
|
226
|
+
Convert map coordinates to world coordinates.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
point: Point in map coordinates.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
point: Point in world coordinates.
|
|
233
|
+
"""
|
|
234
|
+
if len(point) != self.dim:
|
|
235
|
+
raise ValueError("Point dimension does not match map dimension.")
|
|
236
|
+
|
|
237
|
+
return tuple((x + 0.5) * self.resolution + float(self.bounds[i, 0]) for i, x in enumerate(point))
|
|
238
|
+
|
|
239
|
+
def world_to_map(self, point: Tuple[float, ...], discrete: bool = True) -> tuple:
|
|
240
|
+
"""
|
|
241
|
+
Convert world coordinates to map coordinates.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
point: Point in world coordinates.
|
|
245
|
+
discrete: Whether to round the coordinates to the nearest integer.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
point: Point in map coordinates.
|
|
249
|
+
"""
|
|
250
|
+
if len(point) != self.dim:
|
|
251
|
+
raise ValueError("Point dimension does not match map dimension.")
|
|
252
|
+
|
|
253
|
+
point_map = tuple((x - float(self.bounds[i, 0])) * (1.0 / self.resolution) - 0.5 for i, x in enumerate(point))
|
|
254
|
+
if discrete:
|
|
255
|
+
point_map = self.point_float_to_int(point_map)
|
|
256
|
+
return point_map
|
|
257
|
+
|
|
258
|
+
def get_distance(self, p1: Tuple[int, int], p2: Tuple[int, int]) -> float:
|
|
259
|
+
"""
|
|
260
|
+
Get the distance between two points.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
p1: Start point.
|
|
264
|
+
p2: Goal point.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
dist: Distance between two points.
|
|
268
|
+
"""
|
|
269
|
+
return Geometry.dist(p1, p2, type='Euclidean')
|
|
270
|
+
|
|
271
|
+
def within_bounds(self, point: Tuple[int, ...]) -> bool:
|
|
272
|
+
"""
|
|
273
|
+
Check if a point is within the bounds of the grid map.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
point: Point to check.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
bool: True if the point is within the bounds of the map, False otherwise.
|
|
280
|
+
"""
|
|
281
|
+
# if point.dim != self.dim:
|
|
282
|
+
# raise ValueError("Point dimension does not match map dimension.")
|
|
283
|
+
|
|
284
|
+
# return all(0 <= point[i] < self.shape[i] for i in range(self.dim))
|
|
285
|
+
dim = self.dim
|
|
286
|
+
shape = self.shape
|
|
287
|
+
|
|
288
|
+
for i in range(dim):
|
|
289
|
+
if not (0 <= point[i] < shape[i]):
|
|
290
|
+
return False
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
def is_expandable(self, point: Tuple[int, ...], src_point: Tuple[int, ...] = None) -> bool:
|
|
294
|
+
"""
|
|
295
|
+
Check if a point is expandable.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
point: Point to check.
|
|
299
|
+
src_point: Source point.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
expandable: True if the point is expandable, False otherwise.
|
|
303
|
+
"""
|
|
304
|
+
if not self.within_bounds(point):
|
|
305
|
+
return False
|
|
306
|
+
if src_point is not None:
|
|
307
|
+
if self.type_map[src_point] == TYPES.INFLATION and self._esdf[point] >= self._esdf[src_point]:
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
return not self.type_map[point] == TYPES.OBSTACLE and not self.type_map[point] == TYPES.INFLATION
|
|
311
|
+
|
|
312
|
+
def get_neighbors(self,
|
|
313
|
+
node: Node,
|
|
314
|
+
diagonal: bool = True
|
|
315
|
+
) -> list:
|
|
316
|
+
"""
|
|
317
|
+
Get neighbor nodes of a given node.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
node: Node to get neighbor nodes.
|
|
321
|
+
diagonal: Whether to include diagonal neighbors.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
nodes: List of neighbor nodes.
|
|
325
|
+
"""
|
|
326
|
+
if node.dim != self.dim:
|
|
327
|
+
raise ValueError("Node dimension does not match map dimension.")
|
|
328
|
+
|
|
329
|
+
offsets = self._diagonal_offsets if diagonal else self._orthogonal_offsets
|
|
330
|
+
|
|
331
|
+
# Generate all neighbor positions
|
|
332
|
+
# neighbor_positions = current_pos + offsets
|
|
333
|
+
neighbors = [node + offset for offset in offsets]
|
|
334
|
+
filtered_neighbors = []
|
|
335
|
+
|
|
336
|
+
for neighbor in neighbors:
|
|
337
|
+
if self.is_expandable(neighbor.current, node.current):
|
|
338
|
+
filtered_neighbors.append(neighbor)
|
|
339
|
+
|
|
340
|
+
return filtered_neighbors
|
|
341
|
+
|
|
342
|
+
def line_of_sight(self, p1: Tuple[int, ...], p2: Tuple[int, ...]) -> List[Tuple[int, ...]]:
|
|
343
|
+
"""
|
|
344
|
+
N-dimensional line of sight (Bresenham's line algorithm)
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
p1: Start point of the line.
|
|
348
|
+
p2: End point of the line.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
points: List of point on the line of sight.
|
|
352
|
+
"""
|
|
353
|
+
p1 = np.array(p1)
|
|
354
|
+
p2 = np.array(p2)
|
|
355
|
+
|
|
356
|
+
dim = len(p1)
|
|
357
|
+
delta = p2 - p1
|
|
358
|
+
abs_delta = np.abs(delta)
|
|
359
|
+
|
|
360
|
+
# Determine the main direction axis (the dimension with the greatest change)
|
|
361
|
+
primary_axis = np.argmax(abs_delta)
|
|
362
|
+
primary_step = 1 if delta[primary_axis] > 0 else -1
|
|
363
|
+
|
|
364
|
+
# Initialize the error variable
|
|
365
|
+
error = np.zeros(dim, dtype=int)
|
|
366
|
+
delta2 = 2 * abs_delta
|
|
367
|
+
|
|
368
|
+
# Calculate the number of steps and initialize the current point
|
|
369
|
+
steps = abs_delta[primary_axis]
|
|
370
|
+
current = p1
|
|
371
|
+
|
|
372
|
+
# Allocate the result array
|
|
373
|
+
result = []
|
|
374
|
+
result.append(tuple(int(x) for x in current))
|
|
375
|
+
|
|
376
|
+
for i in range(1, steps + 1):
|
|
377
|
+
current[primary_axis] += primary_step
|
|
378
|
+
|
|
379
|
+
# Update the error for the primary dimension
|
|
380
|
+
for d in range(dim):
|
|
381
|
+
if d == primary_axis:
|
|
382
|
+
continue
|
|
383
|
+
|
|
384
|
+
error[d] += delta2[d]
|
|
385
|
+
if error[d] > abs_delta[primary_axis]:
|
|
386
|
+
current[d] += 1 if delta[d] > 0 else -1
|
|
387
|
+
error[d] -= delta2[primary_axis]
|
|
388
|
+
|
|
389
|
+
result.append(tuple(int(x) for x in current))
|
|
390
|
+
|
|
391
|
+
return result
|
|
392
|
+
|
|
393
|
+
def in_collision(self, p1: Tuple[int, ...], p2: Tuple[int, ...]) -> bool:
|
|
394
|
+
"""
|
|
395
|
+
Check if the line of sight between two points is in collision.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
p1: Start point of the line.
|
|
399
|
+
p2: End point of the line.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
in_collision: True if the line of sight is in collision, False otherwise.
|
|
403
|
+
"""
|
|
404
|
+
if not self.is_expandable(p1) or not self.is_expandable(p2, p1):
|
|
405
|
+
return True
|
|
406
|
+
|
|
407
|
+
# Corner Case: Start and end points are the same
|
|
408
|
+
if p1 == p2:
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
p1 = np.array(p1)
|
|
412
|
+
p2 = np.array(p2)
|
|
413
|
+
|
|
414
|
+
# Calculate delta and absolute delta
|
|
415
|
+
delta = p2 - p1
|
|
416
|
+
abs_delta = np.abs(delta)
|
|
417
|
+
|
|
418
|
+
# Determine the primary axis (the dimension with the greatest change)
|
|
419
|
+
primary_axis = np.argmax(abs_delta)
|
|
420
|
+
primary_step = 1 if delta[primary_axis] > 0 else -1
|
|
421
|
+
|
|
422
|
+
# Initialize the error variable
|
|
423
|
+
error = np.zeros_like(delta, dtype=np.int32)
|
|
424
|
+
delta2 = 2 * abs_delta
|
|
425
|
+
|
|
426
|
+
# calculate the number of steps and initialize the current point
|
|
427
|
+
steps = abs_delta[primary_axis]
|
|
428
|
+
current = p1
|
|
429
|
+
|
|
430
|
+
for _ in range(steps):
|
|
431
|
+
last_point = current.copy()
|
|
432
|
+
current[primary_axis] += primary_step
|
|
433
|
+
|
|
434
|
+
# Update the error for the primary dimension
|
|
435
|
+
for d in range(len(delta)):
|
|
436
|
+
if d == primary_axis:
|
|
437
|
+
continue
|
|
438
|
+
|
|
439
|
+
error[d] += delta2[d]
|
|
440
|
+
if error[d] > abs_delta[primary_axis]:
|
|
441
|
+
current[d] += 1 if delta[d] > 0 else -1
|
|
442
|
+
error[d] -= delta2[primary_axis]
|
|
443
|
+
|
|
444
|
+
# Check the current point
|
|
445
|
+
if not self.is_expandable(tuple(current), tuple(last_point)):
|
|
446
|
+
return True
|
|
447
|
+
|
|
448
|
+
return False
|
|
449
|
+
|
|
450
|
+
def fill_boundary_with_obstacles(self) -> None:
|
|
451
|
+
"""
|
|
452
|
+
Fill the boundary of the map with obstacles.
|
|
453
|
+
"""
|
|
454
|
+
for d in range(self.dim):
|
|
455
|
+
# Create a tuple of slice objects to select boundary elements in current dimension
|
|
456
|
+
# First boundary (start index)
|
|
457
|
+
slices_start = [slice(None)] * self.dim
|
|
458
|
+
slices_start[d] = 0
|
|
459
|
+
self.type_map[tuple(slices_start)] = TYPES.OBSTACLE
|
|
460
|
+
|
|
461
|
+
# Last boundary (end index)
|
|
462
|
+
slices_end = [slice(None)] * self.dim
|
|
463
|
+
slices_end[d] = -1
|
|
464
|
+
self.type_map[tuple(slices_end)] = TYPES.OBSTACLE
|
|
465
|
+
|
|
466
|
+
def inflate_obstacles(self, radius: float = 1.0) -> None:
|
|
467
|
+
"""
|
|
468
|
+
Inflate the obstacles in the map.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
radius: Radius of the inflation.
|
|
472
|
+
"""
|
|
473
|
+
self.update_esdf()
|
|
474
|
+
mask = (self.esdf <= radius) & (self.type_map.data == TYPES.FREE)
|
|
475
|
+
self.type_map[mask] = TYPES.INFLATION
|
|
476
|
+
self.inflation_radius = radius
|
|
477
|
+
|
|
478
|
+
def fill_expands(self, expands: Dict[Tuple[int, ...], Node]) -> None:
|
|
479
|
+
"""
|
|
480
|
+
Fill the expands in the map.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
expands: List of expands.
|
|
484
|
+
"""
|
|
485
|
+
for expand in expands.keys():
|
|
486
|
+
if self.type_map[expand] != TYPES.FREE:
|
|
487
|
+
continue
|
|
488
|
+
self.type_map[expand] = TYPES.EXPAND
|
|
489
|
+
|
|
490
|
+
def update_esdf(self) -> None:
|
|
491
|
+
"""
|
|
492
|
+
Update the ESDF (signed Euclidean Distance Field) based on the obstacles in the map.
|
|
493
|
+
- Obstacle grid ESDF = 0
|
|
494
|
+
- Free grid ESDF > 0. The value is the di/stance to the nearest obstacle
|
|
495
|
+
"""
|
|
496
|
+
obstacle_mask = (self.type_map.data == TYPES.OBSTACLE)
|
|
497
|
+
free_mask = ~obstacle_mask
|
|
498
|
+
|
|
499
|
+
# distance to obstacles
|
|
500
|
+
dist_outside = ndimage.distance_transform_edt(free_mask, sampling=self.resolution)
|
|
501
|
+
# distance to free space (internal distance of obstacles)
|
|
502
|
+
dist_inside = ndimage.distance_transform_edt(obstacle_mask, sampling=self.resolution)
|
|
503
|
+
|
|
504
|
+
self._esdf = dist_outside.astype(np.float32)
|
|
505
|
+
self._esdf[obstacle_mask] = -dist_inside[obstacle_mask]
|
|
506
|
+
|
|
507
|
+
def path_map_to_world(self, path: List[tuple]) -> List[Tuple[float, ...]]:
|
|
508
|
+
"""
|
|
509
|
+
Convert path from map coordinates to world coordinates
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
path: a list of map coordinates
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
path: a list of world coordinates
|
|
516
|
+
"""
|
|
517
|
+
return [self.map_to_world(p) for p in path]
|
|
518
|
+
|
|
519
|
+
def path_world_to_map(self, path: List[Tuple[float, ...]], discrete: bool = True) -> List[tuple]:
|
|
520
|
+
"""
|
|
521
|
+
Convert path from world coordinates to map coordinates
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
path: a list of world coordinates
|
|
525
|
+
discrete: whether to round the coordinates to the nearest integer
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
path: a list of map coordinates
|
|
529
|
+
"""
|
|
530
|
+
return [self.world_to_map(p, discrete) for p in path]
|
|
531
|
+
|
|
532
|
+
def point_float_to_int(self, point: Tuple[float, ...]) -> Tuple[int, ...]:
|
|
533
|
+
"""
|
|
534
|
+
Convert a point from float to integer coordinates.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
point: a point in float coordinates
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
point: a point in integer coordinates
|
|
541
|
+
"""
|
|
542
|
+
point_int = []
|
|
543
|
+
for d in range(self.dim):
|
|
544
|
+
point_int.append(max(0, min(self.shape[d] - 1, int(round(point[d])))))
|
|
545
|
+
point_int = tuple(point_int)
|
|
546
|
+
return point_int
|
|
547
|
+
|
|
548
|
+
def _precompute_offsets(self):
|
|
549
|
+
# Generate all possible offsets (-1, 0, +1) in each dimension
|
|
550
|
+
self._diagonal_offsets = np.array(np.meshgrid(*[[-1, 0, 1]]*self.dim), dtype=self.dtype).T.reshape(-1, self.dim)
|
|
551
|
+
# Remove the zero offset (current node itself)
|
|
552
|
+
self._diagonal_offsets = self._diagonal_offsets[np.any(self._diagonal_offsets != 0, axis=1)]
|
|
553
|
+
# self._diagonal_offsets = [Node((offset.tolist(), dtype=self.dtype)) for offset in self._diagonal_offsets]
|
|
554
|
+
self._diagonal_offsets = [Node(tuple(offset.tolist())) for offset in self._diagonal_offsets]
|
|
555
|
+
|
|
556
|
+
# Generate only orthogonal offsets (one dimension changes by ±1)
|
|
557
|
+
self._orthogonal_offsets = np.zeros((2*self.dim, self.dim), dtype=self.dtype)
|
|
558
|
+
for d in range(self.dim):
|
|
559
|
+
self._orthogonal_offsets[2*d, d] = 1
|
|
560
|
+
self._orthogonal_offsets[2*d+1, d] = -1
|
|
561
|
+
# self._orthogonal_offsets = [Node((offset.tolist(), dtype=self.dtype)) for offset in self._orthogonal_offsets]
|
|
562
|
+
self._orthogonal_offsets = [Node(tuple(offset.tolist())) for offset in self._orthogonal_offsets]
|