defmodule Yog.Generator.Random do @moduledoc """ Stochastic graph generators for random graph models. Random generators use randomness to model real-world networks with properties like scale-free distributions, small-world effects, and community structure. ## Available Generators | Generator | Model | Complexity | Key Property | |-----------|-------|------------|--------------| | `erdos_renyi_gnp/2` | G(n, p) | O(n²) | Each edge with probability p | | `erdos_renyi_gnm/2` | G(n, m) | O(m) | Exactly m random edges | | `barabasi_albert/2` | Preferential | O(nm) | Scale-free (power-law degrees) | | `watts_strogatz/3` | Small-world | O(nk) | High clustering + short paths | | `random_tree/1` | Uniform tree | O(n²) | Uniformly random spanning tree | | `random_regular/2` | d-regular | O(nd) | All nodes have degree d | ## Quick Start (Not Doctests - Random Output) # Random network models (output varies due to randomness) # sparse = Yog.Generator.Random.erdos_renyi_gnp(100, 0.05) # Sparse random (p=5%) # exact = Yog.Generator.Random.erdos_renyi_gnm(50, 100) # Exactly 100 edges # scale_free = Yog.Generator.Random.barabasi_albert(1000, 3) # Scale-free network # small_world = Yog.Generator.Random.watts_strogatz(100, 6, 0.1) # Small-world (10% rewire) # tree = Yog.Generator.Random.random_tree(50) # Random spanning tree ## Network Models Explained ### Erdős-Rényi G(n, p) - Each possible edge included independently with probability p - Expected edges: p × n(n-1)/2 (undirected) or p × n(n-1) (directed) - Phase transition at p = 1/n (giant component emerges) - **Use for**: Random network modeling, percolation studies ### Erdős-Rényi G(n, m) - Exactly m edges added uniformly at random - Uniform distribution over all graphs with n nodes and m edges - **Use for**: Fixed edge count requirements, specific density testing ### Barabási-Albert (Preferential Attachment) - Starts with m₀ nodes, adds nodes connecting to m existing nodes - New nodes prefer high-degree nodes ("rich get richer") - Power-law degree distribution: P(k) ~ k^(-3) - **Use for**: Social networks, citation networks, web graphs ### Watts-Strogatz (Small-World) - Starts with ring lattice (high clustering) - Rewires edges with probability p (creates shortcuts) - Balances local clustering with global connectivity - **Use for**: Social networks, neural networks, epidemic modeling ### Random Tree - Builds tree by connecting new nodes to random existing nodes - Produces uniform distribution over all labeled trees - **Use for**: Spanning trees, hierarchical structures ## References - [Erdős-Rényi Model](https://en.wikipedia.org/wiki/Erd%C5%91s%E2%80%93R%C3%A9nyi_model) - [Barabási-Albert Model](https://en.wikipedia.org/wiki/Barab%C3%A1si%E2%80%93Albert_model) - [Watts-Strogatz Model](https://en.wikipedia.org/wiki/Watts%E2%80%93Strogatz_model) - [Scale-Free Networks](https://en.wikipedia.org/wiki/Scale-free_network) - [Small-World Network](https://en.wikipedia.org/wiki/Small-world_network) """ # ============= Erdős-Rényi G(n, p) ============= @doc """ Generates a random graph using the Erdős-Rényi G(n, p) model. Each possible edge is included independently with probability p. For undirected graphs, each unordered pair is considered once. **Time Complexity:** O(n²) ## Examples iex> # Generate a sparse random graph (output varies) ...> sparse = Yog.Generator.Random.erdos_renyi_gnp(10, 0.3) iex> Yog.Model.order(sparse) 10 iex> # Generate a denser random graph ...> dense = Yog.Generator.Random.erdos_renyi_gnp(5, 0.8) iex> Yog.Model.order(dense) 5 ## Properties - Expected number of edges: p × n(n-1)/2 (undirected) or p × n(n-1) (directed) - Phase transition at p = 1/n (giant component emerges) ## Use Cases - Random network modeling - Percolation studies - Average-case algorithm analysis """ @spec erdos_renyi_gnp(integer(), float()) :: Yog.graph() def erdos_renyi_gnp(n, p), do: erdos_renyi_gnp_with_type(n, p, :undirected) @doc """ Generates an Erdős-Rényi G(n, p) graph with specified graph type. """ @spec erdos_renyi_gnp_with_type(integer(), float(), Yog.graph_type()) :: Yog.graph() def erdos_renyi_gnp_with_type(n, p, graph_type) when n > 0 and p >= 0.0 and p <= 1.0 do base = Yog.new(graph_type) graph = Enum.reduce(0..(n - 1), base, fn i, g -> Yog.add_node(g, i, nil) end) # Generate all possible edges and filter by probability all_pairs = case graph_type do :undirected -> for i <- 0..(n - 1), j <- (i + 1)..(n - 1)//1, i < j, do: {i, j} :directed -> for i <- 0..(n - 1), j <- 0..(n - 1)//1, i != j, do: {i, j} end edges = Enum.filter(all_pairs, fn _ -> :rand.uniform() <= p end) Enum.reduce(edges, graph, fn {from, to}, g -> Yog.add_edge!(g, from, to, 1) end) end def erdos_renyi_gnp_with_type(_n, _p, _graph_type), do: Yog.new(:undirected) # ============= Erdős-Rényi G(n, m) ============= @doc """ Generates a random graph using the Erdős-Rényi G(n, m) model. Exactly m edges are added uniformly at random from all possible edges. **Time Complexity:** O(m) ## Examples iex> graph = Yog.Generator.Random.erdos_renyi_gnm(10, 15) iex> Yog.Model.order(graph) 10 ## Properties - Uniform distribution over all graphs with n nodes and m edges - Fixed edge count (unlike G(n,p) which has random edge count) ## Use Cases - Fixed edge count requirements - Specific density testing - Comparative studies """ @spec erdos_renyi_gnm(integer(), integer()) :: Yog.graph() def erdos_renyi_gnm(n, m), do: erdos_renyi_gnm_with_type(n, m, :undirected) @doc """ Generates an Erdős-Rényi G(n, m) graph with specified graph type. """ @spec erdos_renyi_gnm_with_type(integer(), integer(), Yog.graph_type()) :: Yog.graph() def erdos_renyi_gnm_with_type(n, m, graph_type) when n > 0 and m >= 0 do base = Yog.new(graph_type) graph = Enum.reduce(0..(n - 1), base, fn i, g -> Yog.add_node(g, i, nil) end) # Generate all possible edges all_pairs = case graph_type do :undirected -> for i <- 0..(n - 1), j <- (i + 1)..(n - 1)//1, i < j, do: {i, j} :directed -> for i <- 0..(n - 1), j <- 0..(n - 1)//1, i != j, do: {i, j} end # Clamp m to max possible edges max_edges = length(all_pairs) actual_m = min(m, max_edges) # Shuffle and take first m selected_edges = all_pairs |> Enum.shuffle() |> Enum.take(actual_m) Enum.reduce(selected_edges, graph, fn {from, to}, g -> Yog.add_edge!(g, from, to, 1) end) end def erdos_renyi_gnm_with_type(_n, _m, _graph_type), do: Yog.new(:undirected) # ============= Barabási-Albert ============= @doc """ Generates a scale-free graph using the Barabási-Albert preferential attachment model. Starts with m nodes and adds n-m new nodes. Each new node connects to m existing nodes with probability proportional to their degree ("rich get richer"). **Time Complexity:** O(nm) ## Examples iex> ba = Yog.Generator.Random.barabasi_albert(20, 2) iex> Yog.Model.order(ba) 20 ## Properties - Power-law degree distribution: P(k) ~ k^(-3) - Scale-free: no characteristic node degree - High degree nodes (hubs) emerge naturally ## Use Cases - Social networks - Citation networks - Web graphs - Biological networks """ @spec barabasi_albert(integer(), integer()) :: Yog.graph() def barabasi_albert(n, m), do: barabasi_albert_with_type(n, m, :undirected) @doc """ Generates a Barabási-Albert graph with specified graph type. """ @spec barabasi_albert_with_type(integer(), integer(), Yog.graph_type()) :: Yog.graph() def barabasi_albert_with_type(n, m, graph_type) when n >= 1 and m >= 1 and m < n do base = Yog.new(graph_type) # Start with a small complete graph of m nodes initial_nodes = min(m, n) graph = Enum.reduce(0..(initial_nodes - 1), base, fn i, g -> g = Yog.add_node(g, i, nil) # Connect to all previous nodes Enum.reduce(0..(i - 1)//1, g, fn j, acc -> acc = Yog.add_edge!(acc, i, j, 1) if graph_type == :directed, do: Yog.add_edge!(acc, j, i, 1), else: acc end) end) # Add remaining nodes with preferential attachment Enum.reduce(initial_nodes..(n - 1), graph, fn new_node, g -> g = Yog.add_node(g, new_node, nil) # Get current nodes and their degrees existing_nodes = 0..(new_node - 1) if Enum.empty?(existing_nodes) do g else # Calculate degrees (for undirected, count all connections) degrees = Enum.map(existing_nodes, fn node -> neighbors = length(Yog.neighbors(g, node)) {node, max(neighbors, 1)} end) total_degree = Enum.sum(Enum.map(degrees, &elem(&1, 1))) # Preferential attachment: select m nodes targets = select_preferential(degrees, total_degree, m, []) Enum.reduce(targets, g, fn target, acc -> acc = Yog.add_edge!(acc, new_node, target, 1) if graph_type == :directed, do: Yog.add_edge!(acc, target, new_node, 1), else: acc end) end end) end def barabasi_albert_with_type(n, _m, _graph_type) when n >= 1 do # m >= n case: just return n isolated nodes base = Yog.new(:undirected) Enum.reduce(0..(n - 1), base, fn i, g -> Yog.add_node(g, i, nil) end) end def barabasi_albert_with_type(_n, _m, _graph_type), do: Yog.new(:undirected) # Select m nodes with probability proportional to their degree defp select_preferential(_degrees, _total, 0, acc), do: Enum.uniq(acc) defp select_preferential(degrees, total, remaining, acc) when remaining > 0 do pick = :rand.uniform() * total {node, _} = Enum.reduce_while(degrees, {nil, 0.0}, fn {n, deg}, {_, cum} -> new_cum = cum + deg if new_cum >= pick do {:halt, {n, new_cum}} else {:cont, {n, new_cum}} end end) # Retry if we picked a duplicate (simple approach) if node in acc do select_preferential(degrees, total, remaining, acc) else select_preferential(degrees, total, remaining - 1, [node | acc]) end end # ============= Watts-Strogatz ============= @doc """ Generates a small-world graph using the Watts-Strogatz model. Starts with a ring lattice where each node connects to k nearest neighbors. Then rewires each edge with probability p to create shortcuts. **Time Complexity:** O(nk) ## Examples iex> ws = Yog.Generator.Random.watts_strogatz(20, 4, 0.1) iex> Yog.Model.order(ws) 20 ## Properties - High clustering coefficient (like regular lattice) - Short average path length (like random graph) - Tunable with p: p=0 is regular, p=1 is random ## Use Cases - Social networks - Neural networks - Epidemic modeling - Power grids """ @spec watts_strogatz(integer(), integer(), float()) :: Yog.graph() def watts_strogatz(n, k, p), do: watts_strogatz_with_type(n, k, p, :undirected) @doc """ Generates a Watts-Strogatz graph with specified graph type. """ @spec watts_strogatz_with_type(integer(), integer(), float(), Yog.graph_type()) :: Yog.graph() def watts_strogatz_with_type(n, k, p, graph_type) when n > k and k >= 2 and p >= 0.0 and p <= 1.0 do base = Yog.new(graph_type) # Add all nodes graph = Enum.reduce(0..(n - 1)//1, base, fn i, g -> Yog.add_node(g, i, nil) end) # k must be even for the ring lattice construction k_half = div(k, 2) # Build ring lattice: each node connects to k/2 neighbors on each side # For undirected graphs, we create edges in both directions (each node connects forward) # For directed graphs, we create edges in one direction only lattice_edges = for i <- 0..(n - 1)//1, offset <- 1..k_half//1, do: {i, rem(i + offset, n)} # For undirected, also add the reverse edges to ensure each node has k neighbors all_lattice_edges = case graph_type do :undirected -> reverse_edges = Enum.map(lattice_edges, fn {i, j} -> {j, i} end) lattice_edges ++ reverse_edges :directed -> lattice_edges end # Rewire edges with probability p {final_edges, _} = Enum.reduce(all_lattice_edges, {[], MapSet.new()}, fn {from, to}, {edges, used} -> edge_key = case graph_type do :undirected -> {min(from, to), max(from, to)} :directed -> {from, to} end if MapSet.member?(used, edge_key) do # Skip duplicate edges {edges, used} else new_used = MapSet.put(used, edge_key) if :rand.uniform() <= p do # Rewire: connect to a random node candidates = 0..(n - 1) |> Enum.filter(fn x -> x != from and not MapSet.member?(used, {min(from, x), max(from, x)}) end) if candidates == [] do {[{from, to} | edges], new_used} else new_to = Enum.random(candidates) new_edge_key = {min(from, new_to), max(from, new_to)} {[{from, new_to} | edges], MapSet.put(new_used, new_edge_key)} end else {[{from, to} | edges], new_used} end end end) Enum.reduce(final_edges, graph, fn {from, to}, g -> Yog.add_edge!(g, from, to, 1) end) end def watts_strogatz_with_type(_n, _k, _p, _graph_type), do: Yog.new(:undirected) # ============= Random Tree ============= @doc """ Generates a uniformly random tree on n nodes. Each labeled tree has equal probability of being generated. **Time Complexity:** O(n²) ## Examples iex> tree = Yog.Generator.Random.random_tree(10) iex> Yog.Model.order(tree) 10 iex> # A tree has exactly n-1 edges ...> Yog.Model.edge_count(tree) 9 ## Properties - Exactly n-1 edges - Connected and acyclic - Uniform distribution over all labeled trees ## Use Cases - Spanning trees - Hierarchical structures - Network design """ @spec random_tree(integer()) :: Yog.graph() def random_tree(n), do: random_tree_with_type(n, :undirected) @doc """ Generates a random tree with specified graph type. """ @spec random_tree_with_type(integer(), Yog.graph_type()) :: Yog.graph() def random_tree_with_type(n, _graph_type) when n <= 0, do: Yog.new(:undirected) def random_tree_with_type(1, graph_type), do: Yog.new(graph_type) |> Yog.add_node(0, nil) def random_tree_with_type(n, graph_type) do base = Yog.new(graph_type) # Start with node 0 graph = Yog.add_node(base, 0, nil) # Add remaining nodes, each connecting to a random existing node Enum.reduce(1..(n - 1), graph, fn new_node, g -> g = Yog.add_node(g, new_node, nil) parent = :rand.uniform(new_node) - 1 g = Yog.add_edge!(g, new_node, parent, 1) if graph_type == :directed, do: Yog.add_edge!(g, parent, new_node, 1), else: g end) end # ============= Random Regular Graph ============= @doc """ Generates a random d-regular graph on n nodes. A d-regular graph has every node with exactly degree d. This implementation uses a configuration model approach with rewiring to ensure simplicity (no self-loops or parallel edges). **Preconditions:** - n × d must be even (required for any d-regular graph) - d < n (cannot have degree >= number of nodes in simple graph) - d >= 0 **Properties:** - Uniform distribution over all d-regular graphs (approximate) - Exactly n nodes, (n × d) / 2 edges - All nodes have degree exactly d **Time Complexity:** O(n × d) ## Examples iex> # Generate a 3-regular graph with 10 nodes ...> reg = Yog.Generator.Random.random_regular(10, 3) iex> Yog.Model.order(reg) 10 iex> # Every node has degree 3 ...> degrees = for v <- 0..9, do: length(Yog.neighbors(reg, v)) iex> Enum.all?(degrees, fn d -> d == 3 end) true iex> # Total edges = n*d/2 = 15 ...> Yog.Model.edge_count(reg) 15 ## Algorithm Uses a configuration model: 1. Create d "stubs" for each of the n nodes 2. Randomly pair stubs to form edges 3. Reject and retry if self-loops or parallel edges form ## Use Cases - Testing algorithms that need uniform degree distribution - Expander graph approximations - Network models where degree is constrained - Comparison with scale-free networks ## References - [Configuration Model](https://en.wikipedia.org/wiki/Configuration_model) - [Random Regular Graph](https://en.wikipedia.org/wiki/Random_regular_graph) """ @spec random_regular(integer(), integer()) :: Yog.graph() def random_regular(n, d), do: random_regular_with_type(n, d, :undirected) @doc """ Generates a random d-regular graph with specified graph type. """ @spec random_regular_with_type(integer(), integer(), Yog.graph_type()) :: Yog.graph() def random_regular_with_type(n, d, _graph_type) when n <= 0 or d < 0 or d >= n, do: Yog.new(:undirected) def random_regular_with_type(n, d, _graph_type) when rem(n * d, 2) == 1, do: Yog.new(:undirected) def random_regular_with_type(1, 0, graph_type), do: Yog.new(graph_type) |> Yog.add_node(0, nil) def random_regular_with_type(n, 0, graph_type) do # 0-regular: just isolated nodes base = Yog.new(graph_type) Enum.reduce(0..(n - 1), base, fn i, g -> Yog.add_node(g, i, nil) end) end def random_regular_with_type(n, d, graph_type) do # Use configuration model with rejection sampling # Create d stubs per node, randomly match them generate_regular(n, d, graph_type, 100) end # Attempt to generate with max retries defp generate_regular(n, d, graph_type, retries) when retries > 0 do # Create stubs: each node i appears d times in the list stubs = for i <- 0..(n - 1), _ <- 1..d, do: i # Shuffle stubs and pair them shuffled = Enum.shuffle(stubs) case try_pairing(shuffled, n, graph_type) do {:ok, graph} -> graph :retry -> generate_regular(n, d, graph_type, retries - 1) end end defp generate_regular(_n, _d, _graph_type, _retries), do: Yog.new(:undirected) # Try to pair stubs without creating self-loops or parallel edges defp try_pairing(stubs, n, graph_type) do pairs = Enum.chunk_every(stubs, 2) # Check for invalid pairs (self-loops with odd length) if Enum.any?(pairs, fn [a, b] -> a == b _ -> true end) do :retry else # Check for parallel edges edge_set = pairs |> Enum.map(fn [a, b] -> {min(a, b), max(a, b)} end) |> MapSet.new() # If we have unique edges equal to pairs, we're good if MapSet.size(edge_set) == length(pairs) do {:ok, build_regular_graph(n, pairs, graph_type)} else :retry end end end defp build_regular_graph(n, pairs, graph_type) do base = Yog.new(graph_type) graph = Enum.reduce(0..(n - 1), base, fn i, g -> Yog.add_node(g, i, nil) end) Enum.reduce(pairs, graph, fn [from, to], g -> Yog.add_edge!(g, from, to, 1) end) end end