# Copyright 2026, IBM Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Random Graph Generator for QuVINE
This module provides functions to generate various types of random graphs with known
structures or specific key elements that are suitable for testing embedding algorithms.
Includes both classical graph models and biologically-inspired structures.
"""
import networkx as nx
import numpy as np
from typing import Optional, Dict, List, Tuple, Set, Sequence, Any, Iterable
import warnings
[docs]
def generate_erdos_renyi(
n: int,
p: Optional[float] = None,
m: Optional[int] = None,
seed: Optional[int] = None,
directed: bool = False
) -> nx.Graph:
"""
Generate an Erdős-Rényi random graph.
Parameters
----------
n : int
Number of nodes
p : float, optional
Probability of edge creation (G(n,p) model)
m : int, optional
Number of edges (G(n,m) model)
seed : int, optional
Random seed for reproducibility
directed : bool, default=False
If True, generate a directed graph
Returns
-------
nx.Graph or nx.DiGraph
Random graph
"""
if p is not None and m is not None:
raise ValueError("Specify either p or m, not both")
if p is not None:
if directed:
return nx.erdos_renyi_graph(n, p, seed=seed, directed=True)
return nx.erdos_renyi_graph(n, p, seed=seed)
elif m is not None:
if directed:
return nx.gnm_random_graph(n, m, seed=seed, directed=True)
return nx.gnm_random_graph(n, m, seed=seed)
else:
raise ValueError("Must specify either p or m")
[docs]
def generate_barabasi_albert(
n: int,
m: int,
seed: Optional[int] = None,
initial_graph: Optional[nx.Graph] = None
) -> nx.Graph:
"""
Generate a Barabási-Albert scale-free network using preferential attachment.
Parameters
----------
n : int
Number of nodes
m : int
Number of edges to attach from a new node to existing nodes
seed : int, optional
Random seed for reproducibility
initial_graph : nx.Graph, optional
Initial connected graph with at least m nodes
Returns
-------
nx.Graph
Scale-free network
"""
return nx.barabasi_albert_graph(n, m, seed=seed, initial_graph=initial_graph)
[docs]
def generate_watts_strogatz(
n: int,
k: int,
p: float,
seed: Optional[int] = None
) -> nx.Graph:
"""
Generate a Watts-Strogatz small-world network.
Parameters
----------
n : int
Number of nodes
k : int
Each node is connected to k nearest neighbors in ring topology
p : float
Probability of rewiring each edge
seed : int, optional
Random seed for reproducibility
Returns
-------
nx.Graph
Small-world network
"""
return nx.watts_strogatz_graph(n, k, p, seed=seed)
[docs]
def generate_powerlaw_cluster(
n: int,
m: int,
p: float,
seed: Optional[int] = None
) -> nx.Graph:
"""
Generate a random graph with powerlaw degree distribution and clustering.
Parameters
----------
n : int
Number of nodes
m : int
Number of random edges to add for each new node
p : float
Probability of adding a triangle after adding a random edge
seed : int, optional
Random seed for reproducibility
Returns
-------
nx.Graph
Powerlaw cluster graph
"""
return nx.powerlaw_cluster_graph(n, m, p, seed=seed)
[docs]
def generate_stochastic_block_model(
sizes: List[int],
p_matrix: List[List[float]],
seed: Optional[int] = None,
directed: bool = False,
selfloops: bool = False
) -> nx.Graph:
"""
Generate a stochastic block model graph with community structure.
Parameters
----------
sizes : list of int
Sizes of blocks (communities)
p_matrix : list of list of float
Matrix of edge probabilities between and within blocks
seed : int, optional
Random seed for reproducibility
directed : bool, default=False
If True, generate a directed graph
selfloops : bool, default=False
If True, allow self-loops
Returns
-------
nx.Graph or nx.DiGraph
Stochastic block model graph with community structure
"""
return nx.stochastic_block_model(
sizes, p_matrix, seed=seed, directed=directed, selfloops=selfloops
)
[docs]
def generate_random_geometric(
n: int,
radius: float,
dim: int = 2,
seed: Optional[int] = None,
pos: Optional[Dict] = None
) -> nx.Graph:
"""
Generate a random geometric graph in the unit cube.
Parameters
----------
n : int
Number of nodes
radius : float
Distance threshold for edge creation
dim : int, default=2
Dimension of the space
seed : int, optional
Random seed for reproducibility
pos : dict, optional
Dictionary of node positions
Returns
-------
nx.Graph
Random geometric graph with 'pos' node attribute
"""
return nx.random_geometric_graph(n, radius, dim=dim, seed=seed, pos=pos)
[docs]
def generate_modular_network(
num_communities: int,
nodes_per_community: int,
p_intra: float,
p_inter: float,
seed: Optional[int] = None
) -> Tuple[nx.Graph, Dict[int, int]]:
"""
Generate a modular network with clear community structure.
Parameters
----------
num_communities : int
Number of communities
nodes_per_community : int
Number of nodes in each community
p_intra : float
Probability of edges within communities
p_inter : float
Probability of edges between communities
seed : int, optional
Random seed for reproducibility
Returns
-------
G : nx.Graph
Modular network
communities : dict
Mapping from node to community ID
"""
sizes = [nodes_per_community] * num_communities
p_matrix = [[p_intra if i == j else p_inter
for j in range(num_communities)]
for i in range(num_communities)]
G = generate_stochastic_block_model(sizes, p_matrix, seed=seed)
# Create community mapping
communities = {}
node_idx = 0
for comm_id, size in enumerate(sizes):
for _ in range(size):
communities[node_idx] = comm_id
node_idx += 1
# Add community as node attribute
nx.set_node_attributes(G, communities, 'community')
return G, communities
[docs]
def generate_hierarchical_network(
levels: int,
branching_factor: int,
p_level: float = 0.1,
seed: Optional[int] = None
) -> Tuple[nx.Graph, Dict[int, int]]:
"""
Generate a hierarchical network with tree-like structure plus random edges.
Parameters
----------
levels : int
Number of hierarchy levels
branching_factor : int
Number of children per parent node
p_level : float, default=0.1
Probability of random edges within same level
seed : int, optional
Random seed for reproducibility
Returns
-------
G : nx.Graph
Hierarchical network
node_levels : dict
Mapping from node to hierarchy level
"""
if seed is not None:
rng = np.random.default_rng(seed)
else:
rng = np.random.default_rng()
# Create tree structure
G = nx.balanced_tree(branching_factor, levels - 1)
# Track node levels
node_levels = {}
for node in G.nodes():
# Calculate level based on tree structure
level = 0
temp_node = node
while temp_node > 0:
temp_node = (temp_node - 1) // branching_factor
level += 1
node_levels[node] = level
# Add random edges within levels
nodes_by_level = {}
for node, level in node_levels.items():
if level not in nodes_by_level:
nodes_by_level[level] = []
nodes_by_level[level].append(node)
for level, nodes in nodes_by_level.items():
if len(nodes) > 1:
for i, u in enumerate(nodes):
for v in nodes[i+1:]:
if rng.random() < p_level:
G.add_edge(u, v)
nx.set_node_attributes(G, node_levels, 'level')
return G, node_levels
[docs]
def generate_core_periphery(
n_core: int,
n_periphery: int,
p_core: float,
p_core_periphery: float,
p_periphery: float = 0.01,
seed: Optional[int] = None
) -> Tuple[nx.Graph, Set[int], Set[int]]:
"""
Generate a core-periphery network structure.
Parameters
----------
n_core : int
Number of core nodes
n_periphery : int
Number of periphery nodes
p_core : float
Edge probability within core
p_core_periphery : float
Edge probability between core and periphery
p_periphery : float, default=0.01
Edge probability within periphery
seed : int, optional
Random seed for reproducibility
Returns
-------
G : nx.Graph
Core-periphery network
core_nodes : set
Set of core node IDs
periphery_nodes : set
Set of periphery node IDs
"""
if seed is not None:
rng = np.random.default_rng(seed)
else:
rng = np.random.default_rng()
n_total = n_core + n_periphery
G = nx.Graph()
G.add_nodes_from(range(n_total))
core_nodes = set(range(n_core))
periphery_nodes = set(range(n_core, n_total))
# Core edges
for i in core_nodes:
for j in core_nodes:
if i < j and rng.random() < p_core:
G.add_edge(i, j)
# Core-periphery edges
for i in core_nodes:
for j in periphery_nodes:
if rng.random() < p_core_periphery:
G.add_edge(i, j)
# Periphery edges
for i in periphery_nodes:
for j in periphery_nodes:
if i < j and rng.random() < p_periphery:
G.add_edge(i, j)
# Add node attributes
node_types = {node: 'core' if node in core_nodes else 'periphery'
for node in G.nodes()}
nx.set_node_attributes(G, node_types, 'type')
return G, core_nodes, periphery_nodes
[docs]
def generate_bipartite_random(
n1: int,
n2: int,
p: Optional[float] = None,
m: Optional[int] = None,
seed: Optional[int] = None
) -> Tuple[nx.Graph, Set[int], Set[int]]:
"""
Generate a random bipartite graph.
Parameters
----------
n1 : int
Number of nodes in first partition
n2 : int
Number of nodes in second partition
p : float, optional
Probability of edge creation
m : int, optional
Number of edges
seed : int, optional
Random seed for reproducibility
Returns
-------
G : nx.Graph
Bipartite graph
set1 : set
First partition node IDs
set2 : set
Second partition node IDs
"""
if p is not None and m is not None:
raise ValueError("Specify either p or m, not both")
if p is not None:
G = nx.bipartite.random_graph(n1, n2, p, seed=seed)
elif m is not None:
G = nx.bipartite.gnmk_random_graph(n1, n2, m, seed=seed)
else:
raise ValueError("Must specify either p or m")
set1 = {n for n, d in G.nodes(data=True) if d['bipartite'] == 0}
set2 = set(G.nodes()) - set1
return G, set1, set2
[docs]
def add_hub_nodes(
G: nx.Graph,
num_hubs: int,
hub_degree: int,
seed: Optional[int] = None
) -> Tuple[nx.Graph, List[int]]:
"""
Add hub nodes to an existing graph.
Parameters
----------
G : nx.Graph
Input graph
num_hubs : int
Number of hub nodes to add
hub_degree : int
Degree of each hub node
seed : int, optional
Random seed for reproducibility
Returns
-------
G : nx.Graph
Graph with added hubs
hub_nodes : list
List of hub node IDs
"""
if seed is not None:
rng = np.random.default_rng(seed)
else:
rng = np.random.default_rng()
G = G.copy()
existing_nodes = list(G.nodes())
n_existing = len(existing_nodes)
hub_nodes = []
for i in range(num_hubs):
hub_id = n_existing + i
G.add_node(hub_id)
hub_nodes.append(hub_id)
# Connect to random existing nodes
targets = rng.choice(existing_nodes, size=min(hub_degree, n_existing), replace=False)
for target in targets:
G.add_edge(hub_id, target)
return G, hub_nodes
[docs]
def generate_graph_with_seeds_and_targets(
n: int,
num_seeds: int,
num_targets: int,
graph_type: str = 'barabasi_albert',
seed: Optional[int] = None,
**kwargs
) -> Tuple[nx.Graph, List[int], List[int]]:
"""
Generate a random graph with designated seed and target nodes for embedding evaluation.
Parameters
----------
n : int
Total number of nodes
num_seeds : int
Number of seed nodes
num_targets : int
Number of target nodes
graph_type : str, default='barabasi_albert'
Type of graph: 'erdos_renyi', 'barabasi_albert', 'watts_strogatz',
'powerlaw_cluster', 'modular'
seed : int, optional
Random seed for reproducibility
**kwargs
Additional parameters for specific graph types
Returns
-------
G : nx.Graph
Generated graph
seeds : list
List of seed node IDs
targets : list
List of target node IDs
"""
if seed is not None:
rng = np.random.default_rng(seed)
else:
rng = np.random.default_rng()
# Generate base graph
if graph_type == 'erdos_renyi':
p = kwargs.get('p', 0.1)
G = generate_erdos_renyi(n, p=p, seed=seed)
elif graph_type == 'barabasi_albert':
m = kwargs.get('m', 3)
G = generate_barabasi_albert(n, m, seed=seed)
elif graph_type == 'watts_strogatz':
k = kwargs.get('k', 4)
p = kwargs.get('p', 0.3)
G = generate_watts_strogatz(n, k, p, seed=seed)
elif graph_type == 'powerlaw_cluster':
m = kwargs.get('m', 3)
p = kwargs.get('p', 0.3)
G = generate_powerlaw_cluster(n, m, p, seed=seed)
elif graph_type == 'modular':
num_communities = kwargs.get('num_communities', 5)
nodes_per_community = n // num_communities
p_intra = kwargs.get('p_intra', 0.3)
p_inter = kwargs.get('p_inter', 0.01)
G, _ = generate_modular_network(num_communities, nodes_per_community,
p_intra, p_inter, seed=seed)
else:
raise ValueError(f"Unknown graph_type: {graph_type}")
# Select seeds and targets
nodes = list(G.nodes())
selected = rng.choice(nodes, size=num_seeds + num_targets, replace=False)
seeds = selected[:num_seeds].tolist()
targets = selected[num_seeds:].tolist()
# Add node attributes
node_roles = {}
for node in G.nodes():
if node in seeds:
node_roles[node] = 'seed'
elif node in targets:
node_roles[node] = 'target'
else:
node_roles[node] = 'regular'
nx.set_node_attributes(G, node_roles, 'role')
return G, seeds, targets
[docs]
def get_graph_statistics(G: nx.Graph) -> Dict:
"""
Compute comprehensive statistics for a graph.
Parameters
----------
G : nx.Graph
Input graph
Returns
-------
dict
Dictionary of graph statistics
"""
stats = {
'num_nodes': G.number_of_nodes(),
'num_edges': G.number_of_edges(),
'density': nx.density(G),
'is_connected': nx.is_connected(G),
}
if G.number_of_nodes() > 0:
degrees = [d for n, d in G.degree()]
stats['avg_degree'] = float(np.mean(degrees))
stats['max_degree'] = np.max(degrees)
stats['min_degree'] = np.min(degrees)
if nx.is_connected(G):
stats['diameter'] = nx.diameter(G)
stats['avg_shortest_path'] = nx.average_shortest_path_length(G)
else:
stats['num_components'] = nx.number_connected_components(G)
largest_cc = max(nx.connected_components(G), key=len)
stats['largest_cc_size'] = len(largest_cc)
stats['avg_clustering'] = nx.average_clustering(G)
stats['transitivity'] = nx.transitivity(G)
return stats
[docs]
def generate_comprehensive_dataset(
n_instances: int = 30,
base_seed: int = 42,
n_nodes: int = 200,
save_dir: Optional[str] = None
) -> Dict[str, List[Tuple[nx.Graph, Dict]]]:
"""
Generate a comprehensive dataset of random graphs for embedding analysis.
This function generates multiple instances of each graph type with varying
random seeds to capture natural parameter variations.
Graph types included:
- Erdős-Rényi (random)
- Barabási-Albert (scale-free)
- Watts-Strogatz (small-world)
- Powerlaw Cluster (scale-free with clustering)
- Stochastic Block Model (modular/community structure)
- Random Geometric (spatial networks)
- Hierarchical (tree-like structure)
- Core-Periphery (hub-spoke structure)
- Bipartite Random (two-mode networks)
Parameters
----------
n_instances : int, default=30
Number of instances to generate for each graph type
base_seed : int, default=42
Base random seed (each instance uses base_seed + instance_id)
n_nodes : int, default=200
Target number of nodes for each graph
save_dir : str, optional
If provided, save graphs to this directory
Returns
-------
dict
Dictionary mapping graph type names to lists of (graph, metadata) tuples
Examples
--------
>>> dataset = generate_comprehensive_dataset(n_instances=30, n_nodes=200)
>>> print(f"Generated {sum(len(v) for v in dataset.values())} graphs")
>>> print(f"Graph types: {list(dataset.keys())}")
"""
import os
from pathlib import Path
dataset = {}
# 1. Erdős-Rényi (Random)
print(f"Generating {n_instances} Erdős-Rényi graphs...")
dataset['erdos_renyi'] = []
for i in range(n_instances):
seed = base_seed + i
p = 0.05 + (i / n_instances) * 0.10 # Vary density from 0.05 to 0.15
G = generate_erdos_renyi(n_nodes, p=p, seed=seed)
metadata = {
'type': 'erdos_renyi',
'instance': i,
'seed': seed,
'n_nodes': n_nodes,
'p': p,
'params': {'n': n_nodes, 'p': p}
}
dataset['erdos_renyi'].append((G, metadata))
# 2. Barabási-Albert (Scale-Free)
print(f"Generating {n_instances} Barabási-Albert graphs...")
dataset['barabasi_albert'] = []
for i in range(n_instances):
seed = base_seed + i
m = 2 + (i % 5) # Vary m from 2 to 6
G = generate_barabasi_albert(n_nodes, m=m, seed=seed)
metadata = {
'type': 'barabasi_albert',
'instance': i,
'seed': seed,
'n_nodes': n_nodes,
'm': m,
'params': {'n': n_nodes, 'm': m}
}
dataset['barabasi_albert'].append((G, metadata))
# 3. Watts-Strogatz (Small-World)
print(f"Generating {n_instances} Watts-Strogatz graphs...")
dataset['watts_strogatz'] = []
for i in range(n_instances):
seed = base_seed + i
k = 4 + (i % 6) * 2 # Vary k from 4 to 14
p = 0.1 + (i / n_instances) * 0.4 # Vary rewiring prob from 0.1 to 0.5
G = generate_watts_strogatz(n_nodes, k=k, p=p, seed=seed)
metadata = {
'type': 'watts_strogatz',
'instance': i,
'seed': seed,
'n_nodes': n_nodes,
'k': k,
'p': p,
'params': {'n': n_nodes, 'k': k, 'p': p}
}
dataset['watts_strogatz'].append((G, metadata))
# 4. Powerlaw Cluster (Scale-Free with Clustering)
print(f"Generating {n_instances} Powerlaw Cluster graphs...")
dataset['powerlaw_cluster'] = []
for i in range(n_instances):
seed = base_seed + i
m = 2 + (i % 4) # Vary m from 2 to 5
p = 0.1 + (i / n_instances) * 0.3 # Vary triangle prob from 0.1 to 0.4
G = generate_powerlaw_cluster(n_nodes, m=m, p=p, seed=seed)
metadata = {
'type': 'powerlaw_cluster',
'instance': i,
'seed': seed,
'n_nodes': n_nodes,
'm': m,
'p': p,
'params': {'n': n_nodes, 'm': m, 'p': p}
}
dataset['powerlaw_cluster'].append((G, metadata))
# 5. Stochastic Block Model (Modular)
print(f"Generating {n_instances} Stochastic Block Model graphs...")
dataset['stochastic_block_model'] = []
for i in range(n_instances):
seed = base_seed + i
n_communities = 3 + (i % 5) # Vary communities from 3 to 7
p_in = 0.3 + (i / n_instances) * 0.3 # Vary intra-community from 0.3 to 0.6
p_out = 0.01 + (i / n_instances) * 0.04 # Vary inter-community from 0.01 to 0.05
# Create block sizes
block_sizes = [n_nodes // n_communities] * n_communities
# Adjust last block to account for rounding
block_sizes[-1] += n_nodes - sum(block_sizes)
# Create probability matrix
p_matrix = [[p_out] * n_communities for _ in range(n_communities)]
for j in range(n_communities):
p_matrix[j][j] = p_in
G = generate_stochastic_block_model(block_sizes, p_matrix, seed=seed)
metadata = {
'type': 'stochastic_block_model',
'instance': i,
'seed': seed,
'n_nodes': n_nodes,
'n_communities': n_communities,
'p_in': p_in,
'p_out': p_out,
'params': {'block_sizes': block_sizes, 'p_in': p_in, 'p_out': p_out}
}
dataset['stochastic_block_model'].append((G, metadata))
# 6. Random Geometric (Spatial)
print(f"Generating {n_instances} Random Geometric graphs...")
dataset['random_geometric'] = []
for i in range(n_instances):
seed = base_seed + i
radius = 0.1 + (i / n_instances) * 0.15 # Vary radius from 0.1 to 0.25
dim = 2 # 2D space
G = generate_random_geometric(n_nodes, radius=radius, dim=dim, seed=seed)
metadata = {
'type': 'random_geometric',
'instance': i,
'seed': seed,
'n_nodes': n_nodes,
'radius': radius,
'dim': dim,
'params': {'n': n_nodes, 'radius': radius, 'dim': dim}
}
dataset['random_geometric'].append((G, metadata))
# 7. Hierarchical
print(f"Generating {n_instances} Hierarchical graphs...")
dataset['hierarchical'] = []
for i in range(n_instances):
seed = base_seed + i
branching_factor = 2 + (i % 3) # Vary branching from 2 to 4
levels = 4 + (i % 3) # Vary levels from 4 to 6
p_level = 0.01 + (i / n_instances) * 0.09 # Vary cross-level from 0.01 to 0.1
G, node_levels = generate_hierarchical_network(
levels=levels,
branching_factor=branching_factor,
p_level=p_level,
seed=seed
)
metadata = {
'type': 'hierarchical',
'instance': i,
'seed': seed,
'branching_factor': branching_factor,
'levels': levels,
'p_level': p_level,
'params': {'branching_factor': branching_factor, 'levels': levels, 'p_level': p_level}
}
dataset['hierarchical'].append((G, metadata))
# 8. Core-Periphery
print(f"Generating {n_instances} Core-Periphery graphs...")
dataset['core_periphery'] = []
for i in range(n_instances):
seed = base_seed + i
core_size = int(n_nodes * (0.1 + (i / n_instances) * 0.2)) # Core 10-30% of nodes
p_core = 0.5 + (i / n_instances) * 0.3 # Vary core density from 0.5 to 0.8
p_periphery = 0.01 + (i / n_instances) * 0.04 # Vary periphery from 0.01 to 0.05
p_core_periphery = 0.1 + (i / n_instances) * 0.2 # Vary connection from 0.1 to 0.3
G = generate_core_periphery(
n_core=core_size,
n_periphery=n_nodes - core_size,
p_core=p_core,
p_periphery=p_periphery,
p_core_periphery=p_core_periphery,
seed=seed
)
metadata = {
'type': 'core_periphery',
'instance': i,
'seed': seed,
'n_nodes': n_nodes,
'core_size': core_size,
'p_core': p_core,
'p_periphery': p_periphery,
'p_core_periphery': p_core_periphery,
'params': {
'n_core': core_size,
'n_periphery': n_nodes - core_size,
'p_core': p_core,
'p_periphery': p_periphery,
'p_core_periphery': p_core_periphery
}
}
dataset['core_periphery'].append((G, metadata))
# 9. Bipartite Random
print(f"Generating {n_instances} Bipartite Random graphs...")
dataset['bipartite_random'] = []
for i in range(n_instances):
seed = base_seed + i
n1 = n_nodes // 2 + (i % 20) - 10 # Vary split around 50/50
n2 = n_nodes - n1
p = 0.05 + (i / n_instances) * 0.15 # Vary edge probability from 0.05 to 0.2
G = generate_bipartite_random(n1=n1, n2=n2, p=p, seed=seed)
metadata = {
'type': 'bipartite_random',
'instance': i,
'seed': seed,
'n_nodes': n_nodes,
'n1': n1,
'n2': n2,
'p': p,
'params': {'n1': n1, 'n2': n2, 'p': p}
}
dataset['bipartite_random'].append((G, metadata))
# 10. Random Regular / Expander-like
print(f"Generating {n_instances} Random Regular graphs...")
dataset['random_regular'] = []
for i in range(n_instances):
seed = base_seed + i
d = 3 + (i % 8) # Vary degree from 3 to 10
# Ensure n*d is even
if (n_nodes * d) % 2 != 0:
d += 1
G = generate_random_regular_expander_like(n_nodes, d=d, seed=seed)
metadata = dict(G.graph)
metadata['instance'] = i
dataset['random_regular'].append((G, metadata))
# 11. Heterophilic SBM
print(f"Generating {n_instances} Heterophilic SBM graphs...")
dataset['heterophilic_sbm'] = []
for i in range(n_instances):
seed = base_seed + i
n_blocks = 2 + (i % 4) # Vary blocks from 2 to 5
target_avg_degree = 4.0 + (i / n_instances) * 8.0 # Vary from 4 to 12
out_in_ratio = 1.0 + (i / n_instances) * 7.0 # Vary from 1.0 to 8.0
G, labels = generate_heterophilic_sbm(
n=n_nodes,
n_blocks=n_blocks,
target_avg_degree=target_avg_degree,
out_in_ratio=out_in_ratio,
seed=seed
)
metadata = dict(G.graph)
metadata['instance'] = i
dataset['heterophilic_sbm'].append((G, metadata))
# 12. Degree-Corrected SBM
print(f"Generating {n_instances} Degree-Corrected SBM graphs...")
dataset['degree_corrected_sbm'] = []
for i in range(n_instances):
seed = base_seed + i
n_blocks = 3 + (i % 3) # Vary blocks from 3 to 5
target_avg_degree = 4.0 + (i / n_instances) * 8.0 # Vary from 4 to 12
out_in_ratio = 0.1 + (i / n_instances) * 3.9 # Vary from 0.1 to 4.0
dist = 'powerlaw' if i % 2 == 0 else 'lognormal'
G, labels = generate_degree_corrected_sbm(
n=n_nodes,
n_blocks=n_blocks,
target_avg_degree=target_avg_degree,
out_in_ratio=out_in_ratio,
degree_distribution=dist,
seed=seed
)
metadata = dict(G.graph)
metadata['instance'] = i
dataset['degree_corrected_sbm'].append((G, metadata))
# 13. Grid/Torus Lattice
print(f"Generating {n_instances} Grid/Torus Lattice graphs...")
dataset['grid_torus'] = []
for i in range(n_instances):
seed = base_seed + i
periodic = (i % 2 == 0) # Alternate between grid and torus
add_diagonals = (i % 3 == 0) # Add diagonals every 3rd graph
G = generate_grid_torus_lattice(
n=n_nodes,
periodic=periodic,
add_diagonals=add_diagonals,
seed=seed
)
metadata = dict(G.graph)
metadata['instance'] = i
dataset['grid_torus'].append((G, metadata))
# 14. Configuration Model
print(f"Generating {n_instances} Configuration Model graphs...")
dataset['configuration_model'] = []
for i in range(n_instances):
seed = base_seed + i
target_avg_degree = 4.0 + (i / n_instances) * 8.0 # Vary from 4 to 12
if i % 3 == 0:
dist = 'powerlaw'
gamma = 2.2 + (i / n_instances) * 0.8 # Vary gamma from 2.2 to 3.0
elif i % 3 == 1:
dist = 'lognormal'
gamma = 2.5 # Not used for lognormal
else:
dist = 'poisson'
gamma = 2.5 # Not used for poisson
G = generate_configuration_model_graph(
n=n_nodes,
distribution=dist,
target_avg_degree=target_avg_degree,
gamma=gamma,
seed=seed
)
metadata = dict(G.graph)
metadata['instance'] = i
dataset['configuration_model'].append((G, metadata))
# Save graphs if directory provided
if save_dir:
save_dir = Path(save_dir)
save_dir.mkdir(parents=True, exist_ok=True)
print(f"\nSaving graphs to {save_dir}...")
for graph_type, graphs in dataset.items():
type_dir = save_dir / graph_type
type_dir.mkdir(exist_ok=True)
for i, (G, metadata) in enumerate(graphs):
# Save graph
graph_file = type_dir / f"{graph_type}_{i:03d}.graphml"
nx.write_graphml(G, graph_file)
# Save metadata
metadata_file = type_dir / f"{graph_type}_{i:03d}_metadata.json"
import json
with open(metadata_file, 'w') as f:
json.dump(metadata, f, indent=2)
# Print summary
print("\n" + "="*60)
print("Dataset Generation Summary")
print("="*60)
total_graphs = 0
for graph_type, graphs in dataset.items():
print(f"{graph_type:25s}: {len(graphs):3d} graphs")
total_graphs += len(graphs)
print("-"*60)
print(f"{'Total':25s}: {total_graphs:3d} graphs")
print("="*60)
return dataset
[docs]
def load_comprehensive_dataset(load_dir: str) -> Dict[str, List[Tuple[nx.Graph, Dict]]]:
"""
Load a previously saved comprehensive dataset.
Parameters
----------
load_dir : str
Directory containing saved graphs
Returns
-------
dict
Dictionary mapping graph type names to lists of (graph, metadata) tuples
"""
import json
from pathlib import Path
load_dir = Path(load_dir)
dataset = {}
# Find all graph type directories
for type_dir in sorted(load_dir.iterdir()):
if not type_dir.is_dir():
continue
graph_type = type_dir.name
dataset[graph_type] = []
# Load all graphs in this directory
graph_files = sorted(type_dir.glob("*.graphml"))
for graph_file in graph_files:
# Load graph
G = nx.read_graphml(graph_file)
# Load metadata
metadata_file = graph_file.with_suffix('').with_suffix('.json').with_name(
graph_file.stem + '_metadata.json'
)
if metadata_file.exists():
with open(metadata_file, 'r') as f:
metadata = json.load(f)
else:
metadata = {'type': graph_type}
dataset[graph_type].append((G, metadata))
print(f"Loaded {sum(len(v) for v in dataset.values())} graphs from {load_dir}")
return dataset
# ===========================================================================
# 1. Random Regular / Expander-like Graphs
# ===========================================================================
[docs]
def generate_random_regular_expander_like(
n: int,
d: int,
seed: Optional[int] = None,
make_connected: bool = True,
max_tries: int = 25,
) -> nx.Graph:
"""
Generate a random d-regular graph. Random regular graphs are a practical
expander-like family for QuVINE sweeps.
Parameters
----------
n : int
Number of nodes.
d : int
Regular degree. Must satisfy 0 <= d < n and n*d even.
seed : int, optional
Random seed.
make_connected : bool
If True, retry until connected; if retries fail, connect components.
max_tries : int
Number of random draws before bridge-connecting components.
"""
if d < 0 or d >= n:
raise ValueError("d must satisfy 0 <= d < n")
if (n * d) % 2 != 0:
raise ValueError("n * d must be even for a d-regular graph")
last_G = None
for attempt in range(max_tries):
s = None if seed is None else _stable_seed(seed, "rr", attempt)
G = nx.random_regular_graph(d=d, n=n, seed=s)
last_G = G
if not make_connected or nx.is_connected(G):
break
else:
G = _connect_components_by_bridges(last_G, seed=seed)
G = _postprocess_graph(G, seed=seed, make_connected=False)
metadata = {
"type": "random_regular_expander_like",
"n_nodes": int(n),
"d": int(d),
"target_avg_degree": float(d),
"seed": seed,
"params": {"n": int(n), "d": int(d)},
}
metadata.update(_graph_basic_metadata(G))
return _add_metadata(G, metadata)
[docs]
def sweep_random_regular_expander_like(
n_values: Sequence[int] = (1000, 2000, 5000),
d_values: Sequence[int] = (3, 6, 10, 20),
seeds: Sequence[int] = (0, 1, 2),
make_connected: bool = True,
) -> List[Tuple[nx.Graph, Dict[str, Any]]]:
"""Generate sweep of random regular graphs."""
graphs = []
for n, d, seed in product(n_values, d_values, seeds):
if d >= n or (n * d) % 2 != 0:
continue
G = generate_random_regular_expander_like(n, d, seed=seed, make_connected=make_connected)
graphs.append((G, dict(G.graph)))
return graphs
# ===========================================================================
# 2. Heterophilic / Disassortative SBM
# ===========================================================================
[docs]
def generate_heterophilic_sbm(
n: int,
n_blocks: int,
target_avg_degree: float,
out_in_ratio: float,
seed: Optional[int] = None,
make_connected: bool = True,
selfloops: bool = False,
) -> Tuple[nx.Graph, Dict[int, int]]:
"""
Generate an SBM where p_out / p_in is controlled. For out_in_ratio > 1,
between-block edges are more likely than within-block edges, producing a
heterophilic/disassortative block structure.
"""
sizes = _balanced_block_sizes(n, n_blocks)
P = _sbm_prob_matrix_from_out_in_ratio(
sizes=sizes,
target_avg_degree=target_avg_degree,
out_in_ratio=out_in_ratio,
)
G = nx.stochastic_block_model(sizes, P.tolist(), seed=seed, selfloops=selfloops)
G = _postprocess_graph(G, seed=seed, make_connected=make_connected)
labels: Dict[int, int] = {}
start = 0
for b, size in enumerate(sizes):
for node in range(start, start + size):
if node in G:
labels[node] = b
start += size
nx.set_node_attributes(G, labels, "block")
metadata = {
"type": "heterophilic_sbm",
"n_nodes": int(n),
"n_blocks": int(n_blocks),
"target_avg_degree": float(target_avg_degree),
"out_in_ratio": float(out_in_ratio),
"p_in": float(np.diag(P).mean()),
"p_out_mean": float((P.sum() - np.trace(P)) / max(P.size - len(P), 1)),
"block_sizes": sizes,
"seed": seed,
"params": {
"n": int(n),
"n_blocks": int(n_blocks),
"target_avg_degree": float(target_avg_degree),
"out_in_ratio": float(out_in_ratio),
},
}
metadata.update(_graph_basic_metadata(G))
_add_metadata(G, metadata)
return G, labels
[docs]
def sweep_heterophilic_sbm(
n_values: Sequence[int] = (1000, 2000, 5000),
n_blocks_values: Sequence[int] = (2, 4, 8),
avg_degree_values: Sequence[float] = (4, 8, 16),
out_in_ratios: Sequence[float] = (1.0, 2.0, 4.0, 8.0),
seeds: Sequence[int] = (0, 1, 2),
make_connected: bool = True,
) -> List[Tuple[nx.Graph, Dict[str, Any]]]:
"""Generate sweep of heterophilic SBM graphs."""
graphs = []
for n, b, avg_d, ratio, seed in product(
n_values, n_blocks_values, avg_degree_values, out_in_ratios, seeds
):
if b > n:
continue
G, _ = generate_heterophilic_sbm(
n=n,
n_blocks=b,
target_avg_degree=avg_d,
out_in_ratio=ratio,
seed=seed,
make_connected=make_connected,
)
graphs.append((G, dict(G.graph)))
return graphs
# ===========================================================================
# 3. Degree-Corrected SBM
# ===========================================================================
def _sample_degree_weights(
n: int,
distribution: str,
seed: Optional[int] = None,
gamma: float = 2.5,
lognormal_sigma: float = 1.0,
max_weight_quantile: float = 0.995,
) -> np.ndarray:
"""Sample degree weights from specified distribution."""
rng = _rng(seed)
if distribution == "powerlaw":
a = max(float(gamma) - 1.0, 0.2)
w = rng.pareto(a=a, size=n) + 1.0
elif distribution == "lognormal":
w = rng.lognormal(mean=0.0, sigma=float(lognormal_sigma), size=n)
elif distribution == "uniform":
w = np.ones(n, dtype=float)
else:
raise ValueError("distribution must be 'powerlaw', 'lognormal', or 'uniform'")
if n > 10:
cap = np.quantile(w, max_weight_quantile)
w = np.minimum(w, cap)
w = np.maximum(w, 1e-12)
return w
[docs]
def generate_degree_corrected_sbm(
n: int,
n_blocks: int,
target_avg_degree: float,
out_in_ratio: float = 0.1,
degree_distribution: str = "powerlaw",
gamma: float = 2.5,
lognormal_sigma: float = 1.0,
seed: Optional[int] = None,
max_prob: float = 0.95,
make_connected: bool = True,
) -> Tuple[nx.Graph, Dict[int, int]]:
"""
Generate a degree-corrected SBM.
Edge probabilities are::
P_ij = scale * R_{b_i,b_j} * theta_i * theta_j
where theta values are normalized to have mean 1 within each block.
"""
rng = _rng(seed)
sizes = _balanced_block_sizes(n, n_blocks)
blocks = np.empty(n, dtype=int)
start = 0
for b, size in enumerate(sizes):
blocks[start:start + size] = b
start += size
theta = np.empty(n, dtype=float)
start = 0
for b, size in enumerate(sizes):
s = None if seed is None else _stable_seed(seed, "theta", b)
w = _sample_degree_weights(
size,
distribution=degree_distribution,
seed=s,
gamma=gamma,
lognormal_sigma=lognormal_sigma,
)
theta[start:start + size] = w / w.mean()
start += size
R = np.full((n_blocks, n_blocks), float(out_in_ratio), dtype=float)
np.fill_diagonal(R, 1.0)
raw_expected = 0.0
for a in range(n_blocks):
idx_a = np.where(blocks == a)[0]
ta = theta[idx_a]
raw_expected += R[a, a] * (ta.sum() ** 2 - np.sum(ta ** 2)) / 2.0
for b in range(a + 1, n_blocks):
idx_b = np.where(blocks == b)[0]
raw_expected += R[a, b] * ta.sum() * theta[idx_b].sum()
desired_edges = n * float(target_avg_degree) / 2.0
scale = desired_edges / max(raw_expected, 1e-12)
G = nx.Graph()
G.add_nodes_from(range(n))
for a in range(n_blocks):
idx_a = np.where(blocks == a)[0]
ta = theta[idx_a]
Paa = scale * R[a, a] * np.outer(ta, ta)
Paa = np.clip(Paa, 0.0, max_prob)
iu, ju = np.triu_indices(len(idx_a), k=1)
mask = rng.random(len(iu)) < Paa[iu, ju]
edges = [(int(idx_a[i]), int(idx_a[j])) for i, j in zip(iu[mask], ju[mask])]
G.add_edges_from(edges)
for b in range(a + 1, n_blocks):
idx_b = np.where(blocks == b)[0]
tb = theta[idx_b]
Pab = scale * R[a, b] * np.outer(ta, tb)
Pab = np.clip(Pab, 0.0, max_prob)
coords = np.where(rng.random(Pab.shape) < Pab)
edges = [(int(idx_a[i]), int(idx_b[j])) for i, j in zip(coords[0], coords[1])]
G.add_edges_from(edges)
G = _postprocess_graph(G, seed=seed, make_connected=make_connected)
block_dict = {i: int(blocks[i]) for i in range(n)}
theta_dict = {i: float(theta[i]) for i in range(n)}
nx.set_node_attributes(G, block_dict, "block")
nx.set_node_attributes(G, theta_dict, "degree_weight")
metadata = {
"type": "degree_corrected_sbm",
"n_nodes": int(n),
"n_blocks": int(n_blocks),
"target_avg_degree": float(target_avg_degree),
"out_in_ratio": float(out_in_ratio),
"degree_distribution": degree_distribution,
"gamma": float(gamma),
"lognormal_sigma": float(lognormal_sigma),
"scale": float(scale),
"max_prob": float(max_prob),
"block_sizes": sizes,
"seed": seed,
"params": {
"n": int(n),
"n_blocks": int(n_blocks),
"target_avg_degree": float(target_avg_degree),
"out_in_ratio": float(out_in_ratio),
"degree_distribution": degree_distribution,
},
}
metadata.update(_graph_basic_metadata(G))
_add_metadata(G, metadata)
return G, block_dict
[docs]
def sweep_degree_corrected_sbm(
n_values: Sequence[int] = (1000, 2000, 5000),
n_blocks_values: Sequence[int] = (4, 8),
avg_degree_values: Sequence[float] = (4, 8, 16),
out_in_ratios: Sequence[float] = (0.1, 0.5, 1.0, 2.0, 4.0),
degree_distributions: Sequence[str] = ("powerlaw", "lognormal"),
seeds: Sequence[int] = (0, 1, 2),
make_connected: bool = True,
) -> List[Tuple[nx.Graph, Dict[str, Any]]]:
"""Generate sweep of degree-corrected SBM graphs."""
graphs = []
for n, b, avg_d, ratio, dist, seed in product(
n_values,
n_blocks_values,
avg_degree_values,
out_in_ratios,
degree_distributions,
seeds,
):
if b > n:
continue
G, _ = generate_degree_corrected_sbm(
n=n,
n_blocks=b,
target_avg_degree=avg_d,
out_in_ratio=ratio,
degree_distribution=dist,
seed=seed,
make_connected=make_connected,
)
graphs.append((G, dict(G.graph)))
return graphs
# ===========================================================================
# 4. Grid / Torus Lattice
# ===========================================================================
def _side_lengths_for_n(n: int, dim: int) -> Tuple[int, ...]:
"""Calculate side lengths for grid with approximately n nodes."""
side = int(round(n ** (1.0 / dim)))
side = max(side, 2)
return tuple([side] * dim)
[docs]
def generate_grid_torus_lattice(
n: Optional[int] = None,
side_lengths: Optional[Sequence[int]] = None,
dim: int = 2,
periodic: bool = True,
add_diagonals: bool = False,
seed: Optional[int] = None,
) -> nx.Graph:
"""
Generate a regular grid or torus lattice. If periodic=True, this is a torus.
Parameters
----------
n : int, optional
Approximate target number of nodes. Ignored if side_lengths is provided.
side_lengths : sequence of int, optional
Grid shape, e.g. (50, 50) for 2500 nodes.
dim : int
Dimension used when side_lengths is not provided.
periodic : bool
If True, use periodic boundary conditions.
add_diagonals : bool
For 2D grids only, add diagonal lattice edges to increase local cycles.
"""
if side_lengths is None:
if n is None:
raise ValueError("Specify either n or side_lengths")
side_lengths = _side_lengths_for_n(n, dim=dim)
side_lengths = tuple(int(x) for x in side_lengths)
G = nx.grid_graph(dim=list(side_lengths), periodic=periodic)
if add_diagonals and len(side_lengths) == 2:
m, k = side_lengths
for i in range(m):
for j in range(k):
u = (i, j)
candidates = [
((i + 1) % m, (j + 1) % k),
((i + 1) % m, (j - 1) % k),
] if periodic else [
(i + 1, j + 1),
(i + 1, j - 1),
]
for v in candidates:
if 0 <= v[0] < m and 0 <= v[1] < k:
G.add_edge(u, v)
# Relabel to integers and preserve positions
old_nodes = list(G.nodes())
mapping = {node: idx for idx, node in enumerate(old_nodes)}
pos_attrs = {mapping[node]: tuple(float(x) for x in node) for node in old_nodes}
G = nx.relabel_nodes(G, mapping)
nx.set_node_attributes(G, pos_attrs, "pos")
G = _postprocess_graph(G, seed=seed, make_connected=False)
metadata = {
"type": "grid_torus_lattice" if periodic else "grid_lattice",
"side_lengths": side_lengths,
"dim": int(len(side_lengths)),
"periodic": bool(periodic),
"add_diagonals": bool(add_diagonals),
"n_nodes_requested": n,
"seed": seed,
"params": {
"side_lengths": side_lengths,
"periodic": bool(periodic),
"add_diagonals": bool(add_diagonals),
},
}
metadata.update(_graph_basic_metadata(G))
return _add_metadata(G, metadata)
[docs]
def sweep_grid_torus_lattice(
n_values: Sequence[int] = (1024, 2025, 4900),
dims: Sequence[int] = (2,),
periodic_values: Sequence[bool] = (False, True),
diagonal_values: Sequence[bool] = (False, True),
seeds: Sequence[int] = (0,),
) -> List[Tuple[nx.Graph, Dict[str, Any]]]:
"""Generate sweep of grid/torus lattice graphs."""
graphs = []
for n, dim, periodic, diag, seed in product(n_values, dims, periodic_values, diagonal_values, seeds):
if dim != 2 and diag:
continue
G = generate_grid_torus_lattice(n=n, dim=dim, periodic=periodic, add_diagonals=diag, seed=seed)
graphs.append((G, dict(G.graph)))
return graphs
# ===========================================================================
# 5. Configuration Model
# ===========================================================================
[docs]
def sample_degree_sequence(
n: int,
distribution: str,
target_avg_degree: float,
seed: Optional[int] = None,
gamma: float = 2.5,
lognormal_sigma: float = 1.0,
max_degree_fraction: float = 0.1,
) -> np.ndarray:
"""Sample a degree sequence and rescale it to target average degree."""
rng = _rng(seed)
if distribution == "powerlaw":
a = max(float(gamma) - 1.0, 0.2)
raw = rng.pareto(a=a, size=n) + 1.0
elif distribution == "lognormal":
raw = rng.lognormal(mean=0.0, sigma=float(lognormal_sigma), size=n)
elif distribution == "poisson":
raw = rng.poisson(lam=float(target_avg_degree), size=n) + 1
else:
raise ValueError("distribution must be 'powerlaw', 'lognormal', or 'poisson'")
deg = _rescale_degrees_to_target(raw, target_avg_degree=target_avg_degree, n=n)
max_degree = max(1, int(max_degree_fraction * (n - 1)))
deg = np.clip(deg, 1, max_degree)
if deg.sum() % 2 == 1:
idx = int(np.argmax(deg < n - 1))
deg[idx] += 1
return deg.astype(int)
[docs]
def generate_configuration_model_graph(
n: int,
distribution: str = "powerlaw",
target_avg_degree: float = 8,
seed: Optional[int] = None,
gamma: float = 2.5,
lognormal_sigma: float = 1.0,
max_degree_fraction: float = 0.1,
make_connected: bool = True,
) -> nx.Graph:
"""
Generate a simple graph from a configuration model with power-law,
log-normal, or Poisson degree sequence.
"""
deg_seq = sample_degree_sequence(
n=n,
distribution=distribution,
target_avg_degree=target_avg_degree,
seed=seed,
gamma=gamma,
lognormal_sigma=lognormal_sigma,
max_degree_fraction=max_degree_fraction,
)
MG = nx.configuration_model(deg_seq, seed=seed)
G = nx.Graph(MG)
G.remove_edges_from(nx.selfloop_edges(G))
G = _postprocess_graph(G, seed=seed, make_connected=make_connected)
nx.set_node_attributes(G, {i: int(deg_seq[i]) for i in range(n)}, "target_degree")
metadata = {
"type": "configuration_model",
"degree_distribution": distribution,
"n_nodes": int(n),
"target_avg_degree": float(target_avg_degree),
"target_degree_mean": float(deg_seq.mean()),
"target_degree_max": int(deg_seq.max()) if len(deg_seq) else 0,
"gamma": float(gamma),
"lognormal_sigma": float(lognormal_sigma),
"max_degree_fraction": float(max_degree_fraction),
"seed": seed,
"params": {
"n": int(n),
"distribution": distribution,
"target_avg_degree": float(target_avg_degree),
"gamma": float(gamma),
"lognormal_sigma": float(lognormal_sigma),
},
}
metadata.update(_graph_basic_metadata(G))
return _add_metadata(G, metadata)
[docs]
def sweep_configuration_model_graphs(
n_values: Sequence[int] = (1000, 2000, 5000),
distributions: Sequence[str] = ("powerlaw", "lognormal"),
avg_degree_values: Sequence[float] = (4, 8, 16),
gamma_values: Sequence[float] = (2.2, 2.5, 3.0),
lognormal_sigma_values: Sequence[float] = (0.75, 1.0, 1.5),
seeds: Sequence[int] = (0, 1, 2),
make_connected: bool = True,
) -> List[Tuple[nx.Graph, Dict[str, Any]]]:
"""Generate sweep of configuration model graphs."""
graphs = []
for n, dist, avg_d, seed in product(n_values, distributions, avg_degree_values, seeds):
if dist == "powerlaw":
for gamma in gamma_values:
G = generate_configuration_model_graph(
n=n,
distribution=dist,
target_avg_degree=avg_d,
gamma=gamma,
seed=_stable_seed(seed, dist, avg_d, gamma),
make_connected=make_connected,
)
graphs.append((G, dict(G.graph)))
elif dist == "lognormal":
for sigma in lognormal_sigma_values:
G = generate_configuration_model_graph(
n=n,
distribution=dist,
target_avg_degree=avg_d,
lognormal_sigma=sigma,
seed=_stable_seed(seed, dist, avg_d, sigma),
make_connected=make_connected,
)
graphs.append((G, dict(G.graph)))
else:
G = generate_configuration_model_graph(
n=n,
distribution=dist,
target_avg_degree=avg_d,
seed=seed,
make_connected=make_connected,
)
graphs.append((G, dict(G.graph)))
return graphs
# ===========================================================================
# EXTENDED GENERATORS FOR QUVINE
# ===========================================================================
# Added: Random regular, heterophilic SBM, degree-corrected SBM,
# grid/torus lattices, and configuration model graphs
from dataclasses import dataclass
from itertools import product
from typing import Any, Sequence
GraphWithMeta = Tuple[nx.Graph, Dict[str, Any]]
def _rng(seed: Optional[int] = None) -> np.random.Generator:
"""Create numpy random generator."""
return np.random.default_rng(seed)
def _stable_seed(base_seed: int, *items: Any) -> int:
"""Create a reproducible positive int seed from a base seed and parameters."""
h = int(base_seed) & 0xFFFFFFFF
for item in items:
for ch in str(item):
h = (1664525 * h + ord(ch) + 1013904223) & 0xFFFFFFFF
return int(h)
def _add_metadata(G: nx.Graph, metadata: Dict[str, Any]) -> nx.Graph:
"""Attach graph-level metadata to G.graph and return G."""
G.graph.update(metadata)
return G
def _connect_components_by_bridges(G: nx.Graph, seed: Optional[int] = None) -> nx.Graph:
"""
Connect disconnected components by adding one random bridge between adjacent
components in a random component ordering. Preserves all nodes.
"""
if G.number_of_nodes() == 0 or nx.is_connected(G):
return G
rng = _rng(seed)
G = G.copy()
comps = [list(c) for c in nx.connected_components(G)]
rng.shuffle(comps)
for c1, c2 in zip(comps[:-1], comps[1:]):
u = rng.choice(c1)
v = rng.choice(c2)
G.add_edge(int(u), int(v))
return G
def _postprocess_graph(
G: nx.Graph,
seed: Optional[int] = None,
make_connected: bool = True,
remove_selfloops: bool = True,
) -> nx.Graph:
"""Common cleanup for generated graphs."""
if not isinstance(G, nx.Graph) or isinstance(G, nx.DiGraph):
G = nx.Graph(G)
else:
G = G.copy()
if remove_selfloops:
G.remove_edges_from(nx.selfloop_edges(G))
# Ensure integer labels 0..n-1
G = nx.convert_node_labels_to_integers(G, ordering="sorted")
if make_connected and G.number_of_nodes() > 0 and not nx.is_connected(G):
G = _connect_components_by_bridges(G, seed=seed)
return G
def _graph_basic_metadata(G: nx.Graph) -> Dict[str, Any]:
"""Compute basic graph statistics for metadata."""
degrees = np.array([d for _, d in G.degree()], dtype=float)
return {
"n_nodes_actual": int(G.number_of_nodes()),
"n_edges_actual": int(G.number_of_edges()),
"avg_degree_actual": float(degrees.mean()) if degrees.size else 0.0,
"density_actual": float(nx.density(G)) if G.number_of_nodes() > 1 else 0.0,
"is_connected": bool(nx.is_connected(G)) if G.number_of_nodes() > 0 else False,
}
def _even_degree_sequence(deg: np.ndarray, n: int) -> np.ndarray:
"""Clip, round, and force a graphical-ish even degree sequence."""
deg = np.asarray(deg, dtype=float)
deg = np.nan_to_num(deg, nan=1.0, posinf=n - 1, neginf=1.0)
deg = np.clip(np.rint(deg), 1, n - 1).astype(int)
if deg.sum() % 2 == 1:
idx = int(np.argmax(deg < n - 1))
deg[idx] += 1
return deg
def _rescale_degrees_to_target(raw: np.ndarray, target_avg_degree: float, n: int) -> np.ndarray:
"""Rescale degree sequence to target average degree."""
raw = np.asarray(raw, dtype=float)
raw = np.maximum(raw, 1e-12)
raw = raw / raw.mean() * float(target_avg_degree)
return _even_degree_sequence(raw, n=n)
def _balanced_block_sizes(n: int, n_blocks: int) -> List[int]:
"""Create balanced block sizes for SBM."""
sizes = [n // n_blocks] * n_blocks
sizes[-1] += n - sum(sizes)
return sizes
def _sbm_prob_matrix_from_out_in_ratio(
sizes: Sequence[int],
target_avg_degree: float,
out_in_ratio: float,
p_in_floor: float = 1e-8,
max_prob: float = 0.95,
) -> np.ndarray:
"""
Build an SBM probability matrix with p_out / p_in = out_in_ratio and
approximately target_avg_degree expected average degree.
"""
sizes = np.asarray(sizes, dtype=float)
q = float(out_in_ratio)
if q < 0:
raise ValueError("out_in_ratio must be non-negative")
n = int(sizes.sum())
desired_edges = n * float(target_avg_degree) / 2.0
n_blocks = len(sizes)
R = np.full((n_blocks, n_blocks), q, dtype=float)
np.fill_diagonal(R, 1.0)
raw_expected = 0.0
for a in range(n_blocks):
raw_expected += R[a, a] * sizes[a] * (sizes[a] - 1) / 2.0
for b in range(a + 1, n_blocks):
raw_expected += R[a, b] * sizes[a] * sizes[b]
if raw_expected <= 0:
raise ValueError("Invalid SBM expected edge count")
scale = desired_edges / raw_expected
P = np.clip(scale * R, p_in_floor, max_prob)
return P
# Note: Due to length constraints, the full extended generators code
# has been provided in the task description. The complete implementation
# includes 5 new generator families with sweep functions.
#
# To integrate: append the complete extended generator code from the task
# to this file after line 962.