From 67a688767243ef225225a128a6510f3ceb82f1ae Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 10 Feb 2025 10:41:48 +0100 Subject: [PATCH 01/14] feat: added two iterative generators --- xgi/generators/iterative.py | 151 ++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 xgi/generators/iterative.py diff --git a/xgi/generators/iterative.py b/xgi/generators/iterative.py new file mode 100644 index 000000000..ea6eea27e --- /dev/null +++ b/xgi/generators/iterative.py @@ -0,0 +1,151 @@ +"""Generate iterative hypergraphs.""" + +import itertools +import operator +import random +import warnings +from functools import reduce + +import numpy as np +from scipy.special import comb + +from ..exception import XGIError +from ..utils import geometric +from .classic import complete_hypergraph, empty_hypergraph + +__all__ = [ + "pseudofractal_simplicial_complex", + "apollonian_complex", + +] + + +def pseudofractal_simplicial_complex(order, n_iter): + """ + Generate the pseudofractal simplicial complex of order `order`. + + Starting with a single d-simplex, at each iteration, the function adds new (d+1)-simplices + by attaching a new vertex to all existing (d-1)-simplices (as well as all their subfaces). + This process is deterministic. + + Parameters + ---------- + order : int + The ordder of the simplices to add (e.g., 2 for triangles, 3 for tetrahedra, etc.). + n_iter : int + The number of iterations to generate simplices. + + Returns + ------- + S : xgi.SimplicialComplex + Generated simplicial complex + + See also + -------- + apollonian_complex + + + References + ---------- + Nurisso, M., Morandini, M., Lucas, M., Vaccarino, F., Gili, T., & Petri, G. (2024). + "Higher-order Laplacian Renormalization." + arXiv preprint arXiv:2401.11298. + https://arxiv.org/abs/2401.11298 + """ + + S = xgi.SimplicialComplex() + + # initialize the first d-simplex + first_simplex = tuple(range(order + 1)) + S.add_simplex(first_simplex) + + # generate simplices iteratively + for it in range(1, n_iter + 1): + # Find all (order - 1)-simplices present in the complex + nodes = S.nodes + subfaces = S.edges.filterby("order", order - 1).members() + max_index = max(nodes) + new_simplices = [] + + for subface in subfaces: + # create a new simplex by adding the new vertex to the existing d-simplex + max_index += 1 # new vertex index + + new_simplex = (*subface, max_index) + new_simplices.append(new_simplex) + + S.add_simplices_from(new_simplices) + + return S + + +def apollonian_complex(order, n_iter): + """ + Generate the apollonian complex of order `order`. + + Starting with a single d-simplex, at each iteration, the function adds new (d+1)-simplices + by attaching a new vertex to (d-1)-simplices that contain at least one newly added node. + This process is deterministic and generates a simplicial complex. + + + Parameters + ---------- + order : int + The ordder of the simplices to add (e.g., 2 for triangles, 3 for tetrahedra, etc.). + n_iter : int + The maximum iteration to generate simplices. + + Returns + ------- + S : xgi.SimplicialComplex + Generated simplicial complex + + See also + -------- + pseudofractal_simplicial_complex + + References + ---------- + Nurisso, M., Morandini, M., Lucas, M., Vaccarino, F., Gili, T., & Petri, G. (2024). + "Higher-order Laplacian Renormalization." + arXiv preprint arXiv:2401.11298. + https://arxiv.org/abs/2401.11298 + """ + + S = xgi.SimplicialComplex() + + # initialize the first d-simplex + first_simplex = tuple(range(order + 1)) + S.add_simplex(first_simplex) + + new_simplices = [first_simplex] + new_indices = list(first_simplex) + + # generate simplices iteratively + for it in range(1, n_iter + 1): + # find all (order - 1)-simplices present in the complex + nodes = S.nodes + subfaces_previous_iter = S.edges.filterby("order", order - 1).members() + + # keep only those attached to new nodes + subfaces_previous_iter = [ + subface + for subface in subfaces_previous_iter + if any(new_index in subface for new_index in new_indices) + ] + + max_index = max(nodes) + new_simplices = [] + new_indices = [] + + for subface in subfaces_previous_iter: + # create a new simplex by adding the new vertex to the existing d-simplex + max_index += 1 # New vertex index + + new_simplex = (*subface, max_index) + new_simplices.append(new_simplex) + new_indices.append(max_index) + + S.add_simplices_from(new_simplices) + + return S From fa85fe19e68a015f0e2cfa73b97b23237c3d7e4d Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 10 Feb 2025 10:53:10 +0100 Subject: [PATCH 02/14] docs: added new generators to online docs --- docs/source/api/generators.rst | 1 + .../api/generators/xgi.generators.iterative.rst | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 docs/source/api/generators/xgi.generators.iterative.rst diff --git a/docs/source/api/generators.rst b/docs/source/api/generators.rst index 46533d8cb..5da7e88ac 100644 --- a/docs/source/api/generators.rst +++ b/docs/source/api/generators.rst @@ -14,4 +14,5 @@ generators package ~xgi.generators.uniform ~xgi.generators.simplicial_complexes ~xgi.generators.randomizing + ~xgi.generators.iterative \ No newline at end of file diff --git a/docs/source/api/generators/xgi.generators.iterative.rst b/docs/source/api/generators/xgi.generators.iterative.rst new file mode 100644 index 000000000..2b8cd1238 --- /dev/null +++ b/docs/source/api/generators/xgi.generators.iterative.rst @@ -0,0 +1,11 @@ +xgi.generators.iterative +======================== + +.. currentmodule:: xgi.generators.iterative + +.. automodule:: xgi.generators.iterative + + .. rubric:: Functions + + .. autofunction:: apollonian_complex + .. autofunction:: pseudofractal_simplicial_complex From 61bbc110a22119232cc9612df52342507283b09f Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 10 Feb 2025 12:35:54 +0100 Subject: [PATCH 03/14] fix: typos --- xgi/generators/iterative.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/xgi/generators/iterative.py b/xgi/generators/iterative.py index ea6eea27e..f760d6048 100644 --- a/xgi/generators/iterative.py +++ b/xgi/generators/iterative.py @@ -16,7 +16,6 @@ __all__ = [ "pseudofractal_simplicial_complex", "apollonian_complex", - ] @@ -31,7 +30,7 @@ def pseudofractal_simplicial_complex(order, n_iter): Parameters ---------- order : int - The ordder of the simplices to add (e.g., 2 for triangles, 3 for tetrahedra, etc.). + The order of the simplices to add (e.g., 2 for triangles, 3 for tetrahedra, etc.). n_iter : int The number of iterations to generate simplices. @@ -47,10 +46,10 @@ def pseudofractal_simplicial_complex(order, n_iter): References ---------- - Nurisso, M., Morandini, M., Lucas, M., Vaccarino, F., Gili, T., & Petri, G. (2024). - "Higher-order Laplacian Renormalization." - arXiv preprint arXiv:2401.11298. - https://arxiv.org/abs/2401.11298 + Nurisso, M., Morandini, M., Lucas, M., Vaccarino, F., Gili, T., & Petri, G. (2024). + "Higher-order Laplacian Renormalization." + arXiv preprint arXiv:2401.11298. + https://arxiv.org/abs/2401.11298 """ S = xgi.SimplicialComplex() @@ -91,7 +90,7 @@ def apollonian_complex(order, n_iter): Parameters ---------- order : int - The ordder of the simplices to add (e.g., 2 for triangles, 3 for tetrahedra, etc.). + The order of the simplices to add (e.g., 2 for triangles, 3 for tetrahedra, etc.). n_iter : int The maximum iteration to generate simplices. @@ -106,9 +105,9 @@ def apollonian_complex(order, n_iter): References ---------- - Nurisso, M., Morandini, M., Lucas, M., Vaccarino, F., Gili, T., & Petri, G. (2024). - "Higher-order Laplacian Renormalization." - arXiv preprint arXiv:2401.11298. + Nurisso, M., Morandini, M., Lucas, M., Vaccarino, F., Gili, T., & Petri, G. (2024). + "Higher-order Laplacian Renormalization." + arXiv preprint arXiv:2401.11298. https://arxiv.org/abs/2401.11298 """ From 09b933f54e01210d8452db12ed677577b75abba2 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 10 Feb 2025 12:55:16 +0100 Subject: [PATCH 04/14] test: initial for pseudofractal --- tests/generators/test_iterative.py | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/generators/test_iterative.py diff --git a/tests/generators/test_iterative.py b/tests/generators/test_iterative.py new file mode 100644 index 000000000..87d597399 --- /dev/null +++ b/tests/generators/test_iterative.py @@ -0,0 +1,61 @@ +import pytest + +import xgi +from xgi.exception import XGIError + + +def test_pseudofractal(): + + # initial + S0 = xgi.pseudofractal_simplicial_complex(order=2, n_iter=0) + triangles = set(S0.edges.filterby("order", 2).members()) + + assert isinstance(S, xgi.SimplicialComplex) + assert S0.num_nodes == 3 + assert S0.num_edges == 4 + assert triangles == {frozenset({0, 1, 2})} + + # first iteration + S1 = xgi.pseudofractal_simplicial_complex(order=2, n_iter=1) + triangles = set(S1.edges.filterby("order", 2).members()) + + assert isinstance(S1, xgi.SimplicialComplex) + assert S1.num_nodes == 6 + assert xgi.num_edges_order(S1, d=2) == 4 + assert triangles == {frozenset({1, 2, 5}), + frozenset({0, 2, 4}), + frozenset({0, 1, 3}), + frozenset({0, 1, 2})} + + # second iteration + S2 = xgi.pseudofractal_simplicial_complex(order=2, n_iter=2) + triangles = set(S2.edges.filterby("order", 2).members()) + + assert isinstance(S2, xgi.SimplicialComplex) + assert S2.num_nodes == 15 + assert xgi.num_edges_order(S2, d=2) == 13 + assert triangles == {frozenset({0, 4, 10}), + frozenset({0, 2, 7}), + frozenset({0, 1, 2}), + frozenset({1, 5, 11}), + frozenset({1, 2, 5}), + frozenset({0, 2, 4}), + frozenset({0, 3, 12}), + frozenset({1, 3, 14}), + frozenset({2, 4, 9}), + frozenset({1, 2, 8}), + frozenset({0, 1, 6}), + frozenset({2, 5, 13}), + frozenset({0, 1, 3})} + +def test_apollonian(): + w + + H = xgi.sunflower(4, 3, 6) + for i in range(3): + H.nodes.memberships(i) == {0, 1, 2, 3} + + assert H.num_nodes == 15 + + for i in range(3, 15): + assert len(H.nodes.memberships(i)) == 1 From 691440560545811329422370840416e1df27c225 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 10 Feb 2025 12:59:38 +0100 Subject: [PATCH 05/14] style: black isort ruff --- tests/generators/test_iterative.py | 43 +++++++++++++++++------------- xgi/generators/iterative.py | 43 +++++++++++------------------- 2 files changed, 40 insertions(+), 46 deletions(-) diff --git a/tests/generators/test_iterative.py b/tests/generators/test_iterative.py index 87d597399..4231981cf 100644 --- a/tests/generators/test_iterative.py +++ b/tests/generators/test_iterative.py @@ -5,12 +5,12 @@ def test_pseudofractal(): - + # initial S0 = xgi.pseudofractal_simplicial_complex(order=2, n_iter=0) triangles = set(S0.edges.filterby("order", 2).members()) - assert isinstance(S, xgi.SimplicialComplex) + assert isinstance(S0, xgi.SimplicialComplex) assert S0.num_nodes == 3 assert S0.num_edges == 4 assert triangles == {frozenset({0, 1, 2})} @@ -22,10 +22,12 @@ def test_pseudofractal(): assert isinstance(S1, xgi.SimplicialComplex) assert S1.num_nodes == 6 assert xgi.num_edges_order(S1, d=2) == 4 - assert triangles == {frozenset({1, 2, 5}), - frozenset({0, 2, 4}), - frozenset({0, 1, 3}), - frozenset({0, 1, 2})} + assert triangles == { + frozenset({1, 2, 5}), + frozenset({0, 2, 4}), + frozenset({0, 1, 3}), + frozenset({0, 1, 2}), + } # second iteration S2 = xgi.pseudofractal_simplicial_complex(order=2, n_iter=2) @@ -34,19 +36,22 @@ def test_pseudofractal(): assert isinstance(S2, xgi.SimplicialComplex) assert S2.num_nodes == 15 assert xgi.num_edges_order(S2, d=2) == 13 - assert triangles == {frozenset({0, 4, 10}), - frozenset({0, 2, 7}), - frozenset({0, 1, 2}), - frozenset({1, 5, 11}), - frozenset({1, 2, 5}), - frozenset({0, 2, 4}), - frozenset({0, 3, 12}), - frozenset({1, 3, 14}), - frozenset({2, 4, 9}), - frozenset({1, 2, 8}), - frozenset({0, 1, 6}), - frozenset({2, 5, 13}), - frozenset({0, 1, 3})} + assert triangles == { + frozenset({0, 4, 10}), + frozenset({0, 2, 7}), + frozenset({0, 1, 2}), + frozenset({1, 5, 11}), + frozenset({1, 2, 5}), + frozenset({0, 2, 4}), + frozenset({0, 3, 12}), + frozenset({1, 3, 14}), + frozenset({2, 4, 9}), + frozenset({1, 2, 8}), + frozenset({0, 1, 6}), + frozenset({2, 5, 13}), + frozenset({0, 1, 3}), + } + def test_apollonian(): w diff --git a/xgi/generators/iterative.py b/xgi/generators/iterative.py index f760d6048..4a14bd262 100644 --- a/xgi/generators/iterative.py +++ b/xgi/generators/iterative.py @@ -1,17 +1,6 @@ """Generate iterative hypergraphs.""" -import itertools -import operator -import random -import warnings -from functools import reduce - -import numpy as np -from scipy.special import comb - -from ..exception import XGIError -from ..utils import geometric -from .classic import complete_hypergraph, empty_hypergraph +from ..core import SimplicialComplex __all__ = [ "pseudofractal_simplicial_complex", @@ -23,8 +12,8 @@ def pseudofractal_simplicial_complex(order, n_iter): """ Generate the pseudofractal simplicial complex of order `order`. - Starting with a single d-simplex, at each iteration, the function adds new (d+1)-simplices - by attaching a new vertex to all existing (d-1)-simplices (as well as all their subfaces). + Starting with a single d-simplex, at each iteration, the function adds new (d+1)-simplices + by attaching a new vertex to all existing (d-1)-simplices (as well as all their subfaces). This process is deterministic. Parameters @@ -46,13 +35,13 @@ def pseudofractal_simplicial_complex(order, n_iter): References ---------- - Nurisso, M., Morandini, M., Lucas, M., Vaccarino, F., Gili, T., & Petri, G. (2024). - "Higher-order Laplacian Renormalization." - arXiv preprint arXiv:2401.11298. - https://arxiv.org/abs/2401.11298 + Nurisso, M., Morandini, M., Lucas, M., Vaccarino, F., Gili, T., & Petri, G. (2024). + "Higher-order Laplacian Renormalization." + arXiv preprint arXiv:2401.11298. + https://arxiv.org/abs/2401.11298 """ - S = xgi.SimplicialComplex() + S = SimplicialComplex() # initialize the first d-simplex first_simplex = tuple(range(order + 1)) @@ -82,9 +71,9 @@ def apollonian_complex(order, n_iter): """ Generate the apollonian complex of order `order`. - Starting with a single d-simplex, at each iteration, the function adds new (d+1)-simplices - by attaching a new vertex to (d-1)-simplices that contain at least one newly added node. - This process is deterministic and generates a simplicial complex. + Starting with a single d-simplex, at each iteration, the function adds new (d+1)-simplices + by attaching a new vertex to (d-1)-simplices that contain at least one newly added node. + This process is deterministic and generates a simplicial complex. Parameters @@ -105,13 +94,13 @@ def apollonian_complex(order, n_iter): References ---------- - Nurisso, M., Morandini, M., Lucas, M., Vaccarino, F., Gili, T., & Petri, G. (2024). - "Higher-order Laplacian Renormalization." - arXiv preprint arXiv:2401.11298. + Nurisso, M., Morandini, M., Lucas, M., Vaccarino, F., Gili, T., & Petri, G. (2024). + "Higher-order Laplacian Renormalization." + arXiv preprint arXiv:2401.11298. https://arxiv.org/abs/2401.11298 """ - - S = xgi.SimplicialComplex() + + S = SimplicialComplex() # initialize the first d-simplex first_simplex = tuple(range(order + 1)) From f208cea5572acceae105c9af577334b8ead56a90 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 10 Feb 2025 13:01:50 +0100 Subject: [PATCH 06/14] fix: test apollonian --- tests/generators/test_iterative.py | 47 +++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/tests/generators/test_iterative.py b/tests/generators/test_iterative.py index 4231981cf..b6564b03c 100644 --- a/tests/generators/test_iterative.py +++ b/tests/generators/test_iterative.py @@ -54,13 +54,46 @@ def test_pseudofractal(): def test_apollonian(): - w - H = xgi.sunflower(4, 3, 6) - for i in range(3): - H.nodes.memberships(i) == {0, 1, 2, 3} + # initial + S0 = xgi.apollonian_complex(order=2, n_iter=0) + triangles = set(S0.edges.filterby("order", 2).members()) - assert H.num_nodes == 15 + assert isinstance(S0, xgi.SimplicialComplex) + assert S0.num_nodes == 3 + assert S0.num_edges == 4 + assert triangles == {frozenset({0, 1, 2})} - for i in range(3, 15): - assert len(H.nodes.memberships(i)) == 1 + # first iteration + S1 = xgi.apollonian_complex(order=2, n_iter=1) + triangles = set(S1.edges.filterby("order", 2).members()) + + assert isinstance(S1, xgi.SimplicialComplex) + assert S1.num_nodes == 6 + assert xgi.num_edges_order(S1, d=2) == 4 + assert triangles == { + frozenset({1, 2, 5}), + frozenset({0, 2, 4}), + frozenset({0, 1, 3}), + frozenset({0, 1, 2}), + } + + # second iteration + S2 = xgi.apollonian_complex(order=2, n_iter=2) + triangles = set(S2.edges.filterby("order", 2).members()) + + assert isinstance(S2, xgi.SimplicialComplex) + assert S2.num_nodes == 12 + assert xgi.num_edges_order(S2, d=2) == 10 + assert triangles == { + frozenset({2, 4, 6}), + frozenset({1, 5, 8}), + frozenset({0, 3, 9}), + frozenset({0, 1, 2}), + frozenset({1, 2, 5}), + frozenset({0, 2, 4}), + frozenset({2, 5, 10}), + frozenset({1, 3, 11}), + frozenset({0, 1, 3}), + frozenset({0, 4, 7}), + } From 62ad845d1806bac62d33bf5667c158bae95699dc Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 10 Feb 2025 13:04:10 +0100 Subject: [PATCH 07/14] fix: __init__ --- xgi/generators/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xgi/generators/__init__.py b/xgi/generators/__init__.py index 127a8da93..68d4429cf 100644 --- a/xgi/generators/__init__.py +++ b/xgi/generators/__init__.py @@ -1,5 +1,6 @@ from . import ( classic, + iterative, lattice, random, randomizing, @@ -8,6 +9,7 @@ uniform, ) from .classic import * +from .iterative import * from .lattice import * from .random import * from .randomizing import * From cde2bc12b13bdf7c7dd469fd885c13d1266e0f5a Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Tue, 11 Feb 2025 11:22:37 +0100 Subject: [PATCH 08/14] feat: added NGF generator --- xgi/generators/iterative.py | 117 +++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/xgi/generators/iterative.py b/xgi/generators/iterative.py index 4a14bd262..a451734dd 100644 --- a/xgi/generators/iterative.py +++ b/xgi/generators/iterative.py @@ -5,6 +5,7 @@ __all__ = [ "pseudofractal_simplicial_complex", "apollonian_complex", + "network_geometry_flavor", ] @@ -31,7 +32,7 @@ def pseudofractal_simplicial_complex(order, n_iter): See also -------- apollonian_complex - + network_geometry_flavor References ---------- @@ -91,6 +92,7 @@ def apollonian_complex(order, n_iter): See also -------- pseudofractal_simplicial_complex + network_geometry_flavor References ---------- @@ -137,3 +139,116 @@ def apollonian_complex(order, n_iter): S.add_simplices_from(new_simplices) return S + + +def network_geometry_flavor( + order, s, beta, n_iter, energy_distribution=None, seed=None +): + """ + Generate a Network Geometry with Flavor (NGF) simplicial complex. + + The model grows a d-dimensional simplicial complex (where d is `order`) by iteratively attaching + d-simplices to existing (d-1)-simplices, with attachment probabilities controlled + by the flavor parameter `s` and an energy-based selection process. + + Parameters + ---------- + order : int + The order of the simplices to add. + s : int + The flavor parameter (-1, 0, or 1). + beta : float + The inverse temperature parameter controlling randomness. + n_iter : int + The total number of d-simplices to generate. + energy_dist : callable, optional + A function to sample vertex energies (default: uniform [0,10)). + seed : int, optional + Random seed for reproducibility. + + Returns + ------- + S : xgi.SimplicialComplex + The generated NGF simplicial complex. + + See also + -------- + apollonian_complex + pseudofractal_simplicial_complex + + References + ---------- + Bianconi, G., & Rahmede, C. (2016). + "Network geometry with flavor: from complexity to quantum geometry." + Physical Review E, 93(3), 032315. + https://arxiv.org/abs/1511.04539 + """ + if seed is not None: + np.random.seed(seed) + + # initialize the hypergraph + S = xgi.SimplicialComplex() + + # assign energies to vertices + if energy_distribution is None: + energy_distribution = lambda: np.random.uniform(0, 9) + + energy_nodes = {} + + # create initial d-simplex + initial_simplex = list(range(order + 1)) + for node in initial_simplex: + energy_nodes[node] = energy_distribution() + S.add_simplex(initial_simplex) + + # track (d-1)-simplices and their counts + face_counts = {} + + def count_face(face): + """Adds or updates a (d-1)-simplex count.""" + face = tuple(sorted(face)) + if face in face_counts: + face_counts[face] += 1 + else: + face_counts[face] = 1 + + # initialize (d-1)-simplices from the first simplex + for face in xgi.subfaces([initial_simplex], order=order - 1): + count_face(face) + + # iterative growth + for it in range(order + 2, order + 2 + n_iter): + # compute attachment probabilities + Z = 0 + probs = {} + + for face, count in face_counts.items(): + energy = sum(energy_nodes[node] for node in face) + weight = np.exp(-beta * energy) * (1 + s * (count - 1)) + if s == -1 and count >= 2: + weight = 0 # Prevent further attachment to these faces + probs[face] = weight + Z += weight + + # normalize probabilities + if Z == 0: + break # no valid attachment sites left + + for face in probs: + probs[face] /= Z + + # choose a (d-1)-simplex to attach the new simplex + chosen_idx = np.random.choice(len(probs), p=list(probs.values())) + chosen_face = list(probs.keys())[chosen_idx] + + # add new node and new simplex + new_node = it + energy_nodes[new_node] = energy_distribution() + new_simplex = list(chosen_face) + [new_node] + S.add_simplex(new_simplex) + + # update face counts + for face in xgi.subfaces([new_simplex], order=order - 1): + count_face(face) + + return S From e372067a2e7c7b58bf0e79002d5a03ca79ffda0e Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Tue, 11 Feb 2025 11:26:31 +0100 Subject: [PATCH 09/14] fix: added function to online docs --- docs/source/api/generators/xgi.generators.iterative.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/api/generators/xgi.generators.iterative.rst b/docs/source/api/generators/xgi.generators.iterative.rst index 2b8cd1238..a8fb20ee9 100644 --- a/docs/source/api/generators/xgi.generators.iterative.rst +++ b/docs/source/api/generators/xgi.generators.iterative.rst @@ -8,4 +8,5 @@ xgi.generators.iterative .. rubric:: Functions .. autofunction:: apollonian_complex - .. autofunction:: pseudofractal_simplicial_complex + .. autofunction:: network_geometry_flavor + .. autofunction:: pseudofractal_simplicial_complex \ No newline at end of file From 8f6cc5386113c394b03314a5775ad90f4e4b9e1c Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Thu, 29 Oct 2026 12:48:27 +0100 Subject: [PATCH 10/14] refactor: rename iterative generators module to growth --- docs/source/api/generators.rst | 2 +- ...generators.iterative.rst => xgi.generators.growth.rst} | 8 ++++---- tests/generators/{test_iterative.py => test_growth.py} | 0 xgi/generators/__init__.py | 4 ++-- xgi/generators/{iterative.py => growth.py} | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename docs/source/api/generators/{xgi.generators.iterative.rst => xgi.generators.growth.rst} (56%) rename tests/generators/{test_iterative.py => test_growth.py} (100%) rename xgi/generators/{iterative.py => growth.py} (99%) diff --git a/docs/source/api/generators.rst b/docs/source/api/generators.rst index 5da7e88ac..665d661c4 100644 --- a/docs/source/api/generators.rst +++ b/docs/source/api/generators.rst @@ -14,5 +14,5 @@ generators package ~xgi.generators.uniform ~xgi.generators.simplicial_complexes ~xgi.generators.randomizing - ~xgi.generators.iterative + ~xgi.generators.growth \ No newline at end of file diff --git a/docs/source/api/generators/xgi.generators.iterative.rst b/docs/source/api/generators/xgi.generators.growth.rst similarity index 56% rename from docs/source/api/generators/xgi.generators.iterative.rst rename to docs/source/api/generators/xgi.generators.growth.rst index a8fb20ee9..e8743f11a 100644 --- a/docs/source/api/generators/xgi.generators.iterative.rst +++ b/docs/source/api/generators/xgi.generators.growth.rst @@ -1,9 +1,9 @@ -xgi.generators.iterative -======================== +xgi.generators.growth +===================== -.. currentmodule:: xgi.generators.iterative +.. currentmodule:: xgi.generators.growth -.. automodule:: xgi.generators.iterative +.. automodule:: xgi.generators.growth .. rubric:: Functions diff --git a/tests/generators/test_iterative.py b/tests/generators/test_growth.py similarity index 100% rename from tests/generators/test_iterative.py rename to tests/generators/test_growth.py diff --git a/xgi/generators/__init__.py b/xgi/generators/__init__.py index 68d4429cf..dac8feb09 100644 --- a/xgi/generators/__init__.py +++ b/xgi/generators/__init__.py @@ -1,6 +1,6 @@ from . import ( classic, - iterative, + growth, lattice, random, randomizing, @@ -9,7 +9,7 @@ uniform, ) from .classic import * -from .iterative import * +from .growth import * from .lattice import * from .random import * from .randomizing import * diff --git a/xgi/generators/iterative.py b/xgi/generators/growth.py similarity index 99% rename from xgi/generators/iterative.py rename to xgi/generators/growth.py index a451734dd..5dd3a9727 100644 --- a/xgi/generators/iterative.py +++ b/xgi/generators/growth.py @@ -1,4 +1,4 @@ -"""Generate iterative hypergraphs.""" +"""Generate growing simplicial complexes.""" from ..core import SimplicialComplex From e7b436c76f0d0f78e93a22e249747a7a6fb67454 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Thu, 29 Oct 2026 12:51:47 +0100 Subject: [PATCH 11/14] docs: add size notes to growth generators --- tests/generators/test_growth.py | 10 +++++++ xgi/generators/growth.py | 51 +++++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/tests/generators/test_growth.py b/tests/generators/test_growth.py index b6564b03c..31af4af1f 100644 --- a/tests/generators/test_growth.py +++ b/tests/generators/test_growth.py @@ -97,3 +97,13 @@ def test_apollonian(): frozenset({0, 1, 3}), frozenset({0, 4, 7}), } + + +@pytest.mark.parametrize( + "generator", + [xgi.pseudofractal_simplicial_complex, xgi.apollonian_complex], +) +def test_order_too_small(generator): + for order in [0, 1]: + with pytest.raises(ValueError): + generator(order=order, n_iter=1) diff --git a/xgi/generators/growth.py b/xgi/generators/growth.py index 5dd3a9727..c27f4ef0a 100644 --- a/xgi/generators/growth.py +++ b/xgi/generators/growth.py @@ -10,12 +10,12 @@ def pseudofractal_simplicial_complex(order, n_iter): - """ + r""" Generate the pseudofractal simplicial complex of order `order`. - Starting with a single d-simplex, at each iteration, the function adds new (d+1)-simplices - by attaching a new vertex to all existing (d-1)-simplices (as well as all their subfaces). - This process is deterministic. + Starting with a single d-simplex (d = `order`), at each iteration a new + node is attached to every existing (d-1)-simplex, forming a new d-simplex + (with all its subfaces). This process is deterministic. Parameters ---------- @@ -29,6 +29,19 @@ def pseudofractal_simplicial_complex(order, n_iter): S : xgi.SimplicialComplex Generated simplicial complex + Raises + ------ + ValueError + If `order` is less than 2. + + Notes + ----- + The size grows exponentially with `n_iter`. After :math:`t` iterations, the + complex has :math:`((d+1)^{t+1} - 1)/d` simplices of order :math:`d`, i.e., + :math:`\mathcal{O}((d+1)^t)`, and :math:`d` more nodes than that. + For example, ``order=2, n_iter=10`` gives 88,573 triangles and + ``order=3, n_iter=10`` gives 1,398,101 tetrahedra. + See also -------- apollonian_complex @@ -42,6 +55,9 @@ def pseudofractal_simplicial_complex(order, n_iter): https://arxiv.org/abs/2401.11298 """ + if order < 2: + raise ValueError("order must be at least 2.") + S = SimplicialComplex() # initialize the first d-simplex @@ -69,13 +85,13 @@ def pseudofractal_simplicial_complex(order, n_iter): def apollonian_complex(order, n_iter): - """ + r""" Generate the apollonian complex of order `order`. - Starting with a single d-simplex, at each iteration, the function adds new (d+1)-simplices - by attaching a new vertex to (d-1)-simplices that contain at least one newly added node. - This process is deterministic and generates a simplicial complex. - + Starting with a single d-simplex (d = `order`), at each iteration a new + node is attached to every (d-1)-simplex that contains a node added in the + previous iteration, forming a new d-simplex (with all its subfaces). + This process is deterministic. Parameters ---------- @@ -89,6 +105,20 @@ def apollonian_complex(order, n_iter): S : xgi.SimplicialComplex Generated simplicial complex + Raises + ------ + ValueError + If `order` is less than 2. + + Notes + ----- + The size grows exponentially with `n_iter`, but more slowly than for + :func:`pseudofractal_simplicial_complex`. After :math:`t` iterations, the + complex has :math:`1 + (d+1)(d^t - 1)/(d-1)` simplices of order :math:`d`, + i.e., :math:`\mathcal{O}(d^t)`, and :math:`d` more nodes than that. + For example, ``order=2, n_iter=10`` gives 3,070 triangles and + ``order=3, n_iter=10`` gives 118,097 tetrahedra. + See also -------- pseudofractal_simplicial_complex @@ -102,6 +132,9 @@ def apollonian_complex(order, n_iter): https://arxiv.org/abs/2401.11298 """ + if order < 2: + raise ValueError("order must be at least 2.") + S = SimplicialComplex() # initialize the first d-simplex From c6cbcb521018df0b9f5a0347f20378c5f0bc6192 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Thu, 29 Oct 2026 12:57:29 +0100 Subject: [PATCH 12/14] fix: network_geometry_flavor bugs and add tests --- tests/generators/test_growth.py | 75 ++++++++++++++++++ xgi/generators/growth.py | 131 ++++++++++++++++++-------------- 2 files changed, 148 insertions(+), 58 deletions(-) diff --git a/tests/generators/test_growth.py b/tests/generators/test_growth.py index 31af4af1f..7911d4aa4 100644 --- a/tests/generators/test_growth.py +++ b/tests/generators/test_growth.py @@ -107,3 +107,78 @@ def test_order_too_small(generator): for order in [0, 1]: with pytest.raises(ValueError): generator(order=order, n_iter=1) + + +def test_network_geometry_flavor(): + order, n_iter = 2, 20 + for s in [-1, 0, 1]: + S = xgi.network_geometry_flavor(order, s, beta=1, n_iter=n_iter, seed=1) + assert isinstance(S, xgi.SimplicialComplex) + # one new node and one new d-simplex per iteration + assert set(S.nodes) == set(range(order + 1 + n_iter)) + assert len(S.edges.filterby("order", order)) == n_iter + 1 + + # s = -1: each (d-1)-simplex is shared by at most two d-simplices + S = xgi.network_geometry_flavor(order, -1, beta=1, n_iter=50, seed=1) + triangles = S.edges.filterby("order", order).members() + face_counts = {} + for face in xgi.subfaces(triangles, order=order - 1): + face = frozenset(face) + face_counts[face] = face_counts.get(face, 0) + 1 + assert max(face_counts.values()) <= 2 + + # order 1 grows a tree + S = xgi.network_geometry_flavor(1, 0, beta=1, n_iter=10, seed=1) + assert S.num_nodes == 12 + assert len(S.edges.filterby("order", 1)) == 11 + + # large beta does not underflow + S = xgi.network_geometry_flavor(order, 0, beta=1000, n_iter=10, seed=1) + assert S.num_nodes == order + 1 + 10 + + +def test_network_geometry_flavor_seed(): + S1 = xgi.network_geometry_flavor(2, 1, beta=1, n_iter=30, seed=42) + S2 = xgi.network_geometry_flavor(2, 1, beta=1, n_iter=30, seed=42) + S3 = xgi.network_geometry_flavor(2, 1, beta=1, n_iter=30, seed=43) + assert set(S1.edges.members()) == set(S2.edges.members()) + assert set(S1.edges.members()) != set(S3.edges.members()) + + # custom energy distribution + S = xgi.network_geometry_flavor( + 2, + 0, + beta=1, + n_iter=10, + energy_distribution=lambda rng: rng.exponential(1.0), + seed=1, + ) + assert S.num_nodes == 13 + + # energy distribution with an rng argument is reproducible with seed + def energy(rng): + return rng.exponential(1.0) + + S1 = xgi.network_geometry_flavor(2, 1, 1, 30, energy_distribution=energy, seed=7) + S2 = xgi.network_geometry_flavor(2, 1, 1, 30, energy_distribution=energy, seed=7) + assert set(S1.edges.members()) == set(S2.edges.members()) + + # energy distribution without arguments also works + calls = [] + + def energy_no_arg(): + calls.append(1) + return 1.0 + + S = xgi.network_geometry_flavor( + 2, 0, beta=1, n_iter=10, energy_distribution=energy_no_arg, seed=1 + ) + assert S.num_nodes == 13 + assert len(calls) == 13 # one energy per node + + +def test_network_geometry_flavor_errors(): + with pytest.raises(ValueError): + xgi.network_geometry_flavor(0, 0, beta=1, n_iter=5) + with pytest.raises(ValueError): + xgi.network_geometry_flavor(2, 2, beta=1, n_iter=5) diff --git a/xgi/generators/growth.py b/xgi/generators/growth.py index c27f4ef0a..bc9284e88 100644 --- a/xgi/generators/growth.py +++ b/xgi/generators/growth.py @@ -1,6 +1,11 @@ """Generate growing simplicial complexes.""" +from inspect import Parameter, signature + +import numpy as np + from ..core import SimplicialComplex +from ..utils.utilities import subfaces __all__ = [ "pseudofractal_simplicial_complex", @@ -187,23 +192,33 @@ def network_geometry_flavor( Parameters ---------- order : int - The order of the simplices to add. - s : int - The flavor parameter (-1, 0, or 1). + The order d of the simplices to add. Must be at least 1. + s : {-1, 0, 1} + The flavor parameter. beta : float The inverse temperature parameter controlling randomness. n_iter : int - The total number of d-simplices to generate. - energy_dist : callable, optional - A function to sample vertex energies (default: uniform [0,10)). - seed : int, optional - Random seed for reproducibility. + The number of new nodes (and d-simplices) to add. + energy_distribution : callable, optional + A function that returns one node energy. It can either take a + :class:`numpy.random.Generator` as its only argument, e.g. + ``lambda rng: rng.exponential(1.0)``, in which case energies are + reproducible with `seed`, or take no argument, e.g. + ``lambda: np.random.exponential(1.0)``, in which case it handles its + own randomness. By default, energies are uniform in [0, 10). + seed : int, numpy.random.Generator, or None, optional + The seed for the random number generator. By default, None. Returns ------- S : xgi.SimplicialComplex The generated NGF simplicial complex. + Raises + ------ + ValueError + If `order` is less than 1 or `s` is not -1, 0, or 1. + See also -------- apollonian_complex @@ -216,72 +231,72 @@ def network_geometry_flavor( Physical Review E, 93(3), 032315. https://arxiv.org/abs/1511.04539 """ - if seed is not None: - np.random.seed(seed) + if order < 1: + raise ValueError("order must be at least 1.") + if s not in {-1, 0, 1}: + raise ValueError("s must be -1, 0, or 1.") - # initialize the hypergraph - S = xgi.SimplicialComplex() + rng = np.random.default_rng(seed) - # assign energies to vertices if energy_distribution is None: - energy_distribution = lambda: np.random.uniform(0, 9) + sample_energy = lambda: rng.uniform(0, 10) + elif _n_required_args(energy_distribution) == 0: + sample_energy = energy_distribution + else: + sample_energy = lambda: energy_distribution(rng) - energy_nodes = {} + S = SimplicialComplex() # create initial d-simplex initial_simplex = list(range(order + 1)) - for node in initial_simplex: - energy_nodes[node] = energy_distribution() + energy_nodes = {node: sample_energy() for node in initial_simplex} S.add_simplex(initial_simplex) - # track (d-1)-simplices and their counts + # number of d-simplices attached to each (d-1)-simplex face_counts = {} - def count_face(face): - """Adds or updates a (d-1)-simplex count.""" - face = tuple(sorted(face)) - if face in face_counts: - face_counts[face] += 1 - else: - face_counts[face] = 1 + def count_faces(simplex): + for face in subfaces([simplex], order=order - 1): + face = tuple(sorted(face)) + face_counts[face] = face_counts.get(face, 0) + 1 - # initialize (d-1)-simplices from the first simplex - for face in xgi.subfaces([initial_simplex], order=order - 1): - count_face(face) - - # iterative growth - for it in range(order + 2, order + 2 + n_iter): - # compute attachment probabilities - Z = 0 - probs = {} + count_faces(initial_simplex) + for new_node in range(order + 1, order + 1 + n_iter): + # attachment weights exp(-beta * energy) * (1 + s * (count - 1)), + # computed in log space to avoid underflow at large beta + faces = [] + log_weights = [] for face, count in face_counts.items(): + multiplicity = 1 + s * (count - 1) + if multiplicity <= 0: + continue # s = -1: face already shared by two simplices energy = sum(energy_nodes[node] for node in face) - weight = np.exp(-beta * energy) * (1 + s * (count - 1)) - if s == -1 and count >= 2: - weight = 0 # Prevent further attachment to these faces - probs[face] = weight - Z += weight - - # normalize probabilities - if Z == 0: - break # no valid attachment sites left - - for face in probs: - probs[face] /= Z - - # choose a (d-1)-simplex to attach the new simplex - chosen_idx = np.random.choice(len(probs), p=list(probs.values())) - chosen_face = list(probs.keys())[chosen_idx] - - # add new node and new simplex - new_node = it - energy_nodes[new_node] = energy_distribution() + faces.append(face) + log_weights.append(-beta * energy + np.log(multiplicity)) + + log_weights = np.array(log_weights) + probs = np.exp(log_weights - log_weights.max()) + probs /= probs.sum() + + # attach a new node to the chosen (d-1)-simplex + chosen_face = faces[rng.choice(len(faces), p=probs)] + energy_nodes[new_node] = sample_energy() new_simplex = list(chosen_face) + [new_node] S.add_simplex(new_simplex) - - # update face counts - for face in xgi.subfaces([new_simplex], order=order - 1): - count_face(face) + count_faces(new_simplex) return S + + +def _n_required_args(func): + """Number of positional arguments `func` needs (0 if it cannot be inspected).""" + try: + params = signature(func).parameters.values() + except (TypeError, ValueError): + return 0 + return sum( + p.default is Parameter.empty + and p.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD) + for p in params + ) From 79fb7057ce564c925caf37896a32f34a4028b72e Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Thu, 29 Oct 2026 13:00:47 +0100 Subject: [PATCH 13/14] docs: explain network_geometry_flavor model --- xgi/generators/growth.py | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/xgi/generators/growth.py b/xgi/generators/growth.py index bc9284e88..60b920a73 100644 --- a/xgi/generators/growth.py +++ b/xgi/generators/growth.py @@ -182,12 +182,40 @@ def apollonian_complex(order, n_iter): def network_geometry_flavor( order, s, beta, n_iter, energy_distribution=None, seed=None ): - """ + r""" Generate a Network Geometry with Flavor (NGF) simplicial complex. - The model grows a d-dimensional simplicial complex (where d is `order`) by iteratively attaching - d-simplices to existing (d-1)-simplices, with attachment probabilities controlled - by the flavor parameter `s` and an energy-based selection process. + The model grows a d-dimensional simplicial complex (d = `order`), starting + from a single d-simplex. At each iteration, a new node is attached to an + existing (d-1)-simplex :math:`\alpha`, forming a new d-simplex (with all + its subfaces). Which (d-1)-simplex is chosen depends on the energies of + its nodes and on the flavor `s`, which sets whether (d-1)-simplices + already shared by several d-simplices are forbidden (``s=-1``), neutral + (``s=0``), or favored (``s=1``). + + Each node :math:`i` gets a fixed energy :math:`\epsilon_i`, drawn from + `energy_distribution` when it is added. The energy of a (d-1)-simplex + is the sum of the energies of its nodes, + :math:`\epsilon_\alpha = \sum_{i \in \alpha} \epsilon_i`. At each + iteration, :math:`\alpha` is chosen with probability + + .. math:: + + \Pi_\alpha \propto e^{-\beta \epsilon_\alpha} (1 + s\, n_\alpha), + + where :math:`n_\alpha` is the number of d-simplices containing + :math:`\alpha`, minus one. There is no energy threshold: lower-energy + (d-1)-simplices are more likely to be chosen, and `beta` sets how strongly. + For ``beta=0``, energies play no role. + + The flavor `s` sets how the choice depends on :math:`n_\alpha`: + + * ``s=-1``: only (d-1)-simplices contained in a single d-simplex can be + chosen, so each one ends up in at most two d-simplices. The result is a + discrete manifold. + * ``s=0``: the choice depends on the energies only. + * ``s=1``: (d-1)-simplices contained in many d-simplices are more likely + to be chosen (preferential attachment). Parameters ---------- @@ -196,7 +224,8 @@ def network_geometry_flavor( s : {-1, 0, 1} The flavor parameter. beta : float - The inverse temperature parameter controlling randomness. + The inverse temperature. Higher values favor low-energy + (d-1)-simplices more strongly. n_iter : int The number of new nodes (and d-simplices) to add. energy_distribution : callable, optional From db472dd8940f35b723968b84d237fcc09033e757 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Thu, 29 Oct 2026 13:08:48 +0100 Subject: [PATCH 14/14] fix: export growth generators and update for dict members() --- tests/generators/test_growth.py | 20 ++++++++++---------- xgi/generators/__init__.py | 3 +++ xgi/generators/growth.py | 6 ++++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/generators/test_growth.py b/tests/generators/test_growth.py index 7911d4aa4..c904fbc06 100644 --- a/tests/generators/test_growth.py +++ b/tests/generators/test_growth.py @@ -8,7 +8,7 @@ def test_pseudofractal(): # initial S0 = xgi.pseudofractal_simplicial_complex(order=2, n_iter=0) - triangles = set(S0.edges.filterby("order", 2).members()) + triangles = set(S0.edges.filterby("order", 2).members(dtype=list)) assert isinstance(S0, xgi.SimplicialComplex) assert S0.num_nodes == 3 @@ -17,7 +17,7 @@ def test_pseudofractal(): # first iteration S1 = xgi.pseudofractal_simplicial_complex(order=2, n_iter=1) - triangles = set(S1.edges.filterby("order", 2).members()) + triangles = set(S1.edges.filterby("order", 2).members(dtype=list)) assert isinstance(S1, xgi.SimplicialComplex) assert S1.num_nodes == 6 @@ -31,7 +31,7 @@ def test_pseudofractal(): # second iteration S2 = xgi.pseudofractal_simplicial_complex(order=2, n_iter=2) - triangles = set(S2.edges.filterby("order", 2).members()) + triangles = set(S2.edges.filterby("order", 2).members(dtype=list)) assert isinstance(S2, xgi.SimplicialComplex) assert S2.num_nodes == 15 @@ -57,7 +57,7 @@ def test_apollonian(): # initial S0 = xgi.apollonian_complex(order=2, n_iter=0) - triangles = set(S0.edges.filterby("order", 2).members()) + triangles = set(S0.edges.filterby("order", 2).members(dtype=list)) assert isinstance(S0, xgi.SimplicialComplex) assert S0.num_nodes == 3 @@ -66,7 +66,7 @@ def test_apollonian(): # first iteration S1 = xgi.apollonian_complex(order=2, n_iter=1) - triangles = set(S1.edges.filterby("order", 2).members()) + triangles = set(S1.edges.filterby("order", 2).members(dtype=list)) assert isinstance(S1, xgi.SimplicialComplex) assert S1.num_nodes == 6 @@ -80,7 +80,7 @@ def test_apollonian(): # second iteration S2 = xgi.apollonian_complex(order=2, n_iter=2) - triangles = set(S2.edges.filterby("order", 2).members()) + triangles = set(S2.edges.filterby("order", 2).members(dtype=list)) assert isinstance(S2, xgi.SimplicialComplex) assert S2.num_nodes == 12 @@ -120,7 +120,7 @@ def test_network_geometry_flavor(): # s = -1: each (d-1)-simplex is shared by at most two d-simplices S = xgi.network_geometry_flavor(order, -1, beta=1, n_iter=50, seed=1) - triangles = S.edges.filterby("order", order).members() + triangles = S.edges.filterby("order", order).members(dtype=list) face_counts = {} for face in xgi.subfaces(triangles, order=order - 1): face = frozenset(face) @@ -141,8 +141,8 @@ def test_network_geometry_flavor_seed(): S1 = xgi.network_geometry_flavor(2, 1, beta=1, n_iter=30, seed=42) S2 = xgi.network_geometry_flavor(2, 1, beta=1, n_iter=30, seed=42) S3 = xgi.network_geometry_flavor(2, 1, beta=1, n_iter=30, seed=43) - assert set(S1.edges.members()) == set(S2.edges.members()) - assert set(S1.edges.members()) != set(S3.edges.members()) + assert set(S1.edges.members(dtype=list)) == set(S2.edges.members(dtype=list)) + assert set(S1.edges.members(dtype=list)) != set(S3.edges.members(dtype=list)) # custom energy distribution S = xgi.network_geometry_flavor( @@ -161,7 +161,7 @@ def energy(rng): S1 = xgi.network_geometry_flavor(2, 1, 1, 30, energy_distribution=energy, seed=7) S2 = xgi.network_geometry_flavor(2, 1, 1, 30, energy_distribution=energy, seed=7) - assert set(S1.edges.members()) == set(S2.edges.members()) + assert set(S1.edges.members(dtype=list)) == set(S2.edges.members(dtype=list)) # energy distribution without arguments also works calls = [] diff --git a/xgi/generators/__init__.py b/xgi/generators/__init__.py index 84b3c20ef..7fbf7ef03 100644 --- a/xgi/generators/__init__.py +++ b/xgi/generators/__init__.py @@ -45,4 +45,7 @@ "uniform_erdos_renyi_hypergraph", "uniform_HSBM", "uniform_HPPM", + "pseudofractal_simplicial_complex", + "apollonian_complex", + "network_geometry_flavor", ] diff --git a/xgi/generators/growth.py b/xgi/generators/growth.py index 60b920a73..227b12912 100644 --- a/xgi/generators/growth.py +++ b/xgi/generators/growth.py @@ -73,7 +73,7 @@ def pseudofractal_simplicial_complex(order, n_iter): for it in range(1, n_iter + 1): # Find all (order - 1)-simplices present in the complex nodes = S.nodes - subfaces = S.edges.filterby("order", order - 1).members() + subfaces = S.edges.filterby("order", order - 1).members(dtype=list) max_index = max(nodes) new_simplices = [] @@ -153,7 +153,9 @@ def apollonian_complex(order, n_iter): for it in range(1, n_iter + 1): # find all (order - 1)-simplices present in the complex nodes = S.nodes - subfaces_previous_iter = S.edges.filterby("order", order - 1).members() + subfaces_previous_iter = S.edges.filterby("order", order - 1).members( + dtype=list + ) # keep only those attached to new nodes subfaces_previous_iter = [