Source code for xgi.generators.simplicial_complexes

"""Generators for some simplicial complexes.

All the functions in this module return a SimplicialComplex class.

"""

from collections import defaultdict
from itertools import combinations

import numpy as np

from ..core import SimplicialComplex
from ..utils.utilities import find_triangles

__all__ = [
    "random_simplicial_complex",
    "random_flag_complex_d2",
    "random_flag_complex",
    "flag_complex",
    "flag_complex_d2",
]


[docs]def random_simplicial_complex(N, ps, seed=None): """Generates a random hypergraph Generate N nodes, and connect any d+1 nodes by a simplex with probability ps[d-1]. For each simplex, add all its subfaces if they do not already exist. Parameters ---------- N : int Number of nodes ps : list of float List of probabilities (between 0 and 1) to create a hyperedge at each order d between any d+1 nodes. For example, ps[0] is the wiring probability of any edge (2 nodes), ps[1] of any triangles (3 nodes). seed : int, numpy.random.Generator, or None, optional The seed for the random number generator. By default, None. Returns ------- Simplicialcomplex object The generated simplicial complex References ---------- Described as 'random simplicial complex' in "Simplicial Models of Social Contagion", Nature Communications 10(1), 2485, by I. Iacopini, G. Petri, A. Barrat & V. Latora (2019). https://doi.org/10.1038/s41467-019-10431-6 Example ------- >>> import xgi >>> H = xgi.random_simplicial_complex(20, [0.1, 0.01]) """ rng = np.random.default_rng(seed) 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.") nodes = range(N) simplices = [] for i, p in enumerate(ps): d = i + 1 # order, ps[0] is prob of edges (d=1) potential_simplices = combinations(nodes, d + 1) from scipy.special import comb n_comb = comb(N, d + 1, exact=True) mask = rng.random(size=n_comb) <= p # True if simplex to keep simplices_to_add = [e for e, val in zip(potential_simplices, mask) if val] simplices += simplices_to_add S = SimplicialComplex() S.add_nodes_from(nodes) S.add_simplices_from(simplices) return S
[docs]def flag_complex(G, max_order=2, ps=None, seed=None): """Generate a flag (or clique) complex from a NetworkX graph by filling all cliques up to dimension max_order. Parameters ---------- G : Networkx Graph max_order : int maximal dimension of simplices to add to the output simplicial complex ps: list of float List of probabilities (between 0 and 1) to create a hyperedge from a clique, at each order d. For example, ps[0] is the probability of promoting any 3-node clique (triangle) to a 3-hyperedge. seed : int, numpy.random.Generator, or None, optional The seed for the random number generator. By default, None. Returns ------- S : SimplicialComplex Notes ----- Computing all cliques quickly becomes heavy for large networks. `flag_complex_d2` is faster to compute up to order 2. See also -------- flag_complex_d2 """ # This import needs to happen when this function is called, not when it is # defined. Otherwise, a circular import error would happen. from ..core import SimplicialComplex rng = np.random.default_rng(seed) nodes = G.nodes() N = len(nodes) edges = G.edges() cliques_to_add = _cliques_to_fill(G, max_order) S = SimplicialComplex() S.add_nodes_from(nodes) S.add_simplices_from(edges) if not ps: # promote all cliques S.add_simplices_from(cliques_to_add, max_order=max_order) return S # store max cliques per order cliques_d = defaultdict(list) for x in cliques_to_add: cliques_d[len(x)].append(x) # promote cliques with a given probability for i, p in enumerate(ps[: max_order - 1]): d = i + 2 # simplex order cliques_d_to_add = [el for el in cliques_d[d + 1] if rng.random() <= p] S.add_simplices_from(cliques_d_to_add, max_order=max_order) return S
[docs]def flag_complex_d2(G, p2=None, seed=None): """Generate a flag (or clique) complex from a NetworkX graph by filling all cliques up to dimension 2. Parameters ---------- G : networkx Graph Graph to consider p2: float Probability (between 0 and 1) of filling empty triangles in graph G seed : int, numpy.random.Generator, or None, optional The seed for the random number generator. By default, None. Returns ------- S : xgi.SimplicialComplex Notes ----- Computing all cliques quickly becomes heavy for large networks. This is faster than `flag_complex` to compute up to order 2. See also -------- flag_complex """ # This import needs to happen when this function is called, not when it is # defined. Otherwise, a circular import error would happen. from ..core import SimplicialComplex rng = np.random.default_rng(seed) nodes = G.nodes() edges = G.edges() S = SimplicialComplex() S.add_nodes_from(nodes) S.add_simplices_from(edges) triangles_empty = find_triangles(G) if p2 is not None: triangles = [el for el in triangles_empty if rng.random() <= p2] else: triangles = triangles_empty S.add_simplices_from(triangles) return S
[docs]def random_flag_complex_d2(N, p, seed=None): """Generate a maximal simplicial complex (up to order 2) from a :math:`G_{N,p}` Erdős-Rényi random graph. This proceeds by filling all empty triangles in the graph with 2-simplices. Parameters ---------- N : int Number of nodes p : float Probabilities (between 0 and 1) to create an edge between any 2 nodes seed : int, numpy.random.Generator, or None, optional The seed for the random number generator. By default, None. Returns ------- SimplicialComplex Notes ----- Computing all cliques quickly becomes heavy for large networks. """ if (p < 0) or (p > 1): raise ValueError("p must be between 0 and 1 included.") import networkx as nx G = nx.fast_gnp_random_graph(N, p, seed=seed) return flag_complex_d2(G)
[docs]def random_flag_complex(N, p, max_order=2, seed=None): """Generate a flag (or clique) complex from a :math:`G_{N,p}` Erdős-Rényi random graph. This proceeds by filling all cliques up to dimension max_order. Parameters ---------- N : int Number of nodes p : float Probability (between 0 and 1) to create an edge between any 2 nodes max_order : int maximal dimension of simplices to add to the output simplicial complex seed : int, numpy.random.Generator, or None, optional The seed for the random number generator. By default, None. Returns ------- SimplicialComplex Notes ----- Computing all cliques quickly becomes heavy for large networks. """ if (p < 0) or (p > 1): raise ValueError("p must be between 0 and 1 included.") import networkx as nx G = nx.fast_gnp_random_graph(N, p, seed=seed) nodes = G.nodes() cliques = _cliques_to_fill(G, max_order) S = SimplicialComplex() S.add_nodes_from(nodes) S.add_simplices_from(cliques, max_order=max_order) return S
def _cliques_to_fill(G, max_order): """Return cliques to fill for flag complexes, to be passed to `add_simplices_from`. This function was written to speedup flag_complex functions by avoiding adding redundant faces. Parameters ---------- G : networkx Graph Graph to consider max_order: int or None If None, return maximal cliques. If int, return all cliques up to max_order. Returns ------- cliques : list List of cliques """ import networkx as nx if max_order is None: cliques = list(nx.find_cliques(G)) # max cliques else: # avoid adding many unnecessary redundant cliques cliques = [] for clique in nx.enumerate_all_cliques(G): # sorted by size if len(clique) == 1: continue # don't add singletons if len(clique) <= max_order + 1: cliques.append(clique) else: break # dont go over whole list if not necessary return cliques