# `Yog.Model`
[🔗](https://github.com/code-shoily/yog_ex/blob/v1.0.0/lib/yog/model.ex#L1)

Core graph data structures and basic operations for the yog library.

This module defines the fundamental `Graph` type and provides all basic operations
for creating and manipulating graphs. The graph uses an adjacency list representation
with dual indexing (both outgoing and incoming edges) for efficient traversal in both
directions.

## Graph Types

- **Directed Graph**: Edges have a direction (one-way relationships)
- **Undirected Graph**: Edges are bidirectional (mutual relationships)

## Type Parameters

- `node_data`: The type of data stored at each node (e.g., `String`, `City`, `Task`)
- `edge_data`: The type of data stored on edges, typically weights (e.g., `Int`, `Float`)

## Quick Start

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge!(1, 2, 10)
    iex> Yog.Model.successors(graph, 1)
    [{2, 10}]
    iex> Yog.Model.node(graph, 1)
    "A"

## Design Notes

The dual-map representation enables O(1) edge existence checks and O(1) transpose
operations, at the cost of increased memory usage and slightly more complex edge
updates.

# `graph`

```elixir
@type graph() :: Yog.Graph.t()
```

A simple graph data structure that can be directed or undirected.

This is an alias for `Yog.Graph.t()`.

# `graph_type`

```elixir
@type graph_type() :: :directed | :undirected
```

The type of graph: `:directed` or `:undirected`.

# `node_id`

```elixir
@type node_id() :: term()
```

Unique identifier for a node in the graph.

# `add_edge`

```elixir
@spec add_edge(
  graph(),
  keyword()
) :: {:ok, graph()} | {:error, String.t()}
```

Adds an edge to the graph.

For directed graphs, adds a single edge from `from` to `to`.
For undirected graphs, adds edges in both directions.

Returns `{:ok, graph}` or `{:error, reason}`.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.directed()
    ...>   |> Yog.add_node(1, "A")
    ...>   |> Yog.add_node(2, "B")
    ...>   |> Yog.add_edge(from: 1, to: 2, with: 10)
    iex> Yog.successors(graph, 1)
    [{2, 10}]

## With pattern matching for chaining

    iex> graph = Yog.directed() |> Yog.add_node(1, "A") |> Yog.add_node(2, "B")
    iex> {:ok, graph} = Yog.add_edge(graph, from: 1, to: 2, with: 10)
    iex> {:ok, graph} = Yog.add_edge(graph, from: 2, to: 1, with: 5)
    iex> Yog.successors(graph, 2)
    [{1, 5}]

# `add_edge`

```elixir
@spec add_edge(graph(), node_id(), node_id(), term()) ::
  {:ok, graph()} | {:error, String.t()}
```

Adds an edge to the graph with the given weight.

For directed graphs, adds a single edge from `src` to `dst`.
For undirected graphs, adds edges in both directions.

Returns `{:error, reason}` if either endpoint node doesn't exist in `graph.nodes`.
Use `add_edge_ensure/5` to auto-create missing nodes with a default value,
or `add_node/3` to explicitly add nodes before adding edges.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> Yog.Model.successors(graph, 1)
    [{2, 10}]

    iex> # Error when nodes don't exist
    iex> Yog.Model.new(:directed) |> Yog.Model.add_edge(1, 2, 10)
    {:error, "Nodes 1 and 2 do not exist"}

# `add_edge!`

```elixir
@spec add_edge!(
  graph(),
  keyword()
) :: graph()
```

Adds an edge to the graph, raising on error.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph = Yog.directed() |> Yog.add_node(1, "A") |> Yog.add_node(2, "B")
    iex> graph = Yog.add_edge!(graph, from: 1, to: 2, with: 10)
    iex> Yog.successors(graph, 1)
    [{2, 10}]

# `add_edge!`

```elixir
@spec add_edge!(graph(), node_id(), node_id(), term()) :: graph()
```

Same as `add_edge/4` but raises on error.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge!(1, 2, 10)
    iex> Yog.Model.successors(graph, 1)
    [{2, 10}]

# `add_edge_ensure`

```elixir
@spec add_edge_ensure(
  graph(),
  keyword()
) :: graph()
```

Ensures both endpoint nodes exist, then adds an edge.

If `from` or `to` is not already in the graph, it is created with
the supplied `default` node data. Existing nodes are left unchanged.

Always succeeds and returns a `Graph` (never fails).
Use this when you want to build graphs quickly without pre-creating nodes.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph =
    ...>   Yog.directed()
    ...>   |> Yog.add_edge_ensure(from: 1, to: 2, with: 10, default: "anon")
    iex> # Nodes 1 and 2 are auto-created with data "anon"
    iex> Yog.successors(graph, 1)
    [{2, 10}]

# `add_edge_ensure`

```elixir
@spec add_edge_ensure(graph(), node_id(), node_id(), term(), term()) :: graph()
```

Ensures both endpoint nodes exist, then adds an edge.

If `src` or `dst` is not already in the graph, it is created with
the supplied `default` node data before the edge is added. Nodes
that already exist are left unchanged. If no default is submitted `nil`
is used as the default node data.

Always succeeds and returns a `Graph` (never fails).

Time complexity: $\mathcal{O}(1)$

## Example

    iex> # Nodes 1 and 2 are created automatically with data "anon"
    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_edge_ensure(1, 2, 10, "anon")
    iex> Yog.Model.node(graph, 1)
    "anon"

    iex> # Existing nodes keep their data
    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_edge_ensure(1, 2, 5, "B")
    iex> {Yog.Model.node(graph, 1), Yog.Model.node(graph, 2)}
    {"A", "B"}

# `add_edge_with`

```elixir
@spec add_edge_with(graph(), node_id(), node_id(), term(), (node_id() -&gt; term())) ::
  graph()
```

Ensures both endpoint nodes exist using a callback, then adds an edge.

If `src` or `dst` is not already in the graph, it is created by
calling the `make_fn` function with the node ID to generate the node data.
Nodes that already exist are left unchanged.

Raises `ArgumentError` if `make_fn` is not an arity-1 function.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> # Nodes 1 and 2 are created automatically with value that's the same as NodeId
    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_edge_with(1, 2, 10, fn x -> x end)
    iex> Yog.Model.node(graph, 1)
    1

    iex> # Existing nodes keep their data; only missing ones get generated data
    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "1")
    ...>   |> Yog.Model.add_edge_with(1, 2, 5, fn n -> to_string(n) <> ":new" end)
    iex> # Node 1 is still "1", node 2 is "2:new"
    iex> {Yog.Model.node(graph, 1), Yog.Model.node(graph, 2)}
    {"1", "2:new"}

