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,9 @@
from .betweenness import *
from .closeness import *
from .degree import *
from .ego_betweenness import *
from .flowbetweenness import *
from .laplacian import *
from .pagerank import *
from .katz_centrality import *
from .eigenvector import *
@@ -0,0 +1,245 @@
from easygraph.utils import *
from easygraph.utils.decorators import *
__all__ = [
"betweenness_centrality",
]
def betweenness_centrality_parallel(nodes, G, path_length, accumulate):
betweenness = {node: 0.0 for node in G}
for node in nodes:
S, P, sigma = path_length(G, source=node)
betweenness = accumulate(betweenness, S, P, sigma, node)
return betweenness
@not_implemented_for("multigraph")
@hybrid("cpp_betweenness_centrality")
def betweenness_centrality(
G, weight=None, sources=None, normalized=True, endpoints=False, n_workers=None
):
r"""Compute the shortest-basic betweenness centrality for nodes.
.. math::
c_B(v) = \sum_{s,t \in V} \frac{\sigma(s, t|v)}{\sigma(s, t)}
where V is the set of nodes,
.. math::
\sigma(s, t)
is the number of shortest (s, t)-paths, and
.. math::
\sigma(s, t|v)
is the number of those paths passing through some node v other than s, t.
.. math::
If\ s\ =\ t,\ \sigma(s, t) = 1, and\ if\ v \in {s, t}, \sigma(s, t|v) = 0 [2]_.
Parameters
----------
G : graph
A easygraph graph.
weight : None or string, optional (default=None)
If None, all edge weights are considered equal.
Otherwise holds the name of the edge attribute used as weight.
sources : None or nodes list, optional (default=None)
If None, all nodes are considered.
Otherwise,the set of source vertices to consider when calculating shortest paths.
normalized : bool, optional
If True the betweenness values are normalized by `2/((n-1)(n-2))`
for graphs, and `1/((n-1)(n-2))` for directed graphs where `n`
is the number of nodes in G.
endpoints : bool, optional
If True include the endpoints in the shortest basic counts.
Returns
-------
nodes : dictionary
Dictionary of nodes with betweenness centrality as the value.
>>> betweenness_centrality(G,weight="weight")
"""
import functools
if weight is not None:
path_length = functools.partial(_single_source_dijkstra_path, weight=weight)
else:
path_length = functools.partial(_single_source_bfs_path)
if endpoints:
accumulate = functools.partial(_accumulate_endpoints)
else:
accumulate = functools.partial(_accumulate_basic)
if sources is not None:
nodes = sources
else:
nodes = G.nodes
betweenness = dict.fromkeys(G, 0.0)
if n_workers is not None:
# use the parallel version for large graph
import random
from functools import partial
from multiprocessing import Pool
nodes = list(nodes)
random.shuffle(nodes)
if len(nodes) > n_workers * 30000:
nodes = split_len(nodes, step=30000)
else:
nodes = split(nodes, n_workers)
local_function = partial(
betweenness_centrality_parallel,
G=G,
path_length=path_length,
accumulate=accumulate,
)
with Pool(n_workers) as p:
ret = p.imap(local_function, nodes)
for res in ret:
for key in res:
betweenness[key] += res[key]
else:
# use np-parallel version for small graph
for node in nodes:
S, P, sigma = path_length(G, source=node)
betweenness = accumulate(betweenness, S, P, sigma, node)
betweenness = _rescale(
betweenness,
len(G),
normalized=normalized,
directed=G.is_directed(),
endpoints=endpoints,
)
ret = [0.0 for i in range(len(G))]
for i in range(len(ret)):
ret[i] = betweenness[G.index2node[i]]
return ret
def _rescale(betweenness, n, normalized, directed=False, endpoints=False):
if normalized:
if endpoints:
if n < 2:
scale = None # no normalization
else:
# Scale factor should include endpoint nodes
scale = 1 / (n * (n - 1))
elif n <= 2:
scale = None # no normalization b=0 for all nodes
else:
scale = 1 / ((n - 1) * (n - 2))
else: # rescale by 2 for undirected graphs
if not directed:
scale = 0.5
else:
scale = None
if scale is not None:
for v in betweenness:
betweenness[v] *= scale
return betweenness
def _single_source_bfs_path(G, source):
S = []
P = {v: [] for v in G}
sigma = dict.fromkeys(G, 0.0)
D = {}
sigma[source] = 1.0
D[source] = 0
Q = [source]
adj = G.adj
while Q:
v = Q.pop(0)
S.append(v)
Dv = D[v]
sigmav = sigma[v]
for w in adj[v]:
if w not in D:
Q.append(w)
D[w] = Dv + 1
if D[w] == Dv + 1:
sigma[w] += sigmav
P[w].append(v)
return S, P, sigma
def _single_source_dijkstra_path(G, source, weight="weight"):
from heapq import heappop
from heapq import heappush
push = heappush
pop = heappop
S = []
P = {v: [] for v in G}
sigma = dict.fromkeys(G, 0.0)
D = {}
sigma[source] = 1.0
seen = {source: 0}
Q = []
from itertools import count
c = count()
adj = G.adj
push(Q, (0, next(c), source, source))
while Q:
(dist, _, pred, v) = pop(Q)
if v in D:
continue
sigma[v] += sigma[pred]
S.append(v)
D[v] = dist
for w in adj[v]:
vw_dist = dist + adj[v][w].get(weight, 1)
if w not in D and (w not in seen or vw_dist < seen[w]):
seen[w] = vw_dist
push(Q, (vw_dist, next(c), v, w))
sigma[w] = 0.0
P[w] = [v]
elif vw_dist == seen[w]: # handle equal paths
sigma[w] += sigma[v]
P[w].append(v)
return S, P, sigma
def _accumulate_endpoints(betweenness, S, P, sigma, s):
betweenness[s] += len(S) - 1
delta = dict.fromkeys(S, 0)
while S:
w = S.pop()
coeff = (1 + delta[w]) / sigma[w]
for v in P[w]:
delta[v] += sigma[v] * coeff
if w != s:
betweenness[w] += delta[w] + 1
return betweenness
def _accumulate_basic(betweenness, S, P, sigma, s):
delta = dict.fromkeys(S, 0)
while S:
w = S.pop()
coeff = (1 + delta[w]) / sigma[w]
for v in P[w]:
delta[v] += sigma[v] * coeff
if w != s:
betweenness[w] += delta[w]
return betweenness
+105
View File
@@ -0,0 +1,105 @@
from easygraph.functions.basic import *
from easygraph.functions.path import single_source_bfs
from easygraph.functions.path import single_source_dijkstra
from easygraph.utils import *
__all__ = [
"closeness_centrality",
]
def closeness_centrality_parallel(nodes, G, path_length):
ret = []
length = len(G)
for node in nodes:
x = path_length(G, node)
dist = sum(x.values())
cnt = len(x)
if dist == 0:
ret.append([node, 0])
else:
ret.append([node, (cnt - 1) * (cnt - 1) / (dist * (length - 1))])
return ret
@not_implemented_for("multigraph")
@hybrid("cpp_closeness_centrality")
def closeness_centrality(G, weight=None, sources=None, n_workers=None):
r"""
Compute closeness centrality for nodes.
.. math::
C_{WF}(u) = \frac{n-1}{N-1} \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},
Notice that the closeness distance function computes the
outcoming distance to `u` for directed graphs. To use
incoming distance, act on `G.reverse()`.
Parameters
----------
G : graph
A easygraph graph
weight : None or string, optional (default=None)
If None, all edge weights are considered equal.
Otherwise holds the name of the edge attribute used as weight.
sources : None or nodes list, optional (default=None)
If None, all nodes are returned
Otherwise,the set of source vertices to creturn.
Returns
-------
nodes : dictionary
Dictionary of nodes with closeness centrality as the value.
"""
closeness = dict()
if sources is not None:
nodes = sources
else:
nodes = G.nodes
length = len(G)
import functools
if weight is not None:
path_length = functools.partial(single_source_dijkstra, weight=weight)
else:
path_length = functools.partial(single_source_bfs)
if n_workers is not None:
# use parallel version for large graph
import random
from functools import partial
from multiprocessing import Pool
nodes = list(nodes)
random.shuffle(nodes)
if len(nodes) > n_workers * 30000:
nodes = split_len(nodes, step=30000)
else:
nodes = split(nodes, n_workers)
local_function = partial(
closeness_centrality_parallel, G=G, path_length=path_length
)
with Pool(n_workers) as p:
ret = p.imap(local_function, nodes)
res = [x for i in ret for x in i]
closeness = dict(res)
else:
# use np-parallel version for small graph
for node in nodes:
x = path_length(G, node)
dist = sum(x.values())
cnt = len(x)
if dist == 0:
closeness[node] = 0
else:
closeness[node] = (cnt - 1) * (cnt - 1) / (dist * (length - 1))
ret = [0.0 for i in range(len(G))]
for i in range(len(ret)):
ret[i] = closeness[G.index2node[i]]
return ret
+125
View File
@@ -0,0 +1,125 @@
from easygraph.utils.decorators import *
__all__ = ["degree_centrality", "in_degree_centrality", "out_degree_centrality"]
@not_implemented_for("multigraph")
@hybrid("cpp_degree_centrality")
def degree_centrality(G):
"""Compute the degree centrality for nodes in a bipartite network.
The degree centrality for a node v is the fraction of nodes it
is connected to.
parameters
----------
G : graph
A easygraph graph
Returns
-------
nodes : dictionary
Dictionary of nodes with degree centrality as the value.
Notes
-----
The degree centrality are normalized by dividing by n-1 where
n is number of nodes in G.
"""
if len(G) <= 1:
return {n: 1 for n in G}
s = 1.0 / (len(G) - 1.0)
centrality = {n: d * s for n, d in (G.degree()).items()}
return centrality
@not_implemented_for("multigraph")
@only_implemented_for_Directed_graph
@hybrid("cpp_in_degree_centrality")
def in_degree_centrality(G):
"""Compute the in-degree centrality for nodes.
The in-degree centrality for a node v is the fraction of nodes its
incoming edges are connected to.
Parameters
----------
G : graph
A EasyGraph graph
Returns
-------
nodes : dictionary
Dictionary of nodes with in-degree centrality as values.
Raises
------
EasyGraphNotImplemented:
If G is undirected.
See Also
--------
degree_centrality, out_degree_centrality
Notes
-----
The degree centrality values are normalized by dividing by the maximum
possible degree in a simple graph n-1 where n is the number of nodes in G.
For multigraphs or graphs with self loops the maximum degree might
be higher than n-1 and values of degree centrality greater than 1
are possible.
"""
if len(G) <= 1:
return {n: 1 for n in G}
s = 1.0 / (len(G) - 1.0)
centrality = {n: d * s for n, d in G.in_degree().items()}
return centrality
@not_implemented_for("multigraph")
@only_implemented_for_Directed_graph
@hybrid("cpp_out_degree_centrality")
def out_degree_centrality(G):
"""Compute the out-degree centrality for nodes.
The out-degree centrality for a node v is the fraction of nodes its
outgoing edges are connected to.
Parameters
----------
G : graph
A EasyGraph graph
Returns
-------
nodes : dictionary
Dictionary of nodes with out-degree centrality as values.
Raises
------
EasyGraphNotImplemented:
If G is undirected.
See Also
--------
degree_centrality, in_degree_centrality
Notes
-----
The degree centrality values are normalized by dividing by the maximum
possible degree in a simple graph n-1 where n is the number of nodes in G.
For multigraphs or graphs with self loops the maximum degree might
be higher than n-1 and values of degree centrality greater than 1
are possible.
"""
if len(G) <= 1:
return {n: 1 for n in G}
s = 1.0 / (len(G) - 1.0)
centrality = {n: d * s for n, d in G.out_degree().items()}
return centrality
@@ -0,0 +1,57 @@
__all__ = ["ego_betweenness"]
import numpy as np
from easygraph.utils import *
@not_implemented_for("multigraph")
def ego_betweenness(G, node):
"""
ego networks are networks consisting of a single actor (ego) together with the actors they are connected to (alters) and all the links among those alters.[1]
Burt (1992), in his book Structural Holes, provides ample evidence that having high betweenness centrality, which is highly correlated with having many structural holes, can bring benefits to ego.[1]
Returns the betweenness centrality of a ego network whose ego is set
Parameters
----------
G : graph
node : int
Returns
-------
sum : float
the betweenness centrality of a ego network whose ego is set
Examples
--------
Returns the betwenness centrality of node 1.
>>> ego_betweenness(G,node=1)
Reference
---------
.. [1] Martin Everett, Stephen P. Borgatti. "Ego network betweenness." Social Networks, Volume 27, Issue 1, Pages 31-38, 2005.
"""
g = G.ego_subgraph(node)
print(g.edges)
print(g.nodes)
n = len(g)
A = np.zeros((n, n))
for i in range(n):
for j in range(n):
if g.has_edge(g.index2node[i], g.index2node[j]):
A[i, j] = 1
B = A * A
C = np.identity(n) - A
sum = 0
flag = G.is_directed()
for i in range(n):
for j in range(n):
if i != j and C[i, j] == 1 and B[i, j] != 0:
sum += 1.0 / B[i, j]
if flag == False:
sum /= 2
return sum
@@ -0,0 +1,154 @@
import math
import easygraph as eg
from easygraph.utils import *
from easygraph.utils.decorators import *
from scipy import sparse
from scipy.sparse import linalg
import numpy as np
from collections import defaultdict
__all__ = ["eigenvector_centrality"]
@not_implemented_for("multigraph")
@hybrid("cpp_eigenvector_centrality")
def eigenvector_centrality(G, max_iter=100, tol=1.0e-6, nstart=None, weight=None):
"""Calculate eigenvector centrality for nodes in the graph
Eigenvector centrality is based on the idea that a node's importance
depends on the importance of its neighboring nodes.
Specifically, a node's centrality is proportional to the sum of
centrality values of its neighbors.
Parameters
----------
G : graph object
An undirected or directed graph
max_iter : int, optional (default=100)
Maximum number of iterations for the power method
tol : float, optional (default=1.0e-6)
Convergence threshold; algorithm terminates when the difference
between centrality values in consecutive iterations is less than this value
nstart : dictionary, optional (default=None)
Dictionary mapping nodes to initial centrality values
If None, the ARPACK solver is used to directly compute the eigenvector
weight : string or None, optional (default=None)
Name of the edge attribute to be used as edge weight
If None, all edges are considered to have weight 1
Returns
-------
centrality : dictionary
Dictionary mapping nodes to their eigenvector centrality values
Raises
------
EasyGraphPointlessConcept
When input is an empty graph
EasyGraphError
When the algorithm fails to converge within the specified maximum iterations
Notes
-----
This algorithm uses the power iteration method to find the principal eigenvector.
When nstart is not provided, the ARPACK solver is used for efficiency.
The returned centrality values are normalized.
"""
if len(G) == 0:
raise eg.EasyGraphPointlessConcept(
"cannot compute centrality for the null graph"
)
if len(G) == 1:
raise eg.EasyGraphPointlessConcept(
"cannot compute eigenvector centrality for a single node graph"
)
# Build node list and mapping
nodelist = list(G.nodes)
n = len(nodelist)
node_map = {node: i for i, node in enumerate(nodelist)}
# Build weighted adjacency matrix
row, col, data = [], [], []
for u in nodelist:
u_idx = node_map[u]
for v, attrs in G[u].items():
if v in node_map:
v_idx = node_map[v]
w = attrs.get(weight, 1.0) if weight else 1.0
# Build transpose matrix for centrality calculation
row.append(v_idx)
col.append(u_idx)
data.append(float(w))
# Create CSR format sparse matrix
A = sparse.csr_matrix((data, (row, col)), shape=(n, n))
# Detect and handle isolated nodes
row_sums = np.array(A.sum(axis=1)).flatten()
col_sums = np.array(A.sum(axis=0)).flatten()
isolated_nodes = np.where((row_sums == 0) & (col_sums == 0))[0]
has_isolated = len(isolated_nodes) > 0
isolated_indices = []
# Add small self-loops to isolated nodes for stability
if has_isolated:
# Store isolated node indices
isolated_indices = isolated_nodes.tolist()
# Add small self-loop weights to isolated nodes
for idx in isolated_indices:
A[idx, idx] = 1.0e-4 # Small enough to not affect results, but maintains numerical stability
if nstart is not None:
# Use custom initial vector for power iteration
v = np.array([nstart.get(n, 1.0) for n in nodelist], dtype=float)
v = v / np.sum(np.abs(v))
# Power iteration method to compute principal eigenvector
v_last = np.zeros_like(v)
for _ in range(max_iter):
np.copyto(v_last, v)
v = A @ v_last # Sparse matrix multiplication
norm = np.linalg.norm(v)
if norm < 1e-10:
v = v_last.copy()
break
v = v / norm # Normalization
# Check convergence
if np.linalg.norm(v - v_last) < tol:
break
else:
raise eg.EasyGraphError(f"Eigenvector calculation did not converge in {max_iter} iterations")
centrality = v
else:
# Use ARPACK solver to directly compute the principal eigenvector
eigenvalues, eigenvectors = linalg.eigs(A, k=1, which='LR',
maxiter=max_iter, tol=tol)
centrality = np.real(eigenvectors[:,0])
# Ensure positive results and normalize
if centrality.sum() < 0:
centrality = -centrality
centrality = centrality / np.linalg.norm(centrality)
# Set centrality of isolated nodes to zero
if has_isolated:
for idx in isolated_indices:
centrality[idx] = 0.0
# Renormalize if needed
if np.sum(centrality) > 0:
centrality = centrality / np.linalg.norm(centrality)
# Return dictionary of node centrality values
return {nodelist[i]: float(centrality[i]) for i in range(n)}
@@ -0,0 +1,146 @@
import collections
import copy
from easygraph.utils.decorators import *
__all__ = [
"flowbetweenness_centrality",
]
@not_implemented_for("multigraph")
def flowbetweenness_centrality(G):
"""Compute the independent-basic betweenness centrality for nodes in a flow network.
.. math::
c_B(v) =\\sum_{s,t \\in V} \frac{\\sigma(s, t|v)}{\\sigma(s, t)}
where V is the set of nodes,
.. math::
\\sigma(s, t)\\ is\\ the\\ number\\ of\\ independent\\ (s, t)-paths,
.. math::
\\sigma(s, t|v)\\ is\\ the\\ maximum\\ number\\ possible\\ of\\ those\\ paths\\ passing\\ through\\ some\\ node\\ v\\ other\\ than\\ s, t.\
.. math::
If\\ s\\ =\\ t,\\ \\sigma(s, t)\\ =\\ 1,\\ and\\ if\\ v \\in \\{s, t\\},\\ \\sigma(s, t|v)\\ =\\ 0\\ [2]_.
Parameters
----------
G : graph
A easygraph directed graph.
Returns
-------
nodes : dictionary
Dictionary of nodes with independent-basic betweenness centrality as the value.
Notes
-----
A flow network is a directed graph where each edge has a capacity and each edge receives a flow.
"""
if G.is_directed() == False:
print("Please input a directed graph")
return
flow_dict = NumberOfFlow(G)
nodes = G.nodes
result_dict = dict()
for node, _ in nodes.items():
result_dict[node] = 0
for node_v, _ in nodes.items():
for node_s, _ in nodes.items():
for node_t, _ in nodes.items():
num = 1
num_v = 0
if node_s == node_t:
num_v = 0
num = 1
if node_v in [node_s, node_t]:
num_v = 0
num = 1
if node_v != node_s and node_v != node_t and node_s != node_t:
num = flow_dict[node_s][node_t]
num_v = min(flow_dict[node_s][node_v], flow_dict[node_v][node_t])
if num == 0:
pass
else:
result_dict[node_v] = result_dict[node_v] + num_v / num
return result_dict
# flow betweenness
def NumberOfFlow(G):
nodes = G.nodes
result_dict = dict()
for node1, _ in nodes.items():
result_dict[node1] = dict()
for node2, _ in nodes.items():
if node1 == node2:
pass
else:
result_dict[node1][node2] = edmonds_karp(G, node1, node2)
return result_dict
def edmonds_karp(G, source, sink):
nodes = G.nodes
parent = dict()
for node, _ in nodes.items():
parent[node] = -1
adj = copy.deepcopy(G.adj)
max_flow = 0
while bfs(G, source, sink, parent, adj):
path_flow = float("inf")
s = sink
while s != source:
path_flow = min(path_flow, adj[parent[s]][s].get("weight", 1))
s = parent[s]
max_flow += path_flow
v = sink
while v != source:
u = parent[v]
x = adj[u][v].get("weight", 1)
adj[u][v].update({"weight": x})
adj[u][v]["weight"] -= path_flow
flag = 0
if v not in adj:
adj[v] = dict()
if u not in adj[v]:
adj[v][u] = dict()
flag = 1
if flag == 1:
x = 0
else:
x = adj[v][u].get("weight", 1)
adj[v][u].update({"weight": x})
adj[v][u]["weight"] += path_flow
v = parent[v]
return max_flow
def bfs(G, source, sink, parent, adj):
nodes = G.nodes
visited = dict()
for node, _ in nodes.items():
visited[node] = 0
queue = collections.deque()
queue.append(source)
visited[source] = True
while queue:
u = queue.popleft()
if u not in adj:
continue
for v, attr in adj[u].items():
if (visited[v] == False) and (attr.get("weight", 1) > 0):
queue.append(v)
visited[v] = True
parent[v] = u
return visited[sink]
@@ -0,0 +1,105 @@
from easygraph.utils import *
import numpy as np
from easygraph.utils.decorators import *
__all__ = ["katz_centrality"]
@not_implemented_for("multigraph")
@hybrid("cpp_katz_centrality")
def katz_centrality(G, alpha=0.1, beta=1.0, max_iter=1000, tol=1e-6, normalized=True):
r"""
Compute the Katz centrality for nodes in a graph.
Katz centrality computes the influence of a node based on the total number
of walks between nodes, attenuated by a factor of their length. It is
defined as the solution to the linear system:
.. math::
x = \alpha A x + \beta
where:
- \( A \) is the adjacency matrix of the graph,
- \( \alpha \) is a scalar attenuation factor,
- \( \beta \) is the bias vector (typically all ones),
- and \( x \) is the resulting centrality vector.
The algorithm runs an iterative fixed-point method until convergence.
Parameters
----------
G : easygraph.Graph
An EasyGraph graph instance. Must be simple (non-multigraph).
alpha : float, optional (default=0.1)
Attenuation factor, must be smaller than the reciprocal of the largest
eigenvalue of the adjacency matrix to ensure convergence.
beta : float or dict, optional (default=1.0)
Bias term. Can be a constant scalar applied to all nodes, or a dictionary
mapping node IDs to values.
max_iter : int, optional (default=1000)
Maximum number of iterations before the algorithm terminates.
tol : float, optional (default=1e-6)
Convergence tolerance. Iteration stops when the L1 norm of the difference
between successive iterations is below this threshold.
normalized : bool, optional (default=True)
If True, the result vector will be normalized to unit norm (L2).
Returns
-------
dict
A dictionary mapping node IDs to Katz centrality scores.
Raises
------
RuntimeError
If the algorithm fails to converge within `max_iter` iterations.
Examples
--------
>>> import easygraph as eg
>>> from easygraph import katz_centrality
>>> G = eg.Graph()
>>> G.add_edges_from([(0, 1), (1, 2), (2, 3)])
>>> katz_centrality(G, alpha=0.05)
{0: 0.370..., 1: 0.447..., 2: 0.447..., 3: 0.370...}
"""
# Create node ordering
nodes = list(G.nodes)
n = len(nodes)
node_to_index = {node: i for i, node in enumerate(nodes)}
index_to_node = {i: node for i, node in enumerate(nodes)}
# Build adjacency matrix
A = np.zeros((n, n), dtype=np.float64)
for u in G.nodes:
for v in G.adj[u]:
A[node_to_index[u], node_to_index[v]] = 1.0
# Initialize x and beta
x = np.ones(n, dtype=np.float64)
if isinstance(beta, dict):
b = np.array([beta.get(index_to_node[i], 1.0) for i in range(n)])
else:
b = np.ones(n, dtype=np.float64) * beta
# Iterative update using vectorized ops
for _ in range(max_iter):
x_new = alpha * A @ x + b
if np.linalg.norm(x_new - x, ord=1) < tol:
break
x = x_new
else:
raise RuntimeError(f"Katz centrality failed to converge in {max_iter} iterations")
if normalized:
norm = np.linalg.norm(x)
if norm > 0:
x /= norm
result = {index_to_node[i]: float(x[i]) for i in range(n)}
return result
+134
View File
@@ -0,0 +1,134 @@
from easygraph.utils import *
__all__ = ["laplacian"]
@not_implemented_for("multigraph")
def laplacian(G, n_workers=None):
"""Returns the laplacian centrality of each node in the weighted graph
Parameters
----------
G : graph
weighted graph
Returns
-------
CL : dict
the laplacian centrality of each node in the weighted graph
Examples
--------
Returns the laplacian centrality of each node in the weighted graph G
>>> laplacian(G)
Reference
---------
.. [1] Xingqin Qi, Eddie Fuller, Qin Wu, Yezhou Wu, Cun-Quan Zhang.
"Laplacian centrality: A new centrality measure for weighted networks."
Information Sciences, Volume 194, Pages 240-253, 2012.
"""
adj = G.adj
from collections import defaultdict
X = defaultdict(int)
W = defaultdict(int)
CL = {}
if n_workers is not None:
# use the parallel version for large graph
import random
from functools import partial
from multiprocessing import Pool
nodes = list(G.nodes)
random.shuffle(nodes)
if len(nodes) > n_workers * 30000:
nodes = split_len(nodes, step=30000)
else:
nodes = split(nodes, n_workers)
local_function = partial(initialize_parallel, G=G, adj=adj)
with Pool(n_workers) as p:
ret = p.imap(local_function, nodes)
resX, resW = [], []
for i in ret:
for x in i:
resX.append(x[0])
resW.append(x[1])
X = dict(resX)
W = dict(resW)
ELG = sum(X[i] * X[i] for i in G) + sum(W[i] for i in G)
local_function = partial(laplacian_parallel, G=G, X=X, W=W, adj=adj, ELG=ELG)
with Pool(n_workers) as p:
ret = p.imap(local_function, nodes)
res = [x for i in ret for x in i]
CL = dict(res)
else:
# use np-parallel version for small graph
for i in G:
for j in G:
if i in G and j in G[i]:
X[i] += adj[i][j].get("weight", 1)
W[i] += adj[i][j].get("weight", 1) * adj[i][j].get("weight", 1)
ELG = sum(X[i] * X[i] for i in G) + sum(W[i] for i in G)
for i in G:
import copy
Xi = copy.deepcopy(X)
for j in G:
if j in adj.keys() and i in adj[j].keys():
Xi[j] -= adj[j][i].get("weight", 1)
Xi[i] = 0
ELGi = sum(Xi[i] * Xi[i] for i in G) + sum(W[i] for i in G) - 2 * W[i]
if ELG:
CL[i] = (float)(ELG - ELGi) / ELG
return CL
def initialize_parallel(nodes, G, adj):
ret = []
for i in nodes:
X = 0
W = 0
for j in G:
if j in G[i]:
X += adj[i][j].get("weight", 1)
W += adj[i][j].get("weight", 1) * adj[i][j].get("weight", 1)
ret.append([[i, X], [i, W]])
return ret
def laplacian_parallel(nodes, G, X, W, adj, ELG):
ret = []
for i in nodes:
import copy
Xi = copy.deepcopy(X)
for j in G:
if j in adj.keys() and i in adj[j].keys():
Xi[j] -= adj[j][i].get("weight", 1)
Xi[i] = 0
ELGi = sum(Xi[i] * Xi[i] for i in G) + sum(W[i] for i in G) - 2 * W[i]
if ELG:
ret.append([i, (float)(ELG - ELGi) / ELG])
return ret
def sort(data):
return dict(sorted(data.items(), key=lambda x: x[0], reverse=True))
def output(data, path):
import json
data = sort(data)
json_str = json.dumps(data, ensure_ascii=False, indent=4)
with open(path, "w", encoding="utf-8") as json_file:
json_file.write(json_str)
@@ -0,0 +1,58 @@
import easygraph as eg
from easygraph.utils import *
__all__ = ["pagerank"]
@not_implemented_for("multigraph")
@hybrid("cpp_pagerank")
def pagerank(G, alpha=0.85, weight=None):
"""
Returns the PageRank value of each node in G.
Parameters
----------
G : graph
Undirected graph will be considered as directed graph with two directed edges for each undirected edge.
alpha : float
The damping factor. Default is 0.85
weight : None or string, optional (default=None)
If None, all edge weights are considered equal.
Otherwise holds the name of the edge attribute used as weight.
"""
import numpy as np
if len(G) == 0:
return {}
M = google_matrix(G, alpha=alpha, weight=weight)
# use numpy LAPACK solver
eigenvalues, eigenvectors = np.linalg.eig(M.T)
ind = np.argmax(eigenvalues)
# eigenvector of largest eigenvalue is at ind, normalized
largest = np.array(eigenvectors[:, ind]).flatten().real
norm = float(largest.sum())
return dict(zip(G, map(float, largest / norm)))
def google_matrix(G, alpha, weight=None):
import numpy as np
M = eg.to_numpy_array(G, weight=weight).astype(float)
N = len(G)
if N == 0:
return M
# Get dangling nodes(nodes with no out link)
dangling_nodes = np.where(M.sum(axis=1) == 0)[0]
dangling_weights = np.repeat(1.0 / N, N)
for node in dangling_nodes:
M[node] = dangling_weights
M /= M.sum(axis=1)[:, np.newaxis]
return alpha * M + (1 - alpha) * np.repeat(1.0 / N, N)
@@ -0,0 +1,99 @@
import unittest
import easygraph as eg
class Test_betweenness(unittest.TestCase):
def setUp(self):
self.edges = [
(1, 4),
(2, 4),
("String", "Bool"),
(4, 1),
(0, 4),
(4, 256),
((None, None), (None, None)),
]
self.test_graphs = [eg.Graph(), eg.DiGraph()]
self.test_graphs.append(eg.classes.DiGraph(self.edges))
self.undirected = eg.Graph()
self.undirected.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
self.directed = eg.DiGraph()
self.directed.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
self.disconnected = eg.Graph()
self.disconnected.add_edges_from([(0, 1), (2, 3)])
self.single_node = eg.Graph()
self.single_node.add_node(42)
self.two_node = eg.Graph()
self.two_node.add_edge("A", "B")
self.named_nodes = eg.Graph()
self.named_nodes.add_edges_from([("X", "Y"), ("Y", "Z")])
def test_betweenness(self):
for i in self.test_graphs:
print(eg.functions.betweenness_centrality(i))
def test_basic_undirected(self):
result = eg.functions.betweenness_centrality(self.undirected)
self.assertEqual(len(result), len(self.undirected.nodes))
self.assertTrue(all(isinstance(x, float) for x in result))
def test_basic_directed(self):
result = eg.functions.betweenness_centrality(self.directed)
self.assertEqual(len(result), len(self.directed.nodes))
def test_disconnected(self):
result = eg.functions.betweenness_centrality(self.disconnected)
self.assertEqual(len(result), len(self.disconnected.nodes))
self.assertTrue(all(v == 0.0 for v in result))
def test_single_node_graph(self):
result = eg.functions.betweenness_centrality(self.single_node)
self.assertEqual(result, [0.0])
def test_two_node_graph(self):
result = eg.functions.betweenness_centrality(self.two_node)
self.assertEqual(len(result), 2)
self.assertTrue(all(v == 0.0 for v in result))
def test_named_nodes_graph(self):
result = eg.functions.betweenness_centrality(self.named_nodes)
self.assertEqual(len(result), 3)
def test_with_endpoints(self):
result = eg.functions.betweenness_centrality(self.undirected, endpoints=True)
self.assertEqual(len(result), len(self.undirected.nodes))
def test_unormalized(self):
result = eg.functions.betweenness_centrality(self.undirected, normalized=False)
self.assertEqual(len(result), len(self.undirected.nodes))
def test_subset_sources(self):
result = eg.functions.betweenness_centrality(self.undirected, sources=[1, 2])
self.assertEqual(len(result), len(self.undirected.nodes))
def test_parallel_workers(self):
result = eg.functions.betweenness_centrality(self.undirected, n_workers=2)
self.assertEqual(len(result), len(self.undirected.nodes))
def test_multigraph_error(self):
G = eg.MultiGraph()
G.add_edges_from([(0, 1), (0, 1)])
with self.assertRaises(eg.EasyGraphNotImplemented):
eg.functions.betweenness_centrality(G)
def test_all_nodes_type_mix(self):
G = eg.Graph()
G.add_edges_from([(1, 2), ("A", "B"), ((1, 2), (3, 4))])
result = eg.functions.betweenness_centrality(G)
self.assertEqual(len(result), len(G.nodes))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,86 @@
import unittest
import easygraph as eg
from easygraph.classes.multigraph import MultiGraph
from easygraph.functions.centrality import closeness_centrality
class Test_closeness(unittest.TestCase):
def setUp(self):
self.edges = [
(1, 4),
(2, 4),
("String", "Bool"),
(4, 1),
(0, 4),
(4, 256),
((None, None), (None, None)),
]
self.test_graphs = [eg.Graph(), eg.DiGraph()]
self.test_graphs.append(eg.classes.DiGraph(self.edges))
self.simple_graph = eg.Graph()
self.simple_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
self.directed_graph = eg.DiGraph()
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
self.weighted_graph = eg.Graph()
self.weighted_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
for u, v, data in self.weighted_graph.edges:
data["weight"] = 2
self.disconnected_graph = eg.Graph()
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
self.single_node_graph = eg.Graph()
self.single_node_graph.add_node(42)
self.mixed_nodes_graph = eg.Graph()
self.mixed_nodes_graph.add_edges_from([(1, 2), ("X", "Y"), ((1, 2), (3, 4))])
def test_closeness(self):
for i in self.test_graphs:
result = closeness_centrality(i)
self.assertEqual(len(result), len(i))
def test_simple_graph(self):
result = closeness_centrality(self.simple_graph)
self.assertEqual(len(result), len(self.simple_graph))
self.assertTrue(all(isinstance(x, float) for x in result))
def test_directed_graph(self):
result = closeness_centrality(self.directed_graph)
self.assertEqual(len(result), len(self.directed_graph))
def test_weighted_graph(self):
result = closeness_centrality(self.weighted_graph, weight="weight")
self.assertEqual(len(result), len(self.weighted_graph))
def test_disconnected_graph(self):
result = closeness_centrality(self.disconnected_graph)
self.assertEqual(len(result), len(self.disconnected_graph))
self.assertTrue(all(v <= 1.0 for v in result))
def test_single_node_graph(self):
result = closeness_centrality(self.single_node_graph)
self.assertEqual(result, [0.0])
def test_mixed_node_types(self):
result = closeness_centrality(self.mixed_nodes_graph)
self.assertEqual(len(result), len(self.mixed_nodes_graph))
def test_parallel_workers(self):
result = closeness_centrality(self.simple_graph, n_workers=2)
self.assertEqual(len(result), len(self.simple_graph))
def test_multigraph_raises(self):
G = MultiGraph()
G.add_edges_from([(0, 1), (0, 1)])
with self.assertRaises(eg.EasyGraphNotImplemented):
closeness_centrality(G)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,78 @@
import unittest
import easygraph as eg
from easygraph.utils.exception import EasyGraphNotImplemented
class Test_degree(unittest.TestCase):
def setUp(self):
self.edges = [
(1, 4),
(2, 4),
("String", "Bool"),
(4, 1),
(0, 4),
(4, 256),
((None, None), (None, None)),
]
self.test_graphs = [eg.Graph(), eg.DiGraph()]
self.test_graphs.append(eg.classes.DiGraph(self.edges))
self.undirected_graph = eg.Graph()
self.undirected_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
# Directed graph
self.directed_graph = eg.DiGraph()
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
# Single-node graph
self.single_node_graph = eg.Graph()
self.single_node_graph.add_node(0)
# Empty graph
self.empty_graph = eg.Graph()
# Multigraph
self.multigraph = eg.MultiGraph()
self.multigraph.add_edges_from([(0, 1), (0, 1)])
def test_degree(self):
for i in self.test_graphs:
print(i.edges)
print(eg.functions.degree_centrality(i))
print(eg.functions.in_degree_centrality(i))
print(eg.functions.out_degree_centrality(i))
def test_degree_centrality_undirected(self):
result = eg.functions.degree_centrality(self.undirected_graph)
self.assertEqual(len(result), len(self.undirected_graph))
self.assertTrue(all(isinstance(v, float) for v in result.values()))
def test_degree_centrality_directed(self):
result = eg.functions.degree_centrality(self.directed_graph)
self.assertEqual(len(result), len(self.directed_graph))
def test_degree_centrality_single_node(self):
result = eg.functions.degree_centrality(self.single_node_graph)
self.assertEqual(result, {0: 1})
def test_degree_centrality_empty_graph(self):
result = eg.functions.degree_centrality(self.empty_graph)
self.assertEqual(result, {})
def test_in_out_degree_centrality_directed(self):
in_deg = eg.functions.in_degree_centrality(self.directed_graph)
out_deg = eg.functions.out_degree_centrality(self.directed_graph)
self.assertEqual(len(in_deg), len(self.directed_graph))
self.assertEqual(len(out_deg), len(self.directed_graph))
def test_in_out_degree_centrality_single_node(self):
G = eg.DiGraph()
G.add_node(1)
self.assertEqual(eg.functions.in_degree_centrality(G), {1: 1})
self.assertEqual(eg.functions.out_degree_centrality(G), {1: 1})
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,73 @@
import unittest
import easygraph as eg
from easygraph.utils.exception import EasyGraphNotImplemented
class Test_egobetweenness(unittest.TestCase):
def setUp(self):
self.edges = [
(1, 4),
(2, 4),
("String", "Bool"),
(4, 1),
(0, 4),
(4, 256),
((None, None), (None, None)),
]
self.test_graphs = [eg.Graph(), eg.DiGraph()]
self.test_graphs.append(eg.classes.DiGraph(self.edges))
print(self.test_graphs[-1].edges)
self.graph = eg.Graph()
self.graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
self.directed_graph = eg.DiGraph()
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 0)])
self.mixed_nodes_graph = eg.Graph()
self.mixed_nodes_graph.add_edges_from([(1, "A"), ("A", (2, 3)), ((2, 3), "B")])
self.single_node_graph = eg.Graph()
self.single_node_graph.add_node(42)
self.disconnected_graph = eg.Graph()
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)]) # two components
self.multigraph = eg.MultiGraph()
self.multigraph.add_edges_from([(0, 1), (0, 1)]) # parallel edges
def test_egobetweenness(self):
print(eg.functions.ego_betweenness(self.test_graphs[-1], 4))
def test_small_undirected_graph(self):
result = eg.functions.ego_betweenness(self.graph, 1)
self.assertIsInstance(result, float)
self.assertGreaterEqual(result, 0)
def test_directed_graph(self):
result = eg.functions.ego_betweenness(self.directed_graph, 0)
self.assertIsInstance(result, int)
def test_mixed_node_types(self):
result = eg.functions.ego_betweenness(self.mixed_nodes_graph, "A")
self.assertIsInstance(result, float)
def test_single_node_graph(self):
result = eg.functions.ego_betweenness(self.single_node_graph, 42)
self.assertEqual(result, 0.0)
def test_disconnected_graph_component(self):
result_0 = eg.functions.ego_betweenness(self.disconnected_graph, 0)
result_2 = eg.functions.ego_betweenness(self.disconnected_graph, 2)
self.assertIsInstance(result_0, float)
self.assertIsInstance(result_2, float)
def test_raises_on_multigraph(self):
with self.assertRaises(EasyGraphNotImplemented):
eg.functions.ego_betweenness(self.multigraph, 0)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,90 @@
import unittest
import easygraph as eg
from easygraph.utils.exception import EasyGraphNotImplemented
class Test_flowbetweenness(unittest.TestCase):
def setUp(self):
self.edges = [
(1, 2),
(2, 3),
("String", "Bool"),
(2, 1),
(0, 0),
(-99, 256),
((None, None), (None, None)),
]
self.test_graphs = [eg.Graph(), eg.DiGraph()]
self.test_graphs.append(eg.classes.DiGraph(self.edges))
self.directed_graph = eg.DiGraph()
self.directed_graph.add_edges_from(
[
(0, 1, {"weight": 3}),
(1, 2, {"weight": 1}),
(0, 2, {"weight": 1}),
(2, 3, {"weight": 2}),
(1, 3, {"weight": 4}),
]
)
self.graph_with_self_loop = eg.DiGraph()
self.graph_with_self_loop.add_edges_from([(0, 1), (1, 2), (2, 2), (2, 3)])
self.disconnected_graph = eg.DiGraph()
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
self.undirected_graph = eg.Graph()
self.undirected_graph.add_edges_from([(0, 1), (1, 2)])
self.single_node_graph = eg.DiGraph()
self.single_node_graph.add_node(0)
self.mixed_type_graph = eg.DiGraph()
self.mixed_type_graph.add_edges_from([(1, "A"), ("A", (2, 3)), ((2, 3), "B")])
self.multigraph = eg.MultiDiGraph()
self.multigraph.add_edges_from([(0, 1), (0, 1)])
def test_flowbetweenness_centrality(self):
for i in self.test_graphs:
print(i.edges)
print(eg.functions.flowbetweenness_centrality(i))
def test_flowbetweenness_on_directed(self):
result = eg.functions.flowbetweenness_centrality(self.directed_graph)
self.assertIsInstance(result, dict)
self.assertTrue(
all(isinstance(v, float) or isinstance(v, int) for v in result.values())
)
def test_flowbetweenness_on_self_loop(self):
result = eg.functions.flowbetweenness_centrality(self.graph_with_self_loop)
self.assertIsInstance(result, dict)
def test_flowbetweenness_on_disconnected(self):
result = eg.functions.flowbetweenness_centrality(self.disconnected_graph)
self.assertIsInstance(result, dict)
def test_flowbetweenness_on_single_node(self):
result = eg.functions.flowbetweenness_centrality(self.single_node_graph)
self.assertIsInstance(result, dict)
self.assertEqual(result, {0: 0})
def test_flowbetweenness_on_mixed_types(self):
result = eg.functions.flowbetweenness_centrality(self.mixed_type_graph)
self.assertIsInstance(result, dict)
def test_flowbetweenness_on_undirected_warns(self):
result = eg.functions.flowbetweenness_centrality(self.undirected_graph)
self.assertIsNone(result)
def test_flowbetweenness_raises_on_multigraph(self):
with self.assertRaises(EasyGraphNotImplemented):
eg.functions.flowbetweenness_centrality(self.multigraph)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,106 @@
import unittest
import easygraph as eg
from easygraph.utils.exception import EasyGraphNotImplemented
class Test_laplacian(unittest.TestCase):
def setUp(self):
self.edges = [
(1, 2),
(2, 3),
("String", "Bool"),
(2, 1),
(0, 0),
(-99, 256),
((None, None), (None, None)),
]
self.test_graphs = [eg.Graph(), eg.DiGraph()]
self.test_graphs.append(eg.classes.DiGraph(self.edges))
self.weighted_graph = eg.Graph()
self.weighted_graph.add_edges_from(
[
(0, 1, {"weight": 2}),
(1, 2, {"weight": 3}),
(2, 3, {"weight": 4}),
(3, 0, {"weight": 1}),
]
)
self.unweighted_graph = eg.Graph()
self.unweighted_graph.add_edges_from(
[
(0, 1),
(1, 2),
(2, 3),
]
)
self.directed_graph = eg.DiGraph()
self.directed_graph.add_edges_from(
[
(0, 1, {"weight": 2}),
(1, 2, {"weight": 1}),
(2, 0, {"weight": 3}),
]
)
self.self_loop_graph = eg.Graph()
self.self_loop_graph.add_edges_from(
[
(0, 0, {"weight": 2}),
(0, 1, {"weight": 1}),
]
)
self.mixed_type_graph = eg.Graph()
self.mixed_type_graph.add_edges_from(
[
("A", "B"),
("B", (1, 2)),
]
)
self.single_node_graph = eg.Graph()
self.single_node_graph.add_node(42)
self.multigraph = eg.MultiGraph()
self.multigraph.add_edges_from([(0, 1), (0, 1)])
def test_laplacian(self):
for i in self.test_graphs:
print(i.edges)
print(eg.functions.laplacian(i))
def test_weighted_graph(self):
result = eg.functions.laplacian(self.weighted_graph)
self.assertEqual(set(result.keys()), set(self.weighted_graph.nodes))
def test_unweighted_graph(self):
result = eg.functions.laplacian(self.unweighted_graph)
self.assertEqual(set(result.keys()), set(self.unweighted_graph.nodes))
def test_directed_graph(self):
result = eg.functions.laplacian(self.directed_graph)
self.assertEqual(set(result.keys()), set(self.directed_graph.nodes))
def test_self_loop_graph(self):
result = eg.functions.laplacian(self.self_loop_graph)
self.assertEqual(set(result.keys()), set(self.self_loop_graph.nodes))
def test_mixed_node_types(self):
result = eg.functions.laplacian(self.mixed_type_graph)
self.assertEqual(set(result.keys()), set(self.mixed_type_graph.nodes))
def test_single_node_graph(self):
result = eg.functions.laplacian(self.single_node_graph)
self.assertEqual(result, {})
def test_multigraph_raises(self):
with self.assertRaises(EasyGraphNotImplemented):
eg.functions.laplacian(self.multigraph)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,90 @@
import unittest
import easygraph as eg
from easygraph.utils.exception import EasyGraphNotImplemented
class Test_pagerank(unittest.TestCase):
def setUp(self):
edges = [
(1, 2),
(2, 3),
("String", "Bool"),
(2, 1),
(0, 0),
((None, None), (None, None)),
]
self.g = eg.classes.DiGraph(edges)
self.directed_graph = eg.DiGraph()
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 0)])
self.undirected_graph = eg.Graph()
self.undirected_graph.add_edges_from([(0, 1), (1, 2), (2, 0)])
self.disconnected_graph = eg.DiGraph()
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
self.self_loop_graph = eg.DiGraph()
self.self_loop_graph.add_edges_from([(0, 0), (0, 1), (1, 2)])
self.mixed_graph = eg.DiGraph()
self.mixed_graph.add_edges_from([("A", "B"), ("B", "C"), ("C", (1, 2))])
self.single_node_graph = eg.DiGraph()
self.single_node_graph.add_node("solo")
self.multigraph = eg.MultiDiGraph()
self.multigraph.add_edges_from([(0, 1), (0, 1)])
def test_pagerank(self):
test_graphs = [eg.Graph(), eg.DiGraph()]
for i in test_graphs:
print(eg.functions.pagerank(i))
print(self.g.nodes)
print(eg.functions.pagerank(self.g))
"""
def test_google_matrix(self):
test_graphs = [eg.Graph(), eg.DiGraph(), eg.MultiGraph(), eg.MultiDiGraph()]
for g in test_graphs:
print(eg.functions.pagerank.(g))
"""
def test_directed_graph(self):
result = eg.functions.pagerank(self.directed_graph)
self.assertEqual(set(result.keys()), set(self.directed_graph.nodes))
def test_undirected_graph(self):
result = eg.functions.pagerank(self.undirected_graph)
self.assertEqual(set(result.keys()), set(self.undirected_graph.nodes))
def test_disconnected_graph(self):
result = eg.functions.pagerank(self.disconnected_graph)
self.assertEqual(set(result.keys()), set(self.disconnected_graph.nodes))
def test_self_loop_graph(self):
result = eg.functions.pagerank(self.self_loop_graph)
self.assertEqual(set(result.keys()), set(self.self_loop_graph.nodes))
def test_mixed_node_types(self):
result = eg.functions.pagerank(self.mixed_graph)
self.assertEqual(set(result.keys()), set(self.mixed_graph.nodes))
def test_single_node_graph(self):
result = eg.functions.pagerank(self.single_node_graph)
self.assertEqual(result, {"solo": 1.0})
def test_empty_graph(self):
empty_graph = eg.DiGraph()
result = eg.functions.pagerank(empty_graph)
self.assertEqual(result, {})
def test_multigraph_raises(self):
with self.assertRaises(EasyGraphNotImplemented):
eg.functions.pagerank(self.multigraph)
if __name__ == "__main__":
unittest.main()