Skip to content

Commit 1f8a112

Browse files
committed
add some docs & address todos
1 parent 19d0cb7 commit 1f8a112

File tree

4 files changed

+98
-45
lines changed

4 files changed

+98
-45
lines changed

PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py

Lines changed: 3 additions & 0 deletions
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
@dataclass
3437
class Interval:
3538
start_time: int

PathPlanning/TimeBasedPathPlanning/SafeInterval.py

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@
1313
Position,
1414
)
1515
import heapq
16-
from collections.abc import Generator
1716
import random
1817
from dataclasses import dataclass
1918
from functools import total_ordering
20-
19+
import time
2120

2221
# Seed randomness for reproducibility
2322
RANDOM_SEED = 50
@@ -55,6 +54,10 @@ def __eq__(self, other: object):
5554
return NotImplementedError(f"Cannot compare Node with object of type: {type(other)}")
5655
return self.position == other.position and self.time == other.time and self.interval == other.interval
5756

57+
@dataclass
58+
class EntryTimeAndInterval:
59+
entry_time: int
60+
interval: Interval
5861

5962
class NodePath:
6063
path: list[Node]
@@ -130,24 +133,22 @@ def plan(self, verbose: bool = False) -> NodePath:
130133
print(f"Found path to goal after {len(expanded_list)} expansions")
131134
path = []
132135
path_walker: Node = expanded_node
133-
while path_walker.parent_index != -1:
136+
while True:
134137
path.append(path_walker)
138+
if path_walker.parent_index == -1:
139+
break
135140
path_walker = expanded_list[path_walker.parent_index]
136-
# TODO: fix hack around bad while condiiotn
137-
path.append(path_walker)
138141

139142
# reverse path so it goes start -> goal
140143
path.reverse()
141144
return NodePath(path)
142145

143146
expanded_idx = len(expanded_list)
144147
expanded_list.append(expanded_node)
145-
visited_intervals[expanded_node.position.x, expanded_node.position.y].append((expanded_node.time, expanded_node.interval))
148+
entry_time_and_node = EntryTimeAndInterval(expanded_node.time, expanded_node.interval)
149+
add_entry_to_visited_intervals_array(entry_time_and_node, visited_intervals, expanded_node)
146150

147-
# if len(expanded_set) > 100:
148-
# blarg
149-
150-
for child in self.generate_successors(expanded_node, expanded_idx, verbose, safe_intervals, visited_intervals):
151+
for child in self.generate_successors(expanded_node, expanded_idx, safe_intervals, visited_intervals):
151152
heapq.heappush(open_set, child)
152153

