Current section

Files

Jump to
yog src yog@pathfinding.erl
Raw

src/yog@pathfinding.erl

-module(yog@pathfinding).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/pathfinding.gleam").
-export([shortest_path/6, single_source_distances/5, a_star/7, bellman_ford/6]).
-export_type([path/1, bellman_ford_result/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type path(FDH) :: {path, list(integer()), FDH}.
-type bellman_ford_result(FDI) :: {shortest_path, path(FDI)} |
negative_cycle |
no_path.
-file("src/yog/pathfinding.gleam", 104).
-spec compare_frontier(
{FDZ, list(integer())},
{FDZ, list(integer())},
fun((FDZ, FDZ) -> gleam@order:order())
) -> gleam@order:order().
compare_frontier(A, B, Cmp) ->
Cmp(erlang:element(1, A), erlang:element(1, B)).
-file("src/yog/pathfinding.gleam", 114).
?DOC(
" Helper to determine if a node should be explored based on distance comparison.\n"
" Returns True if the node hasn't been visited or if the new distance is shorter.\n"
).
-spec should_explore_node(
gleam@dict:dict(integer(), FEC),
integer(),
FEC,
fun((FEC, FEC) -> gleam@order:order())
) -> boolean().
should_explore_node(Visited, Node, New_dist, Compare) ->
case gleam_stdlib:map_get(Visited, Node) of
{ok, Prev_dist} ->
case Compare(New_dist, Prev_dist) of
lt ->
true;
_ ->
false
end;
{error, nil} ->
true
end.
-file("src/yog/pathfinding.gleam", 56).
-spec do_dijkstra(
yog@model:graph(any(), FDQ),
integer(),
yog@internal@heap:heap({FDQ, list(integer())}),
gleam@dict:dict(integer(), FDQ),
fun((FDQ, FDQ) -> FDQ),
fun((FDQ, FDQ) -> gleam@order:order())
) -> gleam@option:option(path(FDQ)).
do_dijkstra(Graph, Goal, Frontier, Visited, Add, Compare) ->
case yog@internal@heap:find_min(Frontier) of
{error, nil} ->
none;
{ok, {Dist, [Current | _] = Path}} ->
Rest_frontier = begin
_pipe = yog@internal@heap:delete_min(
Frontier,
fun(A, B) -> compare_frontier(A, B, Compare) end
),
gleam@result:unwrap(_pipe, yog@internal@heap:new())
end,
case Current =:= Goal of
true ->
{some, {path, lists:reverse(Path), Dist}};
false ->
Should_explore = should_explore_node(
Visited,
Current,
Dist,
Compare
),
case Should_explore of
false ->
do_dijkstra(
Graph,
Goal,
Rest_frontier,
Visited,
Add,
Compare
);
true ->
New_visited = gleam@dict:insert(
Visited,
Current,
Dist
),
Next_frontier = begin
_pipe@1 = yog@model:successors(Graph, Current),
gleam@list:fold(
_pipe@1,
Rest_frontier,
fun(H, Neighbor) ->
{Next_id, Weight} = Neighbor,
yog@internal@heap:insert(
H,
{Add(Dist, Weight),
[Next_id | Path]},
fun(A@1, B@1) ->
compare_frontier(
A@1,
B@1,
Compare
)
end
)
end
)
end,
do_dijkstra(
Graph,
Goal,
Next_frontier,
New_visited,
Add,
Compare
)
end
end;
{ok, _} ->
none
end.
-file("src/yog/pathfinding.gleam", 39).
?DOC(
" Finds the shortest path between two nodes using Dijkstra's algorithm.\n"
"\n"
" Works with non-negative edge weights only. For negative weights, use `bellman_ford`.\n"
"\n"
" **Time Complexity:** O((V + E) log V) with heap\n"
"\n"
" ## Parameters\n"
"\n"
" - `zero`: The identity element for addition (e.g., 0 for integers)\n"
" - `add`: Function to add two weights\n"
" - `compare`: Function to compare two weights\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" pathfinding.shortest_path(\n"
" in: graph,\n"
" from: 1,\n"
" to: 5,\n"
" with_zero: 0,\n"
" with_add: int.add,\n"
" with_compare: int.compare\n"
" )\n"
" // => Some(Path([1, 2, 5], 15))\n"
" ```\n"
).
-spec shortest_path(
yog@model:graph(any(), FDK),
integer(),
integer(),
FDK,
fun((FDK, FDK) -> FDK),
fun((FDK, FDK) -> gleam@order:order())
) -> gleam@option:option(path(FDK)).
shortest_path(Graph, Start, Goal, Zero, Add, Compare) ->
Frontier = begin
_pipe = yog@internal@heap:new(),
yog@internal@heap:insert(
_pipe,
{Zero, [Start]},
fun(A, B) -> compare_frontier(A, B, Compare) end
)
end,
do_dijkstra(Graph, Goal, Frontier, maps:new(), Add, Compare).
-file("src/yog/pathfinding.gleam", 189).
-spec do_single_source_dijkstra(
yog@model:graph(any(), FEM),
yog@internal@heap:heap({FEM, integer()}),
gleam@dict:dict(integer(), FEM),
fun((FEM, FEM) -> FEM),
fun((FEM, FEM) -> gleam@order:order())
) -> gleam@dict:dict(integer(), FEM).
do_single_source_dijkstra(Graph, Frontier, Distances, Add, Compare) ->
case yog@internal@heap:find_min(Frontier) of
{error, nil} ->
Distances;
{ok, {Dist, Current}} ->
Rest_frontier = begin
_pipe = yog@internal@heap:delete_min(
Frontier,
fun(A, B) ->
Compare(erlang:element(1, A), erlang:element(1, B))
end
),
gleam@result:unwrap(_pipe, yog@internal@heap:new())
end,
Should_explore = should_explore_node(
Distances,
Current,
Dist,
Compare
),
case Should_explore of
false ->
do_single_source_dijkstra(
Graph,
Rest_frontier,
Distances,
Add,
Compare
);
true ->
New_distances = gleam@dict:insert(Distances, Current, Dist),
Next_frontier = begin
_pipe@1 = yog@model:successors(Graph, Current),
gleam@list:fold(
_pipe@1,
Rest_frontier,
fun(H, Neighbor) ->
{Next_id, Weight} = Neighbor,
yog@internal@heap:insert(
H,
{Add(Dist, Weight), Next_id},
fun(A@1, B@1) ->
Compare(
erlang:element(1, A@1),
erlang:element(1, B@1)
)
end
)
end
)
end,
do_single_source_dijkstra(
Graph,
Next_frontier,
New_distances,
Add,
Compare
)
end
end.
-file("src/yog/pathfinding.gleam", 175).
?DOC(
" Computes shortest distances from a source node to all reachable nodes.\n"
"\n"
" Returns a dictionary mapping each reachable node to its shortest distance\n"
" from the source. Unreachable nodes are not included in the result.\n"
"\n"
" This is useful when you need distances to multiple destinations, or want\n"
" to find the closest target among many options. More efficient than running\n"
" `shortest_path` multiple times.\n"
"\n"
" **Time Complexity:** O((V + E) log V) with heap\n"
"\n"
" ## Parameters\n"
"\n"
" - `zero`: The identity element for addition (e.g., 0 for integers)\n"
" - `add`: Function to add two weights\n"
" - `compare`: Function to compare two weights\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" // Find distances from node 1 to all reachable nodes\n"
" let distances = pathfinding.single_source_distances(\n"
" in: graph,\n"
" from: 1,\n"
" with_zero: 0,\n"
" with_add: int.add,\n"
" with_compare: int.compare\n"
" )\n"
" // => dict.from_list([#(1, 0), #(2, 5), #(3, 8), #(4, 15)])\n"
"\n"
" // Find closest target among many options\n"
" let targets = [10, 20, 30]\n"
" let closest = targets\n"
" |> list.filter_map(fn(t) { dict.get(distances, t) })\n"
" |> list.sort(int.compare)\n"
" |> list.first\n"
" ```\n"
"\n"
" ## Use Cases\n"
"\n"
" - Finding nearest target among multiple options\n"
" - Computing distance maps for game AI\n"
" - Network routing table generation\n"
" - Graph analysis (centrality measures)\n"
" - Reverse pathfinding (with `transform.transpose`)\n"
).
-spec single_source_distances(
yog@model:graph(any(), FEG),
integer(),
FEG,
fun((FEG, FEG) -> FEG),
fun((FEG, FEG) -> gleam@order:order())
) -> gleam@dict:dict(integer(), FEG).
single_source_distances(Graph, Source, Zero, Add, Compare) ->
Frontier = begin
_pipe = yog@internal@heap:new(),
yog@internal@heap:insert(
_pipe,
{Zero, Source},
fun(A, B) -> Compare(erlang:element(1, A), erlang:element(1, B)) end
)
end,
do_single_source_dijkstra(Graph, Frontier, maps:new(), Add, Compare).
-file("src/yog/pathfinding.gleam", 292).
-spec do_a_star(
yog@model:graph(any(), FLL),
integer(),
yog@internal@heap:heap({FLI, FLI, list(integer())}),
gleam@dict:dict(integer(), FLI),
fun((FLI, FLL) -> FLI),
fun((FLI, FLI) -> gleam@order:order()),
fun((integer(), integer()) -> FLL)
) -> gleam@option:option(path(FLI)).
do_a_star(Graph, Goal, Frontier, Visited, Add, Compare, H) ->
case yog@internal@heap:find_min(Frontier) of
{error, nil} ->
none;
{ok, {_, Dist, [Current | _] = Path}} ->
Rest_frontier = begin
_pipe = yog@internal@heap:delete_min(
Frontier,
fun(A, B) ->
Compare(erlang:element(1, A), erlang:element(1, B))
end
),
gleam@result:unwrap(_pipe, yog@internal@heap:new())
end,
case Current =:= Goal of
true ->
{some, {path, lists:reverse(Path), Dist}};
false ->
Should_explore = should_explore_node(
Visited,
Current,
Dist,
Compare
),
case Should_explore of
false ->
do_a_star(
Graph,
Goal,
Rest_frontier,
Visited,
Add,
Compare,
H
);
true ->
New_visited = gleam@dict:insert(
Visited,
Current,
Dist
),
Next_frontier = begin
_pipe@1 = yog@model:successors(Graph, Current),
gleam@list:fold(
_pipe@1,
Rest_frontier,
fun(Acc_h, Neighbor) ->
{Next_id, Weight} = Neighbor,
Next_dist = Add(Dist, Weight),
F_score = Add(
Next_dist,
H(Next_id, Goal)
),
yog@internal@heap:insert(
Acc_h,
{F_score,
Next_dist,
[Next_id | Path]},
fun(A@1, B@1) ->
Compare(
erlang:element(1, A@1),
erlang:element(1, B@1)
)
end
)
end
)
end,
do_a_star(
Graph,
Goal,
Next_frontier,
New_visited,
Add,
Compare,
H
)
end
end;
_ ->
none
end.
-file("src/yog/pathfinding.gleam", 274).
?DOC(
" Finds the shortest path using A* search with a heuristic function.\n"
"\n"
" A* is more efficient than Dijkstra when you have a good heuristic estimate\n"
" of the remaining distance to the goal. The heuristic must be admissible\n"
" (never overestimate the actual distance) to guarantee finding the shortest path.\n"
"\n"
" **Time Complexity:** O((V + E) log V), but often faster than Dijkstra in practice\n"
"\n"
" ## Parameters\n"
"\n"
" - `heuristic`: A function that estimates distance from any node to the goal.\n"
" Must be admissible (h(n) ≤ actual distance) to guarantee shortest path.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" // Manhattan distance heuristic for grid\n"
" let h = fn(node, goal) {\n"
" int.absolute_value(node.x - goal.x) + int.absolute_value(node.y - goal.y)\n"
" }\n"
"\n"
" pathfinding.a_star(\n"
" in: graph,\n"
" from: start,\n"
" to: goal,\n"
" with_zero: 0,\n"
" with_add: int.add,\n"
" with_compare: int.compare,\n"
" heuristic: h\n"
" )\n"
" ```\n"
).
-spec a_star(
yog@model:graph(any(), FEV),
integer(),
integer(),
FEV,
fun((FEV, FEV) -> FEV),
fun((FEV, FEV) -> gleam@order:order()),
fun((integer(), integer()) -> FEV)
) -> gleam@option:option(path(FEV)).
a_star(Graph, Start, Goal, Zero, Add, Compare, H) ->
Initial_f = H(Start, Goal),
Frontier = begin
_pipe = yog@internal@heap:new(),
yog@internal@heap:insert(
_pipe,
{Initial_f, Zero, [Start]},
fun(A, B) -> Compare(erlang:element(1, A), erlang:element(1, B)) end
)
end,
do_a_star(Graph, Goal, Frontier, maps:new(), Add, Compare, H).
-file("src/yog/pathfinding.gleam", 428).
-spec relaxation_passes(
yog@model:graph(any(), FFO),
list(integer()),
gleam@dict:dict(integer(), FFO),
gleam@dict:dict(integer(), integer()),
integer(),
fun((FFO, FFO) -> FFO),
fun((FFO, FFO) -> gleam@order:order())
) -> {gleam@dict:dict(integer(), FFO), gleam@dict:dict(integer(), integer())}.
relaxation_passes(
Graph,
Nodes,
Distances,
Predecessors,
Remaining,
Add,
Compare
) ->
case Remaining =< 0 of
true ->
{Distances, Predecessors};
false ->
{New_distances, New_predecessors} = gleam@list:fold(
Nodes,
{Distances, Predecessors},
fun(Acc, U) ->
{Dists, Preds} = Acc,
case gleam_stdlib:map_get(Dists, U) of
{error, nil} ->
Acc;
{ok, U_dist} ->
Neighbors = yog@model:successors(Graph, U),
gleam@list:fold(
Neighbors,
{Dists, Preds},
fun(Inner_acc, Edge) ->
{V, Weight} = Edge,
{Curr_dists, Curr_preds} = Inner_acc,
New_dist = Add(U_dist, Weight),
case gleam_stdlib:map_get(Curr_dists, V) of
{error, nil} ->
{gleam@dict:insert(
Curr_dists,
V,
New_dist
),
gleam@dict:insert(
Curr_preds,
V,
U
)};
{ok, V_dist} ->
case Compare(New_dist, V_dist) of
lt ->
{gleam@dict:insert(
Curr_dists,
V,
New_dist
),
gleam@dict:insert(
Curr_preds,
V,
U
)};
_ ->
Inner_acc
end
end
end
)
end
end
),
relaxation_passes(
Graph,
Nodes,
New_distances,
New_predecessors,
Remaining - 1,
Add,
Compare
)
end.
-file("src/yog/pathfinding.gleam", 492).
-spec has_negative_cycle(
yog@model:graph(any(), FGB),
list(integer()),
gleam@dict:dict(integer(), FGB),
fun((FGB, FGB) -> FGB),
fun((FGB, FGB) -> gleam@order:order())
) -> boolean().
has_negative_cycle(Graph, Nodes, Distances, Add, Compare) ->
gleam@list:any(Nodes, fun(U) -> case gleam_stdlib:map_get(Distances, U) of
{error, nil} ->
false;
{ok, U_dist} ->
_pipe = yog@model:successors(Graph, U),
gleam@list:any(
_pipe,
fun(Edge) ->
{V, Weight} = Edge,
New_dist = Add(U_dist, Weight),
case gleam_stdlib:map_get(Distances, V) of
{error, nil} ->
false;
{ok, V_dist} ->
case Compare(New_dist, V_dist) of
lt ->
true;
_ ->
false
end
end
end
)
end end).
-file("src/yog/pathfinding.gleam", 524).
-spec reconstruct_path(
gleam@dict:dict(integer(), integer()),
integer(),
integer(),
list(integer())
) -> {ok, list(integer())} | {error, nil}.
reconstruct_path(Predecessors, Start, Current, Acc) ->
case Current =:= Start of
true ->
{ok, Acc};
false ->
case gleam_stdlib:map_get(Predecessors, Current) of
{error, nil} ->
{error, nil};
{ok, Pred} ->
reconstruct_path(Predecessors, Start, Pred, [Pred | Acc])
end
end.
-file("src/yog/pathfinding.gleam", 382).
?DOC(
" Finds shortest path with support for negative edge weights using Bellman-Ford.\n"
"\n"
" Unlike Dijkstra and A*, this algorithm can handle negative edge weights.\n"
" It also detects negative cycles reachable from the source node.\n"
"\n"
" **Time Complexity:** O(VE) where V is vertices and E is edges\n"
"\n"
" ## Returns\n"
"\n"
" - `ShortestPath(path)`: If a valid shortest path exists\n"
" - `NegativeCycle`: If a negative cycle is reachable from the start node\n"
" - `NoPath`: If no path exists from start to goal\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" pathfinding.bellman_ford(\n"
" in: graph,\n"
" from: 1,\n"
" to: 5,\n"
" with_zero: 0,\n"
" with_add: int.add,\n"
" with_compare: int.compare\n"
" )\n"
" // => ShortestPath(Path([1, 3, 5], -2)) // Can have negative total weight\n"
" // or NegativeCycle // If cycle detected\n"
" // or NoPath // If unreachable\n"
" ```\n"
).
-spec bellman_ford(
yog@model:graph(any(), FFJ),
integer(),
integer(),
FFJ,
fun((FFJ, FFJ) -> FFJ),
fun((FFJ, FFJ) -> gleam@order:order())
) -> bellman_ford_result(FFJ).
bellman_ford(Graph, Start, Goal, Zero, Add, Compare) ->
All_nodes = yog@model:all_nodes(Graph),
Initial_distances = maps:from_list([{Start, Zero}]),
Initial_predecessors = maps:new(),
Node_count = erlang:length(All_nodes),
{Distances, Predecessors} = relaxation_passes(
Graph,
All_nodes,
Initial_distances,
Initial_predecessors,
Node_count - 1,
Add,
Compare
),
case has_negative_cycle(Graph, All_nodes, Distances, Add, Compare) of
true ->
negative_cycle;
false ->
case gleam_stdlib:map_get(Distances, Goal) of
{error, nil} ->
no_path;
{ok, Dist} ->
case reconstruct_path(Predecessors, Start, Goal, [Goal]) of
{ok, Path} ->
{shortest_path, {path, Path, Dist}};
{error, nil} ->
no_path
end
end
end.