11import numpy as np
22import matplotlib .pyplot as plt
33from enum import Enum
4+
5+
46class Position :
57 x : int
68 y : int
@@ -15,12 +17,16 @@ def as_ndarray(self) -> np.ndarray[int, int]:
1517 def __add__ (self , other ):
1618 if isinstance (other , Position ):
1719 return Position (self .x + other .x , self .y + other .y )
18- raise NotImplementedError (f"Addition not supported for Position and { type (other )} " )
20+ raise NotImplementedError (
21+ f"Addition not supported for Position and { type (other )} "
22+ )
1923
2024 def __sub__ (self , other ):
2125 if isinstance (other , Position ):
2226 return Position (self .x - other .x , self .y - other .y )
23- raise NotImplementedError (f"Subtraction not supported for Position and { type (other )} " )
27+ raise NotImplementedError (
28+ f"Subtraction not supported for Position and { type (other )} "
29+ )
2430
2531 def __eq__ (self , other ):
2632 if isinstance (other , Position ):
@@ -30,12 +36,14 @@ def __eq__(self, other):
3036 def __repr__ (self ):
3137 return f"Position({ self .x } , { self .y } )"
3238
39+
3340class ObstacleArrangement (Enum ):
3441 # Random obstacle positions and movements
3542 RANDOM = 0
3643 # Obstacles start in a line in y at center of grid and move side-to-side in x
3744 ARRANGEMENT1 = 1
3845
46+
3947class Grid :
4048 # Set in constructor
4149 grid_size = None
@@ -50,7 +58,14 @@ class Grid:
5058 # Logging control
5159 verbose = False
5260
53- def __init__ (self , grid_size : np .ndarray [int , int ], num_obstacles : int = 40 , obstacle_avoid_points : list [Position ] = [], obstacle_arrangement : ObstacleArrangement = ObstacleArrangement .RANDOM , time_limit : int = 100 ):
61+ def __init__ (
62+ self ,
63+ grid_size : np .ndarray [int , int ],
64+ num_obstacles : int = 40 ,
65+ obstacle_avoid_points : list [Position ] = [],
66+ obstacle_arrangement : ObstacleArrangement = ObstacleArrangement .RANDOM ,
67+ time_limit : int = 100 ,
68+ ):
5469 self .obstacle_avoid_points = obstacle_avoid_points
5570 self .time_limit = time_limit
5671 self .grid_size = grid_size
@@ -64,17 +79,18 @@ def __init__(self, grid_size: np.ndarray[int, int], num_obstacles: int = 40, obs
6479 elif obstacle_arrangement == ObstacleArrangement .ARRANGEMENT1 :
6580 self .obstacle_paths = self .obstacle_arrangement_1 (num_obstacles )
6681
67- for ( i , path ) in enumerate (self .obstacle_paths ):
68- obs_idx = i + 1 # avoid using 0 - that indicates free space in the grid
69- for ( t , position ) in enumerate (path ):
82+ for i , path in enumerate (self .obstacle_paths ):
83+ obs_idx = i + 1 # avoid using 0 - that indicates free space in the grid
84+ for t , position in enumerate (path ):
7085 # Reserve old & new position at this time step
7186 if t > 0 :
72- self .grid [path [t - 1 ].x , path [t - 1 ].y , t ] = obs_idx
87+ self .grid [path [t - 1 ].x , path [t - 1 ].y , t ] = obs_idx
7388 self .grid [position .x , position .y , t ] = obs_idx
7489
7590 """
7691 Generate dynamic obstacles that move around the grid. Initial positions and movements are random
7792 """
93+
7894 def generate_dynamic_obstacles (self , obs_count : int ) -> list [list [Position ]]:
7995 obstacle_paths = []
8096 for _obs_idx in (0 , obs_count ):
@@ -90,10 +106,18 @@ def generate_dynamic_obstacles(self, obs_count: int) -> list[list[Position]]:
90106 # Encourage obstacles to mostly stay in place - too much movement leads to chaotic planning scenarios
91107 # that are not fun to watch
92108 weights = [0.05 , 0.05 , 0.05 , 0.05 , 0.8 ]
93- diffs = [Position (0 , 1 ), Position (0 , - 1 ), Position (1 , 0 ), Position (- 1 , 0 ), Position (0 , 0 )]
94-
95- for t in range (1 , self .time_limit - 1 ):
96- sampled_indices = np .random .choice (len (diffs ), size = 5 , replace = False , p = weights )
109+ diffs = [
110+ Position (0 , 1 ),
111+ Position (0 , - 1 ),
112+ Position (1 , 0 ),
113+ Position (- 1 , 0 ),
114+ Position (0 , 0 ),
115+ ]
116+
117+ for t in range (1 , self .time_limit - 1 ):
118+ sampled_indices = np .random .choice (
119+ len (diffs ), size = 5 , replace = False , p = weights
120+ )
97121 rand_diffs = [diffs [i ] for i in sampled_indices ]
98122
99123 valid_position = None
@@ -122,6 +146,7 @@ def generate_dynamic_obstacles(self, obs_count: int) -> list[list[Position]]:
122146 Bottom half start moving right, top half start moving left. If `obs_count` is less than the length of
123147 the grid, only the first `obs_count` obstacles will be generated.
124148 """
149+
125150 def obstacle_arrangement_1 (self , obs_count : int ) -> list [list [Position ]]:
126151 obstacle_paths = []
127152 half_grid_x = self .grid_size [0 ] // 2
@@ -132,17 +157,21 @@ def obstacle_arrangement_1(self, obs_count: int) -> list[list[Position]]:
132157 position = Position (half_grid_x , y_idx )
133158 path = [position ]
134159
135- for t in range (1 , self .time_limit - 1 ):
160+ for t in range (1 , self .time_limit - 1 ):
136161 # sit in place every other time step
137162 if t % 2 == 0 :
138163 path .append (position )
139164 continue
140165
141166 # first check if we should switch direction (at edge of grid)
142- if (moving_right and position .x == self .grid_size [0 ] - 1 ) or (not moving_right and position .x == 0 ):
167+ if (moving_right and position .x == self .grid_size [0 ] - 1 ) or (
168+ not moving_right and position .x == 0
169+ ):
143170 moving_right = not moving_right
144171 # step in direction
145- position = Position (position .x + (1 if moving_right else - 1 ), position .y )
172+ position = Position (
173+ position .x + (1 if moving_right else - 1 ), position .y
174+ )
146175 path .append (position )
147176
148177 obstacle_paths .append (path )
@@ -159,8 +188,8 @@ def obstacle_arrangement_1(self, obs_count: int) -> list[list[Position]]:
159188 output:
160189 bool: True if position/time combination is valid, False otherwise
161190 """
162- def valid_position (self , position : Position , t : int ) -> bool :
163191
192+ def valid_position (self , position : Position , t : int ) -> bool :
164193 # Check if new position is in grid
165194 if not self .inside_grid_bounds (position ):
166195 return False
@@ -171,62 +200,87 @@ def valid_position(self, position: Position, t: int) -> bool:
171200 """
172201 Returns True if the given position is valid at time t and is not in the set of obstacle_avoid_points
173202 """
203+
174204 def valid_obstacle_position (self , position : Position , t : int ) -> bool :
175- return self .valid_position (position , t ) and position not in self .obstacle_avoid_points
205+ return (
206+ self .valid_position (position , t )
207+ and position not in self .obstacle_avoid_points
208+ )
176209
177210 """
178211 Returns True if the given position is within the grid's boundaries
179212 """
213+
180214 def inside_grid_bounds (self , position : Position ) -> bool :
181- return position .x >= 0 and position .x < self .grid_size [0 ] and position .y >= 0 and position .y < self .grid_size [1 ]
215+ return (
216+ position .x >= 0
217+ and position .x < self .grid_size [0 ]
218+ and position .y >= 0
219+ and position .y < self .grid_size [1 ]
220+ )
182221
183222 """
184223 Sample a random position that is within the grid's boundaries
185224
186225 output:
187226 np.ndarray[int, int]: (x, y) position
188227 """
228+
189229 def sample_random_position (self ) -> Position :
190- return Position (np .random .randint (0 , self .grid_size [0 ]), np .random .randint (0 , self .grid_size [1 ]))
230+ return Position (
231+ np .random .randint (0 , self .grid_size [0 ]),
232+ np .random .randint (0 , self .grid_size [1 ]),
233+ )
191234
192235 """
193236 Returns a tuple of (x_positions, y_positions) of the obstacles at time t
194237 """
195- def get_obstacle_positions_at_time (self , t : int ) -> tuple [list [int ], list [int ]]:
196238
239+ def get_obstacle_positions_at_time (self , t : int ) -> tuple [list [int ], list [int ]]:
197240 x_positions = []
198241 y_positions = []
199242 for obs_path in self .obstacle_paths :
200243 x_positions .append (obs_path [t ].x )
201244 y_positions .append (obs_path [t ].y )
202245 return (x_positions , y_positions )
203246
247+
204248show_animation = True
205249
250+
206251def main ():
207- grid = Grid (np .array ([11 , 11 ]), num_obstacles = 10 , obstacle_arrangement = ObstacleArrangement .ARRANGEMENT1 )
252+ grid = Grid (
253+ np .array ([11 , 11 ]),
254+ num_obstacles = 10 ,
255+ obstacle_arrangement = ObstacleArrangement .ARRANGEMENT1 ,
256+ )
208257
209258 if not show_animation :
210259 return
211260
212261 fig = plt .figure (figsize = (8 , 7 ))
213- ax = fig .add_subplot (autoscale_on = False , xlim = (0 , grid .grid_size [0 ]- 1 ), ylim = (0 , grid .grid_size [1 ]- 1 ))
214- ax .set_aspect ('equal' )
262+ ax = fig .add_subplot (
263+ autoscale_on = False ,
264+ xlim = (0 , grid .grid_size [0 ] - 1 ),
265+ ylim = (0 , grid .grid_size [1 ] - 1 ),
266+ )
267+ ax .set_aspect ("equal" )
215268 ax .grid ()
216269 ax .set_xticks (np .arange (0 , 11 , 1 ))
217270 ax .set_yticks (np .arange (0 , 11 , 1 ))
218- obs_points , = ax .plot ([], [], 'ro' , ms = 15 )
271+ ( obs_points ,) = ax .plot ([], [], "ro" , ms = 15 )
219272
220273 # for stopping simulation with the esc key.
221- plt .gcf ().canvas .mpl_connect ('key_release_event' ,
222- lambda event : [exit (
223- 0 ) if event . key == 'escape' else None ] )
274+ plt .gcf ().canvas .mpl_connect (
275+ "key_release_event" , lambda event : [exit (0 ) if event . key == "escape" else None ]
276+ )
224277
225278 for i in range (0 , grid .time_limit - 1 ):
226279 obs_positions = grid .get_obstacle_positions_at_time (i )
227280 obs_points .set_data (obs_positions [0 ], obs_positions [1 ])
228281 plt .pause (0.2 )
229282 plt .show ()
230283
231- if __name__ == '__main__' :
284+
285+ if __name__ == "__main__" :
232286 main ()
0 commit comments