Skip to content

Commit a091843

Browse files
authored
Merge pull request #54 from peterchenadded/performance/53-improve-heapq-usage-performance
Performance/53 improve heapq usage performance
2 parents 2990b64 + bb83c7d commit a091843

15 files changed

Lines changed: 305 additions & 14 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,6 @@ target/
6161

6262
# ipython notebook
6363
.ipynb_checkpoints
64+
65+
# python virtual env
66+
venv/

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ flow:
6767

6868
You can run the tests locally using pytest. Take a look at the `test`-folder
6969

70+
You can follow below steps to setup your virtual environment and run the tests.
71+
72+
```bash
73+
# Go to repo
74+
cd python-pathfinding
75+
76+
# Setup virtual env and activate it - Mac/Linux for windows use source venv/Scripts/activate
77+
python3 -m venv venv
78+
source venv/bin/activate
79+
80+
# Install test requirements
81+
pip install -r test/requirements.txt
82+
83+
# Run all the tests
84+
pytest
85+
```
86+
7087
## Contributing
7188

7289
Please use the [issue tracker](https://github.com/brean/python-pathfinding/issues) to submit bug reports and feature requests. Please use merge requests as described [here](/CONTRIBUTING.md) to add/adapt functionality.

notebooks/performance.ipynb

Lines changed: 112 additions & 0 deletions
Large diffs are not rendered by default.

pathfinding/core/heap.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Simple heap with ordering and removal."""
2+
import heapq
3+
from .graph import Graph
4+
from .grid import Grid
5+
from .world import World
6+
7+
class SimpleHeap:
8+
"""Simple wrapper around open_list that keeps track of order and removed nodes automatically."""
9+
10+
def __init__(self, node, grid):
11+
self.grid = grid
12+
self.open_list = [self._get_node_tuple(node, 0)]
13+
self.removed_node_tuples = set()
14+
self.heap_order = {}
15+
self.number_pushed = 0
16+
17+
def _get_node_tuple(self, node, heap_order):
18+
if isinstance(self.grid, Graph):
19+
return (node.f, heap_order, node.node_id)
20+
elif isinstance(self.grid, Grid):
21+
return (node.f, heap_order, node.x, node.y)
22+
elif isinstance(self.grid, World):
23+
return (node.f, heap_order, node.x, node.y, node.grid_id)
24+
else:
25+
assert False, "unsupported heap node node=%s" % node
26+
27+
def _get_node_id(self, node):
28+
if isinstance(self.grid, Graph):
29+
return node.node_id
30+
elif isinstance(self.grid, Grid):
31+
return (node.x, node.y)
32+
elif isinstance(self.grid, World):
33+
return (node.x, node.y, node.grid_id)
34+
35+
36+
def pop_node(self):
37+
"""
38+
Pops node off the heap. i.e. returns the one with the lowest f.
39+
40+
Notes:
41+
1. Checks if that values is in removed_node_tuples first, if not tries again.
42+
2. We use this approach to avoid invalidating the heap structure.
43+
"""
44+
node_tuple = heapq.heappop(self.open_list)
45+
while node_tuple in self.removed_node_tuples:
46+
node_tuple = heapq.heappop(self.open_list)
47+
48+
if isinstance(self.grid, Graph):
49+
node = self.grid.node(node_tuple[2])
50+
elif isinstance(self.grid, Grid):
51+
node = self.grid.node(node_tuple[2], node_tuple[3])
52+
elif isinstance(self.grid, World):
53+
node = self.grid.grids[node_tuple[4]].node(node_tuple[2], node_tuple[3])
54+
55+
return node
56+
57+
def push_node(self, node):
58+
"""
59+
Push node into heap.
60+
61+
:param node: The node to push.
62+
"""
63+
self.number_pushed = self.number_pushed + 1
64+
node_tuple = self._get_node_tuple(node, self.number_pushed)
65+
node_id = self._get_node_id(node)
66+
67+
self.heap_order[node_id] = self.number_pushed
68+
69+
heapq.heappush(self.open_list, node_tuple)
70+
71+
def remove_node(self, node, f):
72+
"""
73+
Remove the node from the heap.
74+
75+
This just stores it in a set and we just ignore the node if it does get popped from the heap.
76+
77+
:param node: The node to remove.
78+
:param f: The old f value of the node.
79+
"""
80+
node_id = self._get_node_id(node)
81+
heap_order = self.heap_order[node_id]
82+
node_tuple = self._get_node_tuple(node, heap_order)
83+
self.removed_node_tuples.add(node_tuple)
84+
85+
def __len__(self):
86+
"""Returns the length of the open_list."""
87+
return len(self.open_list)

pathfinding/finder/a_star.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import heapq # used for the so colled "open list" that stores known nodes
21
from .finder import BY_END, Finder, MAX_RUNS, TIME_LIMIT
32
from ..core.diagonal_movement import DiagonalMovement
43
from ..core.heuristic import manhattan, octile
@@ -50,8 +49,7 @@ def check_neighbors(self, start, end, graph, open_list,
5049
:param open_list: stores nodes that will be processed next
5150
"""
5251
# pop node with minimum 'f' value
53-
node = heapq.nsmallest(1, open_list)[0]
54-
open_list.remove(node)
52+
node = open_list.pop_node()
5553
node.closed = True
5654

5755
# if reached the end position, construct the path and return it

pathfinding/finder/bi_a_star.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .a_star import AStarFinder
33
from .finder import BY_END, BY_START, MAX_RUNS, TIME_LIMIT
44
from ..core.diagonal_movement import DiagonalMovement
5+
from ..core.heap import SimpleHeap
56

67

78
class BiAStarFinder(AStarFinder):
@@ -45,12 +46,12 @@ def find_path(self, start, end, grid):
4546
self.start_time = time.time() # execution time limitation
4647
self.runs = 0 # count number of iterations
4748

48-
start_open_list = [start]
49+
start_open_list = SimpleHeap(start, grid)
4950
start.g = 0
5051
start.f = 0
5152
start.opened = BY_START
5253

53-
end_open_list = [end]
54+
end_open_list = SimpleHeap(end, grid)
5455
end.g = 0
5556
end.f = 0
5657
end.opened = BY_END

pathfinding/finder/breadth_first.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def __init__(self, heuristic=None, weight=1,
1919
self.diagonalMovement = DiagonalMovement.never
2020

2121
def check_neighbors(self, start, end, grid, open_list):
22-
node = open_list.pop(0)
22+
node = open_list.pop_node()
2323
node.closed = True
2424

2525
if node == end:
@@ -30,6 +30,6 @@ def check_neighbors(self, start, end, grid, open_list):
3030
if neighbor.closed or neighbor.opened:
3131
continue
3232

33-
open_list.append(neighbor)
33+
open_list.push_node(neighbor)
3434
neighbor.opened = True
3535
neighbor.parent = node

pathfinding/finder/finder.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import heapq # used for the so colled "open list" that stores known nodes
22
import time # for time limitation
33
from ..core.diagonal_movement import DiagonalMovement
4+
from ..core.heap import SimpleHeap
45

56

67
# max. amount of tries we iterate until we abort the search
@@ -107,20 +108,21 @@ def process_node(
107108
ng = parent.g + graph.calc_cost(parent, node, self.weighted)
108109

109110
if not node.opened or ng < node.g:
111+
old_f = node.f
110112
node.g = ng
111113
node.h = node.h or self.apply_heuristic(node, end)
112114
# f is the estimated total cost from start to goal
113115
node.f = node.g + node.h
114116
node.parent = parent
115117
if not node.opened:
116-
heapq.heappush(open_list, node)
118+
open_list.push_node(node)
117119
node.opened = open_value
118120
else:
119121
# the node can be reached with smaller cost.
120122
# Since its f value has been updated, we have to
121123
# update its position in the open list
122-
open_list.remove(node)
123-
heapq.heappush(open_list, node)
124+
open_list.remove_node(node, old_f)
125+
open_list.push_node(node)
124126

125127
def check_neighbors(self, start, end, graph, open_list,
126128
open_value=True, backtrace_by=None):
@@ -150,7 +152,7 @@ def find_path(self, start, end, grid):
150152
self.runs = 0 # count number of iterations
151153
start.opened = True
152154

153-
open_list = [start]
155+
open_list = SimpleHeap(start, grid)
154156

155157
while len(open_list) > 0:
156158
self.runs += 1

pathfinding/finder/msp.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from collections import deque, namedtuple
44
from ..core import heuristic
55
from ..finder.finder import Finder
6+
from ..core.heap import SimpleHeap
67

78

89
class MinimumSpanningTree(Finder):
@@ -31,14 +32,13 @@ def itertree(self, grid, start):
3132

3233
start.opened = True
3334

34-
open_list = [start]
35+
open_list = SimpleHeap(start, grid)
3536

3637
while len(open_list) > 0:
3738
self.runs += 1
3839
self.keep_running()
3940

40-
node = heapq.nsmallest(1, open_list)[0]
41-
open_list.remove(node)
41+
node = open_list.pop_node()
4242
node.closed = True
4343
yield node
4444

0 commit comments

Comments
 (0)