Skip to content

Commit e658676

Browse files
authored
Merge pull request #54 from petercorke/perf/path-planners
perf: path_BFS, path_UCS and path_Astar no longer scan lists
2 parents 149abc1 + e1c3db2 commit e658676

2 files changed

Lines changed: 276 additions & 18 deletions

File tree

‎src/pgraph/PGraph.py‎

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from matplotlib.transforms import Affine2D, ScaledTranslation
1111
from mpl_toolkits.mplot3d import proj3d
1212
import copy
13+
import heapq
14+
from collections import deque
1315
from collections.abc import Iterable, Iterator
1416
import tempfile
1517
import webbrowser
@@ -3074,7 +3076,10 @@ def path_BFS(
30743076

30753077
# we use lists not sets since the order is instructive in verbose
30763078
# mode, really need ordered sets...
3077-
frontier: list[Vertex] = [S]
3079+
# the frontier is a queue; ``seen`` is everything that has been on it,
3080+
# so that "frontier or explored" is a set lookup, not a list scan
3081+
frontier: deque[Vertex] = deque([S])
3082+
seen: set[Vertex] = {S}
30783083
explored: list[Vertex] = []
30793084
parent: dict[Vertex, Vertex] = {}
30803085
done = False
@@ -3085,7 +3090,7 @@ def path_BFS(
30853090
print("FRONTIER:", ", ".join([str(v.name) for v in frontier]))
30863091
print("EXPLORED:", ", ".join([str(v.name) for v in explored]))
30873092

3088-
x = frontier.pop(0)
3093+
x = frontier.popleft()
30893094
if verbose:
30903095
print(" expand", x.name)
30913096

@@ -3099,8 +3104,9 @@ def path_BFS(
30993104
parent[n] = x
31003105
done = True
31013106
break
3102-
if n not in frontier and n not in explored:
3107+
if n not in seen:
31033108
# add it to the frontier
3109+
seen.add(n)
31043110
frontier.append(n)
31053111
if verbose:
31063112
print(" add", n.name, "to the frontier")
@@ -3122,8 +3128,9 @@ def path_BFS(
31223128
while x is not S:
31233129
p = parent[x]
31243130
length += self._require_cost(p.edgeto(x))
3125-
path.insert(0, p)
3131+
path.append(p)
31263132
x = p
3133+
path.reverse()
31273134

31283135
if summary or verbose:
31293136
print(
@@ -3184,8 +3191,14 @@ def path_UCS(
31843191
S = self._resolve_vertex(S, "start")
31853192
G = self._resolve_vertex(G, "goal")
31863193

3187-
frontier: list[Vertex] = [S]
3188-
explored: list[Vertex] = []
3194+
# The frontier is a dict, used as an ordered set: its order is the order
3195+
# vertices first joined it, which breaks ties between equal ``f``, and
3196+
# its value is that position. ``heap`` finds the minimum ``f``; a
3197+
# vertex that is reparented is pushed again, and the older entry is
3198+
# recognised as stale when it is popped.
3199+
frontier: dict[Vertex, int] = {S: 0}
3200+
heap: list[tuple[float, int, Vertex]] = [(0, 0, S)]
3201+
explored: dict[Vertex, None] = {}
31893202
parent: dict[Vertex, Vertex] = {}
31903203
f: dict[Vertex, float] = {S: 0} # evaluation function
31913204

@@ -3197,8 +3210,11 @@ def path_UCS(
31973210
)
31983211
print("EXPLORED:", ", ".join([str(v.name) for v in explored]))
31993212

3200-
i = np.argmin([f[n] for n in frontier]) # minimum f in frontier
3201-
x = frontier.pop(i)
3213+
while True: # minimum f in frontier, skipping stale heap entries
3214+
fx, _, x = heapq.heappop(heap)
3215+
if x in frontier and f[x] == fx:
3216+
break
3217+
del frontier[x]
32023218
if verbose:
32033219
print(" expand", x.name)
32043220
if x is G:
@@ -3212,7 +3228,8 @@ def path_UCS(
32123228
# add it to the frontier
32133229
parent[n] = x
32143230
f[n] = fnew
3215-
frontier.append(n)
3231+
frontier[n] = len(parent) # one entry per vertex added so far
3232+
heapq.heappush(heap, (fnew, frontier[n], n))
32163233
if verbose:
32173234
print(" add", n.name, "to the frontier")
32183235

@@ -3226,8 +3243,9 @@ def path_UCS(
32263243
)
32273244
f[n] = fnew
32283245
parent[n] = x
3246+
heapq.heappush(heap, (fnew, frontier[n], n))
32293247

3230-
explored.append(x)
3248+
explored[x] = None
32313249
if verbose:
32323250
print(" move", x.name, "to the explored list")
32333251
else:
@@ -3242,8 +3260,9 @@ def path_UCS(
32423260
while x is not S:
32433261
p = parent[x]
32443262
length += self._require_cost(p.edgeto(x))
3245-
path.insert(0, p)
3263+
path.append(p)
32463264
x = p
3265+
path.reverse()
32473266

32483267
parent_names: dict[str, str] = {}
32493268
for v, p in parent.items():
@@ -3310,8 +3329,10 @@ def path_Astar(
33103329
S = self._resolve_vertex(S, "start")
33113330
G = self._resolve_vertex(G, "goal")
33123331

3313-
frontier: list[Vertex] = [S]
3314-
explored: list[Vertex] = []
3332+
# see path_UCS for the frontier and heap
3333+
frontier: dict[Vertex, int] = {S: 0}
3334+
heap: list[tuple[float, int, Vertex]] = [(0, 0, S)]
3335+
explored: dict[Vertex, None] = {}
33153336
parent: dict[Vertex, Vertex] = {}
33163337
g: dict[Vertex, float] = {S: 0} # cost to come
33173338
f: dict[Vertex, float] = {S: 0} # evaluation function
@@ -3324,8 +3345,11 @@ def path_Astar(
33243345
)
33253346
print("EXPLORED:", ", ".join([str(v.name) for v in explored]))
33263347

3327-
i = np.argmin([f[n] for n in frontier]) # minimum f in frontier
3328-
x = frontier.pop(i)
3348+
while True: # minimum f in frontier, skipping stale heap entries
3349+
fx, _, x = heapq.heappop(heap)
3350+
if x in frontier and f[x] == fx:
3351+
break
3352+
del frontier[x]
33293353
if verbose:
33303354
print(" expand", x.name)
33313355
if x is G:
@@ -3336,10 +3360,11 @@ def path_Astar(
33363360
continue # self-loop, never part of a shortest path
33373361
if n not in frontier and n not in explored:
33383362
# add it to the frontier
3339-
frontier.append(n)
33403363
parent[n] = x
3364+
frontier[n] = len(parent) # one entry per vertex added so far
33413365
g[n] = g[x] + self._require_cost(e) # update cost to come
33423366
f[n] = g[n] + n.heuristic_distance(G) # heuristic
3367+
heapq.heappush(heap, (f[n], frontier[n], n))
33433368
if verbose:
33443369
print(" add", n.name, "to the frontier")
33453370
elif n in frontier:
@@ -3355,8 +3380,9 @@ def path_Astar(
33553380
f[n] = g[n] + n.heuristic_distance(G) # heuristic
33563381

33573382
parent[n] = x # reparent
3383+
heapq.heappush(heap, (f[n], frontier[n], n))
33583384

3359-
explored.append(x)
3385+
explored[x] = None
33603386
if verbose:
33613387
print(" move", x.name, "to the explored list")
33623388

@@ -3372,8 +3398,9 @@ def path_Astar(
33723398
while x is not S:
33733399
p = parent[x]
33743400
length += self._require_cost(p.edgeto(x))
3375-
path.insert(0, p)
3401+
path.append(p)
33763402
x = p
3403+
path.reverse()
33773404

33783405
parent_names: dict[str, str] = {}
33793406
for v, p in parent.items():

0 commit comments

Comments
 (0)