chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from .assortativity import *
|
||||
from .centrality import *
|
||||
from .hypergraph_clustering import *
|
||||
from .hypergraph_operation import *
|
||||
from .null_model import *
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Algorithms for finding the degree assortativity of a hypergraph."""
|
||||
|
||||
import random
|
||||
|
||||
from itertools import combinations
|
||||
|
||||
import numpy
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = ["dynamical_assortativity", "degree_assortativity"]
|
||||
|
||||
|
||||
def dynamical_assortativity(H):
|
||||
"""Computes the dynamical assortativity of a uniform hypergraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : eg.Hypergraph
|
||||
Hypergraph of interest
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The dynamical assortativity
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_assortativity
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If the hypergraph is not uniform, or if there are no nodes
|
||||
or no edges
|
||||
|
||||
References
|
||||
----------
|
||||
Nicholas Landry and Juan G. Restrepo,
|
||||
Hypergraph assortativity: A dynamical systems perspective,
|
||||
Chaos 2022.
|
||||
DOI: 10.1063/5.0086905
|
||||
|
||||
"""
|
||||
if len(H.v) == 0:
|
||||
raise EasyGraphError("Hypergraph must contain nodes")
|
||||
elif len(H.e[0]) == 0:
|
||||
raise EasyGraphError("Hypergraph must contain edges!")
|
||||
|
||||
if not H.is_uniform():
|
||||
raise EasyGraphError("Hypergraph must be uniform!")
|
||||
|
||||
if 1 in H.unique_edge_sizes():
|
||||
raise EasyGraphError("No singleton edges!")
|
||||
|
||||
degs = H.deg_v
|
||||
k1 = sum(degs) / len(degs)
|
||||
k2 = np.mean(numpy.array(degs) ** 2)
|
||||
kk1 = np.mean(
|
||||
[degs[n1] * degs[n2] for e in H.e[0] for n1, n2 in combinations(e, 2)]
|
||||
)
|
||||
|
||||
return kk1 * k1**2 / k2**2 - 1
|
||||
|
||||
|
||||
def degree_assortativity(H, kind="uniform", exact=False, num_samples=1000):
|
||||
"""Computes the degree assortativity of a hypergraph
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : Hypergraph
|
||||
The hypergraph of interest
|
||||
kind : str, optional
|
||||
the type of degree assortativity. valid choices are
|
||||
"uniform", "top-2", and "top-bottom". By default, "uniform".
|
||||
exact : bool, optional
|
||||
whether to compute over all edges or sample randomly from the
|
||||
set of edges. By default, False.
|
||||
num_samples : int, optional
|
||||
if not exact, specify the number of samples for the computation.
|
||||
By default, 1000.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
the degree assortativity
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If there are no nodes or no edges
|
||||
|
||||
See Also
|
||||
--------
|
||||
dynamical_assortativity
|
||||
|
||||
References
|
||||
----------
|
||||
Phil Chodrow,
|
||||
Configuration models of random hypergraphs,
|
||||
Journal of Complex Networks 2020.
|
||||
DOI: 10.1093/comnet/cnaa018
|
||||
"""
|
||||
|
||||
if len(H.v) == 0:
|
||||
raise EasyGraphError("Hypergraph must contain nodes")
|
||||
elif len(H.e[0]) == 0:
|
||||
raise EasyGraphError("Hypergraph must contain edges!")
|
||||
|
||||
degs = H.deg_v
|
||||
if exact:
|
||||
k1k2 = [_choose_degrees(e, degs, kind) for e in H.e[0] if len(e) > 1]
|
||||
else:
|
||||
edges = [e for e in H.e[0] if len(e) > 1]
|
||||
k1k2 = [
|
||||
_choose_degrees(random.choice(H.e[0]), degs, kind)
|
||||
for _ in range(num_samples)
|
||||
]
|
||||
|
||||
rho = np.corrcoef(np.array(k1k2).T)[0, 1]
|
||||
if np.isnan(rho):
|
||||
return 0
|
||||
return rho
|
||||
|
||||
|
||||
def _choose_degrees(e, k, kind="uniform"):
|
||||
"""Choose the degrees of two nodes in a hyperedge.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
e : iterable
|
||||
the members in a hyperedge
|
||||
k : dict
|
||||
the degrees where keys are node IDs and values are degrees
|
||||
kind : str, optional
|
||||
the type of degree assortativity, options are "uniform", "top-2",
|
||||
and "top-bottom". By default, "uniform".
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple
|
||||
two degrees selected from the edge
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
if invalid assortativity function chosen
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_assortativity
|
||||
|
||||
References
|
||||
----------
|
||||
Phil Chodrow,
|
||||
Configuration models of random hypergraphs,
|
||||
Journal of Complex Networks 2020.
|
||||
DOI: 10.1093/comnet/cnaa018
|
||||
"""
|
||||
e = list(e)
|
||||
if len(e) > 1:
|
||||
if kind == "uniform":
|
||||
i = np.random.randint(len(e))
|
||||
j = i
|
||||
while i == j:
|
||||
j = np.random.randint(len(e))
|
||||
return (k[e[i]], k[e[j]])
|
||||
|
||||
elif kind == "top-2":
|
||||
degs = sorted([k[i] for i in e])[-2:]
|
||||
random.shuffle(degs)
|
||||
return degs
|
||||
|
||||
elif kind == "top-bottom":
|
||||
# this selects the largest and smallest degrees in one line
|
||||
degs = sorted([k[i] for i in e])[:: len(e) - 1]
|
||||
random.shuffle(degs)
|
||||
return degs
|
||||
|
||||
else:
|
||||
raise EasyGraphError("Invalid choice function!")
|
||||
else:
|
||||
raise EasyGraphError("Edge must have more than one member!")
|
||||
@@ -0,0 +1,5 @@
|
||||
from .cycle_ratio import *
|
||||
from .degree import *
|
||||
from .hypercoreness import *
|
||||
from .s_centrality import *
|
||||
from .vector_centrality import *
|
||||
@@ -0,0 +1,193 @@
|
||||
import copy
|
||||
import itertools
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
__all__ = [
|
||||
"my_all_shortest_paths",
|
||||
"getandJudgeSimpleCircle",
|
||||
"getSmallestCycles",
|
||||
"StatisticsAndCalculateIndicators",
|
||||
"cycle_ratio_centrality",
|
||||
]
|
||||
|
||||
|
||||
def my_all_shortest_paths(G, source, target):
|
||||
pred = eg.predecessor(G, source)
|
||||
if target not in pred:
|
||||
raise eg.EasyGraphNoPath(
|
||||
f"Target {target} cannot be reached from given sources"
|
||||
)
|
||||
sources = {source}
|
||||
seen = {target}
|
||||
stack = [[target, 0]]
|
||||
top = 0
|
||||
while top >= 0:
|
||||
node, i = stack[top]
|
||||
if node in sources:
|
||||
yield [p for p, n in reversed(stack[: top + 1])]
|
||||
if len(pred[node]) > i:
|
||||
stack[top][1] = i + 1
|
||||
next = pred[node][i]
|
||||
if next in seen:
|
||||
continue
|
||||
else:
|
||||
seen.add(next)
|
||||
top += 1
|
||||
if top == len(stack):
|
||||
stack.append([next, 0])
|
||||
else:
|
||||
stack[top][:] = [next, 0]
|
||||
else:
|
||||
seen.discard(node)
|
||||
top -= 1
|
||||
|
||||
|
||||
def getandJudgeSimpleCircle(objectList, G): # 这里添加 G 作为参数
|
||||
numEdge = 0
|
||||
for eleArr in list(itertools.combinations(objectList, 2)):
|
||||
if G.has_edge(eleArr[0], eleArr[1]):
|
||||
numEdge += 1
|
||||
if numEdge != len(objectList):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def getSmallestCycles(G, NodeGirth, Coreness, DEF_IMPOSSLEN):
|
||||
NodeList = list(G.nodes)
|
||||
NodeList.sort()
|
||||
# setp 1
|
||||
curCyc = list()
|
||||
for ix in NodeList[:-2]: # v1
|
||||
if NodeGirth[ix] == 0:
|
||||
continue
|
||||
curCyc.append(ix)
|
||||
for jx in NodeList[NodeList.index(ix) + 1 : -1]: # v2
|
||||
if NodeGirth[jx] == 0:
|
||||
continue
|
||||
curCyc.append(jx)
|
||||
if G.has_edge(ix, jx):
|
||||
for kx in NodeList[NodeList.index(jx) + 1 :]: # v3
|
||||
if NodeGirth[kx] == 0:
|
||||
continue
|
||||
if G.has_edge(kx, ix):
|
||||
curCyc.append(kx)
|
||||
if G.has_edge(kx, jx):
|
||||
yield tuple(curCyc) # 这里改为 yield
|
||||
for i in curCyc:
|
||||
NodeGirth[i] = 3
|
||||
curCyc.pop()
|
||||
curCyc.pop()
|
||||
curCyc.pop()
|
||||
|
||||
# setp 2
|
||||
ResiNodeList = [] # Residual Node List
|
||||
for nod in NodeList:
|
||||
if NodeGirth[nod] == DEF_IMPOSSLEN:
|
||||
ResiNodeList.append(nod)
|
||||
if len(ResiNodeList) == 0:
|
||||
return
|
||||
else:
|
||||
visitedNodes = dict.fromkeys(ResiNodeList, set())
|
||||
for nod in ResiNodeList:
|
||||
if Coreness[nod] == 2 and NodeGirth[nod] < DEF_IMPOSSLEN:
|
||||
continue
|
||||
for nei in list(G.neighbors(nod)):
|
||||
if Coreness[nei] == 2 and NodeGirth[nei] < DEF_IMPOSSLEN:
|
||||
continue
|
||||
if not nei in visitedNodes.keys() or not nod in visitedNodes[nei]:
|
||||
visitedNodes[nod].add(nei)
|
||||
if nei not in visitedNodes.keys():
|
||||
visitedNodes[nei] = set([nod])
|
||||
else:
|
||||
visitedNodes[nei].add(nod)
|
||||
if Coreness[nei] == 2 and NodeGirth[nei] < DEF_IMPOSSLEN:
|
||||
continue
|
||||
G.remove_edge(nod, nei)
|
||||
if eg.single_source_dijkstra(G, nod, nei):
|
||||
for path in my_all_shortest_paths(G, nod, nei):
|
||||
lenPath = len(path)
|
||||
path.sort()
|
||||
yield tuple(path) # 这里改为 yield
|
||||
for i in path:
|
||||
if NodeGirth[i] > lenPath:
|
||||
NodeGirth[i] = lenPath
|
||||
G.add_edge(nod, nei)
|
||||
|
||||
|
||||
def StatisticsAndCalculateIndicators(SmallestCyclesOfNodes, CycLenDict, SmallestCycles):
|
||||
NumSmallCycles = len(SmallestCycles)
|
||||
for cyc in SmallestCycles:
|
||||
lenCyc = len(cyc)
|
||||
CycLenDict[lenCyc] += 1
|
||||
for nod in cyc:
|
||||
SmallestCyclesOfNodes[nod].add(cyc)
|
||||
CycleRatio = {} # 这里将 CycleRatio 作为局部变量
|
||||
for objNode, SmaCycs in SmallestCyclesOfNodes.items():
|
||||
if len(SmaCycs) == 0:
|
||||
continue
|
||||
cycleNeighbors = set()
|
||||
NeiOccurTimes = {}
|
||||
for cyc in SmaCycs:
|
||||
for n in cyc:
|
||||
if n in NeiOccurTimes.keys():
|
||||
NeiOccurTimes[n] += 1
|
||||
else:
|
||||
NeiOccurTimes[n] = 1
|
||||
cycleNeighbors = cycleNeighbors.union(cyc)
|
||||
cycleNeighbors.remove(objNode)
|
||||
del NeiOccurTimes[objNode]
|
||||
sum = 0
|
||||
for nei in cycleNeighbors:
|
||||
sum += float(NeiOccurTimes[nei]) / len(SmallestCyclesOfNodes[nei])
|
||||
CycleRatio[objNode] = sum + 1
|
||||
return CycleRatio
|
||||
|
||||
|
||||
def cycle_ratio_centrality(G):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
G : eg.Graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
cycle ratio centrality of each node in G : dict
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_edges([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4), (1, 5), (2, 5)])
|
||||
>>> cycle_ratio_centrality(G)
|
||||
{1: 4.083333333333333, 2: 4.083333333333333, 3: 2.6666666666666665, 4: 2.6666666666666665, 5: 1.5}
|
||||
|
||||
"""
|
||||
NumNode = G.number_of_nodes() # update
|
||||
DEF_IMPOSSLEN = NumNode + 1 # Impossible simple cycle length
|
||||
NodeGirth = dict()
|
||||
CycLenDict = dict()
|
||||
|
||||
SmallestCyclesOfNodes = {}
|
||||
removeNodes = set()
|
||||
Coreness = dict(zip(list(G.nodes), eg.k_core(G)))
|
||||
for i in list(G.nodes):
|
||||
SmallestCyclesOfNodes[i] = set()
|
||||
if G.degree()[i] <= 1 or Coreness[i] <= 1:
|
||||
NodeGirth[i] = 0
|
||||
removeNodes.add(i)
|
||||
else:
|
||||
NodeGirth[i] = DEF_IMPOSSLEN
|
||||
|
||||
G.remove_nodes_from(removeNodes)
|
||||
|
||||
NodeNum = G.number_of_nodes()
|
||||
for i in range(3, NodeNum + 2):
|
||||
CycLenDict[i] = 0
|
||||
|
||||
SmallestCycles = set(getSmallestCycles(G, NodeGirth, Coreness, DEF_IMPOSSLEN))
|
||||
cycle_ratio = StatisticsAndCalculateIndicators(
|
||||
SmallestCyclesOfNodes, CycLenDict, SmallestCycles
|
||||
)
|
||||
return cycle_ratio
|
||||
@@ -0,0 +1,28 @@
|
||||
__all__ = ["hyepergraph_degree_centrality"]
|
||||
|
||||
|
||||
def hyepergraph_degree_centrality(G):
|
||||
"""
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : eg.Hypergraph
|
||||
The target hypergraph
|
||||
|
||||
Returns
|
||||
----------
|
||||
degree centrality of each node in G : dict
|
||||
|
||||
"""
|
||||
res = {}
|
||||
node_list = G.v
|
||||
# Get hyperedge list
|
||||
edge_list = G.e[0]
|
||||
for node in node_list:
|
||||
res[node] = 0
|
||||
|
||||
for e in edge_list:
|
||||
for n in e:
|
||||
res[n] += 1
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,351 @@
|
||||
from itertools import compress
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
|
||||
__all__ = ["size_independent_hypercoreness", "frequency_based_hypercoreness"]
|
||||
|
||||
|
||||
def size_independent_hypercoreness(h):
|
||||
"""The size_independent_hypercoreness of nodes in hypergraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
h : eg.Hypergraph.
|
||||
|
||||
|
||||
Returns
|
||||
----------
|
||||
dict
|
||||
Centrality, where keys are node IDs and values are lists of centralities.
|
||||
|
||||
References
|
||||
----------
|
||||
Mancastroppa, M., Iacopini, I., Petri, G. et al. Hyper-cores promote localization and efficient seeding in higher-order processes. Nat Commun 14, 6223 (2023). https://doi.org/10.1038/s41467-023-41887-2.
|
||||
|
||||
"""
|
||||
e_list = h.e[0]
|
||||
initial_node_num = h.num_v
|
||||
data = [e_list[i] for i in range(len(e_list)) if len(e_list[i]) > 1]
|
||||
|
||||
data.sort(key=len)
|
||||
L = len(data)
|
||||
size_max = len(data[L - 1])
|
||||
|
||||
size = list([len(data[j]) for j in range(L)])
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=data)
|
||||
IDX = list(range(0, X.num_v))
|
||||
|
||||
M = range(2, size_max + 1)
|
||||
k_step = 1
|
||||
K = range(1, 1200, k_step)
|
||||
k_shell_dict = {}
|
||||
idx_orig = IDX
|
||||
|
||||
IDX_size = range(len(size))
|
||||
k_max = np.zeros(len(M))
|
||||
|
||||
for j in idx_orig:
|
||||
k_shell_dict[j] = np.zeros(len(M))
|
||||
|
||||
for x in range(len(M)):
|
||||
m = M[x]
|
||||
|
||||
D = np.zeros(len(K))
|
||||
|
||||
# consider only hyperedges of size >=m
|
||||
idx_size = list(
|
||||
compress(IDX_size, np.greater_equal(size, m * np.ones(len(size))))
|
||||
)
|
||||
int_sel = list([data[i] for i in idx_size])
|
||||
# build hypergraph with only interactions of size >=m
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=int_sel)
|
||||
node_set = set()
|
||||
for sublist in int_sel:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
# IDX_e = list(X.e[0])
|
||||
|
||||
for y in range(len(K)):
|
||||
kk = K[y]
|
||||
|
||||
d_tot_m = np.zeros(len(IDX))
|
||||
prev_shell = IDX
|
||||
|
||||
for i in range(len(IDX)):
|
||||
d_tot_m[i] = X.degree_node[IDX[i]]
|
||||
|
||||
idx_n_remove = list(
|
||||
compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m))
|
||||
) # nodes with degree<k are removed
|
||||
# X.remove_nodes_from(idx_n_remove)
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for e in now_e_list:
|
||||
new_e = []
|
||||
for n in e:
|
||||
if n not in idx_n_remove:
|
||||
new_e.append(n)
|
||||
if len(new_e) > 0:
|
||||
new_e_list.append(new_e)
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
IDX_e = list(range(0, len(X.e[0])))
|
||||
|
||||
sizes = [
|
||||
len(X.e[0][i]) for i in IDX_e
|
||||
] # hyperedges with size <m are removed
|
||||
idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m]
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for i in range(len(now_e_list)):
|
||||
if i not in idx_e_remove:
|
||||
new_e_list.append(now_e_list[i])
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
while len(idx_n_remove) > 0 or len(idx_e_remove) > 0:
|
||||
d_tot_m = np.zeros(len(IDX))
|
||||
|
||||
for i in range(len(IDX)):
|
||||
d_tot_m[i] = X.degree_node[IDX[i]]
|
||||
|
||||
idx_n_remove = list(
|
||||
compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m))
|
||||
) # nodes with degree<k are removed
|
||||
# X.remove_nodes_from(idx_n_remove)
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for e in now_e_list:
|
||||
new_e = []
|
||||
for n in e:
|
||||
if n not in idx_n_remove:
|
||||
new_e.append(n)
|
||||
if len(new_e) > 0:
|
||||
new_e_list.append(new_e)
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
IDX_e = list(range(len(X.e[0])))
|
||||
sizes = [
|
||||
len(X.e[0][i]) for i in IDX_e
|
||||
] # hyperedges with size <m are removed
|
||||
|
||||
idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m]
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for i in range(len(now_e_list)):
|
||||
if i not in idx_e_remove:
|
||||
new_e_list.append(now_e_list[i])
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
shell_kk = list(sorted(set(prev_shell) - set(IDX)))
|
||||
for j in shell_kk:
|
||||
# if j not in idx_n_remove:
|
||||
# continue
|
||||
k_shell_dict[j][x] = kk - k_step
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
D[y] = len(node_set)
|
||||
if y > 0:
|
||||
if D[y] == 0 and D[y - 1] != 0:
|
||||
# maximum connectivity at order m
|
||||
k_max[x] = kk - k_step
|
||||
# stop the decomposition when the (k,m)-core is empty
|
||||
if D[y] == 0:
|
||||
break
|
||||
|
||||
# size-independent hypercoreness
|
||||
R_dict = {}
|
||||
for y in k_shell_dict:
|
||||
R_dict[y] = sum(np.array(k_shell_dict[y]) / np.array(k_max))
|
||||
|
||||
return R_dict
|
||||
|
||||
|
||||
def frequency_based_hypercoreness(h):
|
||||
r"""The frequency-based hypercoreness of nodes in hypergraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
h : easygraph.Hypergraph
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict : Centrality, where keys are node IDs and values are lists of centralities.
|
||||
|
||||
References
|
||||
----------
|
||||
Mancastroppa, M., Iacopini, I., Petri, G. et al. Hyper-cores promote localization and efficient seeding in higher-order processes. Nat Commun 14, 6223 (2023). https://doi.org/10.1038/s41467-023-41887-2
|
||||
|
||||
"""
|
||||
e_list = h.e[0]
|
||||
initial_node_num = h.num_v
|
||||
data = [e_list[i] for i in range(len(e_list)) if len(e_list[i]) > 1]
|
||||
|
||||
data.sort(key=len)
|
||||
L = len(data)
|
||||
size_max = len(data[L - 1])
|
||||
|
||||
size = list([len(data[j]) for j in range(L)])
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=data)
|
||||
IDX = list(range(0, X.num_v))
|
||||
|
||||
M = range(2, size_max + 1)
|
||||
k_step = 1
|
||||
K = range(1, 1200, k_step)
|
||||
k_shell_dict = {}
|
||||
idx_orig = IDX
|
||||
|
||||
IDX_size = range(len(size))
|
||||
k_max = np.zeros(len(M))
|
||||
|
||||
for j in idx_orig:
|
||||
k_shell_dict[j] = np.zeros(len(M))
|
||||
|
||||
for x in range(len(M)):
|
||||
m = M[x]
|
||||
|
||||
D = np.zeros(len(K))
|
||||
# consider only hyperedges of size >=m
|
||||
idx_size = list(
|
||||
compress(IDX_size, np.greater_equal(size, m * np.ones(len(size))))
|
||||
)
|
||||
int_sel = list([data[i] for i in idx_size])
|
||||
# build hypergraph with only interactions of size >=m
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=int_sel)
|
||||
node_set = set()
|
||||
for sublist in int_sel:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
for y in range(len(K)):
|
||||
kk = K[y]
|
||||
|
||||
d_tot_m = np.zeros(len(IDX))
|
||||
prev_shell = IDX
|
||||
|
||||
for i in range(len(IDX)):
|
||||
d_tot_m[i] = X.degree_node[IDX[i]]
|
||||
|
||||
idx_n_remove = list(
|
||||
compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m))
|
||||
) # nodes with degree<k are removed
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for e in now_e_list:
|
||||
new_e = []
|
||||
for n in e:
|
||||
if n not in idx_n_remove:
|
||||
new_e.append(n)
|
||||
if len(new_e) > 0:
|
||||
new_e_list.append(new_e)
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
IDX_e = list(range(0, len(X.e[0])))
|
||||
|
||||
# hyperedges with size <m are removed
|
||||
sizes = [len(X.e[0][i]) for i in IDX_e]
|
||||
idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m]
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for i in range(len(now_e_list)):
|
||||
if i not in idx_e_remove:
|
||||
new_e_list.append(now_e_list[i])
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
while len(idx_n_remove) > 0 or len(idx_e_remove) > 0:
|
||||
d_tot_m = np.zeros(len(IDX))
|
||||
|
||||
for i in range(len(IDX)):
|
||||
d_tot_m[i] = X.degree_node[IDX[i]]
|
||||
# nodes with degree<k are removed
|
||||
idx_n_remove = list(
|
||||
compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m))
|
||||
)
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for e in now_e_list:
|
||||
new_e = []
|
||||
for n in e:
|
||||
if n not in idx_n_remove:
|
||||
new_e.append(n)
|
||||
if len(new_e) > 0:
|
||||
new_e_list.append(new_e)
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
IDX_e = list(range(len(X.e[0])))
|
||||
# hyperedges with size <m are removed
|
||||
sizes = [len(X.e[0][i]) for i in IDX_e]
|
||||
|
||||
idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m]
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for i in range(len(now_e_list)):
|
||||
if i not in idx_e_remove:
|
||||
new_e_list.append(now_e_list[i])
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
shell_kk = list(sorted(set(prev_shell) - set(IDX)))
|
||||
for j in shell_kk:
|
||||
k_shell_dict[j][x] = kk - k_step
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
D[y] = len(node_set)
|
||||
if y > 0:
|
||||
if D[y] == 0 and D[y - 1] != 0:
|
||||
k_max[x] = kk - k_step # maximum connectivity at order m
|
||||
if D[y] == 0:
|
||||
break # stop the decomposition when the (k,m)-core is empty
|
||||
|
||||
# Psi(m) distribution of hyperedges size
|
||||
Psi = []
|
||||
for m in range(2, size_max + 1):
|
||||
Psi.append(size.count(m) / len(size))
|
||||
# frequency-based hypercoreness
|
||||
R_w_dict = {}
|
||||
for y in k_shell_dict:
|
||||
R_w_dict[y] = sum(np.array(Psi) * np.array(k_shell_dict[y]) / np.array(k_max))
|
||||
return R_w_dict
|
||||
@@ -0,0 +1,89 @@
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
__all__ = ["s_betweenness", "s_closeness", "s_eccentricity"]
|
||||
|
||||
|
||||
def s_betweenness(H, s=1, weight=False, n_workers=None):
|
||||
"""Computes the betweenness centrality for each edge in the hypergraph.
|
||||
|
||||
Computes the betweenness centrality for each edge in the hypergraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : eg.Hypergraph.
|
||||
The hypergraph to compute
|
||||
|
||||
s : int, optional.
|
||||
|
||||
Returns
|
||||
----------
|
||||
dict
|
||||
The keys are the edges and the values are the betweenness centrality.
|
||||
The betweenness centrality for each edge in the hypergraph.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
linegraph = H.get_linegraph(s=s, weight=weight)
|
||||
results = eg.betweenness_centrality(linegraph, n_workers=n_workers)
|
||||
return results
|
||||
|
||||
|
||||
def s_closeness(H, s=1, weight=False, n_workers=None):
|
||||
"""
|
||||
Compute the closeness centrality for each edge in the hypergraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : eg.Hypergraph.
|
||||
s : int, optional
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict. The closeness centrality for each edge in the hypergraph. The keys are the edges and the values are the closeness centrality.
|
||||
"""
|
||||
linegraph = H.get_linegraph(s=s, weight=weight)
|
||||
results = eg.closeness_centrality(linegraph, n_workers=n_workers)
|
||||
return results
|
||||
|
||||
|
||||
def s_eccentricity(H, s=1, edges=True, source=None):
|
||||
r"""
|
||||
The length of the longest shortest path from a vertex $u$ to every other vertex in
|
||||
the s-linegraph.
|
||||
$V$ = set of vertices in the s-linegraph
|
||||
$d$ = shortest path distance
|
||||
|
||||
.. math::
|
||||
|
||||
\text{s-ecc}(u) = \text{max}\{d(u,v): v \in V\}
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : eg.Hypergraph
|
||||
|
||||
s : int, optional
|
||||
|
||||
edges : bool, optional
|
||||
Indicates if method should compute edge linegraph (default) or node linegraph.
|
||||
|
||||
source : str, optional
|
||||
Identifier of node or edge of interest for computing centrality
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict or float
|
||||
returns the s-eccentricity value of the edges(nodes).
|
||||
If source=None a dictionary of values for each s-edge in H is returned.
|
||||
If source then a single value is returned.
|
||||
If the s-linegraph is disconnected, np.inf is returned.
|
||||
|
||||
"""
|
||||
|
||||
g = H.get_linegraph(s=s)
|
||||
result = eg.eccentricity(g)
|
||||
if source:
|
||||
return result[source]
|
||||
else:
|
||||
return result
|
||||
@@ -0,0 +1,72 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestCycleRatioCentrality(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G_triangle = eg.Graph()
|
||||
self.G_triangle.add_edges([(1, 2), (2, 3), (3, 1)])
|
||||
|
||||
self.G_star = eg.Graph()
|
||||
self.G_star.add_edges([(1, 2), (1, 3), (1, 4)])
|
||||
|
||||
self.G_complete = eg.complete_graph(4)
|
||||
|
||||
self.G_disconnected = eg.Graph()
|
||||
self.G_disconnected.add_edges([(1, 2), (3, 4)])
|
||||
|
||||
def test_triangle_graph(self):
|
||||
result = eg.cycle_ratio_centrality(self.G_triangle.copy())
|
||||
self.assertTrue(all(isinstance(v, float) for v in result.values()))
|
||||
self.assertEqual(len(result), 3)
|
||||
|
||||
def test_star_graph(self):
|
||||
result = eg.cycle_ratio_centrality(self.G_star.copy())
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_complete_graph(self):
|
||||
result = eg.cycle_ratio_centrality(self.G_complete.copy())
|
||||
self.assertEqual(len(result), 4)
|
||||
self.assertTrue(all(v > 0 for v in result.values()))
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
result = eg.cycle_ratio_centrality(self.G_disconnected.copy())
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_my_all_shortest_paths_valid(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges([(1, 2), (2, 3), (3, 4)])
|
||||
paths = list(eg.my_all_shortest_paths(G, 1, 4))
|
||||
self.assertIn([1, 2, 3, 4], paths)
|
||||
|
||||
def test_my_all_shortest_paths_invalid(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges([(1, 2), (3, 4)])
|
||||
with self.assertRaises(eg.EasyGraphNoPath):
|
||||
list(eg.my_all_shortest_paths(G, 1, 4))
|
||||
|
||||
def test_getandJudgeSimpleCircle_true(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges([(1, 2), (2, 3), (3, 1)])
|
||||
self.assertTrue(eg.getandJudgeSimpleCircle([1, 2, 3], G))
|
||||
|
||||
def test_getandJudgeSimpleCircle_false(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges([(1, 2), (2, 3)])
|
||||
self.assertFalse(eg.getandJudgeSimpleCircle([1, 2, 3], G))
|
||||
|
||||
def test_statistics_and_calculate_indicators(self):
|
||||
SmallestCyclesOfNodes = {1: set(), 2: set(), 3: set()}
|
||||
CycLenDict = {3: 0}
|
||||
SmallestCycles = {(1, 2, 3)}
|
||||
result = eg.StatisticsAndCalculateIndicators(
|
||||
SmallestCyclesOfNodes, CycLenDict, SmallestCycles
|
||||
)
|
||||
self.assertTrue(isinstance(result, dict))
|
||||
self.assertIn(1, result)
|
||||
self.assertGreater(result[1], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,38 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestHypergraphDegreeCentrality(unittest.TestCase):
|
||||
def test_basic_degree_centrality(self):
|
||||
hg = eg.Hypergraph(num_v=4, e_list=[(0, 1), (1, 2), (2, 3), (0, 2)])
|
||||
result = eg.hyepergraph_degree_centrality(hg)
|
||||
expected = {0: 2, 1: 2, 2: 3, 3: 1}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_empty_hypergraph(self):
|
||||
hg = eg.Hypergraph(num_v=1, e_list=[])
|
||||
result = eg.hyepergraph_degree_centrality(hg)
|
||||
self.assertEqual(result, {0: 0})
|
||||
|
||||
def test_single_edge(self):
|
||||
hg = eg.Hypergraph(num_v=3, e_list=[(0, 1, 2)])
|
||||
result = eg.hyepergraph_degree_centrality(hg)
|
||||
expected = {0: 1, 1: 1, 2: 1}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_singleton_nodes(self):
|
||||
hg = eg.Hypergraph(num_v=3, e_list=[(0,), (1,), (2,)])
|
||||
result = eg.hyepergraph_degree_centrality(hg)
|
||||
expected = {0: 1, 1: 1, 2: 1}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_node_with_no_edges(self):
|
||||
hg = eg.Hypergraph(num_v=4, e_list=[(0, 1), (1, 2)])
|
||||
result = eg.hyepergraph_degree_centrality(hg)
|
||||
expected = {0: 1, 1: 2, 2: 1, 3: 0} # node 3 has no edges
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,51 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestHypercoreness(unittest.TestCase):
|
||||
def test_simple_hypergraph(self):
|
||||
hg = eg.Hypergraph(num_v=5, e_list=[(0, 1), (1, 2, 3), (3, 4)])
|
||||
si = eg.size_independent_hypercoreness(hg)
|
||||
fb = eg.frequency_based_hypercoreness(hg)
|
||||
|
||||
self.assertIsInstance(si, dict)
|
||||
self.assertIsInstance(fb, dict)
|
||||
self.assertTrue(set(si.keys()).issubset(set(hg.v)))
|
||||
self.assertTrue(set(fb.keys()).issubset(set(hg.v)))
|
||||
|
||||
for val in si.values():
|
||||
self.assertIsInstance(val, float)
|
||||
self.assertGreaterEqual(val, 0)
|
||||
|
||||
for val in fb.values():
|
||||
self.assertIsInstance(val, float)
|
||||
self.assertGreaterEqual(val, 0)
|
||||
|
||||
def test_single_hyperedge(self):
|
||||
hg = eg.Hypergraph(num_v=3, e_list=[(0, 1, 2)])
|
||||
si = eg.size_independent_hypercoreness(hg)
|
||||
fb = eg.frequency_based_hypercoreness(hg)
|
||||
|
||||
self.assertTrue(all(v >= 0 for v in si.values()))
|
||||
self.assertTrue(all(v >= 0 for v in fb.values()))
|
||||
|
||||
def test_large_uniform_hypergraph(self):
|
||||
hg = eg.Hypergraph(num_v=10, e_list=[(i, i + 1, i + 2) for i in range(7)])
|
||||
si = eg.size_independent_hypercoreness(hg)
|
||||
fb = eg.frequency_based_hypercoreness(hg)
|
||||
|
||||
self.assertEqual(len(si), 10)
|
||||
self.assertEqual(len(fb), 10)
|
||||
|
||||
def test_empty_hypergraph_raises(self):
|
||||
hg = eg.Hypergraph(num_v=1, e_list=[])
|
||||
with self.assertRaises(IndexError):
|
||||
eg.size_independent_hypercoreness(hg)
|
||||
|
||||
with self.assertRaises(IndexError):
|
||||
eg.frequency_based_hypercoreness(hg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,40 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
|
||||
class TestHypergraphSCentrality(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Simple test hypergraph
|
||||
self.hg = eg.Hypergraph(num_v=5, e_list=[(0, 1), (1, 2, 3), (3, 4)])
|
||||
self.empty_hg = eg.Hypergraph(num_v=1, e_list=[])
|
||||
self.singleton_hg = eg.Hypergraph(num_v=3, e_list=[(0,), (1,), (2,)])
|
||||
|
||||
def test_s_betweenness_normal(self):
|
||||
result = eg.s_betweenness(self.hg)
|
||||
self.assertIsInstance(result, (list, dict))
|
||||
self.assertTrue(all(isinstance(x, (int, float)) for x in result))
|
||||
|
||||
def test_s_closeness_normal(self):
|
||||
result = eg.s_closeness(self.hg)
|
||||
self.assertIsInstance(result, (list, dict))
|
||||
self.assertTrue(all(isinstance(x, (int, float)) for x in result))
|
||||
|
||||
def test_s_eccentricity_all(self):
|
||||
result = eg.s_eccentricity(self.hg)
|
||||
self.assertIsInstance(result, dict)
|
||||
for v in result.values():
|
||||
self.assertIsInstance(v, (int, float, np.integer, np.floating))
|
||||
|
||||
def test_s_eccentricity_edges_false(self):
|
||||
result = eg.s_eccentricity(self.hg, edges=False)
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
def test_s_eccentricity_invalid_source(self):
|
||||
with self.assertRaises(KeyError):
|
||||
eg.s_eccentricity(self.hg, source=(999, 888))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,43 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
from easygraph.exception import EasyGraphError
|
||||
|
||||
|
||||
class TestVectorCentrality(unittest.TestCase):
|
||||
def test_single_edge(self):
|
||||
hg = eg.Hypergraph(num_v=3, e_list=[(0, 1, 2)])
|
||||
result = eg.vector_centrality(hg)
|
||||
self.assertEqual(set(result.keys()), {0, 1, 2})
|
||||
for val in result.values():
|
||||
self.assertEqual(len(val), 2) # because D = 3 → k = 2 and 3
|
||||
|
||||
def test_multiple_edges_different_orders(self):
|
||||
hg = eg.Hypergraph(num_v=4, e_list=[(0, 1), (1, 2, 3)])
|
||||
result = eg.vector_centrality(hg)
|
||||
self.assertEqual(set(result.keys()), {0, 1, 2, 3})
|
||||
for val in result.values():
|
||||
self.assertEqual(len(val), 2)
|
||||
self.assertTrue(all(isinstance(x, (float, np.floating)) for x in val))
|
||||
|
||||
def test_disconnected_hypergraph_raises(self):
|
||||
hg = eg.Hypergraph(num_v=6, e_list=[(0, 1), (2, 3)])
|
||||
with self.assertRaises(EasyGraphError):
|
||||
eg.vector_centrality(hg)
|
||||
|
||||
def test_non_consecutive_node_ids(self):
|
||||
hg = eg.Hypergraph(num_v=5, e_list=[(0, 2, 4)])
|
||||
result = eg.vector_centrality(hg)
|
||||
self.assertEqual(len(result), 5)
|
||||
for val in result.values():
|
||||
self.assertEqual(len(val), 2)
|
||||
|
||||
def test_index_error_due_to_wrong_num_v(self):
|
||||
with self.assertRaises(eg.EasyGraphError):
|
||||
eg.Hypergraph(num_v=3, e_list=[(0, 1, 5)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,91 @@
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
from easygraph.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = ["vector_centrality"]
|
||||
|
||||
|
||||
def vector_centrality(H):
|
||||
"""The vector centrality of nodes in the line graph of the hypergraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : eg.Hypergraph
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Centrality, where keys are node IDs and values are lists of centralities.
|
||||
|
||||
References
|
||||
----------
|
||||
"Vector centrality in hypergraphs", K. Kovalenko, M. Romance, E. Vasilyeva,
|
||||
D. Aleja, R. Criado, D. Musatov, A.M. Raigorodskii, J. Flores, I. Samoylenko,
|
||||
K. Alfaro-Bittner, M. Perc, S. Boccaletti,
|
||||
https://doi.org/10.1016/j.chaos.2022.112397
|
||||
|
||||
"""
|
||||
|
||||
# If the hypergraph is empty, then return an empty dictionary
|
||||
if H.num_v == 0:
|
||||
return dict()
|
||||
|
||||
LG = H.get_linegraph()
|
||||
if not eg.is_connected(LG):
|
||||
raise EasyGraphError("This method is not defined for disconnected hypergraphs.")
|
||||
LGcent = eigenvector_centrality(LG)
|
||||
|
||||
vc = {node: [] for node in range(0, H.num_v)}
|
||||
|
||||
edge_label_dict = {tuple(edge): index for index, edge in enumerate(H.e[0])}
|
||||
|
||||
hyperedge_dims = {tuple(edge): len(edge) for edge in H.e[0]}
|
||||
|
||||
D = max([len(e) for e in H.e[0]])
|
||||
|
||||
for k in range(2, D + 1):
|
||||
c_i = np.zeros(H.num_v)
|
||||
|
||||
for edge, _ in list(filter(lambda x: x[1] == k, hyperedge_dims.items())):
|
||||
for node in edge:
|
||||
try:
|
||||
c_i[node] += LGcent[edge_label_dict[edge]]
|
||||
except IndexError:
|
||||
raise Exception(
|
||||
"Nodes must be written with the Pythonic indexing (0,1,2...)"
|
||||
)
|
||||
|
||||
c_i *= 1 / k
|
||||
|
||||
for node in range(H.num_v):
|
||||
vc[node].append(c_i[node])
|
||||
|
||||
return vc
|
||||
|
||||
|
||||
def eigenvector_centrality(G, max_iter=100, tol=1.0e-6):
|
||||
from collections import defaultdict
|
||||
|
||||
nodes = list(G.nodes)
|
||||
n = len(nodes)
|
||||
x = {v: 1.0 for v in nodes}
|
||||
|
||||
for _ in range(max_iter):
|
||||
x_new = defaultdict(float)
|
||||
for v in G:
|
||||
for nbr in G.neighbors(v):
|
||||
x_new[v] += x[nbr]
|
||||
|
||||
# Normalize
|
||||
norm = sum(v**2 for v in x_new.values()) ** 0.5
|
||||
if norm == 0:
|
||||
return x_new
|
||||
x_new = {k: v / norm for k, v in x_new.items()}
|
||||
|
||||
# Check convergence
|
||||
if all(abs(x_new[v] - x[v]) < tol for v in nodes):
|
||||
return x_new
|
||||
x = x_new
|
||||
@@ -0,0 +1,295 @@
|
||||
"""Algorithms for computing nodal clustering coefficients."""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"hypergraph_clustering_coefficient",
|
||||
"hypergraph_local_clustering_coefficient",
|
||||
"hypergraph_two_node_clustering_coefficient",
|
||||
]
|
||||
|
||||
|
||||
def hypergraph_clustering_coefficient(H):
|
||||
r"""Return the clustering coefficients for
|
||||
each node in a Hypergraph.
|
||||
|
||||
This clustering coefficient is defined as the
|
||||
clustering coefficient of the unweighted pairwise
|
||||
projection of the hypergraph, i.e.,
|
||||
:math:`c = A^3_{i,i}/\binom{k}{2},`
|
||||
where :math:`A` is the adjacency matrix of the network
|
||||
and :math:`k` is the pairwise degree of :math:`i`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : Hypergraph
|
||||
Hypergraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
nodes are keys, clustering coefficients are values.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The clustering coefficient is undefined when the number of
|
||||
neighbors is 0 or 1, but we set the clustering coefficient
|
||||
to 0 in these cases. For more discussion, see
|
||||
https://arxiv.org/abs/0802.2512
|
||||
|
||||
See Also
|
||||
--------
|
||||
local_clustering_coefficient
|
||||
two_node_clustering_coefficient
|
||||
|
||||
References
|
||||
----------
|
||||
"Clustering Coefficients in Protein Interaction Hypernetworks"
|
||||
by Suzanne Gallagher and Debra Goldberg.
|
||||
DOI: 10.1145/2506583.2506635
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import easygraph as eg
|
||||
>>> H = eg.random_hypergraph(3, [1, 1])
|
||||
>>> cc = eg.clustering_coefficient(H)
|
||||
>>> cc
|
||||
{0: 1.0, 1: 1.0, 2: 1.0}
|
||||
"""
|
||||
adj = H.adjacency_matrix()
|
||||
k = np.array(adj.sum(axis=1))
|
||||
l = []
|
||||
for i in k:
|
||||
l.append(i[0])
|
||||
k = np.array(l)
|
||||
denom = k * (k - 1) / 2
|
||||
mat = adj.dot(adj).dot(adj)
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
result = np.nan_to_num(0.5 * mat.diagonal() / denom)
|
||||
r = {}
|
||||
for i in range(0, len(H.v)):
|
||||
r[i] = result[i]
|
||||
return r
|
||||
|
||||
|
||||
def hypergraph_local_clustering_coefficient(H):
|
||||
"""Compute the local clustering coefficient.
|
||||
|
||||
This clustering coefficient is based on the
|
||||
overlap of the edges connected to a given node,
|
||||
normalized by the size of the node's neighborhood.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : Hypergraph
|
||||
Hypergraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
keys are node IDs and values are the
|
||||
clustering coefficients.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The clustering coefficient is undefined when the number of
|
||||
neighbors is 0 or 1, but we set the clustering coefficient
|
||||
to 0 in these cases. For more discussion, see
|
||||
https://arxiv.org/abs/0802.2512
|
||||
|
||||
See Also
|
||||
--------
|
||||
clustering_coefficient
|
||||
two_node_clustering_coefficient
|
||||
|
||||
References
|
||||
----------
|
||||
"Properties of metabolic graphs: biological organization or representation
|
||||
artifacts?" by Wanding Zhou and Luay Nakhleh.
|
||||
https://doi.org/10.1186/1471-2105-12-132
|
||||
|
||||
"Hypergraphs for predicting essential genes using multiprotein complex data"
|
||||
by Florian Klimm, Charlotte M. Deane, and Gesine Reinert.
|
||||
https://doi.org/10.1093/comnet/cnaa028
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import easygraph as eg
|
||||
>>> H = eg.random_hypergraph(3, [1, 1])
|
||||
>>> cc = eg.hypergraph_local_clustering_coefficient(H)
|
||||
>>> cc
|
||||
{0: 1.0, 1: 1.0, 2: 1.0}
|
||||
|
||||
"""
|
||||
result = {}
|
||||
# 节点属于哪些边
|
||||
memberships = []
|
||||
for n in H.v:
|
||||
tmp = set()
|
||||
for index, e in enumerate(H.e[0]):
|
||||
if n in e:
|
||||
tmp.add(index)
|
||||
memberships.append(tmp)
|
||||
|
||||
# 每条边包含哪些节点
|
||||
members = H.e[0]
|
||||
for n in H.v:
|
||||
ev = memberships[n]
|
||||
dv = len(ev)
|
||||
if dv <= 1:
|
||||
result[n] = 0
|
||||
else:
|
||||
total_eo = 0
|
||||
# go over all pairs of edges pairwise
|
||||
for e1 in range(dv):
|
||||
edge1 = members[e1]
|
||||
for e2 in range(e1):
|
||||
edge2 = members[e2]
|
||||
# set differences for the hyperedges
|
||||
D1 = set(edge1) - set(edge2)
|
||||
D2 = set(edge2) - set(edge1)
|
||||
# if edges are the same by definition the extra overlap is zero
|
||||
if len(D1.union(D2)) == 0:
|
||||
eo = 0
|
||||
else:
|
||||
# otherwise we have to look at their neighbors
|
||||
# the neighbors of D1 and D2, respectively.
|
||||
neighD1 = {i for d in D1 for i in H.neighbor_of_node(d)}
|
||||
neighD2 = {i for d in D2 for i in H.neighbor_of_node(d)}
|
||||
# compute extra overlap [len() is used for cardinality of edges]
|
||||
eo = (
|
||||
len(neighD1.intersection(D2))
|
||||
+ len(neighD2.intersection(D1))
|
||||
) / len(
|
||||
D1.union(D2)
|
||||
) # add it up
|
||||
# add it up
|
||||
total_eo = total_eo + eo
|
||||
|
||||
# include normalization by degree k*(k-1)/2
|
||||
result[n] = 2 * total_eo / (dv * (dv - 1))
|
||||
return result
|
||||
|
||||
|
||||
def hypergraph_two_node_clustering_coefficient(H, kind="union"):
|
||||
"""Return the clustering coefficients for
|
||||
each node in a Hypergraph.
|
||||
|
||||
This definition averages over all of the
|
||||
two-node clustering coefficients involving the node.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : Hypergraph
|
||||
Hypergraph
|
||||
kind : string, optional
|
||||
The type of two node clustering coefficient. Options
|
||||
are "union", "max", and "min". By default, "union".
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
nodes are keys, clustering coefficients are values.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The clustering coefficient is undefined when the number of
|
||||
neighbors is 0 or 1, but we set the clustering coefficient
|
||||
to 0 in these cases. For more discussion, see
|
||||
https://arxiv.org/abs/0802.2512
|
||||
|
||||
See Also
|
||||
--------
|
||||
clustering_coefficient
|
||||
local_clustering_coefficient
|
||||
|
||||
References
|
||||
----------
|
||||
"Clustering Coefficients in Protein Interaction Hypernetworks"
|
||||
by Suzanne Gallagher and Debra Goldberg.
|
||||
DOI: 10.1145/2506583.2506635
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import easygraph as eg
|
||||
>>> H = eg.random_hypergraph(3, [1, 1])
|
||||
>>> cc = eg.two_node_clustering_coefficient(H, kind="union")
|
||||
>>> cc
|
||||
{0: 0.5, 1: 0.5, 2: 0.5}
|
||||
"""
|
||||
result = {}
|
||||
memberships = {}
|
||||
for n in H.v:
|
||||
tmp = set()
|
||||
for index, e in enumerate(H.e[0]):
|
||||
if n in e:
|
||||
tmp.add(index)
|
||||
memberships[n] = tmp
|
||||
|
||||
for n in H.v:
|
||||
neighbors = H.neighbor_of_node(n)
|
||||
result[n] = 0.0
|
||||
for v in neighbors:
|
||||
result[n] += _uv_cc(n, v, memberships, kind=kind) / len(neighbors)
|
||||
return result
|
||||
|
||||
|
||||
def _uv_cc(u, v, memberships, kind="union"):
|
||||
"""Helper function to compute the two-node
|
||||
clustering coefficient.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u : hashable
|
||||
First node
|
||||
v : hashable
|
||||
Second node
|
||||
memberships : dict
|
||||
node IDs are keys, edge IDs to which they belong
|
||||
are values.
|
||||
kind : str, optional
|
||||
Type of clustering coefficient to compute, by default "union".
|
||||
Options:
|
||||
|
||||
- "union"
|
||||
- "max"
|
||||
- "min"
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The clustering coefficient
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If an invalid clustering coefficient kind
|
||||
is specified.
|
||||
|
||||
References
|
||||
----------
|
||||
"Clustering Coefficients in Protein Interaction Hypernetworks"
|
||||
by Suzanne Gallagher and Debra Goldberg.
|
||||
DOI: 10.1145/2506583.2506635
|
||||
"""
|
||||
m_u = memberships[u]
|
||||
m_v = memberships[v]
|
||||
|
||||
num = len(m_u.intersection(m_v))
|
||||
|
||||
if kind == "union":
|
||||
denom = len(m_u.union(m_v))
|
||||
elif kind == "min":
|
||||
denom = min(len(m_u), len(m_v))
|
||||
elif kind == "max":
|
||||
denom = max(len(m_u), len(m_v))
|
||||
else:
|
||||
raise EasyGraphError("Invalid kind of clustering.")
|
||||
|
||||
if denom == 0:
|
||||
return np.nan
|
||||
|
||||
return num / denom
|
||||
@@ -0,0 +1,70 @@
|
||||
from easygraph.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"hypergraph_density",
|
||||
]
|
||||
|
||||
|
||||
def hypergraph_density(hg, ignore_singletons=False):
|
||||
r"""Hypergraph density.
|
||||
|
||||
The density of a hypergraph is the number of existing edges divided by the number of
|
||||
possible edges.
|
||||
|
||||
Let `H` have :math:`n` nodes and :math:`m` hyperedges. Then,
|
||||
|
||||
* `density(H) =` :math:`\frac{m}{2^n - 1}`,
|
||||
* `density(H, ignore_singletons=True) =` :math:`\frac{m}{2^n - 1 - n}`.
|
||||
|
||||
Here, :math:`2^n` is the total possible number of hyperedges on `H`, from which we
|
||||
subtract :math:`1` because the empty hyperedge is not considered. We subtract an
|
||||
additional :math:`n` when singletons are not considered.
|
||||
|
||||
Now assume `H` has :math:`a` edges with order :math:`1` and :math:`b` edges with
|
||||
order :math:`2`. Then,
|
||||
|
||||
* `density(H, order=1) =` :math:`\frac{a}{{n \choose 2}}`,
|
||||
* `density(H, order=2) =` :math:`\frac{b}{{n \choose 3}}`,
|
||||
* `density(H, max_order=1) =` :math:`\frac{a}{{n \choose 1} + {n \choose 2}}`,
|
||||
* `density(H, max_order=1, ignore_singletons=True) =` :math:`\frac{a}{{n \choose 2}}`,
|
||||
* `density(H, max_order=2) =` :math:`\frac{m}{{n \choose 1} + {n \choose 2} + {n \choose 3}}`,
|
||||
* `density(H, max_order=2, ignore_singletons=True) =` :math:`\frac{m}{{n \choose 2} + {n \choose 3}}`,
|
||||
|
||||
Parameters
|
||||
---------
|
||||
order : int, optional
|
||||
If not None, only count edges of the specified order.
|
||||
By default, None.
|
||||
|
||||
max_order : int, optional
|
||||
If not None, only count edges of order up to this value, inclusive.
|
||||
By default, None.
|
||||
|
||||
ignore_singletons : bool, optional
|
||||
Whether to consider singleton edges. Ignored if `order` is not None and
|
||||
different from :math:`0`. By default, False.
|
||||
|
||||
See Also
|
||||
--------
|
||||
:func:`incidence_density`
|
||||
|
||||
Notes
|
||||
-----
|
||||
If both `order` and `max_order` are not None, `max_order` is ignored.
|
||||
|
||||
"""
|
||||
n = hg.num_v
|
||||
numer = len(hg.e[0])
|
||||
if n < 1:
|
||||
raise EasyGraphError("Density not defined for empty hypergraph")
|
||||
if numer < 1:
|
||||
return 0.0
|
||||
|
||||
denom = 2**n - 1
|
||||
if ignore_singletons:
|
||||
denom -= n
|
||||
try:
|
||||
return numer / float(denom)
|
||||
except ZeroDivisionError:
|
||||
return 0.0
|
||||
@@ -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ős–Ré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")
|
||||
@@ -0,0 +1,103 @@
|
||||
import math
|
||||
import unittest
|
||||
|
||||
from itertools import combinations
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
class test_assortativity(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.g = eg.get_graph_karateclub()
|
||||
self.edges = [(8, 9), (1, 2), (8, 4), (3, 6), (1, 3), (6, 4)]
|
||||
self.hg = [
|
||||
eg.Hypergraph(num_v=10, e_list=self.edges, e_property=None),
|
||||
eg.Hypergraph(num_v=2, e_list=[(0, 1)]),
|
||||
]
|
||||
# Valid uniform hypergraph
|
||||
self.hg_uniform = eg.Hypergraph(
|
||||
num_v=5,
|
||||
e_list=[
|
||||
(0, 1, 2),
|
||||
(1, 2, 3),
|
||||
(2, 3, 4),
|
||||
],
|
||||
)
|
||||
|
||||
# Non-uniform hypergraph
|
||||
self.hg_non_uniform = eg.Hypergraph(
|
||||
num_v=4,
|
||||
e_list=[
|
||||
(0, 1),
|
||||
(2, 3, 0),
|
||||
],
|
||||
)
|
||||
|
||||
# Singleton edge hypergraph (still needs num_v > 0)
|
||||
self.hg_singleton = eg.Hypergraph(
|
||||
num_v=3,
|
||||
e_list=[
|
||||
(0,),
|
||||
(1, 2),
|
||||
],
|
||||
)
|
||||
|
||||
# "Empty" hypergraph (has 1 node but no edges)
|
||||
self.hg_empty = eg.Hypergraph(
|
||||
num_v=1,
|
||||
e_list=[],
|
||||
)
|
||||
|
||||
def test_dynamical_assortativity(self):
|
||||
for i in self.hg:
|
||||
degs = i.deg_v
|
||||
print(degs)
|
||||
k1 = sum(degs) / len(degs)
|
||||
print(k1)
|
||||
k2 = np.mean(np.array(degs) ** 2)
|
||||
print(k2)
|
||||
kk1 = np.mean(
|
||||
[degs[n1] * degs[n2] for e in i.e[0] for n1, n2 in combinations(e, 2)]
|
||||
)
|
||||
print(kk1)
|
||||
print(eg.dynamical_assortativity(i))
|
||||
print()
|
||||
|
||||
def test_degree_assortativity(self):
|
||||
for i in self.hg:
|
||||
print(eg.degree_assortativity(i))
|
||||
|
||||
def test_dynamical_assortativity_valid(self):
|
||||
result = eg.dynamical_assortativity(self.hg_uniform)
|
||||
self.assertIsInstance(result, float)
|
||||
|
||||
def test_dynamical_assortativity_raises_on_empty(self):
|
||||
with self.assertRaises(EasyGraphError):
|
||||
eg.dynamical_assortativity(self.hg_empty)
|
||||
|
||||
def test_dynamical_assortativity_raises_on_singleton(self):
|
||||
with self.assertRaises(EasyGraphError):
|
||||
eg.dynamical_assortativity(self.hg_singleton)
|
||||
|
||||
def test_dynamical_assortativity_raises_on_nonuniform(self):
|
||||
with self.assertRaises(EasyGraphError):
|
||||
eg.dynamical_assortativity(self.hg_non_uniform)
|
||||
|
||||
def test_degree_assortativity_raises_on_invalid_kind(self):
|
||||
with self.assertRaises(EasyGraphError):
|
||||
eg.degree_assortativity(self.hg_uniform, kind="invalid")
|
||||
|
||||
def test_degree_assortativity_raises_on_singleton(self):
|
||||
with self.assertRaises(EasyGraphError):
|
||||
eg.degree_assortativity(self.hg_singleton)
|
||||
|
||||
def test_degree_assortativity_raises_on_empty(self):
|
||||
with self.assertRaises(EasyGraphError):
|
||||
eg.degree_assortativity(self.hg_empty)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,24 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def g1():
|
||||
e_list = [(0, 1, 2, 5), (0, 1), (2, 3, 4), (1, 2, 4)]
|
||||
g = eg.Hypergraph(6, e_list=e_list)
|
||||
return g
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def g2():
|
||||
e_list = [(1, 2, 3), (0, 1, 3), (0, 1), (2, 4, 3), (2, 3)]
|
||||
e_weight = [0.5, 1, 0.5, 1, 0.5]
|
||||
g = eg.Hypergraph(5, e_list=e_list, e_weight=e_weight)
|
||||
return g
|
||||
|
||||
|
||||
def test_degree_centrality(g1, g2):
|
||||
print(eg.hyepergraph_degree_centrality(g1))
|
||||
print(eg.hyepergraph_degree_centrality(g2))
|
||||
assert eg.hyepergraph_degree_centrality(g1) == {0: 2, 1: 3, 2: 3, 3: 1, 4: 2, 5: 1}
|
||||
assert eg.hyepergraph_degree_centrality(g2) == {0: 2, 1: 3, 2: 3, 3: 4, 4: 1}
|
||||
@@ -0,0 +1,89 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
class test_hypergraph_operation(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.g = eg.get_graph_karateclub()
|
||||
self.edges = [(1, 2), (8, 4)]
|
||||
self.hg = [
|
||||
eg.Hypergraph(num_v=10, e_list=self.edges, e_property=None),
|
||||
eg.Hypergraph(num_v=2, e_list=[(0, 1)]),
|
||||
]
|
||||
|
||||
def test_hypergraph_clustering_coefficient(self):
|
||||
for i in self.hg:
|
||||
print(eg.hypergraph_clustering_coefficient(i))
|
||||
|
||||
def test_hypergraph_local_clustering_coefficient(self):
|
||||
for i in self.hg:
|
||||
print(eg.hypergraph_local_clustering_coefficient(i))
|
||||
|
||||
def test_hypergraph_two_node_clustering_coefficient(self):
|
||||
for i in self.hg:
|
||||
print(eg.hypergraph_two_node_clustering_coefficient(i))
|
||||
|
||||
|
||||
class TestHypergraphClustering(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [(0, 1), (1, 2), (2, 3), (3, 0)]
|
||||
self.hg = eg.Hypergraph(num_v=4, e_list=self.edges)
|
||||
|
||||
def test_hypergraph_clustering_coefficient_basic(self):
|
||||
cc = eg.hypergraph_clustering_coefficient(self.hg)
|
||||
self.assertIsInstance(cc, dict)
|
||||
for k, v in cc.items():
|
||||
self.assertIn(k, self.hg.v)
|
||||
self.assertGreaterEqual(v, 0)
|
||||
|
||||
def test_hypergraph_local_clustering_coefficient_basic(self):
|
||||
cc = eg.hypergraph_local_clustering_coefficient(self.hg)
|
||||
self.assertIsInstance(cc, dict)
|
||||
for k, v in cc.items():
|
||||
self.assertIn(k, self.hg.v)
|
||||
self.assertGreaterEqual(v, 0)
|
||||
|
||||
def test_hypergraph_two_node_clustering_union(self):
|
||||
cc = eg.hypergraph_two_node_clustering_coefficient(self.hg, kind="union")
|
||||
self.assertIsInstance(cc, dict)
|
||||
|
||||
def test_hypergraph_two_node_clustering_min(self):
|
||||
cc = eg.hypergraph_two_node_clustering_coefficient(self.hg, kind="min")
|
||||
self.assertIsInstance(cc, dict)
|
||||
|
||||
def test_hypergraph_two_node_clustering_max(self):
|
||||
cc = eg.hypergraph_two_node_clustering_coefficient(self.hg, kind="max")
|
||||
self.assertIsInstance(cc, dict)
|
||||
|
||||
def test_hypergraph_two_node_clustering_invalid_kind(self):
|
||||
with self.assertRaises(EasyGraphError):
|
||||
eg.hypergraph_two_node_clustering_coefficient(self.hg, kind="invalid")
|
||||
|
||||
def test_single_edge(self):
|
||||
hg = eg.Hypergraph(num_v=2, e_list=[(0, 1)])
|
||||
cc = eg.hypergraph_clustering_coefficient(hg)
|
||||
self.assertTrue(all(k in cc for k in hg.v))
|
||||
|
||||
def test_disconnected_nodes(self):
|
||||
hg = eg.Hypergraph(num_v=4, e_list=[(0, 1)])
|
||||
cc = eg.hypergraph_clustering_coefficient(hg)
|
||||
for v in [2, 3]:
|
||||
self.assertEqual(cc[v], 0)
|
||||
|
||||
def test_fully_connected_hyperedge(self):
|
||||
hg = eg.Hypergraph(num_v=3, e_list=[(0, 1, 2)])
|
||||
cc = eg.hypergraph_clustering_coefficient(hg)
|
||||
for v in cc.values():
|
||||
self.assertEqual(v, 1.0)
|
||||
|
||||
def test_nan_safety_in_two_node_coefficient(self):
|
||||
hg = eg.Hypergraph(num_v=1, e_list=[(0,)])
|
||||
result = eg.hypergraph_two_node_clustering_coefficient(hg)
|
||||
self.assertEqual(result[0], 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,72 @@
|
||||
import math
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
class test_hypergraph_operation(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.g = eg.get_graph_karateclub()
|
||||
self.edges = [(1, 2), (8, 4)]
|
||||
self.hg = [
|
||||
eg.Hypergraph(num_v=10, e_list=self.edges, e_property=None),
|
||||
eg.Hypergraph(num_v=2, e_list=[(0, 1)]),
|
||||
]
|
||||
# checked -- num_v cannot be set to negative number
|
||||
|
||||
def test_hypergraph_operation(self):
|
||||
for i in self.hg:
|
||||
print(eg.hypergraph_density(i))
|
||||
i.draw(v_color="#e6928f", e_color="#4e9595")
|
||||
|
||||
def test_basic_density(self):
|
||||
hg = eg.Hypergraph(num_v=3, e_list=[(0, 1), (1, 2)])
|
||||
expected = 2 / (2**3 - 1)
|
||||
self.assertAlmostEqual(eg.hypergraph_density(hg), expected)
|
||||
|
||||
def test_density_ignore_singletons(self):
|
||||
hg = eg.Hypergraph(num_v=3, e_list=[(0,), (1, 2)])
|
||||
expected = 2 / ((2**3 - 1) - 3)
|
||||
self.assertAlmostEqual(
|
||||
eg.hypergraph_density(hg, ignore_singletons=True), expected
|
||||
)
|
||||
|
||||
def test_density_all_singletons(self):
|
||||
hg = eg.Hypergraph(num_v=3, e_list=[(0,), (1,), (2,)])
|
||||
expected = 3 / (2**3 - 1)
|
||||
self.assertAlmostEqual(eg.hypergraph_density(hg), expected)
|
||||
expected_ignoring = 3 / ((2**3 - 1) - 3)
|
||||
self.assertAlmostEqual(
|
||||
eg.hypergraph_density(hg, ignore_singletons=True), expected_ignoring
|
||||
)
|
||||
|
||||
def test_no_edges_returns_zero(self):
|
||||
hg = eg.Hypergraph(num_v=5, e_list=[])
|
||||
self.assertEqual(eg.hypergraph_density(hg), 0.0)
|
||||
|
||||
def test_single_node_single_edge(self):
|
||||
hg = eg.Hypergraph(num_v=1, e_list=[(0,)])
|
||||
self.assertEqual(eg.hypergraph_density(hg), 1.0)
|
||||
|
||||
def test_density_max_possible_edges(self):
|
||||
n = 4
|
||||
from itertools import chain
|
||||
from itertools import combinations
|
||||
|
||||
powerset = list(
|
||||
chain.from_iterable(combinations(range(n), r) for r in range(1, n + 1))
|
||||
)
|
||||
hg = eg.Hypergraph(num_v=n, e_list=powerset)
|
||||
self.assertAlmostEqual(eg.hypergraph_density(hg), 1.0)
|
||||
|
||||
def test_density_zero_division_guard(self):
|
||||
# Singleton ignored in n=1 graph should not divide by zero
|
||||
hg = eg.Hypergraph(num_v=1, e_list=[(0,)])
|
||||
result = eg.hypergraph_density(hg, ignore_singletons=True)
|
||||
self.assertEqual(result, 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user