Skip to content

Commit 961b15e

Browse files
authored
Merge pull request #835 from compas-dev/astar_network
Add astar_lightest_path and refactor astart_shortest_path
2 parents 272185f + 6f4b6e5 commit 961b15e

File tree

4 files changed

+154
-50
lines changed

4 files changed

+154
-50
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010

1111
### Added
1212

13+
* Added `compas.topology.astar_lightest_path`.
14+
1315
### Changed
1416

17+
* Extended `compas.topology.astar_shortest_path` to work on `compas.datastructures.Mesh` and `compas.datastructures.Network`.
18+
1519
### Removed
1620

1721

src/compas/topology/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
:toctree: generated/
5050
:nosignatures:
5151
52+
astar_lightest_path
5253
astar_shortest_path
5354
breadth_first_ordering
5455
breadth_first_traverse
@@ -69,6 +70,7 @@
6970
breadth_first_traverse,
7071
breadth_first_paths,
7172
shortest_path,
73+
astar_lightest_path,
7274
astar_shortest_path,
7375
dijkstra_distances,
7476
dijkstra_path
@@ -101,6 +103,7 @@
101103
'breadth_first_traverse',
102104
'breadth_first_paths',
103105
'shortest_path',
106+
'astar_lightest_path',
104107
'astar_shortest_path',
105108
'dijkstra_distances',
106109
'dijkstra_path',

src/compas/topology/traversal.py

Lines changed: 80 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'breadth_first_traverse',
1919
'breadth_first_paths',
2020
'shortest_path',
21+
'astar_lightest_path',
2122
'astar_shortest_path',
2223
'dijkstra_distances',
2324
'dijkstra_path'
@@ -323,6 +324,8 @@ def shortest_path(adjacency, root, goal):
323324

324325

325326
def reconstruct_path(came_from, current):
327+
if current not in came_from:
328+
return None
326329
total_path = [current]
327330
while current in came_from:
328331
current = came_from[current]
@@ -331,97 +334,124 @@ def reconstruct_path(came_from, current):
331334
return total_path
332335

333336

