-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathastar.ex
185 lines (151 loc) · 5.07 KB
/
astar.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
defmodule Scurry.Astar do
@moduledoc """
Implementation of [A-star search
algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm) to find the
shortest path in 2D polygon maps.
The basic usage is;
```
# computes the heuristic cost between two nodes
def heur_fun(node_a, node_b) do
...
end
# Define a graph
graph = %{
node_1 => [
{node_2, cost_1_2}, {node_3, cost_1_3},
],
node_2 => [
{node_3, cost_2_3}, {node_4, cost_2_4},
],
...
}
# Do A-star search
state = Astar.search(graph, node_1, node_z, &heur_fun/2)
# Extract path
path = Astart.path(state)
[node1, ..., node_z]
```
"""
@doc """
Find shortest path in `graph` from `start` to `stop`.
## Params
* `graph` a map of node to a list of nodes and cost tuples.
* `start` the node from which to start
* `stop` the node at which to end
* `heur_fun` the heuristic function used to estimate cost from a node in
`graph` to `stop`. It takes two nodes and returns a cost that should be
comparable with itself for ordering. `node, node :: term`.
Returns the algorithms internal state which can be passed to `path/1` to
obtain the actual path.
"""
require Logger
def search(graph, start, stop, heur_fun) do
# Logger.info("----------------------------------------- A-star")
# Logger.info("graph = #{inspect graph, pretty: true}")
queue = [start]
%{
start: start,
stop: stop,
graph: graph,
# (node, node) :: cost function
heur_fun: heur_fun,
queue: queue,
shortest_path_tree: %{},
frontier: %{},
# node => cost - distance from start to node
g_cost: %{},
# vertice => cost, distance from start to vervice + heuristic distance to stop
f_cost: %{}
}
|> pathfind_helper
end
defp sort_queue(queue, f_cost) do
Enum.sort_by(queue, fn e -> Map.get(f_cost, e) end, :asc)
end
defp add_to_queue(queue, node) do
Enum.sort(queue ++ [node])
|> Enum.dedup
end
defp pathfind_helper(%{queue: []}=state) do
state
end
defp pathfind_helper(%{queue: [current|queue]}=state) do
# Logger.info("----------------------------------------- A-star search")
# Logger.info("current = #{inspect current, pretty: true}")
# Logger.info("state = #{inspect Map.delete(state, :graph), pretty: true}")
spt = Map.put(state.shortest_path_tree, current, Map.get(state.frontier, current))
cond do
current == state.stop ->
%{state | shortest_path_tree: spt}
true ->
edges = Map.get(state.graph, current, [])
# Logger.info("edges = #{inspect edges, pretty: true}")
reduce_seed = {state.frontier, queue, state.g_cost, state.f_cost}
{f, q, g_cost, f_cost} =
Enum.reduce(edges, reduce_seed, fn {node, edge_cost}, acc ->
{frontier, queue, g_cost, f_cost} = acc
# H cost
heur_cost = state.heur_fun.(node, state.stop)
# G cost
shortest_distance_from_start = Map.get(g_cost, current, 0) + edge_cost
# F cost = G cost + H cost
total_distance = shortest_distance_from_start + heur_cost
# Logger.info("\t#{inspect node} heur_cost = #{heur_cost}")
# Logger.info("\t#{inspect node} new g_cost = #{shortest_distance_from_start}")
# Logger.info("\t#{inspect node} new cost = #{total_distance}\n")
cond do
node == state.start ->
# No reason to go back
# Logger.info("skip going back to start")
acc
not Map.has_key?(frontier, node) ->
{
Map.put(frontier, node, current),
add_to_queue(queue, node),
Map.put(g_cost, node, shortest_distance_from_start),
Map.put(f_cost, node, total_distance),
}
shortest_distance_from_start < Map.get(g_cost, node, 0) and Map.get(spt, node) == nil ->
{
Map.put(frontier, node, current),
queue,
Map.put(g_cost, node, shortest_distance_from_start),
Map.put(f_cost, node, total_distance),
}
true ->
{frontier, queue, g_cost, f_cost}
end
end)
new_state = %{
state |
queue: sort_queue(q, f_cost),
frontier: f,
f_cost: f_cost,
g_cost: g_cost,
shortest_path_tree: spt,
}
pathfind_helper(new_state)
end
end
@doc """
Get the path from the state returned by `search/4`.
## Params
* `state` the a-star state returned by `search/4`
Returns the path.
"""
def path(state) do
next = state.shortest_path_tree[state.stop]
path(state, state.start, next, [state.stop])
|> Enum.reverse
end
defp path(_state, _start, nil, acc) do
acc
end
defp path(_state, start, start, acc) do
acc ++ [start]
end
defp path(state, start, node, acc) do
next = state.shortest_path_tree[node]
path(state, start, next, acc ++ [node])
end
end