Source code for xgi.generators.random

"""Generate random (non-uniform) hypergraphs."""

import warnings
from collections import defaultdict
from itertools import combinations
from warnings import warn

import numpy as np
from scipy.special import comb

from ..utils import geometric
from .classic import empty_hypergraph
from .lattice import ring_lattice
from .uniform import _index_to_edge_comb

__all__ = [
    "fast_random_hypergraph",
    "random_hypergraph",
    "chung_lu_hypergraph",
    "dcsbm_hypergraph",
    "watts_strogatz_hypergraph",
]


[docs]def fast_random_hypergraph(n, ps, order=None, seed=None): """Generates a random hypergraph with a fast algorithm. Generate `n` nodes, and connect any `d+1` nodes by a hyperedge with probability `ps[d-1]`. This uses a fast method for generating hyperedges. See the references for more details. Parameters ---------- n : int Number of nodes ps : list of float, or float List of probabilities (between 0 and 1) to create a hyperedge at each order d between any d+1 nodes (when `order` is `None`). For example, ps[0] is the wiring probability of any edge (2 nodes), ps[1] of any triangles (3 nodes). If a float, generate a uniform hypergraph. See `order` for advanced options when it is not `None`. order: int, list of ints, or array of ints or None (default) If None (default), ignored. If list or array, generates a hypergraph with edges of orders `order[0]`, `order[1]`, etc. (The length of `ps` must match the length of `order` in this case). seed : int, numpy.random.Generator, or None, optional The seed for the random number generator. By default, None. Returns ------- Hypergraph object The generated hypergraph References ---------- M. Dewar et al. "Subhypergraphs in non-uniform random hypergraphs" https://arxiv.org/abs/1703.07686 Nicholas W. Landry and Juan G. Restrepo, "Opinion disparity in hypergraphs with community structure", Phys. Rev. E **108**, 034311 (2024). https://doi.org/10.1103/PhysRevE.108.034311 See Also -------- random_hypergraph Example ------- >>> import xgi >>> H = xgi.fast_random_hypergraph(50, [0.1, 0.01]) """ rng = np.random.default_rng(seed) ps, order = _check_input_args(ps, order) nodes = range(n) H = empty_hypergraph() H.add_nodes_from(nodes) for d, p in zip(order, ps): if p == 1: H.add_edges_from([e for e in combinations(nodes, d + 1)]) elif p > 0: index = geometric(p, rng=rng) - 1 # -1 b/c zero indexing max_index = comb(n, d + 1, exact=True) - 1 while index <= max_index: e = set(_index_to_edge_comb(index, n, d + 1)) H.add_edge(e) # We no longer subtract 1 because if we did, the minimum # value of the right-hand side would be zero, meaning that # we sample the same index multiple times. index += geometric(p, rng=rng) return H
[docs]def random_hypergraph(n, ps, order=None, seed=None): """Generates a random hypergraph Generate N nodes, and connect any d+1 nodes by a hyperedge with probability ps[d-1]. Parameters ---------- n : int Number of nodes ps : list of float, or float List of probabilities (between 0 and 1) to create a hyperedge at each order d between any d+1 nodes (when `order` is None). For example, ps[0] is the wiring probability of any edge (2 nodes), ps[1] of any triangles (3 nodes). If a float, generate a uniform hypergraph (in this case, order must be specified) See `order` for advanced options when it is not `None`. order: int, list of ints, or array of ints or None (default) If None, ignore. If list or array, generates a hypergraph with edges of orders `order[0]`, `order[1]`, etc. (The length of `ps` must match the length of `order` in this case). seed : int, numpy.random.Generator, or None, optional The seed for the random number generator. By default, None. Returns ------- Hypergraph object The generated hypergraph References ---------- Described as 'random hypergraph' by M. Dewar et al. in https://arxiv.org/abs/1703.07686 Warns ----- warnings.warn Because `fast_random_hypergraph` is a much faster method for generating random hypergraphs. See Also -------- fast_random_hypergraph Example ------- >>> import xgi >>> H = xgi.random_hypergraph(50, [0.1, 0.01]) """ warn("This method is much slower than fast_random_hypergraph") rng = np.random.default_rng(seed) ps, order = _check_input_args(ps, order) nodes = range(n) H = empty_hypergraph() H.add_nodes_from(nodes) for d, p in zip(order, ps): for edge in combinations(nodes, d + 1): if rng.random() <= p: H.add_edge(edge) return H
def _check_input_args(ps, order): """Check input args for random_hypergraph and fast_random_hypergraph""" if order is None: if not isinstance(ps, (list, np.ndarray)): raise ValueError( "If order is not specified, ps must be a list or numpy array!" ) order = [i + 1 for i in range(len(ps))] else: if isinstance(order, int): if not isinstance(ps, float): raise ValueError("If order is an int, ps must be a float") else: order = [order] ps = [ps] elif isinstance(order, (list, np.ndarray)) and isinstance( ps, (list, np.ndarray) ): if len(ps) != len(order): raise ValueError("The length ps must match the length of order") else: raise ValueError("Invalid entries!") ps = np.array(ps) order = np.array(order) if (np.any(np.array(ps) < 0)) or (np.any(np.array(ps) > 1)): raise ValueError("All elements of ps must be between 0 and 1 included.") if (order < 0).any(): raise ValueError("All elements of ps must be between 0 and 1 included.") return ps, order
[docs]def chung_lu_hypergraph(k1, k2, seed=None): """A function to generate a 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. 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 ---------- Implemented by Mirah Shi in HyperNetX and described for bipartite networks by Aksoy et al. in https://doi.org/10.1093/comnet/cnx001 Example ------- >>> import xgi >>> import random >>> n = 100 >>> k1 = {i : random.randint(1, 100) for i in range(n)} >>> k2 = {i : sorted(k1.values())[i] for i in range(n)} >>> H = xgi.chung_lu_hypergraph(k1, k2) """ rng = np.random.default_rng(seed) # sort dictionary by degree in decreasing order node_labels = [n for n, _ in sorted(k1.items(), key=lambda d: d[1], reverse=True)] edge_labels = [m for m, _ in sorted(k2.items(), key=lambda d: d[1], reverse=True)] m = len(k2) if sum(k1.values()) != sum(k2.values()): warnings.warn( "The sum of the degree sequence does not match the sum of the size sequence" ) S = sum(k1.values()) H = empty_hypergraph() H.add_nodes_from(node_labels) for u in node_labels: j = 0 v = edge_labels[j] # start from beginning every time p = min((k1[u] * k2[v]) / S, 1) while j < m: if p != 1: j += geometric(p, rng=rng) if j < m: v = edge_labels[j] q = min((k1[u] * k2[v]) / S, 1) r = rng.random() if r < q / p: # no duplicates H.add_node_to_edge(v, u) p = q j += 1 return H
[docs]def dcsbm_hypergraph(k1, k2, g1, g2, omega, seed=None): """A function to generate a Degree-Corrected Stochastic Block Model (DCSBM) hypergraph. Parameters ---------- k1 : dict This is a dictionary where the keys are node ids and the values are node degrees. k2 : dict This is a dictionary where the keys are edge ids and the values are edge sizes. g1 : dict This a dictionary where the keys are node ids and the values are the group ids to which the node belongs. The keys must match the keys of k1. g2 : dict This a dictionary where the keys are edge ids and the values are the group ids to which the edge belongs. The keys must match the keys of k2. omega : 2D numpy array This is a matrix with entries which specify the number of edges between a given node community and edge community. The number of rows must match the number of node communities and the number of columns must match the number of edge communities. seed : int, numpy.random.Generator, or None, optional The seed for the random number generator. By default, None. Returns ------- 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. Also if the sum of the omega matrix does not match the sum of degrees, a warning is raised. 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. The sum of k1 (and k2) and omega should be the same. If they are not the same, this function returns a warning but still runs and the number of entries in the incidence matrix is determined by the omega matrix. References ---------- Implemented by Mirah Shi in HyperNetX and described for bipartite networks by Larremore et al. in https://doi.org/10.1103/PhysRevE.90.012805 Examples -------- >>> import xgi; import random; import numpy as np >>> n = 50 >>> k1 = {i : random.randint(1, n) for i in range(n)} >>> k2 = {i : sorted(k1.values())[i] for i in range(n)} >>> g1 = {i : random.choice([0, 1]) for i in range(n)} >>> g2 = {i : random.choice([0, 1]) for i in range(n)} >>> omega = np.array([[n//2, 10], [10, n//2]]) >>> # H = xgi.dcsbm_hypergraph(k1, k2, g1, g2, omega) """ rng = np.random.default_rng(seed) # sort dictionary by degree in decreasing order node_labels = [n for n, _ in sorted(k1.items(), key=lambda d: d[1], reverse=True)] edge_labels = [m for m, _ in sorted(k2.items(), key=lambda d: d[1], reverse=True)] # Verify that the sum of node and edge degrees and the sum of node degrees and the # sum of community connection matrix differ by less than a single edge. if abs(sum(k1.values()) - sum(k2.values())) > 1: warnings.warn( "The sum of the degree sequence does not match the sum of the size sequence" ) if abs(sum(k1.values()) - np.sum(omega)) > 1: warnings.warn( "The sum of the degree sequence does not " "match the entries in the omega matrix" ) # get indices for each community community1_nodes = defaultdict(list) for label in node_labels: group = g1[label] community1_nodes[group].append(label) community2_nodes = defaultdict(list) for label in edge_labels: group = g2[label] community2_nodes[group].append(label) H = empty_hypergraph() H.add_nodes_from(node_labels) kappa1 = defaultdict(lambda: 0) kappa2 = defaultdict(lambda: 0) for idx, g in g1.items(): kappa1[g] += k1[idx] for idx, g in g2.items(): kappa2[g] += k2[idx] for group1 in community1_nodes.keys(): for group2 in community2_nodes.keys(): # for each constant probability patch try: group_constant = omega[group1, group2] / ( kappa1[group1] * kappa2[group2] ) except ZeroDivisionError: group_constant = 0 for u in community1_nodes[group1]: j = 0 v = community2_nodes[group2][j] # start from beginning every time # max probability p = min(k1[u] * k2[v] * group_constant, 1) while j < len(community2_nodes[group2]): if p != 1: j += geometric(p, rng=rng) if j < len(community2_nodes[group2]): v = community2_nodes[group2][j] q = min((k1[u] * k2[v]) * group_constant, 1) r = rng.random() if r < q / p: # no duplicates H.add_node_to_edge(v, u) p = q j += 1 return H
[docs]def watts_strogatz_hypergraph(n, d, k, l, p, seed=None): """Generates a Watts-Strogatz hypergraph Parameters ---------- n : int Number of nodes d : int Edge size k : int Number of edges of which a node is a part. Should be a multiple of 2. l : int Overlap between edges p : float The rewiring probability seed : int, numpy.random.Generator, or None, optional The seed for the random number generator. By default, None. Returns ------- xgi.Hypergraph THe Watts-Strogatz hypergraph See Also -------- ~lattice.ring_lattice References ---------- Tanu Raghav, Stefano Boccaletti, and Sarika Jalan, Smallworldness in hypergraphs, https://doi.org/10.1088/2632-072X/acf430 """ rng = np.random.default_rng(seed) H = ring_lattice(n, d, k, l) to_remove = [] to_add = [] for e in H.edges: if rng.random() < p: to_remove.append(e) node = min(H.edges.members(e)) neighbors = rng.choice(H.nodes, size=d - 1) to_add.append(np.append(neighbors, node)) H.remove_edges_from(to_remove) H.add_edges_from(to_add) return H