# `add_edge_with_combine`

```elixir
@spec add_edge_with_combine(graph(), node_id(), node_id(), term(), (term(), term() -&gt;
                                                                term())) ::
  {:ok, graph()} | {:error, String.t()}
```

Adds an edge, but if an edge already exists between `src` and `dst`,
it combines the new weight with the existing one using `with_combine`.

The combine function receives `(existing_weight, new_weight)` and should
return the combined weight.

Returns `{:error, reason}` if either endpoint node doesn't exist in `graph.nodes`
or if `with_combine` is not an arity-2 function.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> {:ok, graph} = Yog.Model.add_edge_with_combine(graph, 1, 2, 5, &Kernel.+/2)
    iex> # Edge 1->2 now has weight 15 (10 + 5)
    iex> Yog.Model.successors(graph, 1)
    [{2, 15}]

## Use Cases

- **Edge contraction** in graph algorithms (Stoer-Wagner min-cut)
- **Multi-graph support** (adding parallel edges with combined weights)
- **Incremental graph building** (accumulating weights from multiple sources)

# `add_edge_with_combine!`

```elixir
@spec add_edge_with_combine!(graph(), node_id(), node_id(), term(), (term(), term() -&gt;
                                                                 term())) ::
  graph()
```

Same as `add_edge_with_combine/5` but raises on error.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge!(1, 2, 10)
    ...>   |> Yog.Model.add_edge_with_combine!(1, 2, 5, &Kernel.+/2)
    iex> Yog.Model.successors(graph, 1)
    [{2, 15}]

# `add_edges`

```elixir
@spec add_edges(graph(), [{node_id(), node_id(), term()}]) ::
  {:ok, graph()} | {:error, String.t()}
```

Adds multiple edges to the graph in a single operation.

Fails fast on the first edge that references non-existent nodes.
Returns `{:error, reason}` if any endpoint node doesn't exist.

This is more ergonomic than chaining multiple `add_edge` calls
as it only requires unwrapping a single `Result`.

Time complexity: $\mathcal{O}(E)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_node(3, "C")
    ...>   |> Yog.Model.add_edges([{1, 2, 10}, {2, 3, 5}, {1, 3, 15}])
    iex> length(Yog.Model.successors(graph, 1))
    2

# `add_edges!`

```elixir
@spec add_edges!(graph(), [{node_id(), node_id(), term()}]) :: graph()
```

Adds multiple edges to the graph, raising on error.

Time complexity: $\mathcal{O}(E)$

