1+ """
2+ Space-time A* Algorithm
3+ This script demonstrates the Space-time A* algorithm for path planning in a grid world with moving obstacles.
4+
5+ Reference: https://www.davidsilver.uk/wp-content/uploads/2020/03/coop-path-AIWisdom.pdf
6+ """
7+
8+ from __future__ import annotations # For typehints of a class within itself
19import numpy as np
210import matplotlib .pyplot as plt
3- import matplotlib .animation as animation
4- from moving_obstacles import Grid , Position
11+ from PathPlanning .TimeBasedPathPlanning .moving_obstacles import Grid , ObstacleArrangement , Position
512import heapq
6- from typing import Generator
13+ from collections . abc import Generator
714import random
8- from __future__ import annotations
915
1016# Seed randomness for reproducibility
1117RANDOM_SEED = 50
@@ -23,7 +29,11 @@ def __init__(self, position: Position, time: int, heuristic: int, parent_index:
2329 self .time = time
2430 self .heuristic = heuristic
2531 self .parent_index = parent_index
26-
32+
33+ """
34+ This is what is used to drive node expansion. The node with the lowest value is expanded next.
35+ This comparison prioritizes the node with the lowest cost-to-come (self.time) + cost-to-go (self.heuristic)
36+ """
2737 def __lt__ (self , other : Node ):
2838 return (self .time + self .heuristic ) < (other .time + other .heuristic )
2939
@@ -32,25 +42,25 @@ def __repr__(self):
3242
3343class NodePath :
3444 path : list [Node ]
45+ positions_at_time : dict [int , Position ] = {}
3546
3647 def __init__ (self , path : list [Node ]):
3748 self .path = path
38-
49+ for node in path :
50+ self .positions_at_time [node .time ] = node .position
51+
52+ """
53+ Get the position of the path at a given time
54+ """
3955 def get_position (self , time : int ) -> Position :
40- # TODO: this is inefficient
41- for i in range (0 , len (self .path ) - 2 ):
42- if self .path [i + 1 ].time > time :
43- print (f"position @ { i } is { self .path [i ].position } " )
44- return self .path [i ].position
45-
46- if len (self .path ) > 0 :
47- return self .path [- 1 ].position
48-
49- return None
50-
56+ return self .positions_at_time .get (time )
57+
58+ """
59+ Time stamp of the last node in the path
60+ """
5161 def goal_reached_time (self ) -> int :
5262 return self .path [- 1 ].time
53-
63+
5464 def __repr__ (self ):
5565 repr_string = ""
5666 for (i , node ) in enumerate (self .path ):
@@ -71,7 +81,6 @@ def plan(self, verbose: bool = False) -> NodePath:
7181 open_set = []
7282 heapq .heappush (open_set , Node (self .start , 0 , self .calculate_heuristic (self .start ), - 1 ))
7383
74- # TODO: is vec good here?
7584 expanded_set = []
7685 while open_set :
7786 expanded_node : Node = heapq .heappop (open_set )
@@ -92,7 +101,7 @@ def plan(self, verbose: bool = False) -> NodePath:
92101 path_walker = expanded_set [path_walker .parent_index ]
93102 # TODO: fix hack around bad while condiiotn
94103 path .append (path_walker )
95-
104+
96105 # reverse path so it goes start -> goal
97106 path .reverse ()
98107 return NodePath (path )
@@ -102,9 +111,12 @@ def plan(self, verbose: bool = False) -> NodePath:
102111
103112 for child in self .generate_successors (expanded_node , expanded_idx , verbose ):
104113 heapq .heappush (open_set , child )
105-
114+
106115 raise Exception ("No path found" )
107-
116+
117+ """
118+ Generate possible successors of the provided `parent_node`
119+ """
108120 def generate_successors (self , parent_node : Node , parent_node_idx : int , verbose : bool ) -> Generator [Node , None , None ]:
109121 diffs = [Position (0 , 1 ), Position (0 , - 1 ), Position (1 , 0 ), Position (- 1 , 0 ), Position (0 , 0 )]
110122 for diff in diffs :
@@ -119,13 +131,12 @@ def calculate_heuristic(self, position) -> int:
119131 diff = self .goal - position
120132 return abs (diff .x ) + abs (diff .y )
121133
122- import imageio .v2 as imageio
123134show_animation = True
124135def main ():
125- start = Position (1 , 1 )
136+ start = Position (1 , 11 )
126137 goal = Position (19 , 19 )
127138 grid_side_length = 21
128- grid = Grid (np .array ([grid_side_length , grid_side_length ]), num_obstacles = 40 , obstacle_avoid_points = [start , goal ])
139+ grid = Grid (np .array ([grid_side_length , grid_side_length ]), num_obstacles = 40 , obstacle_avoid_points = [start , goal ], obstacle_arrangement = ObstacleArrangement . ARRANGEMENT1 )
129140
130141 planner = TimeBasedAStar (grid , start , goal )
131142 verbose = False
@@ -144,7 +155,7 @@ def main():
144155 ax .set_xticks (np .arange (0 , grid_side_length , 1 ))
145156 ax .set_yticks (np .arange (0 , grid_side_length , 1 ))
146157
147- start_and_goal , = ax .plot ([], [], 'mD' , ms = 15 , label = "Start and Goal" )
158+ start_and_goal , = ax .plot ([], [], 'mD' , ms = 15 , label = "Start and Goal" )
148159 start_and_goal .set_data ([start .x , goal .x ], [start .y , goal .y ])
149160 obs_points , = ax .plot ([], [], 'ro' , ms = 15 , label = "Obstacles" )
150161 path_points , = ax .plot ([], [], 'bo' , ms = 10 , label = "Path Found" )
@@ -155,17 +166,13 @@ def main():
155166 lambda event : [exit (
156167 0 ) if event .key == 'escape' else None ])
157168
158- frames = []
159169 for i in range (0 , path .goal_reached_time ()):
160170 obs_positions = grid .get_obstacle_positions_at_time (i )
161171 obs_points .set_data (obs_positions [0 ], obs_positions [1 ])
162172 path_position = path .get_position (i )
163173 path_points .set_data ([path_position .x ], [path_position .y ])
164174 plt .pause (0.2 )
165- plt .savefig (f"frame_{ i :03d} .png" ) # Save each frame as an image
166- frames .append (imageio .imread (f"frame_{ i :03d} .png" ))
167- imageio .mimsave ("path_animation.gif" , frames , fps = 5 ) # Convert images to GIF
168175 plt .show ()
169176
170177if __name__ == '__main__' :
171- main ()
178+ main ()
0 commit comments