Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/api/generators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ generators package
~xgi.generators.uniform
~xgi.generators.simplicial_complexes
~xgi.generators.randomizing
~xgi.generators.iterative

12 changes: 12 additions & 0 deletions docs/source/api/generators/xgi.generators.iterative.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
xgi.generators.iterative
========================

.. currentmodule:: xgi.generators.iterative

.. automodule:: xgi.generators.iterative

.. rubric:: Functions

.. autofunction:: apollonian_complex
.. autofunction:: network_geometry_flavor
.. autofunction:: pseudofractal_simplicial_complex
99 changes: 99 additions & 0 deletions tests/generators/test_iterative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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(S0, 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():

# initial
S0 = xgi.apollonian_complex(order=2, n_iter=0)
triangles = set(S0.edges.filterby("order", 2).members())

assert isinstance(S0, xgi.SimplicialComplex)
assert S0.num_nodes == 3
assert S0.num_edges == 4
assert triangles == {frozenset({0, 1, 2})}

# 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}),
}
2 changes: 2 additions & 0 deletions xgi/generators/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from . import (
classic,
iterative,
lattice,
random,
randomizing,
Expand All @@ -8,6 +9,7 @@
uniform,
)
from .classic import *
from .iterative import *
from .lattice import *
from .random import *
from .randomizing import *
Expand Down
254 changes: 254 additions & 0 deletions xgi/generators/iterative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
"""Generate iterative hypergraphs."""

from ..core import SimplicialComplex

__all__ = [
"pseudofractal_simplicial_complex",
"apollonian_complex",
"network_geometry_flavor",
]


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 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.

Returns
-------
S : xgi.SimplicialComplex
Generated simplicial complex

See also
--------
apollonian_complex
network_geometry_flavor

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 = 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 order 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
network_geometry_flavor

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 = 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


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