## Example

    iex> graph =
    ...>   Yog.directed()
    ...>   |> Yog.add_node(1, "A")
    ...>   |> Yog.add_node(2, "B")
    ...>   |> Yog.add_node(3, "C")
    ...>   |> Yog.add_edges!([{1, 2, 10}, {2, 3, 5}])
    iex> length(Yog.successors(graph, 1))
    1

# `add_node`

```elixir
@spec add_node(graph(), node_id(), term()) :: graph()
```

Adds a node to the graph with the given ID and data.
If a node with this ID already exists, its data will be replaced.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "Node A")
    ...>   |> Yog.Model.add_node(2, "Node B")
    iex> Yog.Model.order(graph)
    2

# `add_nodes_from`

```elixir
@spec add_nodes_from(graph(), Enumerable.t()) :: graph()
```

Adds multiple nodes to the graph from an iterable.

Accepts:
- A list of node IDs: `[1, 2, 3]`
- A list of `{id, data}` tuples: `[{1, "A"}, {2, "B"}]`
- A map: `%{1 => "A", 2 => "B"}`
- Another `Yog.Graph`: copies all nodes (but not edges)

Existing nodes with the same ID will have their data replaced.

Time complexity: $\mathcal{O}(N)$ where $N$ is the number of nodes added.

## Example

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_nodes_from([1, {2, "B"}])
    iex> Yog.Model.order(graph)
    2
    iex> Yog.Model.node(graph, 2)
    "B"

# `add_simple_edges`

```elixir
@spec add_simple_edges(graph(), [{node_id(), node_id()}]) ::
  {:ok, graph()} | {:error, String.t()}
```

Adds multiple simple edges (weight = 1) to the graph.

Fails fast on the first edge that references non-existent nodes.
Convenient for unweighted graphs where all edges have weight 1.

Time complexity: $\mathcal{O}(E)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_node(3, "C")
    ...>   |> Yog.Model.add_simple_edges([{1, 2}, {2, 3}, {1, 3}])
    iex> Yog.Model.successors(graph, 1) |> Enum.sort()
    [{2, 1}, {3, 1}]

# `add_unweighted_edge`

```elixir
@spec add_unweighted_edge(
  graph(),
  keyword()
) :: {:ok, graph()} | {:error, String.t()}
```

Adds an unweighted edge to the graph.

This is a convenience function for graphs where edges have no meaningful weight.
Uses `nil` as the edge data type.

Returns `{:ok, graph}` or `{:error, reason}` if either endpoint node doesn't exist.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.directed()
    ...>   |> Yog.add_node(1, "A")
    ...>   |> Yog.add_node(2, "B")
    ...>   |> Yog.add_unweighted_edge(from: 1, to: 2)
    iex> Yog.successors(graph, 1)
    [{2, nil}]

# `add_unweighted_edges`

```elixir
@spec add_unweighted_edges(graph(), [{node_id(), node_id()}]) ::
  {:ok, graph()} | {:error, String.t()}
```

Adds multiple unweighted edges (weight = nil) to the graph.

Fails fast on the first edge that references non-existent nodes.
Convenient for graphs where edges carry no weight information.

Time complexity: $\mathcal{O}(E)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_node(3, "C")
    ...>   |> Yog.Model.add_unweighted_edges([{1, 2}, {2, 3}, {1, 3}])
    iex> Yog.Model.successors(graph, 1) |> Enum.sort()
    [{2, nil}, {3, nil}]

# `all_edges`

```elixir
@spec all_edges(graph()) :: [{node_id(), node_id(), term()}]
```

Returns all edges in the graph as triplets `{from, to, weight}`.

For directed graphs, returns all edges.
For undirected graphs, returns each edge only once (where `from <= to`).

Time complexity: $\mathcal{O}(V + E)$

## Examples

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, nil)
    ...>   |> Yog.Model.add_node(2, nil)
    ...>   |> Yog.Model.add_edge!(1, 2, 5)
    iex> Yog.Model.all_edges(graph)
    [{1, 2, 5}]

    iex> graph =
    ...>   Yog.Model.new(:undirected)
    ...>   |> Yog.Model.add_node(1, nil)
    ...>   |> Yog.Model.add_node(2, nil)
    ...>   |> Yog.Model.add_edge!(1, 2, 5)
    iex> edges = Yog.Model.all_edges(graph)
    iex> length(edges)
    1

# `all_nodes`

```elixir
@spec all_nodes(graph()) :: [node_id()]
```

Returns all node IDs in the graph.
This includes all nodes, even isolated nodes with no edges.

Time complexity: $\mathcal{O}(V)$

