Current section
Files
Jump to
Current section
Files
lib/yog/pathfinding/a_star.ex
defmodule Yog.Pathfinding.AStar do
@moduledoc """
A* (A-Star) search algorithm for optimal pathfinding with heuristic guidance.
A* combines Dijkstra's algorithm with a heuristic function to efficiently find
the shortest path in weighted graphs. It prioritizes exploration toward the goal
using the evaluation function: f(n) = g(n) + h(n).
## Algorithm Characteristics
- **Time Complexity**: O((V + E) log V) with a good heuristic
- **Space Complexity**: O(V)
- **Requirements**: Non-negative edge weights, admissible heuristic
- **Optimality**: Optimal if heuristic is admissible (never overestimates)
## The Heuristic Function
The heuristic `h(n)` estimates the cost from node `n` to the goal. For A* to be
optimal, the heuristic must be:
- **Admissible**: Never overestimates the true cost
- **Consistent**: h(n) ≤ c(n,n') + h(n') for all edges
## When to Use
- When you have a good heuristic (e.g., Euclidean distance on grids)
- Pathfinding in games and robotics
- When the goal is known and you want faster search than Dijkstra
## Examples
# Grid pathfinding with Manhattan distance heuristic
heuristic = fn {x1, y1}, {x2, y2} -> abs(x1-x2) + abs(y1-y2) end
Yog.Pathfinding.AStar.a_star(graph, start, goal, 0, &(&1+&2), &Integer.compare/2, heuristic)
"""
alias Yog.Pathfinding.Utils
@typedoc "Result type for shortest path queries"
@type path_result(weight) :: {:some, Utils.path(weight)} | :none
# ============================================================
# Keyword-style API (for Pathfinding module delegation)
# ============================================================
@doc """
Find shortest path using A* with keyword options.
## Options
* `:in` - The graph to search
* `:from` - Starting node
* `:to` - Target node
* `:zero` - Identity value for the weight type
* `:add` - Function to add two weights
* `:compare` - Function to compare weights (`:lt`, `:eq`, `:gt`)
* `:heuristic` - Function estimating cost to goal: `fn(node) -> cost`
## Examples
Yog.Pathfinding.AStar.a_star(
in: graph,
from: :a,
to: :c,
zero: 0,
add: &(&1 + &2),
compare: &Integer.compare/2,
heuristic: fn node -> estimate_to_goal(node) end
)
"""
@spec a_star(keyword()) :: path_result(any())
def a_star(opts) do
graph = Keyword.fetch!(opts, :in)
from = Keyword.fetch!(opts, :from)
to = Keyword.fetch!(opts, :to)
zero = Keyword.fetch!(opts, :zero)
add = Keyword.fetch!(opts, :add)
compare = Keyword.fetch!(opts, :compare)
heuristic = Keyword.fetch!(opts, :heuristic)
a_star(graph, from, to, zero, add, compare, heuristic)
end
@doc """
Implicit A* using keyword options.
## Options
* `:from` - Starting state
* `:successors_with_cost` - Function returning neighbors with costs
* `:is_goal` - Function to check if a state is the goal
* `:zero` - Identity value for the weight type
* `:add` - Function to add two weights
* `:compare` - Function to compare weights
* `:heuristic` - Function estimating cost to goal: `fn(state) -> cost`
## Examples
Yog.Pathfinding.AStar.implicit_a_star(
from: 1,
successors_with_cost: fn n -> [{n+1, 1}] end,
is_goal: fn n -> n == 10 end,
zero: 0,
add: &(&1 + &2),
compare: &Integer.compare/2,
heuristic: fn n -> 10 - n end
)
"""
@spec implicit_a_star(keyword()) :: {:some, any()} | :none
def implicit_a_star(opts) do
from = Keyword.fetch!(opts, :from)
successors = Keyword.fetch!(opts, :successors_with_cost)
is_goal = Keyword.fetch!(opts, :is_goal)
zero = Keyword.fetch!(opts, :zero)
add = Keyword.fetch!(opts, :add)
compare = Keyword.fetch!(opts, :compare)
heuristic = Keyword.fetch!(opts, :heuristic)
implicit_a_star(from, successors, is_goal, zero, add, compare, heuristic)
end
@doc """
Implicit A* with key function using keyword options.
## Options
* `:from` - Starting state
* `:successors_with_cost` - Function returning neighbors with costs
* `:visited_by` - Function to extract a key for visited tracking
* `:is_goal` - Function to check if a state is the goal
* `:zero` - Identity value for the weight type
* `:add` - Function to add two weights
* `:compare` - Function to compare weights
* `:heuristic` - Function estimating cost to goal: `fn(state) -> cost`
"""
@spec implicit_a_star_by(keyword()) :: {:some, any()} | :none
def implicit_a_star_by(opts) do
from = Keyword.fetch!(opts, :from)
successors = Keyword.fetch!(opts, :successors_with_cost)
visited_by = Keyword.fetch!(opts, :visited_by)
is_goal = Keyword.fetch!(opts, :is_goal)
zero = Keyword.fetch!(opts, :zero)
add = Keyword.fetch!(opts, :add)
compare = Keyword.fetch!(opts, :compare)
heuristic = Keyword.fetch!(opts, :heuristic)
implicit_a_star_by(from, successors, visited_by, is_goal, zero, add, compare, heuristic)
end
# ============================================================
# Direct API
# ============================================================
@doc """
Find the shortest path using A* with a heuristic function.
## Parameters
* `graph` - The graph to search
* `from` - Starting node
* `to` - Target node
* `zero` - Identity value for the weight type
* `add` - Function to add two weights
* `compare` - Function to compare weights (`:lt`, `:eq`, `:gt`)
* `heuristic` - Function `fn(node, goal) -> cost` estimating cost to goal
## Returns
* `{:some, path}` - A `Path` struct containing the nodes and total weight
* `:none` - No path exists between the nodes
## Examples
iex> graph = Yog.directed()
...> |> Yog.add_node(:a, nil)
...> |> Yog.add_node(:b, nil)
...> |> Yog.add_node(:c, nil)
...> |> Yog.add_edge!(:a, :b, 4)
...> |> Yog.add_edge!(:b, :c, 1)
iex> # Admissible heuristic (never overestimates)
iex> h = fn _, _ -> 0 end # Zero heuristic = Dijkstra
iex> compare = fn a, b -> if a < b, do: :lt, else: (if a > b, do: :gt, else: :eq) end
iex> Yog.Pathfinding.AStar.a_star(graph, :a, :c, 0, &(&1 + &2), compare, h)
{:some, {:path, [:a, :b, :c], 5}}
iex> # Grid with Manhattan distance heuristic
iex> grid = Yog.directed()
...> |> Yog.add_edge!({0,0}, {1,0}, 1)
...> |> Yog.add_edge!({1,0}, {2,0}, 1)
iex> manhattan = fn {x1, y1}, {x2, y2} -> abs(x1-x2) + abs(y1-y2) end
iex> compare = fn a, b -> if a < b, do: :lt, else: (if a > b, do: :gt, else: :eq) end
iex> Yog.Pathfinding.AStar.a_star(grid, {0,0}, {2,0}, 0, &(&1+&2), compare, manhattan)
{:some, {:path, [{0,0}, {1,0}, {2,0}], 2}}
"""
@spec a_star(
Yog.graph(),
Yog.node_id(),
Yog.node_id(),
weight,
(weight, weight -> weight),
(weight, weight -> :lt | :eq | :gt),
(Yog.node_id(), Yog.node_id() -> weight)
) :: path_result(weight)
when weight: var
def a_star(graph, from, to, zero, add, compare, heuristic) do
gleam_compare = fn a, b ->
case compare.(a, b) do
:lt -> :lt
:eq -> :eq
:gt -> :gt
end
end
case :yog@pathfinding@a_star.a_star(graph, from, to, zero, add, gleam_compare, heuristic) do
:none ->
:none
{:some, {:path, nodes, weight}} ->
{:some, Utils.path(nodes, weight)}
end
end
@doc """
Find the shortest path using A* with integer weights.
Uses built-in integer arithmetic for efficient computation.
## Examples
iex> graph = Yog.directed()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge!(1, 2, 4)
...> |> Yog.add_edge!(2, 3, 1)
iex> h = fn _, _ -> 0 end
iex> Yog.Pathfinding.AStar.a_star_int(graph, 1, 3, h)
{:some, {:path, [1, 2, 3], 5}}
"""
@spec a_star_int(Yog.graph(), Yog.node_id(), Yog.node_id(), (Yog.node_id(), Yog.node_id() ->
integer())) ::
path_result(integer())
def a_star_int(graph, from, to, heuristic) do
case :yog@pathfinding@a_star.a_star_int(graph, from, to, heuristic) do
:none ->
:none
{:some, {:path, nodes, weight}} ->
{:some, Utils.path(nodes, weight)}
end
end
@doc """
Find the shortest path using A* with float weights.
Uses built-in float arithmetic for efficient computation.
## Examples
iex> graph = Yog.directed()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge!(1, 2, 4.5)
...> |> Yog.add_edge!(2, 3, 1.5)
iex> h = fn _, _ -> 0.0 end
iex> Yog.Pathfinding.AStar.a_star_float(graph, 1, 3, h)
{:some, {:path, [1, 2, 3], 6.0}}
"""
@spec a_star_float(Yog.graph(), Yog.node_id(), Yog.node_id(), (Yog.node_id(), Yog.node_id() ->
float())) ::
path_result(float())
def a_star_float(graph, from, to, heuristic) do
case :yog@pathfinding@a_star.a_star_float(graph, from, to, heuristic) do
:none ->
:none
{:some, {:path, nodes, weight}} ->
{:some, Utils.path(nodes, weight)}
end
end
@doc """
Run A* on an implicit (generated) graph.
Similar to implicit Dijkstra, but uses a heuristic to guide search toward the goal.
## Parameters
* `from` - Starting state
* `successors` - Function `state -> [{neighbor, cost}]`
* `is_goal` - Function `state -> boolean` to check if goal reached
* `zero` - Identity value for the weight type
* `add` - Function to add two weights
* `compare` - Function to compare weights
* `heuristic` - Function `fn(state) -> cost` estimating cost to goal
## Returns
* `{:some, cost}` - Minimum cost to reach goal
* `:none` - Goal is unreachable
## Examples
# Search with heuristic guidance
iex> successors = fn
...> 1 -> [{2, 1}]
...> 2 -> [{3, 2}]
...> 3 -> [{4, 3}]
...> 4 -> []
...> end
iex> # Heuristic: distance to goal (node 4)
iex> h = fn n -> 4 - n end
iex> compare = fn a, b -> if a < b, do: :lt, else: (if a > b, do: :gt, else: :eq) end
iex> Yog.Pathfinding.AStar.implicit_a_star(
...> 1, successors, fn x -> x == 4 end,
...> 0, &(&1 + &2), compare, h
...> )
{:some, 6}
"""
@spec implicit_a_star(
state,
(state -> [{state, cost}]),
(state -> boolean),
cost,
(cost, cost -> cost),
(cost, cost -> :lt | :eq | :gt),
(state -> cost)
) :: {:some, cost} | :none
when state: var, cost: var
def implicit_a_star(from, successors, is_goal, zero, add, compare, heuristic) do
gleam_compare = fn a, b ->
case compare.(a, b) do
:lt -> :lt
:eq -> :eq
:gt -> :gt
end
end
:yog@pathfinding@a_star.implicit_a_star(
from,
successors,
is_goal,
heuristic,
zero,
add,
gleam_compare
)
end
@doc """
Implicit A* with a key function for visited state tracking.
Similar to `implicit_a_star/7`, but uses a key function for visited tracking.
## Parameters
* `from` - Starting state
* `successors` - Function `state -> [{neighbor, cost}]`
* `key_fn` - Function `state -> key` for visited tracking
* `is_goal` - Function `state -> boolean` to check if goal reached
* `zero` - Identity value for the weight type
* `add` - Function to add two weights
* `compare` - Function to compare weights
* `heuristic` - Function `fn(state) -> cost` estimating cost to goal
## Examples
iex> _successors = fn {x, y, _dir} -> [{{x + 1, y, :east}, 1}, {{x, y + 1, :south}, 1}] end
iex> _key_fn = fn {x, y, _dir} -> {x, y} end
iex> _h = fn {x, y, _} -> (10 - x) + (10 - y) end
iex> _goal_fn = fn {x, y, _} -> x == 10 and y == 10 end
iex> _compare = fn a, b -> if a < b, do: :lt, else: (if a > b, do: :gt, else: :eq) end
iex> #Yog.Pathfinding.AStar.implicit_a_star_by(
...> # {0, 0, :north}, _successors, _key_fn,
...> # _goal_fn, 0, &(&1 + &2), _compare, _h
...> #)
...> # {:some, 20}
"""
@spec implicit_a_star_by(
state,
(state -> [{state, cost}]),
(state -> term()),
(state -> boolean),
cost,
(cost, cost -> cost),
(cost, cost -> :lt | :eq | :gt),
(state -> cost)
) :: {:some, cost} | :none
when state: var, cost: var
def implicit_a_star_by(from, successors, key_fn, is_goal, zero, add, compare, heuristic) do
gleam_compare = fn a, b ->
case compare.(a, b) do
:lt -> :lt
:eq -> :eq
:gt -> :gt
end
end
:yog@pathfinding@a_star.implicit_a_star_by(
from,
successors,
key_fn,
is_goal,
heuristic,
zero,
add,
gleam_compare
)
end
end