Current section
Files
Jump to
Current section
Files
lib/algorithms/search/a_star.ex
defmodule BitGraph.Algorithm.Search.AStar do
@doc """
A* algorithm.
Finds the shortest path in graph from `start` vertex to `goal` vertex.
`dist_fun` is a function that takes 2 vertex indices and returns the distance between them.
`h` is a "heuristic", a function h(n) that estimates the cost to reach goal vertex from node n.
"""
alias InPlace.PriorityQueue, as: Q
#import BitGraph
alias BitGraph.V
@behaviour BitGraph.Algorithm
@infinity :infinity
@impl true
def run(graph, opts) do
start_vertex = Keyword.fetch!(opts, :start)
goal_vertex = Keyword.fetch!(opts, :goal)
opts = Keyword.merge(default_opts(), opts)
dist_fun = Keyword.get(opts, :dist_fun, fn _v1, _v2 -> 1 end)
h_fun = Keyword.get(opts, :h_fun, fn _vertex -> 0 end)
run(graph, start_vertex, goal_vertex, h_fun, dist_fun)
end
@impl true
def preprocess(graph, opts) do
start_vertex = Keyword.fetch!(opts, :start)
goal_vertex = Keyword.fetch!(opts, :goal)
Keyword.merge(default_opts(), opts)
|> Keyword.update!(:dist_fun, fn original_fun ->
fn v1, v2 -> original_fun.(
V.get_vertex(graph, v1),
V.get_vertex(graph, v2))
end
end)
|> Keyword.update!(:h_fun, fn original_fun -> fn v -> original_fun.(V.get_vertex(graph, v)) end end)
|> Keyword.put(:start, V.get_vertex_index(graph, start_vertex))
|> Keyword.put(:goal, V.get_vertex_index(graph, goal_vertex))
end
@impl true
def postprocess(graph, path) do
Enum.map(path, fn vertex_index -> V.get_vertex(graph, vertex_index) end)
end
defp default_opts() do
[
dist_fun: fn _v1, _v2 -> 1 end,
h_fun: fn _v -> 0 end
]
end
def run(graph, start_vertex, goal_vertex, h_fun, dist_fun)
when is_integer(start_vertex) and is_integer(goal_vertex) and
is_function(h_fun, 1) and is_function(dist_fun, 2) do
instance = init(graph, start_vertex, goal_vertex, h_fun, dist_fun)
iterate(graph, instance)
|> finalize()
end
defp init(graph, start_vertex, goal_vertex, h_fun, dist_fun) do
n_vertices = BitGraph.num_vertices(graph)
p_queue = Q.new(n_vertices)
Q.insert(p_queue, start_vertex, apply_h_fun(h_fun, start_vertex))
path = Map.new()
%{
num_vertices: n_vertices,
start: start_vertex,
goal: goal_vertex,
open_set: p_queue,
g_score: Map.new([{start_vertex, 0}]),
path: path,
h_fun: h_fun,
dist_fun: dist_fun
}
end
defp apply_h_fun(h_fun, vertex) do
## Default to 0 if heuristic for the vertex is not defined
h_fun.(vertex) || 0
end
defp iterate(
graph,
%{
goal: goal,
path: path,
open_set: open_set,
g_score: g_score_map,
h_fun: h_fun,
dist_fun: dist_fun
} = state
) do
case Q.extract_min(open_set) do
nil ->
:failure
{vertex, _min_fscore} when vertex == goal ->
{:ok, reconstruct_path(vertex, path)}
{vertex, _min_fscore} ->
vertex_g_score = Map.get(g_score_map, vertex)
{updated_path, updated_g_scores} =
Enum.reduce(
V.out_neighbors(graph, vertex),
{path, g_score_map},
fn neighbor, {path_acc, g_score_acc} = acc ->
tentative_g_score = vertex_g_score + dist_fun.(vertex, neighbor)
if tentative_g_score < Map.get(g_score_acc, neighbor, @infinity) do
# This path to neighbor is better than any previous one. Record it!
Q.insert(
open_set,
neighbor,
tentative_g_score + apply_h_fun(h_fun, neighbor)
)
{
Map.put(path_acc, neighbor, vertex),
Map.put(g_score_acc, neighbor, tentative_g_score)
}
else
acc
end
end
)
iterate(
graph,
state
|> Map.put(:g_score, updated_g_scores)
|> Map.put(:path, updated_path)
)
end
end
defp reconstruct_path(vertex, path) do
reconstruct_path(vertex, path, [vertex])
end
defp reconstruct_path(v, path, acc) do
case Map.get(path, v) do
nil -> acc
next ->
reconstruct_path(next, path, [next | acc])
end
end
defp finalize(result) do
case result do
{:ok, path} ->
path
:failure ->
:failure
end
end
end