## Example

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    iex> Yog.Model.all_nodes(graph) |> Enum.sort()
    [1, 2]

# `degree`

```elixir
@spec degree(graph(), node_id()) :: non_neg_integer()
```

Returns the total degree of a node.

For directed graphs, this is the sum of in-degree and out-degree.
For undirected graphs, this counts each edge once (self-loops count as 2).

Time complexity: $\mathcal{O}(1)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:undirected)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> Yog.Model.degree(graph, 1)
    1

# `edge_count`

```elixir
@spec edge_count(graph()) :: integer()
```

Returns the number of edges in the graph.

For undirected graphs, each edge is counted once (the pair {u, v}).
For directed graphs, each directed edge (u -> v) is counted once.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> Yog.Model.edge_count(graph)
    1

# `edge_data`

```elixir
@spec edge_data(graph(), node_id(), node_id()) :: term() | nil
```

Gets the weight/data of an edge between two nodes.
Returns `nil` if the edge doesn't exist.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph = Yog.directed() |> Yog.add_edge_ensure(1, 2, 10, nil)
    iex> Yog.Model.edge_data(graph, 1, 2)
    10

# `edge_data!`

```elixir
@spec edge_data!(graph(), node_id(), node_id()) :: term()
```

Gets the weight/data of an edge between two nodes, raising if not found.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph = Yog.directed() |> Yog.add_edge_ensure(1, 2, 10, nil)
    iex> Yog.Model.edge_data!(graph, 1, 2)
    10

# `has_edge?`

```elixir
@spec has_edge?(graph(), node_id(), node_id()) :: boolean()
```

Checks if the graph contains an edge between `src` and `dst`.

Returns `true` if an edge exists, `false` otherwise.

Time complexity: $\mathcal{O}(1)$

## Examples

    iex> graph = Yog.from_edges(:directed, [{1, 2, 10}, {1, 3, 20}, {3, 4, 2}])
    iex> Yog.Model.has_edge?(graph, 1, 2)
    true
    iex> Yog.Model.has_edge?(graph, 2, 1)
    false

    iex> graph = Yog.from_edges(:undirected, [{1, 2, 10}, {1, 3, 20}])
    iex> Yog.Model.has_edge?(graph, 2, 1)
    true
    iex> Yog.Model.has_edge?(graph, 2, 4)
    false

# `has_node?`

```elixir
@spec has_node?(graph(), node_id()) :: boolean()
```

Checks if the graph contains a node with the given ID.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph = Yog.undirected() |> Yog.add_node(1, nil)
    iex> Yog.Model.has_node?(graph, 1)
    true
    iex> Yog.Model.has_node?(graph, 2)
    false

# `in_degree`

```elixir
@spec in_degree(graph(), node_id()) :: non_neg_integer()
```

Returns the in-degree of a node (number of incoming edges).

For undirected graphs, this returns the total degree (same as `out_degree/2`).

Time complexity: $\mathcal{O}(1)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> Yog.Model.in_degree(graph, 2)
    1
    iex> Yog.Model.in_degree(graph, 1)
    0

# `neighbor_ids`

```elixir
@spec neighbor_ids(graph(), node_id()) :: [node_id()]
```

Returns all neighbor node IDs (without weights).

Time complexity: $\mathcal{O}(\text{deg}(v))$

## Example

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge_ensure(1, 2, 10)
    ...>   |> Yog.Model.add_edge_ensure(1, 3, 20)
    iex> Yog.Model.neighbor_ids(graph, 1) |> Enum.sort()
    [2, 3]

# `neighbors`

```elixir
@spec neighbors(graph(), node_id()) :: [{node_id(), term()}]
```

Gets all nodes connected to the given node, regardless of direction.
Useful for algorithms like finding "connected components".

For undirected graphs, this is equivalent to successors.
For directed graphs, this combines successors and predecessors without duplicates.

Time complexity: $\mathcal{O}(\text{deg}(v))$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_node(3, "C")
    ...>   |> Yog.Model.add_edges([{1, 2, 10}, {3, 1, 20}])
    iex> Yog.Model.neighbors(graph, 1) |> Enum.sort()
    [{2, 10}, {3, 20}]

# `new`

```elixir
@spec new(graph_type()) :: graph()
```

Creates a new empty graph of the specified type (`:directed` or `:undirected`).

Raises `ArgumentError` if graph_type is invalid.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph = Yog.Model.new(:directed)
    iex> Yog.Model.order(graph)
    0

# `node`

```elixir
@spec node(graph(), node_id()) :: term() | nil
```

Gets the data associated with a node.

