finder.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. # -*- coding: utf-8 -*-
  2. import heapq # used for the so colled "open list" that stores known nodes
  3. import time # for time limitation
  4. from pathfinding.core.util import SQRT2
  5. from pathfinding.core.diagonal_movement import DiagonalMovement
  6. # max. amount of tries we iterate until we abort the search
  7. MAX_RUNS = float('inf')
  8. # max. time after we until we abort the search (in seconds)
  9. TIME_LIMIT = float('inf')
  10. # used for backtrace of bi-directional A*
  11. BY_START = 1
  12. BY_END = 2
  13. class ExecutionTimeException(Exception):
  14. def __init__(self, message):
  15. super(ExecutionTimeException, self).__init__(message)
  16. class ExecutionRunsException(Exception):
  17. def __init__(self, message):
  18. super(ExecutionRunsException, self).__init__(message)
  19. class Finder(object):
  20. def __init__(self, heuristic=None, weight=1,
  21. diagonal_movement=DiagonalMovement.never,
  22. weighted=True,
  23. time_limit=TIME_LIMIT,
  24. max_runs=MAX_RUNS):
  25. """
  26. find shortest path
  27. :param heuristic: heuristic used to calculate distance of 2 points
  28. (defaults to manhatten)
  29. :param weight: weight for the edges
  30. :param diagonal_movement: if diagonal movement is allowed
  31. (see enum in diagonal_movement)
  32. :param weighted: the algorithm supports weighted nodes
  33. (should be True for A* and Dijkstra)
  34. :param time_limit: max. runtime in seconds
  35. :param max_runs: max. amount of tries until we abort the search
  36. (optional, only if we enter huge grids and have time constrains)
  37. <=0 means there are no constrains and the code might run on any
  38. large map.
  39. """
  40. self.time_limit = time_limit
  41. self.max_runs = max_runs
  42. self.weighted = weighted
  43. self.diagonal_movement = diagonal_movement
  44. self.weight = weight
  45. self.heuristic = heuristic
  46. def calc_cost(self, node_a, node_b):
  47. """
  48. get the distance between current node and the neighbor (cost)
  49. """
  50. if node_b.x - node_a.x == 0 or node_b.y - node_a.y == 0:
  51. # direct neighbor - distance is 1
  52. ng = 1
  53. else:
  54. # not a direct neighbor - diagonal movement
  55. ng = SQRT2
  56. # weight for weighted algorithms
  57. if self.weighted:
  58. ng *= node_b.weight
  59. return node_a.g + ng
  60. def apply_heuristic(self, node_a, node_b, heuristic=None):
  61. """
  62. helper function to apply heuristic
  63. """
  64. if not heuristic:
  65. heuristic = self.heuristic
  66. return heuristic(
  67. abs(node_a.x - node_b.x),
  68. abs(node_a.y - node_b.y))
  69. def find_neighbors(self, grid, node, diagonal_movement=None):
  70. '''
  71. find neighbor, same for Djikstra, A*, Bi-A*, IDA*
  72. '''
  73. if not diagonal_movement:
  74. diagonal_movement = self.diagonal_movement
  75. return grid.neighbors(node, diagonal_movement=diagonal_movement)
  76. def keep_running(self):
  77. """
  78. check, if we run into time or iteration constrains.
  79. :returns: True if we keep running and False if we run into a constraint
  80. """
  81. if self.runs >= self.max_runs:
  82. raise ExecutionRunsException(
  83. '{} run into barrier of {} iterations without '
  84. 'finding the destination'.format(
  85. self.__class__.__name__, self.max_runs))
  86. if time.time() - self.start_time >= self.time_limit:
  87. raise ExecutionTimeException(
  88. '{} took longer than {} seconds, aborting!'.format(
  89. self.__class__.__name__, self.time_limit))
  90. def process_node(self, node, parent, end, open_list, open_value=True):
  91. '''
  92. we check if the given node is path of the path by calculating its
  93. cost and add or remove it from our path
  94. :param node: the node we like to test
  95. (the neighbor in A* or jump-node in JumpPointSearch)
  96. :param parent: the parent node (the current node we like to test)
  97. :param end: the end point to calculate the cost of the path
  98. :param open_list: the list that keeps track of our current path
  99. :param open_value: needed if we like to set the open list to something
  100. else than True (used for bi-directional algorithms)
  101. '''
  102. # calculate cost from current node (parent) to the next node (neighbor)
  103. ng = self.calc_cost(parent, node)
  104. if not node.opened or ng < node.g:
  105. node.g = ng
  106. node.h = node.h or \
  107. self.apply_heuristic(node, end) * self.weight
  108. # f is the estimated total cost from start to goal
  109. node.f = node.g + node.h
  110. node.parent = parent
  111. if not node.opened:
  112. heapq.heappush(open_list, node)
  113. node.opened = open_value
  114. else:
  115. # the node can be reached with smaller cost.
  116. # Since its f value has been updated, we have to
  117. # update its position in the open list
  118. open_list.remove(node)
  119. heapq.heappush(open_list, node)
  120. def find_path(self, start, end, grid):
  121. """
  122. find a path from start to end node on grid by iterating over
  123. all neighbors of a node (see check_neighbors)
  124. :param start: start node
  125. :param end: end node
  126. :param grid: grid that stores all possible steps/tiles as 2D-list
  127. :return:
  128. """
  129. self.start_time = time.time() # execution time limitation
  130. self.runs = 0 # count number of iterations
  131. start.opened = True
  132. open_list = [start]
  133. while len(open_list) > 0:
  134. self.runs += 1
  135. self.keep_running()
  136. path = self.check_neighbors(start, end, grid, open_list)
  137. if path:
  138. return path, self.runs
  139. # failed to find path
  140. return [], self.runs