Generating benchmarks¶
Requires the generate extra: pip install "netgamut[generate]".
Generative benchmarks from the modular flow of real networks.
Community-detection benchmarks are usually synthetic: the ground truth is planted, but the networks look nothing like the systems the methods are for. This subpackage inverts the map equation’s flow model so that any network in the pool can serve as a blueprint – read its modular flow (degrees, module sizes, per-module mixing), then sample fresh networks with the same statistics and a known planted partition:
import netgamut
from netgamut import generate
G = netgamut.load("polbooks").to_networkx()
partition = generate.detect(G) # or bring your own
net = generate.benchmark(G, partition)
Every generator returns a network object – a InMemoryNetwork,
a StateNetwork or a MultilayerNetwork –
with the same methods as a downloaded Network. The planted
partition is net.partition, the sampler’s diagnostics are net.info:
H = net.to_networkx() # the planted module is node attribute "module"
net.to_pajek("benchmarks/") # -> Path("benchmarks/polbooks-benchmark.net")
Three routes, increasingly detached from the blueprint:
benchmark()andweighted_benchmark()resample a blueprint directly – microcanonical stub matching that preserves the degree sequence, each module’s mixing and connectivity, for directed and undirected, two-level and hierarchical blueprints alike; the weighted variant adds flow-model edge weights, which is what dense weighted blueprints need to stay recoverable.fit_families()andfit_mixing_beta()reduce a blueprint to a handful of fitted parameters, andparametric()generates from those alone – a generalized LFR whose degree and community-size families are chosen by AIC instead of assumed to be power laws.lfr()runs the Lancichinetti–Fortunato LFR generators themselves – the authors’ C++, fetched and compiled on first use – covering the binary, weighted, directed, hierarchical and multiplex benchmarks, with overlapping communities and a seed that fully determines the network.lfr_networkx()keeps networkx’s reimplementation of the binary case, with retries around its flaky convergence, for callers who would rather not have a compiler in the loop.multilayer_gm()runs MultilayerGM (Bazzi, Jeub et al.): a multilayer partition sampled with tunable dependence between layers, multiplex or temporal, then a degree-corrected SBM in every layer.
Higher-order blueprints keep the same flow model and change only the sampler:
memory_benchmark() and multilayer_benchmark() pair typed stubs,
so a memory link matches at its physical junction and a multiplex link stays
inside its layer. overlapping_memory_benchmark() needs no blueprint: it plants
communities that overlap on the physical nodes and are disjoint on the states –
Lancichinetti’s trigram sampler, seeded, and the one generator here that needs only the
standard library.
The flow model is public too – ModuleTree, flow_matrix(),
cross_R() and the flow-model distances – for callers that want the
joint distribution P rather than a sample of it. Comparing two partitions
as flow models is gmd() (the generative map distance) with its
similarity gms(), and the adjusted pair agmd() / agms()
calibrated against the one-module null partition.
Everything here needs numpy and networkx (Infomap only when no partition is
given, scipy only for fit_families()):
pip install "netgamut[generate]"
The samplers implement the generative map equation manuscript and are kept faithful to the code behind its published numbers.
- netgamut.generate.benchmark(G, partition=None, **kwargs)[source]¶
Sample one benchmark network from a blueprint’s modular flow.
The default sampler is microcanonical stub matching: degrees follow the blueprint’s visit rates, each module keeps its observed mixing
mu = q/p(exactly the LFR mixing parameter), and boundary stubs route through the module tree under the non-redundant constraint. The result is a simple connected graph with partition planted as ground truth. With power-law degrees, power-law module sizes and flat mixing this reduces to LFR; on real blueprints it keeps what LFR throws away.- Parameters:
G (Any) – Blueprint network, a networkx graph (weights honoured) or any network of this package. A directed blueprint gets the in-/out-degree-preserving directed sampler.
partition (dict[Any, Any] | None) –
{node: module id or path tuple}; hierarchical paths welcome. When omitted, Infomap detects one (needs theinfomappackage) – with the samenum_trialsandseedthe sampler would use – and that becomes the planted partition.**kwargs (Any) – Passed to the sampler –
target_edges,mixing_scale(> 1 blurs communities for detectability sweeps),method("stub"or"multinomial"),connect,min_degree,match_clustering,rng,seed.
- Returns:
A
InMemoryNetworknamed<blueprint>-benchmark, with the plantedpartitionand the sampler’s diagnostics ininfo.seedseeds Infomap only; passrngto make the sample itself reproducible.- Raises:
ImportError – numpy or networkx is missing, or Infomap is needed but not installed.
- Return type:
- netgamut.generate.detect(G, *, two_level=False, num_trials=20, seed=1, directed=None, extra='')[source]¶
Detect a partition with Infomap, in the shape the samplers expect.
- Parameters:
G (Any) – A networkx graph (weights honoured).
two_level (bool) – Restrict the search to a two-level solution; the default is the full hierarchical search, which the samplers exploit.
num_trials (int) – Number of Infomap outer-loop trials.
seed (int) – Infomap seed.
directed (bool | None) – Directed flow; defaults to
G.is_directed().extra (str) – Extra Infomap CLI arguments.
- Returns:
{node: module path tuple}– leaf modules as tuples, ready to pass as partition to the samplers.- Raises:
ImportError – infomap (or numpy/networkx) is missing.
- Return type:
- netgamut.generate.fit_clustering_profile(graph, min_degree=2)[source]¶
Fit the local-clustering profile
c(k) = c0 k**-alphaof graph.The clustering component of a blueprint’s fingerprint, so that
parametric()can be given a clustering target without a blueprint at sampling time. Fit by least squares onlog cover the nodes of degree at least min_degree with nonzero clustering; a graph with fewer than three such nodes gets a flat profile at its mean clustering.- Parameters:
- Returns:
{"c0", "alpha", "n_fit", "c_mean"}, ready to pass toparametric()as clustering_fit.- Raises:
ImportError – networkx is missing.
- Return type:
- netgamut.generate.fit_families(values, support_min=None)[source]¶
Fit discrete families to values and pick a winner by AIC.
Candidates: Zipf power law, geometric, truncated Poisson and discretized log-normal, all fit by maximum likelihood on the shifted variable
k' = k - (support_min - 1) >= 1– otherwise a family anchored at 1 is structurally misfit to quantities on a shifted support, such as module sizes starting at 5.- Parameters:
- Returns:
One
{"params", "loglik", "aic", "aic_weight"}per family, plus"winner","support_min"and"n_values". Feed the whole dict toparametric(), which uses the winner.- Raises:
ImportError – numpy or scipy is missing.
- Return type:
- netgamut.generate.fit_mixing_beta(mixing, flows)[source]¶
Fit a flow-weighted Beta (method of moments) to per-module mixing.
- Parameters:
- Returns:
{"family": "beta", "alpha", "beta", "mu", "var"}, or a{"family": "delta", "mu", "var"}fallback when the weighted variance is degenerate. Feed it toparametric().- Raises:
ImportError – numpy is missing.
- Return type:
- netgamut.generate.lfr_networkx(n, tau1, tau2, mu, *, average_degree=None, min_degree=None, max_degree=None, min_community=None, max_community=None, seed=1, max_iters=800, retries=5)[source]¶
Generate a classic LFR benchmark with networkx’s reimplementation.
networkx.LFR_benchmark_graphfails to converge for perfectly reasonable parameters depending on the seed. This wrapper retries with escalating seeds and iteration budgets, strips self-loops, and returns the planted communities as a plain partition dict instead of node attribute sets.This is the undirected unweighted case only.
lfr()runs the authors’ own generator instead – weighted, directed, hierarchical and multiplex too, and without the convergence failures – at the cost of a C++ compiler and one download.- Parameters:
n (int) – Number of nodes.
tau1 (float) – Degree power-law exponent (> 1).
tau2 (float) – Community-size power-law exponent (> 1).
mu (float) – Mixing parameter – the fraction of a node’s links that leave its community.
average_degree (float | None) – Mean degree (give this or min_degree).
min_degree (int | None) – Minimum degree, the alternative to average_degree.
max_degree (int | None) – Maximum degree.
min_community (int | None) – Smallest community size.
max_community (int | None) – Largest community size.
seed (int) – Base seed; retry i uses
seed + 1000 * i.max_iters (int) – Base iteration budget; retry i uses
max_iters * (i + 1).retries (int) – Attempts before giving up.
- Returns:
A
InMemoryNetworkwithpartition{node: community index}.info["seed"]is the seed of the attempt that converged.- Raises:
ImportError – networkx is missing.
RuntimeError – No attempt converged.
- Return type:
- netgamut.generate.memory_benchmark(links, windows, partition, rng=None, seed=None, topup=True)[source]¶
Sample a memory-network benchmark from a state-level blueprint.
Typed stub matching: a state link
a -> bis realizable only where the windows overlap inm - 1nodes, so every sampled link is a valid memory transition; seememory_benchmark()for the construction.A
StateNetworkblueprint passes asmemory_benchmark(net.links, net.states, partition).- Parameters:
links (dict[tuple[Any, Any], float]) –
{(state, state): weight}blueprint state links.windows (dict[Any, tuple[Any, ...]]) –
{state: tuple of physical nodes}, oldest first.partition (dict[Any, Any]) –
{state: module}planted ground truth.rng (Any) –
numpyGenerator, or a seed for a fresh one.seed (int | None) –
numpyGenerator, or a seed for a fresh one.topup (bool) – Place stubs the non-redundant rule could not.
- Returns:
A
StateNetworkwith the plantedpartitionkeyed by state id and the diagnostics ininfo. State and physical ids are the blueprint’s labels when those are non-negative integers, and numbered from 1 otherwise, the labels kept as names.- Return type:
- netgamut.generate.multilayer_benchmark(layers, partition=None, rng=None, seed=None, **kwargs)[source]¶
Sample a multiplex benchmark layer by layer from a state-level blueprint.
Intra-layer links pair within their layer, so each layer is resampled on its own with the state partition restricted to it; inter-layer coupling is left to the relax rate at analysis time. See
multilayer_benchmark()for what is and is not preserved – notably, a weighted layer keeps its link count but not its total weight.- Parameters:
layers (Any) –
{layer: networkx graph},{layer: {(u, v): weight}}, or aMultilayerNetworkblueprint.partition (dict[tuple[Any, Any], Any] | None) –
{(node, layer): module}planted ground truth. Defaults to the blueprint’s own partition when layers is aMultilayerNetworkthat has one.rng (Any) –
numpyGenerator, or a seed for a fresh one.seed (int | None) –
numpyGenerator, or a seed for a fresh one.**kwargs (Any) – Passed to the per-layer sampler;
connectdefaults toFalsehere.
- Returns:
A
MultilayerNetworkwith intra-layer links only, the plantedpartitionand the diagnostics ininfo.- Raises:
ValueError – No partition given and none on the blueprint.
- Return type:
- netgamut.generate.overlapping_memory_benchmark(memberships, n_trigrams, *, n_nodes=256, community_size=64, mu=0.1, seed=None)[source]¶
Sample a second-order memory network with planted overlapping communities.
- Parameters:
memberships (int) – Communities per physical node (
omin the published file names).n_trigrams (int) – Trigrams to draw (
E); the link weights sum to this unless the cross-community draw runs out of candidates (a warning says so).n_nodes (int) – Physical nodes (
N).community_size (int) – Physical nodes per community (
nc).mu (float) – Fraction of the trigrams that cross communities.
seed (int | None) – Seeds a
random.Random; the published files use the sample id.Nonedraws an unreproducible network.
- Returns:
A second-order
StateNetworkover physical nodes1..n_nodes. State ids follow the order pairs were planted in;partitionmaps each state id to its planted community (from 0), andinfoholds the parameters and each community’s physical nodes. Run it directed (infomap -d).- Raises:
ValueError – The parameters admit no such network.
- Return type:
- netgamut.generate.parametric(n, degree_fit, size_fit, mixing_fit, *, min_size=2, connect=True, k_mean=None, k_max=None, size_max=None, clustering_fit=None, clustering_kwargs=None, rng=None, seed=None)[source]¶
Generate a two-level benchmark purely from fitted parameters.
The generalized-LFR generator: draw module sizes and node degrees from their fitted families, per-module mixing from the fitted Beta (or a delta), then stub-match exactly like the blueprint sampler. Mirroring LFR’s
(tau, <k>, k_max, max_community)parameterization, drawn degrees can be rescaled to k_mean and capped at k_max, and sizes capped at size_max, so different family choices stay density-matched.- Parameters:
n (int) – Number of nodes (the realized count can differ by up to min_size because the last module is trimmed).
degree_fit (dict[str, Any]) – A
fit_families()result (its AIC winner is used) or an explicit{"family", "params", "support_min"}dict.mixing_fit (dict[str, Any]) – A
fit_mixing_beta()result, or a flat LFR-style mixing as{"family": "delta", "mu": 0.15}.min_size (int) – Smallest allowed module.
connect (bool) – Repair connectivity with degree-preserving swaps.
clustering_fit (dict[str, Any] | None) – A
fit_clustering_profile()result. Given, the sample runs the same clustering pass as the blueprint sampler, driven by the fitted profilec(k)instead of a blueprint’s triangle counts (LFR’s absence of clustering isc == 0).clustering_kwargs (dict[str, Any] | None) – Extra arguments for that pass, e.g.
{"time_cap": 120}.k_mean (float | None) – Rescale drawn degrees to this mean.
k_max (int | None) – Cap on node degree.
size_max (int | None) – Cap on module size.
rng (Any) – A
numpy.random.Generator; takes precedence over seed.seed (int | None) – Seed for a fresh generator when rng is omitted.
- Returns:
A
InMemoryNetworkwith the planted{node: module}partition and the diagnostics ininfo.- Raises:
ImportError – numpy or networkx is missing.
ValueError – A fit carries no usable family.
- Return type:
- netgamut.generate.weighted_benchmark(G, partition=None, *, degree_target='plain', num_trials=20, seed=1, **kwargs)[source]¶
Sample a benchmark topology and give its edges flow-model weights.
Dense weighted blueprints carry their modular structure in the link weights, not the topology, so an unweighted sample of one can be unrecoverable even when the blueprint compresses perfectly. This sampler puts the structure back into the weights: each realized edge
(u, v)gets a weight proportional to the joint flowp_u p_v R(A_u, A_v), rescaled so the total edge weight matches the blueprint’s.Works in the undirected frame throughout, like the paper’s weighted variant: a directed blueprint is symmetrized (weights summed over both directions) before sampling.
- Parameters:
G (Any) – Blueprint network (weights honoured; a DiGraph is symmetrized), a networkx graph or any network of this package.
partition (dict[Any, Any] | None) –
{node: module id or path tuple}. When omitted, Infomap detects a hierarchical partition of the symmetrized blueprint.degree_target (str) – Which degree sequence the topology targets.
"plain"(default) draws it from the blueprint’s unweighted degree sequence, which is feasible as a simple graph by construction, and lets the weights carry all of the flow structure – the mode to use for weighted blueprints, since multi-edges aggregate to link weights in the map equation."flow"targets the flow-apportioned (strength-proportional) degrees instead; faithful to the flow model but infeasible when weights are very skewed, where it strands stubs on hub saturation and no assignment of weights can rescue a topology that was never realized.num_trials (int) – Infomap trials when detecting the partition.
seed (int) – Infomap seed when detecting the partition.
**kwargs (Any) – Passed to the topology sampler (see
benchmark()).
- Returns:
An undirected weighted
InMemoryNetworknamed<blueprint>-weighted-benchmark, with the plantedpartitionand the diagnostics ininfo;info["degree_target"]records the mode used.- Raises:
ImportError – numpy or networkx is missing, or Infomap is needed but not installed.
ValueError – degree_target is not
"plain"or"flow".
- Return type:
Warning
Called without
partitionthis plants with the RECURSIVE search (infomap_partitionpasses noextra). The generative-map-equation project runs the columnar engine (-C) on both sides of every plant/re-detect comparison, so a partition-less call there would silently make its recovery numbers measure cross-engine agreement instead. Every call site in that project passes a partition today (checked 2026-09-19), and the default is left alone because this package is shared with other projects that may rely on it; passpartition=explicitly, orinfomap_partition(..., extra="-C"), when the engine matters.