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() and weighted_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() and fit_mixing_beta() reduce a blueprint to a handful of fitted parameters, and parametric() 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 the infomap package) – with the same num_trials and seed the 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 InMemoryNetwork named <blueprint>-benchmark, with the planted partition and the sampler’s diagnostics in info. seed seeds Infomap only; pass rng to make the sample itself reproducible.

Raises:

ImportError – numpy or networkx is missing, or Infomap is needed but not installed.

Return type:

InMemoryNetwork

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:

dict[Any, tuple[int, …]]

netgamut.generate.fit_clustering_profile(graph, min_degree=2)[source]

Fit the local-clustering profile c(k) = c0 k**-alpha of 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 on log c over 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:
  • graph (Any) – A networkx graph (directed input is read undirected).

  • min_degree (int) – Smallest degree entering the fit.

Returns:

{"c0", "alpha", "n_fit", "c_mean"}, ready to pass to parametric() as clustering_fit.

Raises:

ImportError – networkx is missing.

Return type:

dict[str, Any]

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:
  • values (Any) – Positive integers – a degree sequence, module sizes, …

  • support_min (int | None) – Smallest value of the support; defaults to min(values).

Returns:

One {"params", "loglik", "aic", "aic_weight"} per family, plus "winner", "support_min" and "n_values". Feed the whole dict to parametric(), which uses the winner.

Raises:

ImportError – numpy or scipy is missing.

Return type:

dict[str, Any]

netgamut.generate.fit_mixing_beta(mixing, flows)[source]

Fit a flow-weighted Beta (method of moments) to per-module mixing.

Parameters:
  • mixing (Any) – Per-module mixing values mu_A = q_A / p_A, in [0, 1].

  • flows (Any) – Per-module flows p_A, used as weights.

Returns:

{"family": "beta", "alpha", "beta", "mu", "var"}, or a {"family": "delta", "mu", "var"} fallback when the weighted variance is degenerate. Feed it to parametric().

Raises:

ImportError – numpy is missing.

Return type:

dict[str, Any]

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_graph fails 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 InMemoryNetwork with partition {node: community index}. info["seed"] is the seed of the attempt that converged.

Raises:
Return type:

InMemoryNetwork

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 -> b is realizable only where the windows overlap in m - 1 nodes, so every sampled link is a valid memory transition; see memory_benchmark() for the construction.

A StateNetwork blueprint passes as memory_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) – numpy Generator, or a seed for a fresh one.

  • seed (int | None) – numpy Generator, or a seed for a fresh one.

  • topup (bool) – Place stubs the non-redundant rule could not.

Returns:

A StateNetwork with the planted partition keyed by state id and the diagnostics in info. 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:

StateNetwork

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 a MultilayerNetwork blueprint.

  • partition (dict[tuple[Any, Any], Any] | None) – {(node, layer): module} planted ground truth. Defaults to the blueprint’s own partition when layers is a MultilayerNetwork that has one.

  • rng (Any) – numpy Generator, or a seed for a fresh one.

  • seed (int | None) – numpy Generator, or a seed for a fresh one.

  • **kwargs (Any) – Passed to the per-layer sampler; connect defaults to False here.

Returns:

A MultilayerNetwork with intra-layer links only, the planted partition and the diagnostics in info.

Raises:

ValueError – No partition given and none on the blueprint.

Return type:

MultilayerNetwork

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 (om in 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. None draws an unreproducible network.

Returns:

A second-order StateNetwork over physical nodes 1..n_nodes. State ids follow the order pairs were planted in; partition maps each state id to its planted community (from 0), and info holds the parameters and each community’s physical nodes. Run it directed (infomap -d).

Raises:

ValueError – The parameters admit no such network.

Return type:

StateNetwork

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.

  • size_fit (dict[str, Any]) – The same, for module sizes.

  • 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 profile c(k) instead of a blueprint’s triangle counts (LFR’s absence of clustering is c == 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 InMemoryNetwork with the planted {node: module} partition and the diagnostics in info.

Raises:
Return type:

InMemoryNetwork

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 flow p_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 InMemoryNetwork named <blueprint>-weighted-benchmark, with the planted partition and the diagnostics in info; 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:

InMemoryNetwork

Warning

Called without partition this plants with the RECURSIVE search (infomap_partition passes no extra). 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; pass partition= explicitly, or infomap_partition(..., extra="-C"), when the engine matters.