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.
Files changed (123) hide show
  1. python_motion_planning/__init__.py +4 -4
  2. python_motion_planning/common/__init__.py +3 -0
  3. python_motion_planning/common/env/__init__.py +6 -0
  4. python_motion_planning/common/env/map/__init__.py +2 -0
  5. python_motion_planning/common/env/map/base_map.py +119 -0
  6. python_motion_planning/common/env/map/grid.py +562 -0
  7. python_motion_planning/common/env/node.py +111 -0
  8. python_motion_planning/common/env/robot/__init__.py +3 -0
  9. python_motion_planning/common/env/robot/base_robot.py +214 -0
  10. python_motion_planning/common/env/robot/circular_robot.py +47 -0
  11. python_motion_planning/common/env/robot/diff_drive_robot.py +109 -0
  12. python_motion_planning/common/env/types.py +19 -0
  13. python_motion_planning/common/env/world/__init__.py +1 -0
  14. python_motion_planning/common/env/world/base_world.py +120 -0
  15. python_motion_planning/common/env/world/toy_simulator.py +206 -0
  16. python_motion_planning/common/utils/__init__.py +3 -0
  17. python_motion_planning/common/utils/child_tree.py +22 -0
  18. python_motion_planning/common/utils/frame_transformer.py +218 -0
  19. python_motion_planning/common/utils/geometry.py +94 -0
  20. python_motion_planning/common/visualizer/__init__.py +3 -0
  21. python_motion_planning/common/visualizer/base_visualizer.py +165 -0
  22. python_motion_planning/common/visualizer/visualizer_2d.py +406 -0
  23. python_motion_planning/common/visualizer/visualizer_3d.py +242 -0
  24. python_motion_planning/controller/__init__.py +3 -0
  25. python_motion_planning/controller/base_controller.py +191 -0
  26. python_motion_planning/controller/path_tracker/__init__.py +6 -0
  27. python_motion_planning/controller/path_tracker/apf.py +231 -0
  28. python_motion_planning/controller/path_tracker/dwa.py +250 -0
  29. python_motion_planning/controller/path_tracker/path_tracker.py +260 -0
  30. python_motion_planning/controller/path_tracker/pid.py +82 -0
  31. python_motion_planning/controller/path_tracker/pure_pursuit.py +71 -0
  32. python_motion_planning/controller/path_tracker/rpp.py +111 -0
  33. python_motion_planning/controller/random_controller.py +27 -0
  34. python_motion_planning/path_planner/__init__.py +4 -0
  35. python_motion_planning/path_planner/base_path_planner.py +122 -0
  36. python_motion_planning/path_planner/graph_search/__init__.py +6 -0
  37. python_motion_planning/path_planner/graph_search/a_star.py +94 -0
  38. python_motion_planning/path_planner/graph_search/dijkstra.py +97 -0
  39. python_motion_planning/path_planner/graph_search/gbfs.py +100 -0
  40. python_motion_planning/path_planner/graph_search/jps.py +199 -0
  41. python_motion_planning/path_planner/graph_search/lazy_theta_star.py +113 -0
  42. python_motion_planning/path_planner/graph_search/theta_star.py +116 -0
  43. python_motion_planning/path_planner/hybrid_search/__init__.py +1 -0
  44. python_motion_planning/path_planner/hybrid_search/voronoi_planner.py +204 -0
  45. python_motion_planning/path_planner/sample_search/__init__.py +3 -0
  46. python_motion_planning/path_planner/sample_search/rrt.py +243 -0
  47. python_motion_planning/path_planner/sample_search/rrt_connect.py +237 -0
  48. python_motion_planning/path_planner/sample_search/rrt_star.py +279 -0
  49. python_motion_planning/traj_optimizer/__init__.py +2 -0
  50. python_motion_planning/traj_optimizer/base_curve_generator.py +53 -0
  51. python_motion_planning/traj_optimizer/curve_generator/__init__.py +2 -0
  52. python_motion_planning/traj_optimizer/curve_generator/point_based/__init__.py +2 -0
  53. python_motion_planning/traj_optimizer/curve_generator/point_based/bspline.py +256 -0
  54. python_motion_planning/traj_optimizer/curve_generator/point_based/cubic_spline.py +115 -0
  55. python_motion_planning/traj_optimizer/curve_generator/pose_based/__init__.py +4 -0
  56. python_motion_planning/traj_optimizer/curve_generator/pose_based/bezier.py +121 -0
  57. python_motion_planning/traj_optimizer/curve_generator/pose_based/dubins.py +355 -0
  58. python_motion_planning/traj_optimizer/curve_generator/pose_based/polynomial.py +197 -0
  59. python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py +606 -0
  60. {python_motion_planning-1.1.dist-info → python_motion_planning-2.0.dist-info}/METADATA +115 -146
  61. python_motion_planning-2.0.dist-info/RECORD +64 -0
  62. {python_motion_planning-1.1.dist-info → python_motion_planning-2.0.dist-info}/WHEEL +1 -1
  63. python_motion_planning/curve_generation/__init__.py +0 -9
  64. python_motion_planning/curve_generation/bezier_curve.py +0 -131
  65. python_motion_planning/curve_generation/bspline_curve.py +0 -271
  66. python_motion_planning/curve_generation/cubic_spline.py +0 -128
  67. python_motion_planning/curve_generation/curve.py +0 -64
  68. python_motion_planning/curve_generation/dubins_curve.py +0 -348
  69. python_motion_planning/curve_generation/fem_pos_smooth.py +0 -114
  70. python_motion_planning/curve_generation/polynomial_curve.py +0 -226
  71. python_motion_planning/curve_generation/reeds_shepp.py +0 -736
  72. python_motion_planning/global_planner/__init__.py +0 -3
  73. python_motion_planning/global_planner/evolutionary_search/__init__.py +0 -4
  74. python_motion_planning/global_planner/evolutionary_search/aco.py +0 -186
  75. python_motion_planning/global_planner/evolutionary_search/evolutionary_search.py +0 -87
  76. python_motion_planning/global_planner/evolutionary_search/pso.py +0 -356
  77. python_motion_planning/global_planner/graph_search/__init__.py +0 -28
  78. python_motion_planning/global_planner/graph_search/a_star.py +0 -124
  79. python_motion_planning/global_planner/graph_search/d_star.py +0 -291
  80. python_motion_planning/global_planner/graph_search/d_star_lite.py +0 -188
  81. python_motion_planning/global_planner/graph_search/dijkstra.py +0 -77
  82. python_motion_planning/global_planner/graph_search/gbfs.py +0 -78
  83. python_motion_planning/global_planner/graph_search/graph_search.py +0 -87
  84. python_motion_planning/global_planner/graph_search/jps.py +0 -165
  85. python_motion_planning/global_planner/graph_search/lazy_theta_star.py +0 -114
  86. python_motion_planning/global_planner/graph_search/lpa_star.py +0 -230
  87. python_motion_planning/global_planner/graph_search/s_theta_star.py +0 -133
  88. python_motion_planning/global_planner/graph_search/theta_star.py +0 -171
  89. python_motion_planning/global_planner/graph_search/voronoi.py +0 -200
  90. python_motion_planning/global_planner/sample_search/__init__.py +0 -6
  91. python_motion_planning/global_planner/sample_search/informed_rrt.py +0 -152
  92. python_motion_planning/global_planner/sample_search/rrt.py +0 -151
  93. python_motion_planning/global_planner/sample_search/rrt_connect.py +0 -147
  94. python_motion_planning/global_planner/sample_search/rrt_star.py +0 -77
  95. python_motion_planning/global_planner/sample_search/sample_search.py +0 -135
  96. python_motion_planning/local_planner/__init__.py +0 -15
  97. python_motion_planning/local_planner/apf.py +0 -144
  98. python_motion_planning/local_planner/dwa.py +0 -212
  99. python_motion_planning/local_planner/local_planner.py +0 -262
  100. python_motion_planning/local_planner/lqr.py +0 -146
  101. python_motion_planning/local_planner/mpc.py +0 -214
  102. python_motion_planning/local_planner/pid.py +0 -158
  103. python_motion_planning/local_planner/rpp.py +0 -147
  104. python_motion_planning/utils/__init__.py +0 -19
  105. python_motion_planning/utils/agent/__init__.py +0 -0
  106. python_motion_planning/utils/agent/agent.py +0 -135
  107. python_motion_planning/utils/environment/__init__.py +0 -0
  108. python_motion_planning/utils/environment/env.py +0 -134
  109. python_motion_planning/utils/environment/node.py +0 -85
  110. python_motion_planning/utils/environment/point2d.py +0 -96
  111. python_motion_planning/utils/environment/pose2d.py +0 -91
  112. python_motion_planning/utils/helper/__init__.py +0 -3
  113. python_motion_planning/utils/helper/math_helper.py +0 -65
  114. python_motion_planning/utils/planner/__init__.py +0 -0
  115. python_motion_planning/utils/planner/control_factory.py +0 -27
  116. python_motion_planning/utils/planner/curve_factory.py +0 -29
  117. python_motion_planning/utils/planner/planner.py +0 -40
  118. python_motion_planning/utils/planner/search_factory.py +0 -51
  119. python_motion_planning/utils/plot/__init__.py +0 -0
  120. python_motion_planning/utils/plot/plot.py +0 -274
  121. python_motion_planning-1.1.dist-info/RECORD +0 -64
  122. {python_motion_planning-1.1.dist-info → python_motion_planning-2.0.dist-info/licenses}/LICENSE +0 -0
  123. {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 .utils import *
2
- from .global_planner import *
3
- from .local_planner import *
4
- from .curve_generation import *
1
+ from .common import *
2
+ from .path_planner import *
3
+ from .controller import *
4
+ from .traj_optimizer import *
@@ -0,0 +1,3 @@
1
+ from .utils import *
2
+ from .env import *
3
+ from .visualizer import *
@@ -0,0 +1,6 @@
1
+ # from .world import *
2
+ from .node import *
3
+ from .types import *
4
+ from .map import *
5
+ from .world import *
6
+ from .robot import *
@@ -0,0 +1,2 @@
1
+ from .base_map import *
2
+ from .grid 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]