Current section
Files
Jump to
Current section
Files
lib/clarity/graph/util.ex
# credo:disable-for-this-file Credo.Check.Refactor.Nesting
defmodule Clarity.Graph.Util do
@moduledoc false
@doc """
Return a MapSet of all vertices reachable from `root_vertex` within
**`max_outgoing_steps` forward hops** *or* **`max_incoming_steps` backward hops**.
The traversal is breadth‑first in each direction, so the overall complexity
remains linear in the size of the resulting vertex set (`O(Vₛ)`).
## Parameters
* `original_graph` – the `:digraph` you want to sample from.
* `root_vertex` – where traversal starts.
* `max_outgoing_steps` – how far to follow *outgoing* edges.
* `max_incoming_steps` – how far to follow *incoming* edges.
"""
@spec vertices_within_steps(
original_graph :: :digraph.graph(),
root_vertex :: :digraph.vertex(),
max_outgoing_steps :: non_neg_integer(),
max_incoming_steps :: non_neg_integer()
) :: MapSet.t(:digraph.vertex())
def vertices_within_steps(original_graph, root_vertex, max_outgoing_steps, max_incoming_steps)
when max_outgoing_steps >= 0 and max_incoming_steps >= 0 do
visited_vertices =
bfs_direction(
original_graph,
MapSet.new([root_vertex]),
root_vertex,
max_outgoing_steps,
&:digraph.out_neighbours/2
)
bfs_direction(
original_graph,
visited_vertices,
root_vertex,
max_incoming_steps,
&:digraph.in_neighbours/2
)
end
@spec bfs_direction(
original_graph :: :digraph.graph(),
visited :: MapSet.t(:digraph.vertex()),
start_vertex :: :digraph.vertex(),
max_steps :: non_neg_integer(),
neighbours_fun :: (:digraph.graph(), :digraph.vertex() -> [:digraph.vertex()])
) :: MapSet.t(:digraph.vertex())
defp bfs_direction(_original_graph, visited, _start_vertex, 0, _), do: visited
defp bfs_direction(original_graph, visited, start_vertex, max_steps, neighbours_fun) do
queue = :queue.in({start_vertex, 0}, :queue.new())
bfs_loop(queue, original_graph, visited, max_steps, neighbours_fun)
end
@spec bfs_loop(
queue :: :queue.queue(),
original_graph :: :digraph.graph(),
visited :: MapSet.t(:digraph.vertex()),
max_steps :: non_neg_integer(),
neighbours_fun :: (:digraph.graph(), :digraph.vertex() -> [:digraph.vertex()])
) :: MapSet.t(:digraph.vertex())
defp bfs_loop(queue, original_graph, visited, max_steps, neighbours_fun) do
case :queue.out(queue) do
{:empty, _} ->
visited
{{:value, {current_vertex, current_depth}}, queue_tail} ->
{updated_queue, updated_visited} =
if current_depth < max_steps do
expand(
current_vertex,
current_depth,
original_graph,
visited,
queue_tail,
neighbours_fun
)
else
{queue_tail, visited}
end
bfs_loop(
updated_queue,
original_graph,
updated_visited,
max_steps,
neighbours_fun
)
end
end
@spec expand(
vertex :: :digraph.vertex(),
depth :: non_neg_integer(),
original_graph :: :digraph.graph(),
visited :: MapSet.t(:digraph.vertex()),
queue :: :queue.queue(),
neighbours_fun :: (:digraph.graph(), :digraph.vertex() -> [:digraph.vertex()])
) :: {:queue.queue(), MapSet.t(:digraph.vertex())}
defp expand(vertex, depth, original_graph, visited, queue, neighbours_fun) do
neighbours = neighbours_fun.(original_graph, vertex)
Enum.reduce(neighbours, {queue, visited}, fn neighbour, {q_acc, s_acc} ->
if MapSet.member?(s_acc, neighbour) do
{q_acc, s_acc}
else
{:queue.in({neighbour, depth + 1}, q_acc), MapSet.put(s_acc, neighbour)}
end
end)
end
end