Returns `nil` if the node doesn't exist.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    iex> Yog.Model.node(graph, 1)
    "A"

# `node_count`

```elixir
@spec node_count(graph()) :: integer()
```

Returns the number of nodes in the graph.
Equivalent to `order/1`.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    iex> Yog.Model.node_count(graph)
    2

# `nodes`

```elixir
@spec nodes(graph()) :: map()
```

Returns all nodes in the graph as a map.

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    iex> nodes = Yog.Model.nodes(graph)
    iex> nodes[1]
    "A"

# `order`

```elixir
@spec order(graph()) :: integer()
```

Returns the number of nodes in the graph (graph order).

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    iex> Yog.Model.order(graph)
    2

# `out_degree`

```elixir
@spec out_degree(graph(), node_id()) :: non_neg_integer()
```

Returns the out-degree of a node (number of outgoing edges).

For undirected graphs, this returns the total degree (same as `in_degree/2`).

Time complexity: $\mathcal{O}(1)$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> Yog.Model.out_degree(graph, 1)
    1
    iex> Yog.Model.out_degree(graph, 2)
    0

# `predecessor_ids`

```elixir
@spec predecessor_ids(graph(), node_id()) :: [node_id()]
```

Returns all predecessor node IDs (without weights).

Time complexity: $\mathcal{O}(\text{deg}_{\text{in}}(v))$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> Yog.Model.predecessor_ids(graph, 2)
    [1]

# `predecessors`

```elixir
@spec predecessors(graph(), node_id()) :: [{node_id(), term()}]
```

Gets nodes you came FROM to reach the given node (predecessors).
Returns a list of tuples containing the source node ID and edge data.

Time complexity: $\mathcal{O}(\text{deg}_{\text{in}}(v))$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> Yog.Model.predecessors(graph, 2)
    [{1, 10}]

# `remove_edge`

```elixir
@spec remove_edge(graph(), node_id(), node_id()) :: graph()
```

Removes a directed edge from `src` to `dst`.

For **directed graphs**, this removes the single directed edge from `src` to `dst`.
For **undirected graphs**, this removes the edges in both directions
(from `src` to `dst` and from `dst` to `src`).

Time complexity: $\mathcal{O}(1)$

## Example

    iex> # Directed graph - removes single directed edge
    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> graph = Yog.Model.remove_edge(graph, 1, 2)
    iex> # Edge 1->2 is removed
    iex> Yog.Model.successors(graph, 1)
    []

    iex> # Undirected graph - removes both directions
    iex> {:ok, graph} =
    ...>   Yog.Model.new(:undirected)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> graph = Yog.Model.remove_edge(graph, 1, 2)
    iex> # Edge between 1 and 2 is fully removed
    iex> Yog.Model.successors(graph, 1)
    []

# `remove_node`

```elixir
@spec remove_node(graph(), node_id()) :: graph()
```

Removes a node and all its connected edges (incoming and outgoing).

Time complexity: $\mathcal{O}(\text{deg}(v))$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_node(3, "C")
    ...>   |> Yog.Model.add_edges([{1, 2, 10}, {2, 3, 20}])
    iex> graph = Yog.Model.remove_node(graph, 2)
    iex> # Node 2 is removed, along with edges 1->2 and 2->3
    iex> Yog.Model.order(graph)
    2

# `successor_ids`

```elixir
@spec successor_ids(graph(), node_id()) :: [node_id()]
```

Returns all successor node IDs (without weights).

Time complexity: $\mathcal{O}(\text{deg}_{\text{out}}(v))$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> Yog.Model.successor_ids(graph, 1)
    [2]

# `successors`

```elixir
@spec successors(graph(), node_id()) :: [{node_id(), term()}]
```

Gets nodes you can travel TO from the given node (successors).
Returns a list of tuples containing the destination node ID and edge data.

Time complexity: $\mathcal{O}(\text{deg}_{\text{out}}(v))$

## Example

    iex> {:ok, graph} =
    ...>   Yog.Model.new(:directed)
    ...>   |> Yog.Model.add_node(1, "A")
    ...>   |> Yog.Model.add_node(2, "B")
    ...>   |> Yog.Model.add_edge(1, 2, 10)
    iex> Yog.Model.successors(graph, 1)
    [{2, 10}]

# `type`

```elixir
@spec type(graph()) :: graph_type()
```

Gets the type of the graph (`:directed` or `:undirected`).

Time complexity: $\mathcal{O}(1)$

## Example

    iex> graph = Yog.Model.new(:directed)
    iex> Yog.Model.type(graph)
    :directed

---

*Consult [api-reference.md](api-reference.md) for complete listing*
