Source code for xgi.generators.random

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

import random
import warnings
from collections import defaultdict
from itertools import combinations
from math import floor, log
from warnings import warn

import numpy as np
from scipy.special import comb

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 : integer or None (default) Seed for the random number generator. 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]) """ if seed is not None: random.seed(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: r = random.random() index = floor(log(r) / log(1 - p)) - 1 # -1 b/c zero indexing max_index = comb(n, d + 1, exact=True) 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. r = random.random() index += floor(log(r) / log(1 - p)) 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 : integer, random_state, or None (default) Indicator of random number generation state. 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") if seed is not None: random.seed(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 random.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 : integer or None (default) The seed for the random number generator. 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) """ if seed is not None: random.seed(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: r = random.random() try: j = j + floor(log(r) / log(1 - p)) except ZeroDivisionError: j = np.inf if j < m: v = edge_labels[j] q = min((k1[u] * k2[v]) / S, 1) r = random.random() if r < q / p: # no duplicates H.add_node_to_edge(v, u) p = q j = 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 or None (default) Seed for the random number generator. 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) """ if seed is not None: random.seed(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: r = random.random() try: j = j + floor(log(r) / log(1 - p)) except ZeroDivisionError: j = np.inf if j < len(community2_nodes[group2]): v = community2_nodes[group2][j] q = min((k1[u] * k2[v]) * group_constant, 1) r = random.random() if r < q / p: # no duplicates H.add_node_to_edge(v, u) p = q j = 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, 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 """ if seed is not None: np.random.seed(seed) H = ring_lattice(n, d, k, l) to_remove = [] to_add = [] for e in H.edges: if np.random.random() < p: to_remove.append(e) node = min(H.edges.members(e)) neighbors = np.random.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