Skip to content

Commit 1308e76

Browse files
authored
Add expanded node set to SpaceTime AStar (#1183)
* speed up spacetime astar * forgot to include hash impl on Position * add condition to test on node expansions * remove heuristic from Node __hash__ impl * update rst with note about optimization
1 parent 73ebcd8 commit 1308e76

File tree

4 files changed

+57
-15
lines changed

4 files changed

+57
-15
lines changed

PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ def __sub__(self, other):
3030
f"Subtraction not supported for Position and {type(other)}"
3131
)
3232

33+
def __hash__(self):
34+
return hash((self.x, self.y))
35+
3336

3437
class ObstacleArrangement(Enum):
3538
# Random obstacle positions and movements

PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py

+36-15
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import random
2121
from dataclasses import dataclass
2222
from functools import total_ordering
23-
23+
import time
2424

2525
# Seed randomness for reproducibility
2626
RANDOM_SEED = 50
@@ -48,11 +48,17 @@ def __lt__(self, other: object):
4848
return NotImplementedError(f"Cannot compare Node with object of type: {type(other)}")
4949
return (self.time + self.heuristic) < (other.time + other.heuristic)
5050

51+
"""
52+
Note: cost and heuristic are not included in eq or hash, since they will always be the same
53+
for a given (position, time) pair. Including either cost or heuristic would be redundant.
54+
"""
5155
def __eq__(self, other: object):
5256
if not isinstance(other, Node):
5357
return NotImplementedError(f"Cannot compare Node with object of type: {type(other)}")
5458
return self.position == other.position and self.time == other.time
5559

60+
def __hash__(self):
61+
return hash((self.position, self.time))
5662

5763
class NodePath:
5864
path: list[Node]
@@ -86,6 +92,8 @@ class SpaceTimeAStar:
8692
grid: Grid
8793
start: Position
8894
goal: Position
95+
# Used to evaluate solutions
96+
expanded_node_count: int = -1
8997

9098
def __init__(self, grid: Grid, start: Position, goal: Position):
9199
self.grid = grid
@@ -98,7 +106,8 @@ def plan(self, verbose: bool = False) -> NodePath:
98106
open_set, Node(self.start, 0, self.calculate_heuristic(self.start), -1)
99107
)
100108

101-
expanded_set: list[Node] = []
109+
expanded_list: list[Node] = []
110+
expanded_set: set[Node] = set()
102111
while open_set:
103112
expanded_node: Node = heapq.heappop(open_set)
104113
if verbose:
@@ -110,23 +119,25 @@ def plan(self, verbose: bool = False) -> NodePath:
110119
continue
111120

112121
if expanded_node.position == self.goal:
113-
print(f"Found path to goal after {len(expanded_set)} expansions")
122+
print(f"Found path to goal after {len(expanded_list)} expansions")
114123
path = []
115124
path_walker: Node = expanded_node
116125
while True:
117126
path.append(path_walker)
118127
if path_walker.parent_index == -1:
119128
break
120-
path_walker = expanded_set[path_walker.parent_index]
129+
path_walker = expanded_list[path_walker.parent_index]
121130

122131
# reverse path so it goes start -> goal
123132
path.reverse()
133+
self.expanded_node_count = len(expanded_set)
124134
return NodePath(path)
125135

126-
expanded_idx = len(expanded_set)
127-
expanded_set.append(expanded_node)
136+
expanded_idx = len(expanded_list)
137+
expanded_list.append(expanded_node)
138+
expanded_set.add(expanded_node)
128139

129-
for child in self.generate_successors(expanded_node, expanded_idx, verbose):
140+
for child in self.generate_successors(expanded_node, expanded_idx, verbose, expanded_set):
130141
heapq.heappush(open_set, child)
131142

132143
raise Exception("No path found")
@@ -135,7 +146,7 @@ def plan(self, verbose: bool = False) -> NodePath:
135146
Generate possible successors of the provided `parent_node`
136147
"""
137148
def generate_successors(
138-
self, parent_node: Node, parent_node_idx: int, verbose: bool
149+
self, parent_node: Node, parent_node_idx: int, verbose: bool, expanded_set: set[Node]
139150
) -> Generator[Node, None, None]:
140151
diffs = [
141152
Position(0, 0),
@@ -146,13 +157,17 @@ def generate_successors(
146157
]
147158
for diff in diffs:
148159
new_pos = parent_node.position + diff
160+
new_node = Node(
161+
new_pos,
162+
parent_node.time + 1,
163+
self.calculate_heuristic(new_pos),
164+
parent_node_idx,
165+
)
166+
167+
if new_node in expanded_set:
168+
continue
169+
149170
if self.grid.valid_position(new_pos, parent_node.time + 1):
150-
new_node = Node(
151-
new_pos,
152-
parent_node.time + 1,
153-
self.calculate_heuristic(new_pos),
154-
parent_node_idx,
155-
)
156171
if verbose:
157172
print("\tNew successor node: ", new_node)
158173
yield new_node
@@ -166,9 +181,12 @@ def calculate_heuristic(self, position) -> int:
166181
verbose = False
167182

168183
def main():
169-
start = Position(1, 11)
184+
start = Position(1, 5)
170185
goal = Position(19, 19)
171186
grid_side_length = 21
187+
188+
start_time = time.time()
189+
172190
grid = Grid(
173191
np.array([grid_side_length, grid_side_length]),
174192
num_obstacles=40,
@@ -179,6 +197,9 @@ def main():
179197
planner = SpaceTimeAStar(grid, start, goal)
180198
path = planner.plan(verbose)
181199

200+
runtime = time.time() - start_time
201+
print(f"Planning took: {runtime:.5f} seconds")
202+
182203
if verbose:
183204
print(f"Path: {path}")
184205

docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst

+17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ Using a time-based cost and heuristic ensures the path found is optimal in terms
1616
The cost is the amount of time it takes to reach a given node, and the heuristic is the minimum amount of time it could take to reach the goal from that node, disregarding all obstacles.
1717
For a simple scenario where the robot can move 1 cell per time step and stop and go as it pleases, the heuristic for time is equivalent to the heuristic for distance.
1818

19+
One optimization that was added in `this PR <https://github.com/AtsushiSakai/PythonRobotics/pull/1183>`__ was to add an expanded set to the algorithm. The algorithm will not expand nodes that are already in that set. This greatly reduces the number of node expansions needed to find a path, since no duplicates are expanded. It also helps to reduce the amount of memory the algorithm uses.
20+
21+
Before::
22+
23+
Found path to goal after 204490 expansions
24+
Planning took: 1.72464 seconds
25+
Memory usage (RSS): 68.19 MB
26+
27+
28+
After::
29+
30+
Found path to goal after 2348 expansions
31+
Planning took: 0.01550 seconds
32+
Memory usage (RSS): 64.85 MB
33+
34+
When starting at (1, 11) in the structured obstacle arrangement (second of the two gifs above).
35+
1936
References:
2037
~~~~~~~~~~~
2138

tests/test_space_time_astar.py

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def test_1():
2828
# path should end at the goal
2929
assert path.path[-1].position == goal
3030

31+
assert planner.expanded_node_count < 1000
3132

3233
if __name__ == "__main__":
3334
conftest.run_this_test(__file__)

0 commit comments

Comments
 (0)