chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:36:30 +08:00
commit 55ab4e4a73
473 changed files with 72932 additions and 0 deletions
@@ -0,0 +1,5 @@
from .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