153154
raise Exception("No path found")
@@ -157,7 +158,7 @@ def plan(self, verbose: bool = False) -> NodePath:
157158
"""
158159
# TODO: is intervals being passed by ref? (i think so?)
159160
def generate_successors(
160-
self, parent_node: Node, parent_node_idx: int, verbose: bool, intervals: np.ndarray, visited_intervals: np.ndarray
161+
self, parent_node: Node, parent_node_idx: int, intervals: np.ndarray, visited_intervals: np.ndarray
161162
) -> list[Node]:
162163
new_nodes = []
163164
diffs = [
@@ -185,60 +186,69 @@ def generate_successors(
185186
if interval.start_time > current_interval.end_time:
186187
break
187188

188-
# TODO: this bit feels wonky
189189
# if we have already expanded a node in this interval with a <= starting time, continue
190190
better_node_expanded = False
191191
for visited in visited_intervals[new_pos.x, new_pos.y]:
192-
if interval == visited[1] and visited[0] <= parent_node.time + 1:
192+
if interval == visited.interval and visited.entry_time <= parent_node.time + 1:
193193
better_node_expanded = True
194194
break
195195
if better_node_expanded:
196196
continue
197197

198198
# We know there is some overlap. Generate successor at the earliest possible time the
199199
# new interval can be entered
200-
# TODO: dont love the optionl usage here
201-
new_node_t = None
202200
for possible_t in range(max(parent_node.time + 1, interval.start_time), min(current_interval.end_time, interval.end_time)):
203201
if self.grid.valid_position(new_pos, possible_t):
204-
new_node_t = possible_t
202+
new_nodes.append(Node(
203+
new_pos,
204+
# entry is max of interval start and parent node start time (get there as soon as possible)
205+
max(parent_node.time + 1, interval.start_time),
206+
self.calculate_heuristic(new_pos),
207+
parent_node_idx,
208+
interval,
209+
))
210+
# break because all t's after this will make nodes with a higher cost, the same heuristic, and are in the same interval
205211
break
206212

207-
if new_node_t:
208-
# TODO: should be able to break here?
209-
new_nodes.append(Node(
210-
new_pos,
211-
# entry is max of interval start and parent node start time (get there as soon as possible)
212-
max(parent_node.time + 1, interval.start_time),
213-
self.calculate_heuristic(new_pos),
214-
parent_node_idx,
215-
interval,
216-
))
217-
218213
return new_nodes
219214

220215
def calculate_heuristic(self, position) -> int:
221216
diff = self.goal - position
222217
return abs(diff.x) + abs(diff.y)
223218

224219

220+
def add_entry_to_visited_intervals_array(entry_time_and_interval: EntryTimeAndInterval, visited_intervals: np.ndarray, expanded_node: Node):
221+
# if entry is present, update entry time if better
222+
for existing_entry_and_interval in visited_intervals[expanded_node.position.x, expanded_node.position.y]:
223+
if existing_entry_and_interval.interval == entry_time_and_interval.interval:
224+
existing_entry_and_interval.entry_time = min(existing_entry_and_interval.entry_time, entry_time_and_interval.entry_time)
225+
226+
# Otherwise, append
227+
visited_intervals[expanded_node.position.x, expanded_node.position.y].append(entry_time_and_interval)
228+
229+
225230
show_animation = True
226231
verbose = False
227232

228233
def main():
229-
start = Position(1, 1)
234+
start = Position(1, 18)
230235
goal = Position(19, 19)
231236
grid_side_length = 21
237+
238+
start_time = time.time()
239+
232240
grid = Grid(
233241
np.array([grid_side_length, grid_side_length]),
234242
num_obstacles=250,
235243
obstacle_avoid_points=[start, goal],
236-
# obstacle_arrangement=ObstacleArrangement.ARRANGEMENT1,
237-
obstacle_arrangement=ObstacleArrangement.RANDOM,
244+
obstacle_arrangement=ObstacleArrangement.ARRANGEMENT1,
245+
# obstacle_arrangement=ObstacleArrangement.RANDOM,
238246
)
239247

240248
planner = SafeIntervalPathPlanner(grid, start, goal)
241249
path = planner.plan(verbose)
250+
runtime = time.time() - start_time
251+
print(f"Planning took: {runtime:.5f} seconds")
242252

243253
if verbose:
244254
print(f"Path: {path}")

PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py

Lines changed: 29 additions & 15 deletions
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
@@ -53,6 +53,8 @@ def __eq__(self, other: object):
5353
return NotImplementedError(f"Cannot compare Node with object of type: {type(other)}")
5454
return self.position == other.position and self.time == other.time
5555

56+
def __hash__(self):
57+
return hash((self.position, self.time, self.heuristic))
5658

5759
class NodePath:
5860
path: list[Node]
@@ -98,7 +100,8 @@ def plan(self, verbose: bool = False) -> NodePath:
98100
open_set, Node(self.start, 0, self.calculate_heuristic(self.start), -1)
99101
)
100102

101-
expanded_set: list[Node] = []
103+
expanded_list: list[Node] = []
104+
expanded_set: set[Node] = set()
102105
while open_set:
103106
expanded_node: Node = heapq.heappop(open_set)
104107
if verbose:
@@ -110,23 +113,24 @@ def plan(self, verbose: bool = False) -> NodePath:
110113
continue
111114

112115
if expanded_node.position == self.goal:
113-
print(f"Found path to goal after {len(expanded_set)} expansions")
116+
print(f"Found path to goal after {len(expanded_list)} expansions")
114117
path = []
115118
path_walker: Node = expanded_node
116119
while True:
117120
path.append(path_walker)
118121
if path_walker.parent_index == -1:
119122
break
120-
path_walker = expanded_set[path_walker.parent_index]
123+
path_walker = expanded_list[path_walker.parent_index]
121124

122125
# reverse path so it goes start -> goal
123126
path.reverse()
124127
return NodePath(path)
125128

126-
expanded_idx = len(expanded_set)
127-
expanded_set.append(expanded_node)
129+
expanded_idx = len(expanded_list)
130+
expanded_list.append(expanded_node)
131+
expanded_set.add(expanded_node)
128132

129-
for child in self.generate_successors(expanded_node, expanded_idx, verbose):
133+
for child in self.generate_successors(expanded_node, expanded_idx, verbose, expanded_set):
130134
heapq.heappush(open_set, child)
131135

132136
raise Exception("No path found")
@@ -135,7 +139,7 @@ def plan(self, verbose: bool = False) -> NodePath:
135139
Generate possible successors of the provided `parent_node`
136140
"""
137141
def generate_successors(
138-
self, parent_node: Node, parent_node_idx: int, verbose: bool
142+
self, parent_node: Node, parent_node_idx: int, verbose: bool, expanded_set: set[Node]
139143
) -> Generator[Node, None, None]:
140144
diffs = [
141145
Position(0, 0),
@@ -146,13 +150,17 @@ def generate_successors(
146150
]
147151
for diff in diffs:
148152
new_pos = parent_node.position + diff
153+
new_node = Node(
154+
new_pos,
155+
parent_node.time + 1,
156+
self.calculate_heuristic(new_pos),
157+
parent_node_idx,
158+
)
159+
160+
if new_node in expanded_set:
161+
continue
162+
149163
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-
)
156164
if verbose:
157165
print("\tNew successor node: ", new_node)
158166
yield new_node
@@ -166,9 +174,12 @@ def calculate_heuristic(self, position) -> int:
166174
verbose = False
167175