334-
def astar_shortest_path(network, root, goal):
335-
"""Find the shortest path between two vertices of a network using the A* search algorithm.
337+
def astar_lightest_path(adjacency, weights, heuristic, root, goal):
338+
"""Find the path of least weight between two vertices of a graph using the A* search algorithm.
336339
337340
Parameters
338341
----------
339-
network : instance of the Network class
342+
adjacency : dict
343+
An adjacency dictionary. Each key represents a vertex
344+
and maps to a list of neighboring vertex keys.
345+
weights : dict
346+
A dictionary of edge weights.
347+
heuristic : dict
348+
A dictionary of guesses of weights of paths from a node to the goal.
340349
root : hashable
341-
The identifier of the starting node.
350+
The start vertex.
342351
goal : hashable
343-
The identifier of the ending node.
352+
The end vertex.
344353
345354
Returns
346355
-------
347356
list, None
348357
The path from root to goal, or None, if no path exists between the vertices.
349358
350-
Examples
351-
--------
352-
>>>
353-
354359
References
355360
----------
356361
https://en.wikipedia.org/wiki/A*_search_algorithm
357362
"""
358-
root_coords = network.vertex_coordinates(root)
359-
goal_coords = network.vertex_coordinates(goal)
360-
361-
# The set of nodes already evaluated
362363
visited_set = set()
363364

364-
# The set of currently discovered nodes that are not evaluated yet.
365-
# Initially, only the start node is known.
366365
candidates_set = {root}
367366
best_candidate_heap = PriorityQueue()
368-
best_candidate_heap.put((0, root))
367+
best_candidate_heap.put((heuristic[root], root))
369368

370-
# For each node, which node it can most efficiently be reached from.
371-
# If a node can be reached from many nodes, came_from will eventually contain the
372-
# most efficient previous step.
373369
came_from = dict()
374370

375-
# g_score is a dict mapping node index to the cost of getting from the root node to that node.
376-
# The default value is Infinity.
377-
# The cost of going from start to start is zero.
378371
g_score = dict()
379-
380-
for v in network.vertices():
381-
g_score[v] = float("inf")
382-
372+
for v in adjacency:
373+
g_score[v] = float('inf')
383374
g_score[root] = 0
384375

385-
# For each node, the total cost of getting from the start node to the goal
386-
# by passing by that node. That value is partly known, partly heuristic.
387-
# The default value of f_score is Infinity
388-
f_score = dict()
389-
390-
for v in network.vertices():
391-
f_score[v] = float("inf")
392-
393-
# For the first node, that value is completely heuristic.
394-
f_score[root] = distance_point_point(root_coords, goal_coords)
395-
396376
while not best_candidate_heap.empty():
397377
_, current = best_candidate_heap.get()
398378
if current == goal:
399379
break
400380

401381
visited_set.add(current)
402-
current_coords = network.vertex_coordinates(current)
403-
for neighbor in network.vertex_neighbors(current):
382+
for neighbor in adjacency[current]:
404383
if neighbor in visited_set:
405-
continue # Ignore the neighbor which is already evaluated.
384+
continue
406385

407-
# The distance from start to a neighbor
408-
neighbor_coords = network.vertex_coordinates(neighbor)
409-
tentative_gScore = g_score[current] + distance_point_point(current_coords, neighbor_coords)
410-
if neighbor not in candidates_set: # Discover a new node
386+
tentative_g_score = g_score[current] + weights[(current, neighbor)]
387+
if neighbor not in candidates_set:
411388
candidates_set.add(neighbor)
412-
elif tentative_gScore >= g_score[neighbor]:
389+
elif tentative_g_score >= g_score[neighbor]:
413390
continue
414391

415-
# This path is the best until now. Record it!
416392
came_from[neighbor] = current
417-
g_score[neighbor] = tentative_gScore
418-
new_fscore = g_score[neighbor] + distance_point_point(neighbor_coords, goal_coords)
419-
f_score[neighbor] = new_fscore
420-
best_candidate_heap.put((new_fscore, neighbor))
393+
g_score[neighbor] = tentative_g_score
394+
new_f_score = g_score[neighbor] + heuristic[neighbor]
395+
best_candidate_heap.put((new_f_score, neighbor))
421396

422397
return reconstruct_path(came_from, goal)
423398

424399

400+
def _get_coordinates(key, structure):
401+
if hasattr(structure, 'node_attributes'):
402+
return structure.node_attributes(key, 'xyz')
403+
if hasattr(structure, 'vertex_coordinates'):
404+
return structure.vertex_coordinates(key)
405+
raise Exception("Coordinates cannot be found for object of type {}".format(type(structure)))
406+
407+
408+
def _get_points(structure):
409+
if hasattr(structure, 'nodes'):
410+
return structure.nodes()
411+
if hasattr(structure, 'vertices'):
412+
return structure.vertices()
413+
raise Exception("Points cannot be found for object of type {}".format(type(structure)))
414+
415+
416+
def astar_shortest_path(network, root, goal):
417+
"""Find the shortest path between two vertices of a network or mesh using the A* search algorithm.
418+
419+
Parameters
420+
----------
421+
network : instance of the Network or Mesh class
422+
root : hashable
423+
The identifier of the starting node.
424+
goal : hashable
425+
The identifier of the ending node.
426+
427+
Returns
428+
-------
429+
list, None
430+
The path from root to goal, or None, if no path exists between the vertices.
431+
432+
References
433+
----------
434+
https://en.wikipedia.org/wiki/A*_search_algorithm
435+
"""
436+
adjacency = network.adjacency
437+
weights = {}
438+
for u, v in network.edges():
439+
u_coords = _get_coordinates(u, network)
440+
v_coords = _get_coordinates(v, network)
441+
distance = distance_point_point(u_coords, v_coords)
442+
weights[(u, v)] = distance
443+
weights[(v, u)] = distance
444+
445+
heuristic = {}
446+
goal_coords = _get_coordinates(goal, network)
447+
points = _get_points(network)
448+
for u in points:
449+
u_coords = _get_coordinates(u, network)
450+
heuristic[u] = distance_point_point(u_coords, goal_coords)
451+
452+
return astar_lightest_path(adjacency, weights, heuristic, root, goal)
453+
454+
425455
def dijkstra_distances(adjacency, weight, target):
426456
"""Compute Dijkstra distances from all vertices in a connected set to one target vertex.
427457
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from compas.datastructures import Graph
2+
from compas.datastructures import Mesh
3+
from compas.datastructures import Network
4+
from compas.geometry import Box, Frame
5+
from compas.topology import astar_shortest_path
6+
from compas.topology.traversal import astar_lightest_path
7+
8+
9+
def test_astar_shortest_path():
10+
n = Network()
11+
a = n.add_node(x=1, y=2, z=0)
12+
b = n.add_node(x=3, y=1, z=0)
13+
n.add_edge(a, b)
14+
path = astar_shortest_path(n, a, b)
15+
assert path == [a, b]
16+
17+
18+
def test_astar_shortest_path_cycle():
19+
n = Network()
20+
a = n.add_node(x=1, y=0, z=0)
21+
b = n.add_node(x=2, y=0, z=0)
22+
c = n.add_node(x=3, y=0, z=0)
23+
d = n.add_node(x=4, y=0, z=0)
24+
e = n.add_node(x=3.5, y=5, z=0)
25+
n.add_edge(a, b)
26+
n.add_edge(a, e)
27+
n.add_edge(b, c)
28+
n.add_edge(c, d)
29+
n.add_edge(e, d)
30+
path = astar_shortest_path(n, a, d)
31+
assert path == [a, b, c, d]
32+
33+
34+
def test_astar_shortest_path_disconnected():
35+
n = Network()
36+
a = n.add_node(x=1, y=0, z=0)
37+
b = n.add_node(x=2, y=0, z=0)
38+
c = n.add_node(x=3, y=0, z=0)
39+
n.add_edge(a, b)
40+
path = astar_shortest_path(n, a, c)
41+
assert path is None
42+
43+
44+
def test_astar_shortest_path_mesh():
45+
mesh = Mesh.from_shape(Box(Frame.worldXY(), 1, 1, 1))
46+
a, b = mesh.get_any_vertices(2)
47+
path = astar_shortest_path(mesh, a, b)
48+
assert path is not None
49+
50+
51+
def test_astar_lightest_path():
52+
g = Graph()
53+
for i in range(4):
54+
g.add_node(i)
55+
g.add_edge(0, 1)
56+
g.add_edge(0, 2)
57+
g.add_edge(1, 3)
58+
g.add_edge(2, 3)
59+
weights = {
60+
(0, 1): 1,
61+
(0, 2): 1,
62+
(1, 3): 2,
63+
(2, 3): 1,
64+
}
65+
heuristic = {i: 1 for i in range(4)}
66+
path = astar_lightest_path(g.adjacency, weights, heuristic, 0, 3)
67+
assert path == [0, 2, 3]

0 commit comments

Comments
 (0)