From fc168c88127bbda21e877acb49334ec21c9f11da Mon Sep 17 00:00:00 2001 From: Mathias Rav Date: Mon, 1 May 2023 10:36:12 +0200 Subject: [PATCH] Cut the dijkstra_algorithm.py time in half with various tricks Implement a number of performance optimizations that altogether cut down the running time to about half, as tested with a 2905 * 4029 raster with 1 start point and 1 end point. * Don't use queue.PriorityQueue, as it is a threadsafe queue with unnecessary locking - use heapq.heappush() and heappop() instead. * Don't use function calls - instead, inline all functions. Almost all the functions in the old code were only used in one place. * Instead of dicts and sets, use lists of lists, which have faster lookup. * Reduce the number of calls to feedback.setProgress(). --- dijkstra_algorithm.py | 162 ++++++++++++++++++------------------------ 1 file changed, 69 insertions(+), 93 deletions(-) diff --git a/dijkstra_algorithm.py b/dijkstra_algorithm.py index 2277e70..369abd8 100644 --- a/dijkstra_algorithm.py +++ b/dijkstra_algorithm.py @@ -31,7 +31,7 @@ __revision__ = '$Format:%H$' from math import sqrt -import queue +from heapq import heappush, heappop import collections sqrt2 = sqrt(2) @@ -39,83 +39,34 @@ def dijkstra(start_tuple, end_tuples, block, find_nearest, feedback=None): - class Grid: - def __init__(self, matrix): - self.map = matrix - self.h = len(matrix) - self.w = len(matrix[0]) - self.manhattan_boundry = None - self.curr_boundry = None - - def _in_bounds(self, id): - x, y = id - return 0 <= x < self.h and 0 <= y < self.w - - def _passable(self, id): - x, y = id - return self.map[x][y] is not None - - def is_valid(self, id): - return self._in_bounds(id) and self._passable(id) - - def neighbors(self, id): - x, y = id - results = [(x + 1, y), (x, y - 1), (x - 1, y), (x, y + 1), - (x + 1, y - 1), (x + 1, y + 1), (x - 1, y - 1), (x - 1, y + 1)] - results = list(filter(self.is_valid, results)) - return results - - @staticmethod - def manhattan_distance(id1, id2): - x1, y1 = id1 - x2, y2 = id2 - return abs(x1 - x2) + abs(y1 - y2) - - @staticmethod - def min_manhattan(curr_node, end_nodes): - return min(map(lambda node: Grid.manhattan_distance(curr_node, node), end_nodes)) - - @staticmethod - def max_manhattan(curr_node, end_nodes): - return max(map(lambda node: Grid.manhattan_distance(curr_node, node), end_nodes)) - - @staticmethod - def all_manhattan(curr_node, end_nodes): - return {end_node: Grid.manhattan_distance(curr_node, end_node) for end_node in end_nodes} - - def simple_cost(self, cur, nex): - cx, cy = cur - nx, ny = nex - currV = self.map[cx][cy] - offsetV = self.map[nx][ny] - if cx == nx or cy == ny: - return (currV + offsetV) / 2 - else: - return sqrt2 * (currV + offsetV) / 2 + block_h = len(block) + block_w = len(block[0]) - result = [] - grid = Grid(block) + flags = [[0 for _ in line] for line in block] + IS_END = 1 + IS_VISITED = 2 - end_dict = collections.defaultdict(list) + end_dict = {} for end_tuple in end_tuples: - end_dict[end_tuple[0]].append(end_tuple) - end_row_cols = set(end_dict.keys()) - end_row_col_list = list(end_row_cols) - start_row_col = start_tuple[0] + end_dict.setdefault(end_tuple[0], []).append(end_tuple) + end_row_col_list = list(end_dict.keys()) + for (end_x, end_y), *_ in end_tuples: + flags[end_x][end_y] |= IS_END + start_x, start_y = start_tuple[0] + frontier = [(0.0, start_x, start_y)] + came_from = [[None for _ in row] for row in block] + cost_so_far = [[None for _ in row] for row in block] - frontier = queue.PriorityQueue() - frontier.put((0, start_row_col)) - came_from = {} - cost_so_far = {} - decided = set() - - if not grid.is_valid(start_row_col): - return result + if not (0 <= start_x < block_h and 0 <= start_y < block_w and block[start_x][start_y] is not None): + return [] # init progress index = 0 - distance_dic = grid.all_manhattan(start_row_col, end_row_cols) + distance_dic = { + (x2, y2): abs(start_x - x2) + abs(start_y - y2) + for x2, y2 in end_row_col_list + } if find_nearest: total_manhattan = min(distance_dic.values()) else: @@ -126,23 +77,32 @@ def simple_cost(self, cur, nex): if feedback: feedback.setProgress(1 + 100 * (1 - bound / total_manhattan)) - came_from[start_row_col] = None - cost_so_far[start_row_col] = 0 + came_from[start_x][start_y] = None + cost_so_far[start_x][start_y] = 0.0 + + update_every = len(block) * len(block[0]) // 10000 + next_update = update_every - while not frontier.empty(): - _, current_node = frontier.get() - if current_node in decided: + result = [] + ends_found = 0 + + while frontier: + its += 1 + _, cx, cy = heappop(frontier) + if flags[cx][cy] & IS_VISITED: continue - decided.add(current_node) + flags[cx][cy] |= IS_VISITED # update the progress bar - if feedback: + next_update -= 1 + if feedback and next_update == 0: + next_update = update_every if feedback.isCanceled(): return None index = (index + 1) % len(end_row_col_list) target_node = end_row_col_list[index] - new_manhattan = grid.manhattan_distance(current_node, target_node) + new_manhattan = abs(cx - target_node[0]) + abs(cy - target_node[1]) if new_manhattan < distance_dic[target_node]: if find_nearest: curr_bound = new_manhattan @@ -157,34 +117,50 @@ def simple_cost(self, cur, nex): feedback.setProgress(1 + 100 * (1 - bound / total_manhattan)*(1 - bound / total_manhattan)) # reacn destination - if current_node in end_row_cols: + if flags[cx][cy] & IS_END: path = [] costs = [] - traverse_node = current_node + traverse_node = (cx, cy) while traverse_node is not None: path.append(traverse_node) - costs.append(cost_so_far[traverse_node]) - traverse_node = came_from[traverse_node] + costs.append(cost_so_far[traverse_node[0]][traverse_node[1]]) + traverse_node = came_from[traverse_node[0]][traverse_node[1]] # start point and end point overlaps if len(path) == 1: - path.append(start_row_col) + path.append((start_x, start_y)) costs.append(0.0) path.reverse() costs.reverse() - result.append((path, costs, end_dict[current_node])) + result.append((path, costs, end_dict[cx, cy])) - end_row_cols.remove(current_node) - end_row_col_list.remove(current_node) - if len(end_row_cols) == 0 or find_nearest: + ends_found += 1 + if len(end_row_col_list) == ends_found or find_nearest: break # relax distance - for nex in grid.neighbors(current_node): - new_cost = cost_so_far[current_node] + grid.simple_cost(current_node, nex) - if nex not in cost_so_far or new_cost < cost_so_far[nex]: - cost_so_far[nex] = new_cost - frontier.put((new_cost, nex)) - came_from[nex] = current_node + currV = block[cx][cy] + + # Test all 8 neighbors + for nx, ny in [(cx + 1, cy), (cx, cy - 1), (cx - 1, cy), (cx, cy + 1), + (cx + 1, cy - 1), (cx + 1, cy + 1), (cx - 1, cy - 1), (cx - 1, cy + 1)]: + if not (0 <= nx < block_h and 0 <= ny < block_w): + # neighbor outside grid + continue + offsetV = block[nx][ny] + if offsetV is None: + # nodata neighbor + continue + + # Cost evaluation + if cx == nx or cy == ny: + new_cost = cost_so_far[cx][cy] + (currV + offsetV) / 2 + else: + new_cost = cost_so_far[cx][cy] + sqrt2 * (currV + offsetV) / 2 + + if cost_so_far[nx][ny] is None or new_cost < cost_so_far[nx][ny]: + cost_so_far[nx][ny] = new_cost + heappush(frontier, (new_cost, nx, ny)) + came_from[nx][ny] = (cx, cy) return result