168176
def main():
169-
start = Position(1, 11)
177+
start = Position(1, 5)
170178
goal = Position(19, 19)
171179
grid_side_length = 21
180+
181+
start_time = time.time()
182+
172183
grid = Grid(
173184
np.array([grid_side_length, grid_side_length]),
174185
num_obstacles=40,
@@ -179,6 +190,9 @@ def main():
179190
planner = SpaceTimeAStar(grid, start, goal)
180191
path = planner.plan(verbose)
181192

193+
runtime = time.time() - start_time
194+
print(f"Planning took: {runtime:.5f} seconds")
195+
182196
if verbose:
183197
print(f"Path: {path}")
184198

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,33 @@ 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+
Safe Interval Path Planning
20+
~~~~~~~~~~~~~~~~~~~~~~
21+
22+
The safe interval path planning algorithm is described in this paper: https://www.cs.cmu.edu/~maxim/files/sipp_icra11.pdf
23+
It is faster than space-time A* because it pre-computes the intervals of time that are unoccupied in each cell. This allows it to reduce the number of successor node it generates by avoiding nodes within the same interval.
24+
25+
## Comparison with Space-time A*:
26+
Arrangement1 startings at (1, 18)
27+
28+
SafeInterval planner:
29+
```
30+
Found path to goal after 322 expansions
31+
Planning took: 0.00730 seconds
32+
```
33+
34+
SpaceTimeAStar:
35+
```
36+
Found path to goal after 2717154 expansions
37+
Planning took: 20.51330 seconds
38+
```
39+
40+
.. image:: <gif-1>
41+
42+
.. image:: <gif-2>
43+
1944
References:
2045
~~~~~~~~~~~
2146

2247
- `Cooperative Pathfinding <https://www.davidsilver.uk/wp-content/uploads/2020/03/coop-path-AIWisdom.pdf>`__
48+
- `SIPP: Safe Interval Path Planning for Dynamic Environments <https://www.cs.cmu.edu/~maxim/files/sipp_icra11.pdf>`__

0 commit comments

Comments
 (0)