-
Notifications
You must be signed in to change notification settings - Fork 44
feat: Add nested hypergraph generators (Kim et al. 2023, Barrett et al. 2025) #683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
9bddfce
bcc5184
112daad
22c9239
5f9d725
617e009
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
| import numpy as np | ||
| from scipy.special import comb | ||
|
|
||
| from ..utils import geometric | ||
| from ..utils import geometric, powerset | ||
| from .classic import empty_hypergraph | ||
| from .lattice import ring_lattice | ||
| from .uniform import _index_to_edge_comb | ||
|
|
@@ -17,8 +17,10 @@ | |
| "fast_random_hypergraph", | ||
| "random_hypergraph", | ||
| "chung_lu_hypergraph", | ||
| "simplicial_chung_lu_hypergraph", | ||
| "dcsbm_hypergraph", | ||
| "watts_strogatz_hypergraph", | ||
| "random_nested_hypergraph", | ||
| ] | ||
|
|
||
|
|
||
|
|
@@ -463,3 +465,213 @@ def watts_strogatz_hypergraph(n, d, k, l, p, seed=None): | |
| H.remove_edges_from(to_remove) | ||
| H.add_edges_from(to_add) | ||
| return H | ||
|
|
||
|
|
||
| def simplicial_chung_lu_hypergraph(k1, k2, p, seed=None): | ||
| """A function to generate a simplicial Chung-Lu hypergraph. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| k1 : dictionary | ||
| Dictionary where the keys are node ids | ||
| and the values are node degrees. | ||
| k2 : dictionary | ||
| Dictionary where the keys are edge ids | ||
| and the values are edge sizes. | ||
| p : float | ||
| Probability (between 0 and 1) of generating a simplicial edge | ||
| instead of a Chung-Lu edge. Controls the amount of nestedness. | ||
| seed : int, numpy.random.Generator, or None, optional | ||
| The seed for the random number generator. By default, None. | ||
|
|
||
| Returns | ||
| ------- | ||
| Hypergraph object | ||
| The generated hypergraph | ||
|
|
||
| Warns | ||
| ----- | ||
| warnings.warn | ||
| If the sums of the edge sizes and node degrees are not equal, the | ||
| algorithm still runs, but raises a warning. | ||
|
|
||
| Notes | ||
| ----- | ||
| The sums of k1 and k2 should be the same. If they are not the same, | ||
| this function returns a warning but still runs. | ||
|
|
||
| References | ||
| ---------- | ||
| Jordan Barrett, Paweł Prałat, Aaron Smith, and François Théberge, | ||
| "Counting simplicial pairs in hypergraphs", | ||
| J. Complex Netw. **13**, cnaf021 (2025). | ||
| https://doi.org/10.1093/comnet/cnaf021 | ||
|
|
||
| See Also | ||
| -------- | ||
| chung_lu_hypergraph | ||
|
|
||
| Example | ||
| ------- | ||
| >>> import xgi | ||
| >>> import numpy as np | ||
| >>> n = 50 | ||
| >>> rng = np.random.default_rng(0) | ||
| >>> k1 = {i : rng.integers(1, 11) for i in range(n)} | ||
| >>> k2 = {i : sorted(k1.values())[i] for i in range(n)} | ||
| >>> H = xgi.simplicial_chung_lu_hypergraph(k1, k2, p=0.5) | ||
|
|
||
| """ | ||
| if not 0 <= p <= 1: | ||
| raise ValueError("p must be between 0 and 1.") | ||
| rng = np.random.default_rng(seed) | ||
|
|
||
| if sum(k1.values()) != sum(k2.values()): | ||
| warnings.warn( | ||
| "The sum of the degree sequence does not match the sum of the size sequence" | ||
| ) | ||
|
|
||
| node_labels = np.array(list(k1.keys())) | ||
| degrees = np.array([k1[v] for v in node_labels], dtype=float) | ||
| S = degrees.sum() | ||
| if S == 0: | ||
| H = empty_hypergraph() | ||
| H.add_nodes_from(k1.keys()) | ||
| return H | ||
| node_probs = degrees / S | ||
|
|
||
| # Build the edge-size sequence in random order. | ||
| size_sequence = rng.permutation(list(k2.values())).tolist() | ||
|
|
||
| edges = [] # list of frozensets | ||
|
|
||
| for k in size_sequence: | ||
| if rng.random() < p: | ||
| # Generate a simplicial edge. | ||
| edges_not_k = [e for e in edges if len(e) != k] | ||
|
|
||
| if not edges_not_k: | ||
| # No edges of different size exist → plain Chung-Lu edge | ||
| e_new = frozenset(rng.choice(node_labels, size=k, replace=True, p=node_probs)) | ||
| else: | ||
| # Sample an existing edge of a different size. | ||
| e_prime = edges_not_k[rng.integers(len(edges_not_k))] | ||
| if len(e_prime) > k: | ||
| # Take a random subset of the sampled edge. | ||
| e_new = frozenset(rng.choice(list(e_prime), size=k, replace=False)) | ||
| else: | ||
| # Extend the sampled edge with additional Chung-Lu nodes. | ||
| extra = frozenset( | ||
| rng.choice(node_labels, size=k - len(e_prime), replace=True, p=node_probs) | ||
| ) | ||
| e_new = e_prime | extra | ||
| else: | ||
| # Generate a plain Chung-Lu edge. | ||
| e_new = frozenset(rng.choice(node_labels, size=k, replace=True, p=node_probs)) | ||
|
|
||
| edges.append(e_new) | ||
|
|
||
| H = empty_hypergraph() | ||
| H.add_nodes_from(k1.keys()) | ||
| H.add_edges_from(edges) | ||
| return H | ||
|
|
||
|
|
||
| def random_nested_hypergraph(n, m, d, epsilon, seed=None): | ||
| """A function to generate a random nested hypergraph. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| n : int | ||
| Number of nodes. | ||
| m : int | ||
| Number of facets. | ||
| d : int | ||
| Size of each facet. | ||
| epsilon : float or list of float | ||
| Retention probability. If a float, the same retention probability | ||
| is used for all sub-facet sizes. If a list, ``epsilon[i]`` is the | ||
| retention probability for hyperedges of size ``i+2``, for | ||
| ``i = 0, ..., d-3``. Hyperedges of size ``t`` are rewired with | ||
| probability ``1 - epsilon[t-2]``. | ||
| seed : int, numpy.random.Generator, or None, optional | ||
| The seed for the random number generator. By default, None. | ||
|
|
||
| Returns | ||
| ------- | ||
| Hypergraph object | ||
| The generated hypergraph | ||
|
|
||
| Notes | ||
| ----- | ||
| The algorithm proceeds as follows: | ||
|
|
||
| 1. Generate m facets of size d by uniform random node assignment, | ||
| rejecting exact duplicate facets. | ||
| 2. For each facet, enumerate all proper subsets of sizes 2 through | ||
| d − 1 (singletons excluded). Together with the facets | ||
| themselves, these form the initial edge set. Duplicates across | ||
| facets are removed. | ||
| 3. For each hyperedge of size t < d: with probability | ||
| 1 − ``epsilon[t-2]``, rewire the edge by picking one pivot node | ||
| uniformly at random from the edge and replacing the other t − 1 | ||
| nodes with nodes sampled uniformly from {0, …, n − 1}. | ||
|
|
||
| The nestedness is controlled primarily by ``epsilon``: larger values | ||
| preserve more induced subedges and therefore produce more nested | ||
| hypergraphs, with ``epsilon=1`` yielding a fully nested construction | ||
| and ``epsilon=0`` rewiring every non-facet edge. If ``epsilon`` is a | ||
| list, it tunes nestedness separately for each subedge size. | ||
|
|
||
| References | ||
| ---------- | ||
| Jihye Kim, Deok-Sun Lee, and K-I. Goh, | ||
| "Contagion dynamics on hypergraphs with nested hyperedges", | ||
| Phys. Rev. E **108**, 034313 (2023). | ||
| https://doi.org/10.1103/PhysRevE.108.034313 | ||
|
|
||
| Example | ||
| ------- | ||
| >>> import xgi | ||
| >>> H = xgi.random_nested_hypergraph(20, 5, 4, 0.8) | ||
|
|
||
| """ | ||
| if isinstance(epsilon, (int, float)): | ||
| epsilon = [epsilon] * (d - 2) | ||
| rng = np.random.default_rng(seed) | ||
|
|
||
| nodes = range(n) | ||
|
|
||
| # Step 1: Generate m unique facets of size d | ||
| facets = set() | ||
| while len(facets) < m: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may go into an infinite loop if
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's fair! Actual condition is m > (n choose d). How much optimization is worth it to the library? |
||
| facet = frozenset(rng.choice(nodes, size=d, replace=False)) | ||
| facets.add(facet) | ||
|
|
||
| # Step 2: For each facet, enumerate all subsets of sizes 2..d | ||
| # (facets themselves are included as edges; singletons excluded) | ||
| all_edges = set() | ||
| for facet in facets: | ||
| all_edges.update( | ||
| frozenset(subset) | ||
| for subset in powerset(facet, include_singletons=False, include_full=True) | ||
| ) | ||
|
|
||
| # Step 3: Rewire hyperedges of size t < d | ||
| final_edges = set() | ||
| for e in all_edges: | ||
| t = len(e) | ||
| if t < d: | ||
| eps_t = epsilon[t - 2] | ||
| if rng.random() > eps_t: | ||
| # Rewire: pick one pivot, replace the rest | ||
| pivot = rng.choice(list(e)) | ||
| others = [v for v in nodes if v != pivot] | ||
| new_nodes = rng.choice(others, size=t - 1, replace=False) | ||
| e = frozenset([pivot, *new_nodes]) | ||
| final_edges.add(e) | ||
|
|
||
| H = empty_hypergraph() | ||
| H.add_nodes_from(nodes) | ||
| H.add_edges_from(final_edges) | ||
| return H | ||
Uh oh!
There was an error while loading. Please reload this page.