1414import heapq
1515import inspect
1616
17+ from enum import Enum
18+
1719
1820class 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.
2225 """
2326
2427 # Some miscellaneous notes:
@@ -70,8 +73,9 @@ def reverse_path(node):
7073 """
7174 result = []
7275 while node is not None :
73- result .insert ( 0 , node ['vertex' ])
76+ result .append ( node ['vertex' ])
7477 node = node ['parent' ]
78+ result .reverse ()
7579 return result
7680
7781 def find_path (self , graph , start , end , heuristic_fn ):
@@ -191,14 +195,11 @@ def find_path(self, graph, start, end, heuristic_fn):
191195 if _open [i ][2 ] == neighbor :
192196 found = i
193197 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 )
197199 # TODO: I'm not certain about the performance characteristics of doing this with heapq, nor if
198200 # TODO: it would be better to delete heapify and push or rather than replace
199201
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 )
202203 counter += 1
203204 heapq .heapify (_open )
204205 _open_lookup [neighbor ] = {'vertex' : neighbor ,
@@ -226,3 +227,272 @@ def get_code():
226227 returns the code for the current class
227228 """
228229 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