Current section

Files

Jump to
yog_ex lib yog builder grid_graph.ex
Raw

lib/yog/builder/grid_graph.ex

defmodule Yog.Builder.GridGraph do
@moduledoc """
Grid graph builder result.
A grid graph is a structured graph where nodes are arranged in a 2D grid
with edges connecting adjacent cells according to a specified topology.
## Fields
- `graph` - The underlying Yog graph
- `rows` - Number of rows in the grid
- `cols` - Number of columns in the grid
- `topology` - Connection pattern (`:rook`, `:queen`, `:king`, etc.)
- `predicate` - Optional function to filter valid cells
## Topologies
- `:rook` - 4-connected (up, down, left, right)
- `:queen` - 8-connected (rook + diagonals)
- `:king` - Same as queen
- `:bishop` - Diagonal connections only
## Examples
iex> graph = Yog.undirected() |> Yog.add_node(0, "data")
iex> grid = %Yog.Builder.GridGraph{
...> graph: graph,
...> rows: 3,
...> cols: 3,
...> topology: :rook
...> }
iex> grid.rows
3
iex> Yog.Builder.GridGraph.coord_to_id(grid, 0, 0)
0
"""
@enforce_keys [:graph, :rows, :cols]
defstruct [:graph, :rows, :cols, topology: :rook, predicate: nil]
@type t :: %__MODULE__{
graph: Yog.graph(),
rows: non_neg_integer(),
cols: non_neg_integer(),
topology: atom(),
predicate: (non_neg_integer(), non_neg_integer() -> boolean()) | nil
}
@doc """
Creates a new grid graph result.
## Examples
iex> graph = Yog.undirected()
iex> grid = Yog.Builder.GridGraph.new(graph, 3, 4)
iex> grid.rows
3
iex> grid.cols
4
iex> grid.topology
:rook
"""
@spec new(Yog.graph(), non_neg_integer(), non_neg_integer()) :: t()
def new(graph, rows, cols) do
%__MODULE__{
graph: graph,
rows: rows,
cols: cols
}
end
@doc """
Creates a new grid graph result with topology.
## Examples
iex> graph = Yog.undirected()
iex> grid = Yog.Builder.GridGraph.new(graph, 3, 4, :queen)
iex> grid.topology
:queen
"""
@spec new(Yog.graph(), non_neg_integer(), non_neg_integer(), atom()) :: t()
def new(graph, rows, cols, topology) do
%__MODULE__{
graph: graph,
rows: rows,
cols: cols,
topology: topology
}
end
@doc """
Unwraps the grid graph to return the plain graph.
## Examples
iex> graph = Yog.undirected()
iex> grid = %Yog.Builder.GridGraph{graph: graph, rows: 2, cols: 2}
iex> Yog.Builder.GridGraph.to_graph(grid) == graph
true
"""
@spec to_graph(t()) :: Yog.graph()
def to_graph(%__MODULE__{graph: graph}), do: graph
@doc """
Gets the cell data at a specific grid coordinate.
Returns `{:ok, data}` if the cell exists, or `{:error, nil}` otherwise.
## Examples
iex> graph = Yog.undirected() |> Yog.add_node(4, "some_data")
iex> grid = %Yog.Builder.GridGraph{graph: graph, rows: 3, cols: 3}
iex> Yog.Builder.GridGraph.get_cell(grid, 1, 1)
{:ok, "some_data"}
iex> Yog.Builder.GridGraph.get_cell(grid, 10, 10)
{:error, nil}
"""
@spec get_cell(t(), non_neg_integer(), non_neg_integer()) :: {:ok, term()} | {:error, nil}
def get_cell(%__MODULE__{graph: graph, rows: rows, cols: cols}, row, col) do
if valid_coord?(rows, cols, row, col) do
node_id = coord_to_id_raw(cols, row, col)
case Yog.Model.node(graph, node_id) do
nil -> {:error, nil}
data -> {:ok, data}
end
else
{:error, nil}
end
end
@doc """
Converts grid coordinates to a node ID.
The default mapping is: `row * cols + col`
## Examples
iex> graph = Yog.undirected()
iex> grid = %Yog.Builder.GridGraph{graph: graph, rows: 3, cols: 4}
iex> Yog.Builder.GridGraph.coord_to_id(grid, 0, 0)
0
iex> Yog.Builder.GridGraph.coord_to_id(grid, 1, 2)
6
iex> Yog.Builder.GridGraph.coord_to_id(grid, 2, 3)
11
"""
@spec coord_to_id(t(), non_neg_integer(), non_neg_integer()) :: Yog.Model.node_id()
def coord_to_id(%__MODULE__{cols: cols}, row, col) do
coord_to_id_raw(cols, row, col)
end
@doc """
Converts a node ID back to grid coordinates.
Returns `{row, col}`.
## Examples
iex> graph = Yog.undirected()
iex> grid = %Yog.Builder.GridGraph{graph: graph, rows: 3, cols: 4}
iex> Yog.Builder.GridGraph.id_to_coord(grid, 0)
{0, 0}
iex> Yog.Builder.GridGraph.id_to_coord(grid, 6)
{1, 2}
iex> Yog.Builder.GridGraph.id_to_coord(grid, 11)
{2, 3}
"""
@spec id_to_coord(t(), Yog.Model.node_id()) :: {non_neg_integer(), non_neg_integer()}
def id_to_coord(%__MODULE__{cols: cols}, node_id) when is_integer(node_id) do
row = div(node_id, cols)
col = rem(node_id, cols)
{row, col}
end
@doc """
Checks if a coordinate is within the grid bounds.
## Examples
iex> graph = Yog.undirected()
iex> grid = %Yog.Builder.GridGraph{graph: graph, rows: 3, cols: 4}
iex> Yog.Builder.GridGraph.valid_coord?(grid, 1, 2)
true
iex> Yog.Builder.GridGraph.valid_coord?(grid, 5, 5)
false
"""
@spec valid_coord?(t(), non_neg_integer(), non_neg_integer()) :: boolean()
def valid_coord?(%__MODULE__{rows: rows, cols: cols}, row, col) do
valid_coord?(rows, cols, row, col)
end
@doc """
Backward compatibility: convert from legacy map format.
"""
@spec from_map(map()) :: t()
def from_map(%{graph: g, rows: r, cols: c} = map) do
topology = Map.get(map, :topology, :rook)
predicate = Map.get(map, :predicate)
%__MODULE__{
graph: g,
rows: r,
cols: c,
topology: topology,
predicate: predicate
}
end
@doc """
Convert to legacy map format.
"""
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = grid) do
%{
graph: grid.graph,
rows: grid.rows,
cols: grid.cols
}
end
# ============================================================
# Private Helpers
# ============================================================
defp coord_to_id_raw(cols, row, col), do: row * cols + col
defp valid_coord?(rows, cols, row, col) do
row >= 0 and row < rows and col >= 0 and col < cols
end
end