Current section
Files
Jump to
Current section
Files
lib/yog/generator/random.ex
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(), integer() | nil) :: Yog.graph()
def erdos_renyi_gnp(n, p, seed \\ nil), do: erdos_renyi_gnp_with_type(n, p, :undirected, seed)
@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(), integer() | nil) ::
Yog.graph()
def erdos_renyi_gnp_with_type(n, p, graph_type, seed \\ nil)
def erdos_renyi_gnp_with_type(n, p, graph_type, seed)
when n > 0 and p >= 0.0 and p <= 1.0 do
with_seed(seed, fn ->
base = Yog.new(graph_type)
graph =
Enum.reduce(0..(n - 1), base, fn i, g ->
Yog.add_node(g, i, nil)
end)
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)
end
def erdos_renyi_gnp_with_type(_n, _p, _graph_type, _seed),
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(), integer() | nil) :: Yog.graph()
def erdos_renyi_gnm(n, m, seed \\ nil), do: erdos_renyi_gnm_with_type(n, m, :undirected, seed)
@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(), integer() | nil) ::
Yog.graph()
def erdos_renyi_gnm_with_type(n, m, graph_type, seed \\ nil)
def erdos_renyi_gnm_with_type(n, m, graph_type, seed)
when n > 0 and m >= 0 do
with_seed(seed, fn ->
base = Yog.new(graph_type)
graph =
Enum.reduce(0..(n - 1), base, fn i, g ->
Yog.add_node(g, i, nil)
end)
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
max_edges = length(all_pairs)
actual_m = min(m, max_edges)
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)
end
def erdos_renyi_gnm_with_type(_n, _m, _graph_type, _seed),
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(), integer() | nil) :: Yog.graph()
def barabasi_albert(n, m, seed \\ nil), do: barabasi_albert_with_type(n, m, :undirected, seed)
@doc """
Generates a Barabási-Albert graph with specified graph type.
"""
@spec barabasi_albert_with_type(integer(), integer(), Yog.graph_type(), integer() | nil) ::
Yog.graph()
def barabasi_albert_with_type(n, m, graph_type, seed \\ nil)
def barabasi_albert_with_type(n, m, graph_type, seed)
when n >= 1 and m >= 1 and m < n do
with_seed(seed, fn ->
base = Yog.new(graph_type)
initial_nodes = min(m, n)
graph =
Enum.reduce(0..(initial_nodes - 1), base, fn i, g ->
g = Yog.add_node(g, i, nil)
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)
Enum.reduce(initial_nodes..(n - 1), graph, fn new_node, g ->
g = Yog.add_node(g, new_node, nil)
existing_nodes = 0..(new_node - 1)
if Enum.empty?(existing_nodes) do
g
else
degrees =
Enum.map(existing_nodes, fn node ->
deg = Yog.Model.degree(g, node)
{node, max(deg, 1)}
end)
total_degree = Enum.sum(Enum.map(degrees, &elem(&1, 1)))
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)
end
def barabasi_albert_with_type(n, _m, _graph_type, _seed) when n >= 1 do
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, _seed),
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)
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(), integer() | nil) :: Yog.graph()
def watts_strogatz(n, k, p, seed \\ nil),
do: watts_strogatz_with_type(n, k, p, :undirected, seed)
@doc """
Generates a Watts-Strogatz graph with specified graph type.
"""
@spec watts_strogatz_with_type(integer(), integer(), float(), Yog.graph_type(), integer() | nil) ::
Yog.graph()
def watts_strogatz_with_type(n, k, p, graph_type, seed \\ nil)
def watts_strogatz_with_type(n, k, p, graph_type, seed)
when n > k and k >= 2 and p >= 0.0 and p <= 1.0 do
with_seed(seed, fn ->
base = Yog.new(graph_type)
graph =
Enum.reduce(0..(n - 1)//1, base, fn i, g ->
Yog.add_node(g, i, nil)
end)
k_half = div(k, 2)
lattice_edges =
for i <- 0..(n - 1)//1,
offset <- 1..k_half//1,
do: {i, rem(i + offset, n)}
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
{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)
end
def watts_strogatz_with_type(_n, _k, _p, _graph_type, _seed),
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(), integer() | nil) :: Yog.graph()
def random_tree(n, seed \\ nil), do: random_tree_with_type(n, :undirected, seed)
@doc """
Generates a random tree with specified graph type.
"""
@spec random_tree_with_type(integer(), Yog.graph_type(), integer() | nil) :: Yog.graph()
def random_tree_with_type(n, graph_type, seed \\ nil)
def random_tree_with_type(n, _graph_type, _seed) when n <= 0, do: Yog.new(:undirected)
def random_tree_with_type(1, graph_type, _seed), do: Yog.new(graph_type) |> Yog.add_node(0, nil)
def random_tree_with_type(n, graph_type, seed) when is_integer(n) and n > 1 do
with_seed(seed, fn ->
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)
end
# =============================================================================
# Seed Handling Helpers
# =============================================================================
# Executes the given function with a temporarily seeded random number generator.
# If seed is nil, uses the current global RNG state (no change).
# If seed is provided, temporarily sets :rand to that seed, executes the function,
# then restores the previous RNG state.
defp with_seed(nil, fun), do: fun.()
defp with_seed(seed, fun) do
old_state = :rand.export_seed()
:rand.seed(:exsss, seed)
result = fun.()
if old_state != :undefined do
:rand.seed(old_state)
end
result
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(), integer() | nil) :: Yog.graph()
def random_regular(n, d, seed \\ nil), do: random_regular_with_type(n, d, :undirected, seed)
@doc """
Generates a random d-regular graph with specified graph type.
"""
@spec random_regular_with_type(integer(), integer(), Yog.graph_type(), integer() | nil) ::
Yog.graph()
def random_regular_with_type(n, d, graph_type, seed \\ nil)
def random_regular_with_type(n, d, _graph_type, _seed) when n <= 0 or d < 0 or d >= n,
do: Yog.new(:undirected)
def random_regular_with_type(n, d, _graph_type, _seed) when rem(n * d, 2) == 1,
do: Yog.new(:undirected)
def random_regular_with_type(1, 0, graph_type, _seed),
do: Yog.new(graph_type) |> Yog.add_node(0, nil)
def random_regular_with_type(n, 0, graph_type, _seed) when is_integer(n) and n > 1 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, seed) do
with_seed(seed, fn ->
generate_regular(n, d, graph_type, 100)
end)
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
# ============= Stochastic Block Model =============
@doc """
Generates a graph using the Stochastic Block Model (SBM).
Nodes are assigned to communities, and edges are added with probabilities
depending on community membership (higher probability within communities).
## Parameters
- `n` - Number of nodes
- `k` - Number of communities
- `p_in` - Probability of edge within community
- `p_out` - Probability of edge between communities
## Options
- `:seed` - Random seed for reproducibility
- `:community_sizes` - List of community sizes (must sum to `n`)
- `:balanced` - Whether to use equal-sized communities (default: `true`)
## Examples
iex> sbm = Yog.Generator.Random.sbm(100, 4, 0.3, 0.05)
iex> Yog.Model.order(sbm)
100
"""
@spec sbm(integer(), integer(), float(), float(), keyword()) :: Yog.graph()
def sbm(n, k, p_in, p_out, opts \\ []) do
{graph, _communities} = sbm_with_labels(n, k, p_in, p_out, opts)
graph
end
@doc """
Generates an SBM graph with specified graph type.
"""
@spec sbm_with_type(integer(), integer(), float(), float(), Yog.graph_type(), keyword()) ::
Yog.graph()
def sbm_with_type(n, k, p_in, p_out, graph_type, opts \\ []) do
{graph, _communities} = sbm_with_labels_and_type(n, k, p_in, p_out, graph_type, opts)
graph
end
@doc """
Returns the SBM graph along with community assignments.
## Examples
iex> {_graph, communities} = Yog.Generator.Random.sbm_with_labels(100, 4, 0.3, 0.05)
iex> map_size(communities)
100
iex> communities[0] in 0..3
true
"""
@spec sbm_with_labels(integer(), integer(), float(), float(), keyword()) ::
{Yog.graph(), %{Yog.node_id() => integer()}}
def sbm_with_labels(n, k, p_in, p_out, opts \\ []) do
sbm_with_labels_and_type(n, k, p_in, p_out, :undirected, opts)
end
@spec sbm_with_labels_and_type(
integer(),
integer(),
float(),
float(),
Yog.graph_type(),
keyword()
) ::
{Yog.graph(), %{Yog.node_id() => integer()}}
def sbm_with_labels_and_type(n, k, p_in, p_out, graph_type, opts \\ [])
def sbm_with_labels_and_type(n, k, p_in, p_out, graph_type, opts)
when n > 0 and k >= 1 and p_in >= 0.0 and p_in <= 1.0 and p_out >= 0.0 and p_out <= 1.0 do
with_seed(opts[:seed], fn ->
community_sizes = get_community_sizes(n, k, opts)
valid =
length(community_sizes) == k and Enum.sum(community_sizes) == n and
Enum.all?(community_sizes, &(&1 >= 0))
if valid do
base = Yog.new(graph_type)
graph =
Enum.reduce(0..(n - 1), base, fn i, g ->
Yog.add_node(g, i, nil)
end)
communities = build_communities(community_sizes)
edges =
case graph_type do
:undirected ->
for u <- 0..(n - 1),
v <- (u + 1)..(n - 1)//1,
p = if(communities[u] == communities[v], do: p_in, else: p_out),
:rand.uniform() <= p,
do: {u, v}
:directed ->
for u <- 0..(n - 1),
v <- 0..(n - 1)//1,
u != v,
p = if(communities[u] == communities[v], do: p_in, else: p_out),
:rand.uniform() <= p,
do: {u, v}
end
final_graph =
Enum.reduce(edges, graph, fn {from, to}, g ->
Yog.add_edge!(g, from, to, 1)
end)
{final_graph, communities}
else
{Yog.new(:undirected), %{}}
end
end)
end
def sbm_with_labels_and_type(_n, _k, _p_in, _p_out, _graph_type, _opts),
do: {Yog.new(:undirected), %{}}
defp get_community_sizes(n, k, opts) when n > 0 and k > 0 do
case Keyword.get(opts, :community_sizes) do
nil ->
base_size = div(n, k)
remainder = rem(n, k)
List.duplicate(base_size + 1, remainder) ++ List.duplicate(base_size, k - remainder)
sizes ->
sizes
end
end
defp get_community_sizes(_n, _k, _opts), do: []
defp build_communities(community_sizes) do
community_sizes
|> Enum.with_index()
|> Enum.flat_map(fn {size, comm} ->
start = Enum.sum(Enum.take(community_sizes, comm))
Enum.map(start..(start + size - 1), fn node -> {node, comm} end)
end)
|> Map.new()
end
@doc """
Generates a Degree-Corrected Stochastic Block Model (DCSBM).
Extends SBM with node-specific degree parameters, allowing more realistic
degree distributions while preserving community structure.
## Options
- `:degree_dist` - Degree distribution: `:power_law`, `:poisson`, or custom list
- `:gamma` - Power-law exponent (default: 2.5)
- `:seed` - Random seed
- `:community_sizes` - List of community sizes (must sum to `n`)
## Examples
iex> dcsbm = Yog.Generator.Random.dcsbm(100, 3, 0.3, 0.02,
...> degree_dist: :power_law, gamma: 2.5)
iex> Yog.Model.order(dcsbm)
100
"""
@spec dcsbm(integer(), integer(), float(), float(), keyword()) :: Yog.graph()
def dcsbm(n, k, p_in, p_out, opts \\ []) do
with_seed(opts[:seed], fn ->
community_sizes = get_community_sizes(n, k, opts)
valid =
n > 0 and k >= 1 and p_in >= 0.0 and p_in <= 1.0 and p_out >= 0.0 and p_out <= 1.0 and
length(community_sizes) == k and Enum.sum(community_sizes) == n
if valid do
base = Yog.new(:undirected)
graph =
Enum.reduce(0..(n - 1), base, fn i, g ->
Yog.add_node(g, i, nil)
end)
communities = build_communities(community_sizes)
thetas = generate_thetas(n, opts) |> Enum.shuffle()
edges =
for u <- 0..(n - 1),
v <- (u + 1)..(n - 1)//1,
p_base = if(communities[u] == communities[v], do: p_in, else: p_out),
p = min(1.0, Enum.at(thetas, u) * Enum.at(thetas, v) * p_base),
:rand.uniform() <= p,
do: {u, v}
Enum.reduce(edges, graph, fn {from, to}, g ->
Yog.add_edge!(g, from, to, 1)
end)
else
Yog.new(:undirected)
end
end)
end
defp generate_thetas(n, opts) do
degree_dist = Keyword.get(opts, :degree_dist, :power_law)
gamma = Keyword.get(opts, :gamma, 2.5)
thetas =
case degree_dist do
:power_law ->
for i <- 1..n, do: :math.pow(i, -gamma)
:poisson ->
for _ <- 1..n, do: 0.5 + :rand.uniform()
list when is_list(list) ->
if length(list) == n, do: list, else: List.duplicate(1.0, n)
_ ->
List.duplicate(1.0, n)
end
mean = Enum.sum(thetas) / n
if mean > 0, do: Enum.map(thetas, fn t -> t / mean end), else: thetas
end
@doc """
Generates a hierarchical SBM with nested communities.
## Options
- `:levels` - Number of hierarchy levels (default: 2)
- `:branching` - Branching factor at each level (default: 2)
- `:p_in` - Probability within leaf communities (default: 0.3)
- `:p_out` - Probability between root communities (default: 0.01)
- `:probs` - Explicit probability list of length `levels + 1`
- `:seed` - Random seed
## Examples
iex> hsbm = Yog.Generator.Random.hsbm(80,
...> levels: 2, branching: 2, p_in: 0.4, p_mid: 0.1, p_out: 0.01)
iex> Yog.Model.order(hsbm)
80
"""
@spec hsbm(integer(), keyword()) :: Yog.graph()
def hsbm(n, opts \\ []) do
with_seed(opts[:seed], fn ->
levels = Keyword.get(opts, :levels, 2)
branching = Keyword.get(opts, :branching, 2)
valid = n > 0 and levels >= 1 and branching >= 2
if valid do
leaf_blocks = Integer.pow(branching, levels)
base_leaf_size = div(n, leaf_blocks)
if base_leaf_size >= 1 do
probs = get_hsbm_probs(levels, opts)
powers = for l <- 0..levels, do: Integer.pow(branching, l)
graph =
Enum.reduce(0..(n - 1), Yog.new(:undirected), fn i, g ->
Yog.add_node(g, i, nil)
end)
edges =
for u <- 0..(n - 1),
v <- (u + 1)..(n - 1)//1,
lca_level = hsbm_lca_level(u, v, base_leaf_size, n, powers),
p = Enum.at(probs, lca_level, 0.0),
:rand.uniform() <= p,
do: {u, v}
Enum.reduce(edges, graph, fn {from, to}, g ->
Yog.add_edge!(g, from, to, 1)
end)
else
Yog.new(:undirected)
end
else
Yog.new(:undirected)
end
end)
end
defp get_hsbm_probs(levels, opts) do
case Keyword.get(opts, :probs) do
nil ->
p_in = Keyword.get(opts, :p_in, 0.3)
p_out = Keyword.get(opts, :p_out, 0.01)
if levels == 2 and Keyword.has_key?(opts, :p_mid) do
[p_in, opts[:p_mid], p_out]
else
for l <- 0..levels//1 do
p_in + (p_out - p_in) * l / levels
end
end
probs when is_list(probs) ->
probs
end
end
defp hsbm_lca_level(u, v, leaf_size, n, powers) do
_leaf_blocks = div(n, leaf_size)
bu = div(u, leaf_size)
bv = div(v, leaf_size)
if bu == bv do
0
else
find_lca_level(bu, bv, powers)
end
end
defp find_lca_level(bu, bv, powers) do
Enum.find(1..(length(powers) - 1), length(powers) - 1, fn l ->
div(bu, Enum.at(powers, l)) == div(bv, Enum.at(powers, l))
end)
end
# ============= Configuration Model =============
@doc """
Generates a random graph with specified degree sequence using the configuration model.
Creates a random graph where each node has exactly the degree specified,
using the stub-matching (configuration model) approach.
## Parameters
- `degrees` - List of desired degrees for each node [d1, d2, ..., dn]
## Options
- `:seed` - Random seed for reproducibility
- `:allow_multiedges` - Allow parallel edges (default: false)
- `:allow_selfloops` - Allow self-loops (default: false)
- `:max_retries` - Maximum attempts to create simple graph (default: 100)
## Returns
`{:ok, graph}` on success, `{:error, reason}` if impossible or retries exceeded
## Examples
iex> # Create graph with specific degree sequence
...> degrees = [3, 3, 2, 2, 2]
...> {:ok, g} = Yog.Generator.Random.configuration_model(degrees)
iex> Yog.Model.order(g)
5
## Algorithm
1. Create stubs: For each node i, create d_i "half-edges"
2. Random matching: Randomly pair up all stubs
3. Form edges: Each pair of stubs becomes an edge
For simple graphs (no self-loops or multi-edges), the algorithm rejects
invalid configurations and retries up to max_retries.
"""
@spec configuration_model([integer()], keyword()) ::
{:ok, Yog.graph()} | {:error, term()}
def configuration_model(degrees, opts \\ []) do
opts =
Keyword.merge(
[seed: nil, allow_multiedges: false, allow_selfloops: false, max_retries: 100],
opts
)
with_seed(opts[:seed], fn ->
do_configuration_model(degrees, opts)
end)
end
defp do_configuration_model(degrees, opts) do
# Validate input
cond do
Enum.empty?(degrees) ->
{:error, :empty_degree_sequence}
Enum.any?(degrees, &(&1 < 0)) ->
{:error, :negative_degrees}
rem(Enum.sum(degrees), 2) != 0 ->
{:error, :odd_degree_sum}
true ->
# Valid degree sequence - try to generate
max_retries = opts[:max_retries]
allow_selfloops = opts[:allow_selfloops]
allow_multiedges = opts[:allow_multiedges]
try_configuration_model(
degrees,
allow_selfloops,
allow_multiedges,
max_retries
)
end
end
defp try_configuration_model(_degrees, _allow_self, _allow_multi, 0) do
{:error, :max_retries_exceeded}
end
defp try_configuration_model(degrees, allow_selfloops, allow_multiedges, retries) do
# Create stubs: node i appears degrees[i] times
stubs =
degrees
|> Enum.with_index()
|> Enum.flat_map(fn {deg, node} -> List.duplicate(node, deg) end)
# Shuffle and pair
shuffled = Enum.shuffle(stubs)
# Pair consecutive elements
pairs = Enum.chunk_every(shuffled, 2)
# Check all pairs are valid (have 2 elements)
if Enum.any?(pairs, &(length(&1) != 2)) do
# Should not happen with even sum, but handle gracefully
{:error, :invalid_pairing}
else
# Build edge list
edges =
pairs
|> Enum.map(fn [a, b] -> {min(a, b), max(a, b)} end)
|> Enum.filter(fn {a, b} -> a != b or allow_selfloops end)
# Check for issues
has_selfloops = Enum.any?(pairs, fn [a, b] -> a == b end)
edge_set = MapSet.new(edges)
has_multiedges = MapSet.size(edge_set) < length(edges)
# Validate
valid =
(not has_selfloops or allow_selfloops) and
(not has_multiedges or allow_multiedges)
if valid do
# Build graph
n = length(degrees)
graph =
Enum.reduce(0..(n - 1), Yog.new(:undirected), fn i, g ->
Yog.add_node(g, i, nil)
end)
final_graph =
Enum.reduce(MapSet.to_list(edge_set), graph, fn {from, to}, g ->
Yog.add_edge!(g, from, to, 1)
end)
{:ok, final_graph}
else
# Retry
try_configuration_model(
degrees,
allow_selfloops,
allow_multiedges,
retries - 1
)
end
end
end
@doc """
Generates a random graph matching the degree sequence of a given graph.
Creates a randomized version of the input graph with the same degree
sequence but random connections (configuration model applied to observed degrees).
## Options
- `:seed` - Random seed for reproducibility
- `:max_retries` - Maximum attempts (default: 100)
## Examples
iex> original = Yog.Generator.Classic.star(5)
...> {:ok, randomized} = Yog.Generator.Random.randomize_degree_sequence(original)
iex> Yog.Model.order(randomized)
5
## Use Cases
- Null models in network analysis
- Degree-preserving randomization
- Testing which network properties are explained by degree sequence alone
"""
@spec randomize_degree_sequence(Yog.graph(), keyword()) ::
{:ok, Yog.graph()} | {:error, term()}
def randomize_degree_sequence(graph, opts \\ []) do
nodes = Map.keys(graph.nodes)
n = length(nodes)
if n == 0 do
{:ok, Yog.new(:undirected)}
else
# Extract degree sequence using actual node IDs
degrees = Enum.map(nodes, &Yog.Model.degree(graph, &1))
# Use configuration model with same degrees
# Note: configuration_model creates nodes with integer IDs 0..n-1
# We need to map back to original node IDs if they're not integers
case configuration_model(degrees, opts) do
{:ok, int_graph} ->
# Remap node IDs if necessary
if nodes == Enum.to_list(0..(n - 1)) do
{:ok, int_graph}
else
{:ok, remap_node_ids(int_graph, nodes)}
end
error ->
error
end
end
end
# Remaps integer node IDs (0, 1, 2, ...) to original node IDs
defp remap_node_ids(graph, original_nodes) do
# Create mapping from integer ID to original ID
id_mapping =
original_nodes
|> Enum.with_index()
|> Map.new(fn {orig, idx} -> {idx, orig} end)
# Build new graph with remapped node IDs
base = Yog.new(:undirected)
# Add nodes with original IDs and their data
graph_with_nodes =
Enum.reduce(original_nodes, base, fn orig_id, g ->
data = Map.get(graph.nodes, Map.get(id_mapping, orig_id, orig_id), nil)
Yog.add_node(g, orig_id, data)
end)
# Add edges with remapped IDs
Enum.reduce(Yog.all_edges(graph), graph_with_nodes, fn {from, to, weight}, g ->
orig_from = Map.get(id_mapping, from, from)
orig_to = Map.get(id_mapping, to, to)
Yog.add_edge!(g, orig_from, orig_to, weight)
end)
end
@doc """
Generates a random graph with power-law degree distribution.
Creates a graph with degrees following a power law P(k) ~ k^(-gamma),
using the configuration model approach.
## Options
- `:gamma` - Power-law exponent (default: 2.5, must be > 2)
- `:k_min` - Minimum degree (default: 1)
- `:k_max` - Maximum degree (default: n-1)
- `:seed` - Random seed
- `:max_retries` - Maximum attempts (default: 100)
## Examples
iex> # Generate with fixed seed for reproducibility
...> result = Yog.Generator.Random.power_law_graph(50, gamma: 2.5, seed: 42)
...> case result do
...> {:ok, pl} -> Yog.Model.order(pl)
...> {:error, _} -> 0 # May fail due to retries
...> end
50
## Notes
The power-law distribution is generated using the configuration model,
which differs from the Barabási-Albert preferential attachment model.
This approach generates the degree sequence first, then randomizes connections.
For gamma > 2, the expected degree is finite and the graph is well-defined.
"""
@spec power_law_graph(integer(), keyword()) ::
{:ok, Yog.graph()} | {:error, term()}
def power_law_graph(n, opts \\ []) when n > 0 do
gamma = Keyword.get(opts, :gamma, 2.5)
k_min = Keyword.get(opts, :k_min, 1)
k_max = min(Keyword.get(opts, :k_max, n - 1), n - 1)
cond do
gamma <= 2 ->
{:error, :gamma_must_be_greater_than_2}
k_min < 0 or k_max < k_min ->
{:error, :invalid_degree_bounds}
true ->
with_seed(opts[:seed], fn ->
# Generate power-law distributed degrees using discrete distribution
degrees = generate_power_law_degrees(n, gamma, k_min, k_max)
# Ensure even sum for handshaking lemma
degrees = ensure_even_degree_sum(degrees, k_min, k_max)
# Generate graph using configuration model
configuration_model(degrees, opts)
end)
end
end
defp generate_power_law_degrees(n, gamma, k_min, k_max) do
# Use inverse transform sampling for power law
# CDF: F(k) = 1 - (k/k_min)^(1-gamma) for k >= k_min
# Normalize constant
zeta = :math.pow(k_min, 1 - gamma) - :math.pow(k_max + 1, 1 - gamma)
for _ <- 1..n do
u = :rand.uniform()
# Inverse CDF
k =
:math.pow(
:math.pow(k_min, 1 - gamma) - u * zeta,
1 / (1 - gamma)
)
# Round and clamp
round_k = round(k)
max(k_min, min(k_max, round_k))
end
end
defp ensure_even_degree_sum(degrees, k_min, k_max) do
sum = Enum.sum(degrees)
if rem(sum, 2) == 0 do
degrees
else
# Make sum even by adjusting one node's degree
# Prefer increasing if possible, otherwise decrease
idx = :rand.uniform(length(degrees)) - 1
current = Enum.at(degrees, idx)
new_value =
cond do
current < k_max -> current + 1
current > k_min -> current - 1
true -> current
end
List.replace_at(degrees, idx, new_value)
end
end
# ============= Kronecker Graphs =============
@doc """
Generates a Kronecker graph using recursive expansion.
Starting from a small initiator matrix, recursively expands using Kronecker
multiplication to create a graph with realistic properties like power-law
degree distributions, small diameter, and high clustering.
## Parameters
- `k` - Number of recursive iterations (results in 2^k nodes)
- `initiator` - 2×2 probability matrix [[a, b], [c, d]]
## Options
- `:n_edges` - Target number of edges (default: estimated from probabilities)
- `:seed` - Random seed
- `:directed` - Whether graph should be directed (default: true)
## Examples
iex> # Standard Kronecker initiator
...> initiator = [[0.9, 0.5], [0.5, 0.1]]
...> kron = Yog.Generator.Random.kronecker(4, initiator)
iex> Yog.Model.order(kron)
16
iex> # Undirected version
...> initiator = [[0.9, 0.5], [0.5, 0.1]]
...> kron = Yog.Generator.Random.kronecker(3, initiator, directed: false)
iex> Yog.Model.order(kron)
8
## References
- Leskovec et al., "Kronecker graphs: An approach to modeling networks", JMLR 2010
"""
@spec kronecker(integer(), [[float()]], keyword()) :: Yog.graph()
def kronecker(k, initiator, opts \\ []) when k >= 0 do
opts = Keyword.merge([directed: true, seed: nil, n_edges: nil], opts)
with_seed(opts[:seed], fn ->
n = Integer.pow(2, k)
# Validate initiator
[[a, b], [c, d]] = initiator
total_prob = a + b + c + d
# Estimate edges if not provided
n_edges =
opts[:n_edges] ||
round(n * n * (a + b + c + d) / 4 * if(opts[:directed], do: 1, else: 0.5))
# Use R-MAT algorithm for efficient generation
params =
if opts[:directed] do
# For directed: a, b, c, d as given
normalize_params(a, b, c, d, total_prob)
else
# For undirected: symmetrize (b = c)
avg_bc = (b + c) / 2
normalize_params(a, avg_bc, avg_bc, d, a + 2 * avg_bc + d)
end
do_rmat(n, n_edges, params, opts[:directed])
end)
end
@doc """
Generates a Kronecker graph using the fast R-MAT (Recursive Matrix) algorithm.
The R-MAT algorithm generates Kronecker graphs in O(E × log V) time instead
of O(V²), making it scalable to billions of edges. Used in the Graph500 benchmark.
## Parameters
- `n_nodes` - Number of nodes (must be power of 2)
- `n_edges` - Number of edges to generate
- `a, b, c, d` - Probabilities for the four quadrants (should sum to 1.0)
## Options
- `:seed` - Random seed
- `:noise` - Add small noise to probabilities to avoid self-loops (default: true)
- `:undirected` - Make undirected by symmetrizing (default: false)
## Examples
iex> # Standard R-MAT parameters (creates hub structure)
...> rmat = Yog.Generator.Random.rmat(1024, 8192, 0.45, 0.15, 0.15, 0.25)
iex> Yog.Model.order(rmat)
1024
iex> # Undirected version
...> undir = Yog.Generator.Random.rmat(512, 2048, 0.5, 0.2, 0.2, 0.1,
...> undirected: true)
iex> Yog.Model.order(undir)
512
## References
- Chakrabarti et al., "R-MAT: A recursive model for graph mining", SDM 2004
- Graph500 benchmark: https://graph500.org/
"""
@spec rmat(integer(), integer(), float(), float(), float(), float(), keyword()) ::
Yog.graph()
def rmat(n_nodes, n_edges, a, b, c, d, opts \\ [])
when n_nodes > 0 and n_edges >= 0 do
opts = Keyword.merge([seed: nil, noise: true, undirected: false], opts)
# Validate n_nodes is power of 2
unless power_of_two?(n_nodes) do
raise ArgumentError, "n_nodes must be a power of 2"
end
with_seed(opts[:seed], fn ->
# Normalize probabilities
_total = a + b + c + d
# Add small noise if requested (helps avoid self-loops)
{na, nb, nc, nd} =
if opts[:noise] do
add_noise(a, b, c, d)
else
{a, b, c, d}
end
params = normalize_params(na, nb, nc, nd, na + nb + nc + nd)
undirected = opts[:undirected]
do_rmat(n_nodes, n_edges, params, not undirected)
end)
end
@doc """
Generates a Kronecker graph with a larger initiator matrix.
Supports 3×3 or larger initiators for more complex community structure.
## Parameters
- `k` - Number of iterations (results in n^k nodes where n is initiator size)
- `initiator` - n×n initiator matrix
## Options
- `:seed` - Random seed
- `:directed` - Whether graph should be directed (default: true)
## Examples
iex> # 3x3 initiator for 3-community structure (use k=1 for simplicity)
...> init_3x3 = [[0.8, 0.3, 0.2], [0.3, 0.6, 0.2], [0.2, 0.2, 0.5]]
...> kron3 = Yog.Generator.Random.kronecker_general(1, init_3x3)
iex> Yog.Model.order(kron3)
3
"""
@spec kronecker_general(integer(), [[float()]], keyword()) :: Yog.graph()
def kronecker_general(k, initiator, opts \\ []) when k >= 0 do
opts = Keyword.merge([directed: true, seed: nil], opts)
with_seed(opts[:seed], fn ->
n = length(initiator)
num_nodes = Integer.pow(n, k)
# For k=0, return empty graph with 1 node
if k == 0 do
Yog.new(:undirected) |> Yog.add_node(0, nil)
else
# Flatten initiator and normalize
flat_init = List.flatten(initiator)
total = Enum.sum(flat_init)
probs = Enum.map(flat_init, &(&1 / total))
# Calculate cumulative distribution for sampling
cumprobs = cumsum(probs)
# Generate edges
directed = opts[:directed]
# Estimate edges: each node has expected degree based on initiator
n_edges = max(1, div(num_nodes * 2, 1))
base = if directed, do: Yog.directed(), else: Yog.undirected()
graph =
Enum.reduce(0..(num_nodes - 1), base, fn i, g ->
Yog.add_node(g, i, nil)
end)
# Generate edges using simultaneous quadrant selection
# At each level, we choose one of n^2 entries, determining both u and v
edges =
for _ <- 1..n_edges do
{u, v} = kronecker_general_choose_edge(k, n, cumprobs)
{u, v}
end
# Add edges (with deduplication for undirected)
edge_set =
if directed do
MapSet.new(edges)
else
MapSet.new(edges, fn {u, v} -> {min(u, v), max(u, v)} end)
end
Enum.reduce(MapSet.to_list(edge_set), graph, fn {from, to}, g ->
if from != to and from >= 0 and from < num_nodes and to >= 0 and
to < num_nodes do
Yog.add_edge!(g, from, to, 1)
else
g
end
end)
end
end)
end
# ============ R-MAT Implementation ============
defp do_rmat(n, m, {a, b, c, d}, directed) do
base = if directed, do: Yog.directed(), else: Yog.undirected()
# Add all nodes
graph =
Enum.reduce(0..(n - 1), base, fn i, g ->
Yog.add_node(g, i, nil)
end)
# Generate m edges using recursive quadrant selection
# The key insight: at each level, we choose ONE quadrant (a,b,c,d)
# which determines the bits of BOTH u and v simultaneously
edges =
for _ <- 1..m do
{u, v} = rmat_choose_edge(n, a, b, c, d)
{u, v}
end
# Deduplicate and filter self-loops
edge_set =
if directed do
MapSet.new(edges)
else
MapSet.new(edges, fn {u, v} -> {min(u, v), max(u, v)} end)
end
# Add edges to graph
Enum.reduce(MapSet.to_list(edge_set), graph, fn {from, to}, g ->
if from != to do
Yog.add_edge!(g, from, to, 1)
else
g
end
end)
end
# Choose an edge (u, v) by recursively selecting quadrants
# At each level, one of a, b, c, d is chosen, determining bits of both u and v
defp rmat_choose_edge(n, a, b, c, d) do
rmat_choose_edge_recursive(0, n - 1, 0, n - 1, a, b, c, d)
end
defp rmat_choose_edge_recursive(u_lo, u_hi, v_lo, v_hi, _a, _b, _c, _d)
when u_lo == u_hi and v_lo == v_hi do
{u_lo, v_lo}
end
defp rmat_choose_edge_recursive(u_lo, u_hi, v_lo, v_hi, a, b, c, d) do
u_mid = div(u_lo + u_hi, 2)
v_mid = div(v_lo + v_hi, 2)
r = :rand.uniform()
cond do
# Quadrant a: u in first half, v in first half (top-left)
r < a ->
rmat_choose_edge_recursive(u_lo, u_mid, v_lo, v_mid, a, b, c, d)
# Quadrant b: u in first half, v in second half (top-right)
r < a + b ->
rmat_choose_edge_recursive(u_lo, u_mid, v_mid + 1, v_hi, a, b, c, d)
# Quadrant c: u in second half, v in first half (bottom-left)
r < a + b + c ->
rmat_choose_edge_recursive(u_mid + 1, u_hi, v_lo, v_mid, a, b, c, d)
# Quadrant d: u in second half, v in second half (bottom-right)
true ->
rmat_choose_edge_recursive(u_mid + 1, u_hi, v_mid + 1, v_hi, a, b, c, d)
end
end
# Choose an edge (u, v) for general n×n initiator
# At each level, choose one of n^2 entries, determining digits of both u and v
defp kronecker_general_choose_edge(k, n, cumprobs) do
kronecker_general_choose_edge_recursive(k, n, cumprobs, 0, 0)
end
defp kronecker_general_choose_edge_recursive(0, _n, _cumprobs, u, v) do
{u, v}
end
defp kronecker_general_choose_edge_recursive(level, n, cumprobs, u_acc, v_acc) do
r = :rand.uniform()
# Choose one of n^2 entries based on initiator probabilities
entry = find_quadrant(r, cumprobs)
# entry determines the "digit" for both u and v
# entry = u_digit * n + v_digit
u_digit = div(entry, n)
v_digit = rem(entry, n)
new_u = u_acc * n + u_digit
new_v = v_acc * n + v_digit
kronecker_general_choose_edge_recursive(level - 1, n, cumprobs, new_u, new_v)
end
defp find_quadrant(r, cumprobs) do
Enum.find_index(cumprobs, &(&1 >= r)) || length(cumprobs) - 1
end
# ============ Helper Functions ============
defp normalize_params(a, b, c, d, total) do
{a / total, b / total, c / total, d / total}
end
defp add_noise(a, b, c, d, epsilon \\ 0.01) do
# Add small random noise to probabilities
na = max(0, a + (:rand.uniform() - 0.5) * epsilon)
nb = max(0, b + (:rand.uniform() - 0.5) * epsilon)
nc = max(0, c + (:rand.uniform() - 0.5) * epsilon)
nd = max(0, d + (:rand.uniform() - 0.5) * epsilon)
{na, nb, nc, nd}
end
defp power_of_two?(n) when n <= 0, do: false
defp power_of_two?(n), do: Bitwise.band(n, n - 1) == 0
defp cumsum(list), do: cumsum(list, 0, [])
defp cumsum([], _acc, result), do: Enum.reverse(result)
defp cumsum([h | t], acc, result), do: cumsum(t, acc + h, [acc + h | result])
# ============= Geometric Random Graphs =============
@doc """
Generates a random geometric graph in 2D unit square.
Nodes are placed uniformly at random in [0,1]×[0,1], and edges connect
nodes within distance r.
## Options
- `:radius` or `:r` - Connection threshold distance (default: 0.1)
- `:seed` - Random seed for reproducibility
- `:metric` - Distance metric: `:euclidean` (default) or `:manhattan`
- `:periodic` - Whether to use periodic boundary conditions (torus, default: false)
## Examples
iex> # Dense network with large radius
...> dense = Yog.Generator.Random.geometric(100, radius: 0.3)
iex> Yog.Model.order(dense)
100
iex> # Sparse network with small radius
...> sparse = Yog.Generator.Random.geometric(100, radius: 0.05)
iex> Yog.Model.order(sparse)
100
## References
- Penrose, M. "Random Geometric Graphs." Oxford University Press, 2003.
"""
@spec geometric(integer(), keyword()) :: Yog.graph()
def geometric(n, opts \\ []) when n >= 0 do
opts = Keyword.merge([radius: 0.1, seed: nil, metric: :euclidean, periodic: false], opts)
radius = Keyword.get(opts, :r, opts[:radius])
with_seed(opts[:seed], fn ->
do_geometric(n, radius, opts[:metric], opts[:periodic])
end)
end
defp do_geometric(n, radius, metric, periodic) do
base = Yog.undirected()
# Generate random positions for nodes
positions =
for i <- 0..(n - 1) do
{i, {:rand.uniform(), :rand.uniform()}}
end
|> Map.new()
# Add nodes
graph =
Enum.reduce(0..(n - 1), base, fn i, g ->
Yog.add_node(g, i, nil)
end)
# Find edges: O(n²) naive approach
edges =
for i <- 0..(n - 1),
j <- (i + 1)..(n - 1)//1,
within_distance?(positions[i], positions[j], radius, metric, periodic) do
{i, j}
end
Enum.reduce(edges, graph, fn {from, to}, g ->
Yog.add_edge!(g, from, to, 1)
end)
end
@doc """
Generates a random geometric graph in d-dimensional unit hypercube.
## Options
- `:dimensions` or `:d` - Number of dimensions (default: 2)
- `:radius` - Connection threshold (default: 0.1)
- `:seed` - Random seed
## Examples
iex> # 3D spatial network
...> net3d = Yog.Generator.Random.geometric_nd(100, dimensions: 3, radius: 0.2)
iex> Yog.Model.order(net3d)
100
"""
@spec geometric_nd(integer(), keyword()) :: Yog.graph()
def geometric_nd(n, opts \\ []) when n >= 0 do
opts = Keyword.merge([dimensions: 2, radius: 0.1, seed: nil], opts)
dims = Keyword.get(opts, :d, opts[:dimensions])
radius = opts[:radius]
with_seed(opts[:seed], fn ->
do_geometric_nd(n, dims, radius)
end)
end
defp do_geometric_nd(n, dimensions, radius) do
base = Yog.undirected()
radius_sq = radius * radius
# Generate random positions in d-dimensional unit hypercube
positions =
for i <- 0..(n - 1) do
pos = List.duplicate(nil, dimensions) |> Enum.map(fn _ -> :rand.uniform() end)
{i, pos}
end
|> Map.new()
# Add nodes
graph =
Enum.reduce(0..(n - 1), base, fn i, g ->
Yog.add_node(g, i, nil)
end)
# Find edges using squared Euclidean distance (avoiding sqrt)
edges =
for i <- 0..(n - 1),
j <- (i + 1)..(n - 1)//1,
euclidean_distance_sq_nd(positions[i], positions[j]) <= radius_sq do
{i, j}
end
Enum.reduce(edges, graph, fn {from, to}, g ->
Yog.add_edge!(g, from, to, 1)
end)
end
@doc """
Generates a Waxman graph (distance-based probabilistic edges).
Unlike strict geometric graphs, Waxman graphs connect nodes with
probability decreasing with distance: P(u,v) = β × exp(-d(u,v)/(α×L))
where L is the maximum distance.
## Options
- `:alpha` - Distance decay parameter (default: 0.1)
- `:beta` - Edge probability scaling (default: 0.5)
- `:seed` - Random seed
## Examples
iex> wax = Yog.Generator.Random.waxman(50, alpha: 0.15, beta: 0.4)
iex> Yog.Model.order(wax)
50
## References
- Waxman, B. M. "Routing of multipoint connections." IEEE JSAC, 1988.
"""
@spec waxman(integer(), keyword()) :: Yog.graph()
def waxman(n, opts \\ []) when n >= 0 do
opts = Keyword.merge([alpha: 0.1, beta: 0.5, seed: nil], opts)
alpha = opts[:alpha]
beta = opts[:beta]
with_seed(opts[:seed], fn ->
do_waxman(n, alpha, beta)
end)
end
defp do_waxman(n, alpha, beta) do
base = Yog.undirected()
# Generate random positions in unit square
positions =
for i <- 0..(n - 1) do
{i, {:rand.uniform(), :rand.uniform()}}
end
|> Map.new()
# Maximum distance in unit square is sqrt(2)
max_dist = :math.sqrt(2)
# Add nodes
graph =
Enum.reduce(0..(n - 1), base, fn i, g ->
Yog.add_node(g, i, nil)
end)
# Add edges probabilistically based on distance
edges =
for i <- 0..(n - 1),
j <- (i + 1)..(n - 1)//1,
waxman_prob(positions[i], positions[j], alpha, beta, max_dist) >=
:rand.uniform() do
{i, j}
end
Enum.reduce(edges, graph, fn {from, to}, g ->
Yog.add_edge!(g, from, to, 1)
end)
end
# ============ Geometric Helper Functions ============
defp within_distance?({x1, y1}, {x2, y2}, radius, metric, periodic) do
{dx, dy} =
if periodic do
# Periodic boundary conditions (torus)
dx = periodic_diff(x1, x2)
dy = periodic_diff(y1, y2)
{dx, dy}
else
{x1 - x2, y1 - y2}
end
case metric do
:euclidean -> dx * dx + dy * dy <= radius * radius
:manhattan -> abs(dx) + abs(dy) <= radius
:chebyshev -> max(abs(dx), abs(dy)) <= radius
_ -> dx * dx + dy * dy <= radius * radius
end
end
defp periodic_diff(a, b) do
# Shortest distance on a unit torus
d = abs(a - b)
if d > 0.5, do: 1.0 - d, else: d
end
defp euclidean_distance_sq_nd(p1, p2) do
Enum.zip(p1, p2)
|> Enum.reduce(0.0, fn {a, b}, acc -> acc + (a - b) * (a - b) end)
end
defp waxman_prob({x1, y1}, {x2, y2}, alpha, beta, max_dist) do
dist = :math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
beta * :math.exp(-dist / (alpha * max_dist))
end
# Unused - kept for potential future use
# defp estimate_edges(num_nodes, total_initiator, init_size, _k, directed) do
# # Rough estimate: average probability * possible edges
# avg_prob = total_initiator / (init_size * init_size)
# possible = if directed, do: num_nodes * num_nodes, else: num_nodes * (num_nodes - 1) / 2
# round(possible * avg_prob * 0.1) # Scale factor to avoid too many edges
# end
end