chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:36:30 +08:00
commit 55ab4e4a73
473 changed files with 72932 additions and 0 deletions
@@ -0,0 +1,5 @@
from .hypergraph_classic import *
from .lattice import *
from .random import *
from .simple import *
from .uniform import *
@@ -0,0 +1,39 @@
import itertools
import easygraph as eg
from easygraph.utils.exception import EasyGraphError
__all__ = ["empty_hypergraph", "complete_hypergraph"]
def empty_hypergraph(N=1):
"""
Parameters
----------
N number of node in Hypergraph, default 1
Returns
-------
A eg.Hypergraph with n_num node, without any hyperedge.
"""
return eg.Hypergraph(N)
def complete_hypergraph(n, include_singleton=False):
if n == 0:
raise EasyGraphError("The number of nodes in a Hypergraph can not be zero")
# init
# print("easygraph:",eg)
hypergraph = eg.Hypergraph(n)
total_hyperedegs = []
if n > 1:
start = 1 if include_singleton else 2
for size in range(start, n + 1):
hyperedges = itertools.combinations(list(range(n)), size)
total_hyperedegs.extend(list(hyperedges))
hypergraph.add_hyperedges(total_hyperedegs)
return hypergraph
@@ -0,0 +1,69 @@
"""Generators for some lattice hypergraphs.
All the functions in this module return a Hypergraph class (i.e. a simple, undirected
hypergraph).
"""
from warnings import warn
from easygraph.utils.exception import EasyGraphError
__all__ = [
"ring_lattice",
]
def ring_lattice(n, d, k, l):
"""A ring lattice hypergraph.
A d-uniform hypergraph on n nodes where each node is part of k edges and the
overlap between consecutive edges is d-l.
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
Returns
-------
Hypergraph
The generated hypergraph
Raises
------
EasyGraphError
If k is negative.
Notes
-----
ring_lattice(n, 2, k, 0) is a ring lattice graph where each node has k//2 edges on
either side.
"""
from easygraph.classes.hypergraph import Hypergraph
if k < 0:
raise EasyGraphError("Invalid k value!")
if k < 2:
warn("This creates a completely disconnected hypergraph!")
if k % 2 != 0:
warn("k is not divisible by 2")
edges = [
[node] + [(start + l + i) % n for i in range(d - 1)]
for node in range(n)
for start in range(node + 1, node + k // 2 + 1)
]
H = Hypergraph(num_v=n)
H.add_hyperedges(edges)
return H
@@ -0,0 +1,446 @@
import math
import random
import warnings
from collections import defaultdict
from itertools import combinations
import easygraph as eg
import numpy as np
# from easygraph.classes.hypergraph import Hypergraph
from easygraph.utils.exception import EasyGraphError
from scipy.special import comb
from .lattice import *
__all__ = [
"random_hypergraph",
"chung_lu_hypergraph",
"dcsbm_hypergraph",
"watts_strogatz_hypergraph",
"uniform_hypergraph_Gnp",
]
def uniform_hypergraph_Gnp_parallel(edges, prob):
remain_edges = [e for e in edges if random.random() < prob]
return remain_edges
def split_edges(edges, worker):
import math
edges_size = len(edges)
group_size = math.ceil(edges_size / worker)
group_lst = []
for i in range(0, edges_size, group_size):
group_lst.append(edges[i : i + group_size])
return group_lst
def uniform_hypergraph_Gnp(k: int, num_v: int, prob: float, n_workers=None):
r"""Return a random ``k``-uniform hypergraph with ``num_v`` vertices and probability ``prob`` of choosing a hyperedge.
Args:
``num_v`` (``int``): The Number of vertices.
``k`` (``int``): The Number of vertices in each hyperedge.
``prob`` (``float``): Probability of choosing a hyperedge.
Examples:
>>> import easygraph as eg
>>> hg = eg.random.uniform_hypergraph_Gnp(3, 5, 0.5)
>>> hg.e
([(0, 1, 3), (0, 1, 4), (0, 2, 4), (1, 3, 4), (2, 3, 4)], [1.0, 1.0, 1.0, 1.0, 1.0])
"""
# similar to BinomialRandomUniform in sagemath, https://doc.sagemath.org/html/en/reference/graphs/sage/graphs/hypergraph_generators.html
assert num_v > 1, "num_v must be greater than 1"
assert k > 1, "k must be greater than 1"
assert 0 <= prob <= 1, "prob must be between 0 and 1"
import random
if n_workers is not None:
# use the parallel version for large graph
from functools import partial
from multiprocessing import Pool
edges = combinations(range(num_v), k)
edges_parallel = split_edges(edges=list(edges), worker=n_workers)
local_function = partial(uniform_hypergraph_Gnp_parallel, prob=prob)
res_edges = []
with Pool(n_workers) as p:
ret = p.imap(local_function, edges_parallel)
for res in ret:
res_edges.extend(res)
res_hypergraph = eg.Hypergraph(num_v=num_v, e_list=res_edges)
return res_hypergraph
else:
edges = combinations(range(num_v), k)
edges = [e for e in edges if random.random() < prob]
return eg.Hypergraph(num_v=num_v, e_list=edges)
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 easygraph as eg; 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 = eg.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 = eg.Hypergraph(num_v=len(node_labels))
kappa1 = defaultdict(lambda: 0)
kappa2 = defaultdict(lambda: 0)
for id, g in g1.items():
kappa1[g] += k1[id]
for id, g in g2.items():
kappa2[g] += k2[id]
tmp_hyperedges = []
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 + math.floor(math.log(r) / math.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
if v < len(tmp_hyperedges):
if u not in tmp_hyperedges[v]:
tmp_hyperedges[v].append(u)
else:
tmp_hyperedges.append([u])
p = q
j = j + 1
H.add_hyperedges(tmp_hyperedges)
return H
def watts_strogatz_hypergraph(n, d, k, l, p, seed=None):
"""
Parameters
----------
n : int
The 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 probability of rewiring each edge
seed
Returns
-------
"""
if seed is not None:
np.random.seed(seed)
H = ring_lattice(n, d, k, l)
to_remove = []
to_add = []
H_edges = H.e[0]
for e in H_edges:
if np.random.random() < p:
to_remove.append(e)
node = min(e)
neighbors = np.random.choice(H.v, size=d - 1)
to_add.append(np.append(neighbors, node))
for e in to_remove:
if e in H_edges:
H_edges.remove(e)
for e in to_add:
H_edges.append(e)
H = eg.Hypergraph(num_v=n, e_list=H_edges)
# H.remove_hyperedges(to_remove)
# print("watts_strogatz:",H.e)
# H.add_hyperedges(to_add)
return H
def chung_lu_hypergraph(k1, k2, seed=None):
"""A function to generate a Chung-Lu hypergraph
Parameters
----------
k1 : dict
Dict where the keys are node ids
and the values are node degrees.
k2 : dict
dict 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 easygraph as eg
>>> 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 = eg.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 = eg.Hypergraph(len(node_labels))
tmp_hyperedges = []
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 + math.floor(math.log(r) / math.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
if v < len(tmp_hyperedges):
tmp_hyperedges[v].append(u)
else:
tmp_hyperedges.append([u])
p = q
j = j + 1
H.add_hyperedges(tmp_hyperedges)
return H
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
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).
order: int of None (default)
If None, ignore. If int, generates a uniform hypergraph with edges
of order `order` (ps must have only one element).
seed : integer or None (default)
Seed for the random number generator.
Returns
-------
Hypergraph object
The generated hypergraph
References
----------
Described as 'random hypergraph' by M. Dewar et al. in https://arxiv.org/abs/1703.07686
Example
-------
>>> import easygraph as eg
>>> H = eg.random_hypergraph(50, [0.1, 0.01])
"""
if seed is not None:
np.random.seed(seed)
if order is not None:
if len(ps) != 1:
raise EasyGraphError("ps must contain a single element if order is an int")
if (np.any(np.array(ps) < 0)) or (np.any(np.array(ps) > 1)):
raise EasyGraphError("All elements of ps must be between 0 and 1 included.")
nodes = range(N)
hyperedges = []
for i, p in enumerate(ps):
if order is not None:
d = order
else:
d = i + 1 # order, ps[0] is prob of edges (d=1)
potential_edges = combinations(nodes, d + 1)
n_comb = comb(N, d + 1, exact=True)
mask = np.random.random(size=n_comb) <= p # True if edge to keep
edges_to_add = [e for e, val in zip(potential_edges, mask) if val]
hyperedges += edges_to_add
H = eg.Hypergraph(num_v=N)
H.add_hyperedges(hyperedges)
return H
@@ -0,0 +1,77 @@
from itertools import combinations
import easygraph as eg
from easygraph.utils.exception import EasyGraphError
__all__ = [
"star_clique",
]
def star_clique(n_star, n_clique, d_max):
"""Generate a star-clique structure
That is a star network and a clique network,
connected by one pairwise edge connecting the centre of the star to the clique.
network, the each clique is promoted to a hyperedge
up to order d_max.
Parameters
----------
n_star : int
Number of legs of the star
n_clique : int
Number of nodes in the clique
d_max : int
Maximum order up to which to promote
cliques to hyperedges
Returns
-------
H : Hypergraph
Examples
--------
>>> import easygraph as eg
>>> H = eg.star_clique(6, 7, 2)
Notes
-----
The total number of nodes is n_star + n_clique.
"""
if n_star <= 0:
raise ValueError("n_star must be an integer > 0.")
if n_clique <= 0:
raise ValueError("n_clique must be an integer > 0.")
if d_max < 0:
raise ValueError("d_max must be an integer >= 0.")
elif d_max > n_clique - 1:
raise ValueError("d_max must be <= n_clique - 1.")
nodes_star = range(n_star)
nodes_clique = range(n_star, n_star + n_clique)
nodes = list(nodes_star) + list(nodes_clique)
H = eg.Hypergraph(num_v=len(nodes))
# add star edges (center of the star is 0-th node)
H.add_hyperedges([[nodes_star[0], nodes_star[i]] for i in range(1, n_star)])
# connect clique and star by adding last star leg
H.add_hyperedges([nodes_star[0], nodes_clique[0]])
# add clique hyperedges up to order d_max
H.add_hyperedges(
[
e
for d in range(1, d_max + 1)
for e in list(combinations(nodes_clique, d + 1))
]
)
return H
@@ -0,0 +1,85 @@
import easygraph as eg
import pytest
from easygraph.utils.exception import EasyGraphError
class TestClassic:
def test_complete_hypergraph(self):
print(eg.complete_hypergraph(10))
assert eg.complete_hypergraph(10) is not None
def test_random_hypergraph(self):
import random
import numpy as np
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 = eg.chung_lu_hypergraph(k1, k2)
H2 = eg.watts_strogatz_hypergraph(n=n, d=10, k=16, p=0.5, l=3)
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]])
H3 = eg.dcsbm_hypergraph(k1, k2, g1, g2, omega)
assert H != None
assert H2 != None
assert H3 != None
def test_simple_hypergraph(self):
H = eg.star_clique(6, 7, 2)
print(H)
def test_uniform_hypergraph(self):
n = 1000
m = 3
k = {0: 1, 1: 2, 2: 3, 3: 3}
H = eg.uniform_hypergraph_configuration_model(k, m)
print(H)
H2 = eg.uniform_erdos_renyi_hypergraph(10, 5, 0.5, "prob")
# H2 = eg.uniform_HSBM(n,5,[3,4,5],[0.5,0.5,0.5])
print("H2:", H2)
H3 = eg.uniform_HPPM(10, 6, 0.9, 10, 0.9)
print("H3:", H3)
class TestHypergraphGenerators:
def test_empty_hypergraph_default(self):
hg = eg.empty_hypergraph()
assert hg.num_v == 1
assert len(hg.e[0]) == 0
def test_empty_hypergraph_custom_size(self):
hg = eg.empty_hypergraph(5)
assert hg.num_v == 5
assert len(hg.e[0]) == 0
def test_complete_hypergraph_zero_nodes_raises(self):
with pytest.raises(EasyGraphError):
eg.complete_hypergraph(0)
def test_complete_hypergraph_n_1_excludes_singletons(self):
hg = eg.complete_hypergraph(1, include_singleton=False)
assert hg.num_v == 1
assert len(hg.e[0]) == 0
def test_complete_hypergraph_n_3_excludes_singletons(self):
hg = eg.complete_hypergraph(3, include_singleton=False)
expected_edges = [[0, 1], [0, 2], [1, 2], [0, 1, 2]]
assert sorted(sorted(e) for e in hg.e[0]) == sorted(
sorted(e) for e in expected_edges
)
def test_complete_hypergraph_n_3_includes_singletons(self):
hg = eg.complete_hypergraph(3, include_singleton=True)
expected_edges = [[0], [1], [2], [0, 1], [0, 2], [1, 2], [0, 1, 2]]
assert sorted(sorted(e) for e in hg.e[0]) == sorted(
sorted(e) for e in expected_edges
)
@@ -0,0 +1,49 @@
import easygraph as eg
import pytest
from easygraph.utils.exception import EasyGraphError
class TestRingLatticeHypergraph:
def test_valid_ring_lattice(self):
H = eg.ring_lattice(n=10, d=3, k=4, l=1)
assert isinstance(H, eg.Hypergraph)
assert H.num_v == 10
assert all(len(edge) == 3 for edge in H.e[0])
def test_k_less_than_zero_raises_error(self):
with pytest.raises(EasyGraphError, match="Invalid k value!"):
eg.ring_lattice(n=10, d=3, k=-2, l=1)
def test_k_less_than_two_warns(self):
with pytest.warns(UserWarning, match="disconnected"):
H = eg.ring_lattice(n=10, d=3, k=1, l=1)
assert isinstance(H, eg.Hypergraph)
def test_k_odd_warns(self):
with pytest.warns(UserWarning, match="divisible by 2"):
H = eg.ring_lattice(n=10, d=3, k=3, l=1)
assert isinstance(H, eg.Hypergraph)
def test_ring_lattice_with_d_eq_1(self):
H = eg.ring_lattice(n=5, d=1, k=2, l=0)
assert all(len(edge) == 1 for edge in H.e[0])
def test_ring_lattice_with_overlap_zero(self):
H = eg.ring_lattice(n=6, d=2, k=2, l=0)
assert all(len(edge) == 2 for edge in H.e[0])
def test_large_n(self):
H = eg.ring_lattice(n=100, d=4, k=6, l=2)
assert H.num_v == 100
assert all(len(e) == 4 for e in H.e[0])
def test_n_equals_1(self):
H = eg.ring_lattice(n=1, d=1, k=2, l=0)
assert H.num_v == 1
assert isinstance(H, eg.Hypergraph)
def test_k_zero(self):
H = eg.ring_lattice(n=5, d=2, k=0, l=1)
assert H.num_v == 5
assert len(H.e[0]) == 0
@@ -0,0 +1,60 @@
from itertools import combinations
import easygraph as eg
import pytest
class TestStarCliqueHypergraph:
def test_valid_star_clique(self):
H = eg.star_clique(n_star=5, n_clique=4, d_max=2)
assert isinstance(H, eg.Hypergraph)
assert H.num_v == 9 # 5 star nodes + 4 clique nodes
assert any(0 in edge for edge in H.e[0]) # star center connected
def test_minimum_valid_values(self):
H = eg.star_clique(n_star=2, n_clique=2, d_max=1)
assert H.num_v == 4
assert len(H.e[0]) >= 2
def test_n_star_zero_raises(self):
with pytest.raises(ValueError, match="n_star must be an integer > 0."):
eg.star_clique(0, 3, 1)
def test_n_clique_zero_raises(self):
with pytest.raises(ValueError, match="n_clique must be an integer > 0."):
eg.star_clique(3, 0, 1)
def test_d_max_negative_raises(self):
with pytest.raises(ValueError, match="d_max must be an integer >= 0."):
eg.star_clique(3, 4, -1)
def test_d_max_too_large_raises(self):
with pytest.raises(ValueError, match="d_max must be <= n_clique - 1."):
eg.star_clique(3, 4, 5)
def test_no_clique_edges_if_d_max_zero(self):
H = eg.star_clique(3, 3, 0)
clique_nodes = set(range(3, 6))
for edge in H.e[0]:
assert not clique_nodes.issubset(edge)
def test_clique_hyperedges_match_combinations(self):
n_star, n_clique, d_max = 3, 4, 2
H = eg.star_clique(n_star, n_clique, d_max)
clique_nodes = list(range(n_star, n_star + n_clique))
expected = {
tuple(sorted(e))
for d in range(1, d_max + 1)
for e in combinations(clique_nodes, d + 1)
}
actual = {
tuple(sorted(e)) for e in H.e[0] if all(node in clique_nodes for node in e)
}
assert expected.issubset(actual)
def test_star_legs_connect_to_center(self):
H = eg.star_clique(5, 4, 1)
star_nodes = list(range(5))
center = star_nodes[0]
for i in range(1, 4): # last star leg is used to connect to clique
assert any({center, i}.issubset(edge) for edge in H.e[0])
@@ -0,0 +1,455 @@
"""Generate random uniform hypergraphs."""
import itertools
import operator
import random
import warnings
from functools import reduce
import easygraph as eg
import numpy as np
from easygraph.utils.exception import EasyGraphError
__all__ = [
"uniform_hypergraph_configuration_model",
"uniform_HSBM",
"uniform_HPPM",
"uniform_erdos_renyi_hypergraph",
"uniform_hypergraph_Gnm",
]
def split_num_e(num_e, worker):
import math
res = []
group_size = num_e // worker
for i in range(worker):
res.append(group_size)
return res
def uniform_hypergraph_Gnm_parallel(num_e, num_v, k):
random.seed()
edges = set()
while len(edges) < num_e:
e = random.sample(range(num_v), k)
e = tuple(sorted(e))
if e not in edges:
edges.add(e)
return list(edges)
def uniform_hypergraph_Gnm(k: int, num_v: int, num_e: int, n_workers=None):
r"""Return a random ``k``-uniform hypergraph with ``num_v`` vertices and ``num_e`` hyperedges.
Args:
``k`` (``int``): The Number of vertices in each hyperedge.
``num_v`` (``int``): The Number of vertices.
``num_e`` (``int``): The Number of hyperedges.
Examples:
>>> import easygraph as eg
>>> hg = eg.uniform_hypergraph_Gnm(3, 5, 4)
>>> hg.e
([(0, 1, 2), (0, 1, 3), (0, 3, 4), (2, 3, 4)], [1.0, 1.0, 1.0, 1.0])
"""
# similar to UniformRandomUniform in sagemath, https://doc.sagemath.org/html/en/reference/graphs/sage/graphs/hypergraph_generators.html
assert k > 1, "k must be greater than 1" # TODO ?
assert num_v > 1, "num_v must be greater than 1"
assert num_e > 0, "num_e must be greater than 0"
if n_workers is not None:
# use the parallel version for large graph
edges = set()
from functools import partial
from multiprocessing import Pool
# res_edges = set()
edges_parallel = split_num_e(num_e=num_e, worker=n_workers)
local_function = partial(uniform_hypergraph_Gnm_parallel, num_v=num_v, k=k)
res_edges = set()
import time
with Pool(n_workers) as p:
ret = p.imap(local_function, edges_parallel)
for res in ret:
for r in res:
res_edges.add(r)
while len(res_edges) < num_e:
e = random.sample(range(num_v), k)
e = tuple(sorted(e))
if e not in res_edges:
res_edges.add(e)
res_hypergraph = eg.Hypergraph(num_v=num_v, e_list=list(res_edges))
return res_hypergraph
else:
edges = set()
while len(edges) < num_e:
e = random.sample(range(num_v), k)
e = tuple(sorted(e))
if e not in edges:
edges.add(e)
return eg.Hypergraph(num_v, list(edges))
def uniform_hypergraph_configuration_model(k, m, seed=None):
"""
A function to generate an m-uniform configuration model
Parameters
----------
k : dictionary
This is a dictionary where the keys are node ids
and the values are node degrees.
m : int
specifies the hyperedge size
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 degrees are not divisible by m, the
algorithm still runs, but raises a warning and adds an
additional connection to random nodes to satisfy this
condition.
Notes
-----
This algorithm normally creates multi-edges and loopy hyperedges.
We remove the loopy hyperedges.
References
----------
"The effect of heterogeneity on hypergraph contagion models"
by Nicholas W. Landry and Juan G. Restrepo
https://doi.org/10.1063/5.0020034
Example
-------
>>> import easygraph as eg
>>> import random
>>> n = 1000
>>> m = 3
>>> k = {1: 1, 2: 2, 3: 3, 4: 3}
>>> H = eg.uniform_hypergraph_configuration_model(k, m)
"""
if seed is not None:
random.seed(seed)
# Making sure we have the right number of stubs
remainder = sum(k.values()) % m
if remainder != 0:
warnings.warn(
"This degree sequence is not realizable. "
"Increasing the degree of random nodes so that it is."
)
random_ids = random.sample(list(k.keys()), int(round(m - remainder)))
for id in random_ids:
k[id] = k[id] + 1
stubs = []
# Creating the list to index through
for id in k:
stubs.extend([id] * int(k[id]))
H = eg.Hypergraph(num_v=len(k))
while len(stubs) != 0:
u = random.sample(range(len(stubs)), m)
edge = set()
for index in u:
edge.add(stubs[index])
if len(edge) == m:
H.add_hyperedges(list(edge))
for index in sorted(u, reverse=True):
del stubs[index]
return H
def uniform_HSBM(n, m, p, sizes, seed=None):
"""Create a uniform hypergraph stochastic block model (HSBM).
Parameters
----------
n : int
The number of nodes
m : int
The hyperedge size
p : m-dimensional numpy array
tensor of probabilities between communities
sizes : list or 1D numpy array
The sizes of the community blocks in order
seed : integer or None (default)
The seed for the random number generator
Returns
-------
Hypergraph
The constructed SBM hypergraph
Raises
------
EasyGraphError
- If the length of sizes and p do not match.
- If p is not a tensor with every dimension equal
- If p is not m-dimensional
- If the entries of p are not in the range [0, 1]
- If the sum of the vector of sizes does not equal the number of nodes.
Exception
If there is an integer overflow error
See Also
--------
uniform_HPPM
References
----------
Nicholas W. Landry and Juan G. Restrepo.
"Polarization in hypergraphs with community structure."
Preprint, 2023. https://doi.org/10.48550/arXiv.2302.13967
"""
# Check if dimensions match
if len(sizes) != np.size(p, axis=0):
raise EasyGraphError("'sizes' and 'p' do not match.")
if len(np.shape(p)) != m:
raise EasyGraphError("The dimension of p does not match m")
# Check that p has the same length over every dimension.
if len(set(np.shape(p))) != 1:
raise EasyGraphError("'p' must be a square tensor.")
if np.max(p) > 1 or np.min(p) < 0:
raise EasyGraphError("Entries of 'p' not in [0,1].")
if np.sum(sizes) != n:
raise EasyGraphError("Sum of sizes does not match n")
if seed is not None:
np.random.seed(seed)
node_labels = range(n)
H = eg.Hypergraph(num_v=n)
block_range = range(len(sizes))
# Split node labels in a partition (list of sets).
size_cumsum = [sum(sizes[0:x]) for x in range(0, len(sizes) + 1)]
partition = [
list(node_labels[size_cumsum[x] : size_cumsum[x + 1]])
for x in range(0, len(size_cumsum) - 1)
]
for block in itertools.product(block_range, repeat=m):
if p[block] == 1: # Test edges cases p_ij = 0 or 1
edges = itertools.product((partition[i] for i in block_range))
for e in edges:
H.add_hyperedges(list(e))
elif p[block] > 0:
partition_sizes = [len(partition[i]) for i in block]
max_index = reduce(operator.mul, partition_sizes, 1)
if max_index < 0:
raise Exception("Index overflow error!")
index = np.random.geometric(p[block]) - 1
while index < max_index:
indices = _index_to_edge_partition(index, partition_sizes, m)
e = {partition[block[i]][indices[i]] for i in range(m)}
if len(e) == m:
H.add_hyperedges(list(e))
index += np.random.geometric(p[block])
return H
def uniform_HPPM(n, m, rho, k, epsilon, seed=None):
"""Construct the m-uniform hypergraph planted partition model (m-HPPM)
Parameters
----------
n : int > 0
Number of nodes
m : int > 0
Hyperedge size
rho : float between 0 and 1
The fraction of nodes in community 1
k : float > 0
Mean degree
epsilon : float > 0
Imbalance parameter
seed : integer or None (default)
The seed for the random number generator
Returns
-------
Hypergraph
The constructed m-HPPM hypergraph.
Raises
------
EasyGraphError
- If rho is not between 0 and 1
- If the mean degree is negative.
- If epsilon is not between 0 and 1
See Also
--------
uniform_HSBM
References
----------
Nicholas W. Landry and Juan G. Restrepo.
"Polarization in hypergraphs with community structure."
Preprint, 2023. https://doi.org/10.48550/arXiv.2302.13967
"""
if rho < 0 or rho > 1:
raise EasyGraphError("The value of rho must be between 0 and 1")
if k < 0:
raise EasyGraphError("The mean degree must be non-negative")
if epsilon < 0 or epsilon > 1:
raise EasyGraphError("epsilon must be between 0 and 1")
sizes = [int(rho * n), n - int(rho * n)]
p = k / (m * n ** (m - 1))
# ratio of inter- to intra-community edges
q = rho**m + (1 - rho) ** m
r = 1 / q - 1
p_in = (1 + r * epsilon) * p
p_out = (1 - epsilon) * p
p = p_out * np.ones([2] * m)
p[tuple([0] * m)] = p_in
p[tuple([1] * m)] = p_in
return uniform_HSBM(n, m, p, sizes, seed=seed)
def uniform_erdos_renyi_hypergraph(n, m, p, p_type="degree", seed=None):
"""Generate an m-uniform ErdősRényi hypergraph
This creates a hypergraph with `n` nodes where
hyperedges of size `m` are created at random to
obtain a mean degree of `k`.
Parameters
----------
n : int > 0
Number of nodes
m : int > 0
Hyperedge size
p : float or int > 0
Mean expected degree if p_type="degree" and
probability of an m-hyperedge if p_type="prob"
p_type : str
"degree" or "prob", by default "degree"
seed : integer or None (default)
The seed for the random number generator
Returns
-------
Hypergraph
The Erdos Renyi hypergraph
See Also
--------
random_hypergraph
"""
if seed is not None:
np.random.seed(seed)
H = eg.Hypergraph(num_v=n)
if p_type == "degree":
q = p / (m * n ** (m - 1)) # wiring probability
elif p_type == "prob":
q = p
else:
raise EasyGraphError("Invalid p_type!")
if q > 1 or q < 0:
raise EasyGraphError("Probability not in [0,1].")
index = np.random.geometric(q) - 1 # -1 b/c zero indexing
max_index = n**m
while index < max_index:
e = set(_index_to_edge(index, n, m))
if len(e) == m:
H.add_hyperedges(list(e))
index += np.random.geometric(q)
return H
def _index_to_edge(index, n, m):
"""Generate a hyperedge given an index in the list of possible edges.
Parameters
----------
index : int > 0
The index of the hyperedge in the list of all possible hyperedges.
n : int > 0
The number of nodes
m : int > 0
The hyperedge size.
Returns
-------
list
The reconstructed hyperedge
See Also
--------
_index_to_edge_partition
References
----------
https://stackoverflow.com/questions/53834707/element-at-index-in-itertools-product
"""
return [(index // (n**r) % n) for r in range(m - 1, -1, -1)]
def _index_to_edge_partition(index, partition_sizes, m):
"""Generate a hyperedge given an index in the list of possible edges
and a partition of community labels.
Parameters
----------
index : int > 0
The index of the hyperedge in the list of all possible hyperedges.
n : int > 0
The number of nodes
m : int > 0
The hyperedge size.
Returns
-------
list
The reconstructed hyperedge
See Also
--------
_index_to_edge
"""
try:
return [
int(index // np.prod(partition_sizes[r + 1 :]) % partition_sizes[r])
for r in range(m)
]
except KeyError:
raise Exception("Invalid parameters")