14
14
import heapq
15
15
import inspect
16
16
17
+ from enum import Enum
18
+
17
19
18
20
class OneDirectionalAStar (object ):
19
- """AStar object
20
- Finds the optimal path between two nodes on
21
- a graph while taking into account weights.
21
+ """OneDirectionalAStar object
22
+ Finds the optimal path between two nodes on a graph while taking
23
+ into account weights. Expands the start node first until it finds
24
+ the end node.
22
25
"""
23
26
24
27
# Some miscellaneous notes:
@@ -70,8 +73,9 @@ def reverse_path(node):
70
73
"""
71
74
result = []
72
75
while node is not None :
73
- result .insert ( 0 , node ['vertex' ])
76
+ result .append ( node ['vertex' ])
74
77
node = node ['parent' ]
78
+ result .reverse ()
75
79
return result
76
80
77
81
def find_path (self , graph , start , end , heuristic_fn ):
@@ -191,14 +195,11 @@ def find_path(self, graph, start, end, heuristic_fn):
191
195
if _open [i ][2 ] == neighbor :
192
196
found = i
193
197
break
194
- if found is None :
195
- raise Exception ('A vertex is in the _open lookup but not in _open. '
196
- 'This is impossible, please submit an issue + include the graph!' )
198
+ assert (found is not None )
197
199
# TODO: I'm not certain about the performance characteristics of doing this with heapq, nor if
198
200
# TODO: it would be better to delete heapify and push or rather than replace
199
201
200
- # TODO: Local variable 'i' could be referenced before assignment
201
- _open [i ] = (pred_total_dist_through_neighbor_to_end , counter , neighbor )
202
+ _open [found ] = (pred_total_dist_through_neighbor_to_end , counter , neighbor )
202
203
counter += 1
203
204
heapq .heapify (_open )
204
205
_open_lookup [neighbor ] = {'vertex' : neighbor ,
@@ -226,3 +227,272 @@ def get_code():
226
227
returns the code for the current class
227
228
"""
228
229
return inspect .getsource (OneDirectionalAStar )
230
+
231
+ class BiDirectionalAStar (object ):
232
+ """BiDirectionalAStar object
233
+ Finds the optimal path between two nodes on a graph while taking
234
+ account weights. Expands from the start node and the end node
235
+ simultaneously
236
+ """
237
+
238
+ class NodeSource (Enum ):
239
+ """NodeSource enum
240
+ Used to distinguish how a node was located
241
+ """
242
+
243
+ BY_START = 1 ,
244
+ BY_END = 2
245
+
246
+ def __init__ (self ):
247
+ pass
248
+
249
+ @staticmethod
250
+ def reverse_path (node_from_start , node_from_end ):
251
+ """
252
+ Reconstructs the path formed by walking from
253
+ node_from_start backward to start and combining
254
+ it with the path formed by walking from
255
+ node_from_end to end. Both the start and end are
256
+ detected where 'parent' is None.
257
+ :param node_from_start: dict containing { 'vertex': any hashable, 'parent': dict or None }
258
+ :param node_from_end: dict containing { 'vertex' any hashable, 'parent': dict or None }
259
+ :return: list of vertices starting at the start and ending at the end
260
+ """
261
+ list_from_start = []
262
+ current = node_from_start
263
+ while current is not None :
264
+ list_from_start .append (current ['vertex' ])
265
+ current = current ['parent' ]
266
+ list_from_start .reverse ()
267
+
268
+ list_from_end = []
269
+ current = node_from_end
270
+ while current is not None :
271
+ list_from_end .append (current ['vertex' ])
272
+ current = current ['parent' ]
273
+
274
+ return list_from_start + list_from_end
275
+
276
+ def find_path (self , graph , start , end , heuristic_fn ):
277
+ """
278
+ Calculates the optimal path from the start to the end. The
279
+ search occurs from both the start and end at the same rate,
280
+ which makes this algorithm have more consistent performance
281
+ if you regularly are trying to find paths where the destination
282
+ is unreachable and in a small room.
283
+
284
+ The heuristic requirements are the same as in unidirectional A*
285
+ (it must be admissable).
286
+
287
+ :param graph: the graph with 'graph' and 'get_edge_weight' (see WeightedUndirectedGraph)
288
+ :param start: the start vertex (must be hashable and same type as the graph)
289
+ :param end: the end vertex (must be hashable and same type as the graph)
290
+ :param heuristic_fn: an admissable heuristic. signature: function(graph, start, end) returns numeric
291
+ :return: a list of vertices starting at start ending at end or None
292
+ """
293
+
294
+ # This algorithm is really just repeating unidirectional A* twice,
295
+ # but unfortunately it's just different enough that it requires
296
+ # even more work to try to make a single function that can be called
297
+ # twice.
298
+
299
+
300
+ # Note: The nodes in by_start will have heuristic distance to the end,
301
+ # whereas the nodes in by_end will have heuristic distance to the start.
302
+ # This means that the total predicted distance for the exact same node
303
+ # might not match depending on which side we found it from. However,
304
+ # it won't make a difference since as soon as we evaluate the same node
305
+ # on both sides we've finished.
306
+ #
307
+ # This also means that we can use the same lookup table for both.
308
+
309
+ open_by_start = []
310
+ open_by_end = []
311
+ open_lookup = {}
312
+
313
+ closed = set ()
314
+
315
+ # used to avoid hashing the dict.
316
+ counter_arr = [0 ]
317
+
318
+ total_heur_distance = heuristic_fn (graph , start , end )
319
+ heapq .heappush (open_by_start , (total_heur_distance , counter_arr [0 ], start ))
320
+ counter_arr [0 ] += 1
321
+ open_lookup [start ] = { 'vertex' : start ,
322
+ 'parent' : None ,
323
+ 'source' : self .NodeSource .BY_START ,
324
+ 'dist_start_to_here' : 0 ,
325
+ 'pred_dist_here_to_end' : total_heur_distance ,
326
+ 'pred_total_dist' : total_heur_distance }
327
+
328
+ heapq .heappush (open_by_end , (total_heur_distance , counter_arr , end ))
329
+ counter_arr [0 ] += 1
330
+ open_lookup [end ] = { 'vertex' : end ,
331
+ 'parent' : None ,
332
+ 'source' : self .NodeSource .BY_END ,
333
+ 'dist_end_to_here' : 0 ,
334
+ 'pred_dist_here_to_start' : total_heur_distance ,
335
+ 'pred_total_dist' : total_heur_distance }
336
+
337
+ # If the start runs out then the start is in a closed room,
338
+ # if the end runs out then the end is in a closed room,
339
+ # either way there is no path from start to end.
340
+ while len (open_by_start ) > 0 and len (open_by_end ) > 0 :
341
+ result = self ._evaluate_from_start (graph , start , end , heuristic_fn , open_by_start , open_by_end , open_lookup , closed , counter_arr )
342
+ if result is not None :
343
+ return result
344
+
345
+ result = self ._evaluate_from_end (graph , start , end , heuristic_fn , open_by_start , open_by_end , open_lookup , closed , counter_arr )
346
+ if result is not None :
347
+ return result
348
+
349
+ return None
350
+
351
+ def _evaluate_from_start (self , graph , start , end , heuristic_fn , open_by_start , open_by_end , open_lookup , closed , counter_arr ):
352
+ """
353
+ Intended for internal use only. Expands one node from the open_by_start list.
354
+
355
+ :param graph: the graph (see WeightedUndirectedGraph)
356
+ :param start: the start node
357
+ :param end: the end node
358
+ :heuristic_fn: the heuristic function (signature function(graph, start, end) returns numeric)
359
+ :open_by_start: the open vertices from the start
360
+ :open_by_end: the open vertices from the end
361
+ :open_lookup: dictionary of vertices -> dicts
362
+ :closed: the already expanded vertices (set)
363
+ :counter_arr: arr of one integer (counter)
364
+ """
365
+ current = heapq .heappop (open_by_start )
366
+ current_vertex = current [2 ]
367
+ current_dict = open_lookup [current_vertex ]
368
+ del open_lookup [current_vertex ]
369
+ closed .update (current_vertex )
370
+
371
+ neighbors = graph .graph [current_vertex ]
372
+ for neighbor in neighbors :
373
+ if neighbor in closed :
374
+ continue
375
+
376
+ neighbor_dict = open_lookup .get (neighbor , None )
377
+ if neighbor_dict is not None and neighbor_dict ['source' ] is self .NodeSource .BY_END :
378
+ return self .reverse_path (current_dict , neighbor_dict )
379
+
380
+ dist_to_neighb_through_curr_from_start = current_dict ['dist_start_to_here' ] \
381
+ + graph .get_edge_weight (current_vertex , neighbor )
382
+
383
+ if neighbor_dict is not None :
384
+ assert (neighbor_dict ['source' ] is self .NodeSource .BY_START )
385
+
386
+ if neighbor_dict ['dist_start_to_here' ] <= dist_to_neighb_through_curr_from_start :
387
+ continue
388
+
389
+ pred_dist_neighbor_to_end = neighbor_dict ['pred_dist_here_to_end' ]
390
+ pred_total_dist_through_neighbor = dist_to_neighb_through_curr_from_start + pred_dist_neighbor_to_end
391
+ open_lookup [neighbor ] = { 'vertex' : neighbor ,
392
+ 'parent' : current_dict ,
393
+ 'source' : self .NodeSource .BY_START ,
394
+ 'dist_start_to_here' : dist_to_neighb_through_curr_from_start ,
395
+ 'pred_dist_here_to_end' : pred_dist_neighbor_to_end ,
396
+ 'pred_total_dist' : pred_total_dist_through_neighbor }
397
+
398
+ # TODO: I'm pretty sure theres a faster way to do this
399
+ found = None
400
+ for i in range (0 , len (open_by_start )):
401
+ if open_by_start [i ][2 ] == neighbor :
402
+ found = i
403
+ break
404
+ assert (found is not None )
405
+
406
+ open_by_start [found ] = (pred_total_dist_through_neighbor , counter_arr [0 ], neighbor )
407
+ counter_arr [0 ] += 1
408
+ heapq .heapify (open_by_start )
409
+ continue
410
+
411
+ pred_dist_neighbor_to_end = heuristic_fn (graph , neighbor , end )
412
+ pred_total_dist_through_neighbor = dist_to_neighb_through_curr_from_start + pred_dist_neighbor_to_end
413
+ open_lookup [neighbor ] = { 'vertex' : neighbor ,
414
+ 'parent' : current_dict ,
415
+ 'source' : self .NodeSource .BY_START ,
416
+ 'dist_start_to_here' : dist_to_neighb_through_curr_from_start ,
417
+ 'pred_dist_here_to_end' : pred_dist_neighbor_to_end ,
418
+ 'pred_total_dist' : pred_total_dist_through_neighbor }
419
+ heapq .heappush (open_by_start , (pred_total_dist_through_neighbor , counter_arr [0 ], neighbor ))
420
+ counter_arr [0 ] += 1
421
+
422
+ def _evaluate_from_end (self , graph , start , end , heuristic_fn , open_by_start , open_by_end , open_lookup , closed , counter_arr ):
423
+ """
424
+ Intended for internal use only. Expands one node from the open_by_end list.
425
+
426
+ :param graph: the graph (see WeightedUndirectedGraph)
427
+ :param start: the start node
428
+ :param end: the end node
429
+ :heuristic_fn: the heuristic function (signature function(graph, start, end) returns numeric)
430
+ :open_by_start: the open vertices from the start
431
+ :open_by_end: the open vertices from the end
432
+ :open_lookup: dictionary of vertices -> dicts
433
+ :closed: the already expanded vertices (set)
434
+ :counter_arr: arr of one integer (counter)
435
+ """
436
+ current = heapq .heappop (open_by_end )
437
+ current_vertex = current [2 ]
438
+ current_dict = open_lookup [current_vertex ]
439
+ del open_lookup [current_vertex ]
440
+ closed .update (current_vertex )
441
+
442
+ neighbors = graph .graph [current_vertex ]
443
+ for neighbor in neighbors :
444
+ if neighbor in closed :
445
+ continue
446
+
447
+ neighbor_dict = open_lookup .get (neighbor , None )
448
+ if neighbor_dict is not None and neighbor_dict ['source' ] is self .NodeSource .BY_START :
449
+ return self .reverse_path (neighbor_dict , current_dict )
450
+
451
+ dist_to_neighb_through_curr_from_end = current_dict ['dist_end_to_here' ] \
452
+ + graph .get_edge_weight (current_vertex , neighbor )
453
+
454
+ if neighbor_dict is not None :
455
+ assert (neighbor_dict ['source' ] is self .NodeSource .BY_END )
456
+
457
+ if neighbor_dict ['dist_end_to_here' ] <= dist_to_neighb_through_curr_from_end :
458
+ continue
459
+
460
+ pred_dist_neighbor_to_start = neighbor_dict ['pred_dist_here_to_start' ]
461
+ pred_total_dist_through_neighbor = dist_to_neighb_through_curr_from_end + pred_dist_neighbor_to_start
462
+ open_lookup [neighbor ] = { 'vertex' : neighbor ,
463
+ 'parent' : current_dict ,
464
+ 'source' : self .NodeSource .BY_END ,
465
+ 'dist_end_to_here' : dist_to_neighb_through_curr_from_end ,
466
+ 'pred_dist_here_to_start' : pred_dist_neighbor_to_start ,
467
+ 'pred_total_dist' : pred_total_dist_through_neighbor }
468
+
469
+ # TODO: I'm pretty sure theres a faster way to do this
470
+ found = None
471
+ for i in range (0 , len (open_by_end )):
472
+ if open_by_end [i ][2 ] == neighbor :
473
+ found = i
474
+ break
475
+ assert (found is not None )
476
+
477
+ open_by_end [found ] = (pred_total_dist_through_neighbor , counter_arr [0 ], neighbor )
478
+ counter_arr [0 ] += 1
479
+ heapq .heapify (open_by_end )
480
+ continue
481
+
482
+ pred_dist_neighbor_to_start = heuristic_fn (graph , neighbor , start )
483
+ pred_total_dist_through_neighbor = dist_to_neighb_through_curr_from_end + pred_dist_neighbor_to_start
484
+ open_lookup [neighbor ] = { 'vertex' : neighbor ,
485
+ 'parent' : current_dict ,
486
+ 'source' : self .NodeSource .BY_END ,
487
+ 'dist_end_to_here' : dist_to_neighb_through_curr_from_end ,
488
+ 'pred_dist_here_to_start' : pred_dist_neighbor_to_start ,
489
+ 'pred_total_dist' : pred_total_dist_through_neighbor }
490
+ heapq .heappush (open_by_end , (pred_total_dist_through_neighbor , counter_arr [0 ], neighbor ))
491
+ counter_arr [0 ] += 1
492
+
493
+ @staticmethod
494
+ def get_code ():
495
+ """
496
+ returns the code for the current class
497
+ """
498
+ return inspect .getsource (BiDirectionalAStar )
0 commit comments