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
+5
View File
@@ -0,0 +1,5 @@
from .average_shortest_path_length import *
from .bridges import *
from .diameter import *
from .mst import *
from .path import *
@@ -0,0 +1,116 @@
import warnings
import easygraph as eg
from easygraph.utils.decorators import *
from easygraph.functions.path.path import *
@hybrid("cpp_average_shortest_path_length")
def average_shortest_path_length(G, weight=None, method=None):
r"""Returns the average shortest path length.
The average shortest path length is
.. math::
a =\sum_{\substack{s,t \in V \\ s\neq t}} \frac{d(s, t)}{n(n-1)}
where `V` is the set of nodes in `G`,
`d(s, t)` is the shortest path from `s` to `t`,
and `n` is the number of nodes in `G`.
.. versionchanged:: 3.0
An exception is raised for directed graphs that are not strongly
connected.
Parameters
----------
G : EasyGraph graph
weight : None, string or function, optional (default = None)
If None, every edge has weight/distance/cost 1.
If a string, use this edge attribute as the edge weight.
Any edge attribute not present defaults to 1.
If this is a function, the weight of an edge is the value
returned by the function. The function must accept exactly
three positional arguments: the two endpoints of an edge and
the dictionary of edge attributes for that edge.
The function must return a number.
method : string, optional (default = 'unweighted' or 'dijkstra')
The algorithm to use to compute the path lengths.
Supported options are 'unweighted', 'dijkstra', 'bellman-ford',
'floyd-warshall' and 'floyd-warshall-numpy'.
Other method values produce a ValueError.
The default method is 'unweighted' if `weight` is None,
otherwise the default method is 'dijkstra'.
Raises
------
NetworkXPointlessConcept
If `G` is the null graph (that is, the graph on zero nodes).
NetworkXError
If `G` is not connected (or not strongly connected, in the case
of a directed graph).
ValueError
If `method` is not among the supported options.
Examples
--------
>>> G = eg.path_graph(5)
>>> eg.average_shortest_path_length(G)
2.0
For disconnected graphs, you can compute the average shortest path
length for each component
>>> G = eg.Graph([(1, 2), (3, 4)])
>>> for C in (G.subgraph(c).copy() for c in eg.connected_components(G)):
... print(eg.average_shortest_path_length(C))
1.0
1.0
"""
single_source_methods = ["single_source_bfs", "dijkstra"]
all_pairs_methods = ["Floyed"]
supported_methods = single_source_methods + all_pairs_methods
if method is None:
method = "single_source_bfs" if weight is None else "dijkstra"
if method not in supported_methods:
raise ValueError(f"method not supported: {method}")
n = len(G)
# For the special case of the null graph, raise an exception, since
# there are no paths in the null graph.
if n == 0:
msg = (
"the null graph has no paths, thus there is no average shortest path length"
)
raise eg.EasyGraphPointlessConcept(msg)
# For the special case of the trivial graph, return zero immediately.
if n == 1:
return 0
# Shortest path length is undefined if the graph is not strongly connected.
if G.is_directed() and not eg.is_strongly_connected(G):
raise eg.EasyGraphError("Graph is not strongly connected.")
# Shortest path length is undefined if the graph is not connected.
if not G.is_directed() and not eg.is_connected(G):
raise eg.EasyGraphError("Graph is not connected.")
# Compute all-pairs shortest paths.
def path_length(v):
if method == "single_source_bfs":
return eg.single_source_bfs(G, v)
elif method == "dijkstra":
return eg.Dijkstra(G, v, weight=weight)
if method in single_source_methods:
# Sum the distances for each (ordered) pair of source and target node.
s = sum(l for u in G for l in path_length(u).values())
else:
all_pairs = eg.Floyed(G, weight=weight)
s = sum(sum(t.values()) for t in all_pairs.values())
return s / (n * (n - 1))
+201
View File
@@ -0,0 +1,201 @@
from itertools import chain
import easygraph as eg
from easygraph.utils.decorators import *
__all__ = ["bridges", "has_bridges"]
@not_implemented_for("multigraph")
@only_implemented_for_UnDirected_graph
def bridges(G, root=None):
"""Generate all bridges in a graph.
A *bridge* in a graph is an edge whose removal causes the number of
connected components of the graph to increase. Equivalently, a bridge is an
edge that does not belong to any cycle.
Parameters
----------
G : undirected graph
root : node (optional)
A node in the graph `G`. If specified, only the bridges in the
connected component containing this node will be returned.
Yields
------
e : edge
An edge in the graph whose removal disconnects the graph (or
causes the number of connected components to increase).
Raises
------
NodeNotFound
If `root` is not in the graph `G`.
Examples
--------
>>> list(eg.bridges(G))
[(9, 10)]
Notes
-----
This is an implementation of the algorithm described in _[1]. An edge is a
bridge if and only if it is not contained in any chain. Chains are found
using the :func:`chain_decomposition` function.
Ignoring polylogarithmic factors, the worst-case time complexity is the
same as the :func:`chain_decomposition` function,
$O(m + n)$, where $n$ is the number of nodes in the graph and $m$ is
the number of edges.
References
----------
.. [1] https://en.wikipedia.org/wiki/Bridge_%28graph_theory%29#Bridge-Finding_with_Chain_Decompositions
"""
if root is not None and root not in G.nodes:
raise eg.NodeNotFound(f"Node {root} is not in the graph.")
chains = chain_decomposition(G, root=root)
chain_edges = set(chain.from_iterable(chains))
for u, v, t in G.edges:
if (u, v) not in chain_edges and (v, u) not in chain_edges:
yield u, v
@not_implemented_for("multigraph")
@only_implemented_for_UnDirected_graph
def has_bridges(G, root=None):
"""Decide whether a graph has any bridges.
A *bridge* in a graph is an edge whose removal causes the number of
connected components of the graph to increase.
Parameters
----------
G : undirected graph
root : node (optional)
A node in the graph `G`. If specified, only the bridges in the
connected component containing this node will be considered.
Returns
-------
bool
Whether the graph (or the connected component containing `root`)
has any bridges.
Raises
------
NodeNotFound
If `root` is not in the graph `G`.
Examples
--------
>>> eg.has_bridges(G)
True
Notes
-----
This implementation uses the :func:`easygraph.bridges` function, so
it shares its worst-case time complexity, $O(m + n)$, ignoring
polylogarithmic factors, where $n$ is the number of nodes in the
graph and $m$ is the number of edges.
"""
try:
next(bridges(G, root))
except StopIteration:
return False
else:
return True
def chain_decomposition(G, root=None):
def _dfs_cycle_forest(G, root=None):
H = eg.DiGraph()
nodes = []
for u, v, d in dfs_labeled_edges(G, source=root):
if d == "forward":
# `dfs_labeled_edges()` yields (root, root, 'forward')
# if it is beginning the search on a new connected
# component.
if u == v:
H.add_node(v, parent=None)
nodes.append(v)
else:
H.add_node(v, parent=u)
H.add_edge(v, u, nontree=False)
nodes.append(v)
# `dfs_labeled_edges` considers nontree edges in both
# orientations, so we need to not add the edge if it its
# other orientation has been added.
elif d == "nontree" and v not in H[u]:
H.add_edge(v, u, nontree=True)
else:
# Do nothing on 'reverse' edges; we only care about
# forward and nontree edges.
pass
return H, nodes
def _build_chain(G, u, v, visited):
while v not in visited:
yield u, v
visited.add(v)
u, v = v, G.nodes[v]["parent"]
yield u, v
H, nodes = _dfs_cycle_forest(G, root)
visited = set()
for u in nodes:
visited.add(u)
# For each nontree edge going out of node u...
edges = []
for w, v, d in H.edges:
if w == u and d["nontree"] == True:
edges.append((w, v))
# edges = ((u, v) for u, v, d in H.out_edges(u, data="nontree") if d)
for u, v in edges:
# Create the cycle or cycle prefix starting with the
# nontree edge.
chain = list(_build_chain(H, u, v, visited))
yield chain
def dfs_labeled_edges(G, source=None, depth_limit=None):
if source is None:
# edges for all components
nodes = G
else:
# edges for components with source
nodes = [source]
visited = set()
if depth_limit is None:
depth_limit = len(G)
for start in nodes:
if start in visited:
continue
yield start, start, "forward"
visited.add(start)
stack = [(start, depth_limit, iter(G[start]))]
while stack:
parent, depth_now, children = stack[-1]
try:
child = next(children)
if child in visited:
yield parent, child, "nontree"
else:
yield parent, child, "forward"
visited.add(child)
if depth_now > 1:
stack.append((child, depth_now - 1, iter(G[child])))
except StopIteration:
stack.pop()
if stack:
yield stack[-1][0], parent, "reverse"
yield start, start, "reverse"
+109
View File
@@ -0,0 +1,109 @@
import easygraph as eg
import easygraph.functions.path
from easygraph.utils.decorators import *
__all__ = ["diameter", "eccentricity"]
@hybrid("cpp_eccentricity")
def eccentricity(G, v=None, sp=None):
"""Returns the eccentricity of nodes in G.
The eccentricity of a node v is the maximum distance from v to
all other nodes in G.
Parameters
----------
G : EasyGraph graph
A graph
v : node, optional
Return value of specified node
sp : dict of dicts, optional
All pairs shortest path lengths as a dictionary of dictionaries
Returns
-------
ecc : dictionary
A dictionary of eccentricity values keyed by node.
Examples
--------
>>> G = eg.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
>>> dict(eg.eccentricity(G))
{1: 2, 2: 3, 3: 2, 4: 2, 5: 3}
>>> dict(eg.eccentricity(G, v=[1, 5])) # This returns the eccentrity of node 1 & 5
{1: 2, 5: 3}
"""
# if v is None: # none, use entire graph
# nodes=G.nodes()
# elif v in G: # is v a single node
# nodes=[v]
# else: # assume v is a container of nodes
# nodes=v
order = G.order()
e = {}
for n in G.nbunch_iter(v):
if sp is None:
length = eg.single_source_dijkstra(G, n)
L = len(length)
else:
try:
length = sp[n]
L = len(length)
except TypeError as err:
raise eg.EasyGraphError('Format of "sp" is invalid.') from err
if L != order:
if G.is_directed():
msg = (
"Found infinite path length because the digraph is not"
" strongly connected"
)
else:
msg = "Found infinite path length because the graph is not connected"
raise eg.EasyGraphError(msg)
e[n] = max(length.values())
if v in G:
return e[v] # return single value
else:
return e
def diameter(G, e=None):
"""Returns the diameter of the graph G.
The diameter is the maximum eccentricity.
Parameters
----------
G : EasyGraph graph
A graph
e : eccentricity dictionary, optional
A precomputed dictionary of eccentricities.
Returns
-------
d : integer
Diameter of graph
Examples
--------
>>> G = eg.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
>>> eg.diameter(G)
3
See Also
--------
eccentricity
"""
if e is None:
e = eccentricity(G)
return max(e.values())
+685
View File
@@ -0,0 +1,685 @@
from heapq import heappop
from heapq import heappush
from itertools import count
from math import isnan
from operator import itemgetter
from easygraph.utils.decorators import *
__all__ = [
"minimum_spanning_edges",
"maximum_spanning_edges",
"minimum_spanning_tree",
"maximum_spanning_tree",
]
@hybrid("cpp_boruvka_mst_edges")
def boruvka_mst_edges(G, minimum=True, weight="weight", data=True, ignore_nan=False):
"""Iterate over edges of a Borůvka's algorithm min/max spanning tree.
Parameters
----------
G : EasyGraph Graph
The edges of `G` must have distinct weights,
otherwise the edges may not form a tree.
minimum : bool (default: True)
Find the minimum (True) or maximum (False) spanning tree.
weight : string (default: 'weight')
The name of the edge attribute holding the edge weights.
data : bool (default: True)
Flag for whether to yield edge attribute dicts.
If True, yield edges `(u, v, d)`, where `d` is the attribute dict.
If False, yield edges `(u, v)`.
ignore_nan : bool (default: False)
If a NaN is found as an edge weight normally an exception is raised.
If `ignore_nan is True` then that edge is ignored instead.
"""
# Initialize a forest, assuming initially that it is the discrete
# partition of the nodes of the graph.
forest = UnionFind(G)
def best_edge(component):
"""Returns the optimum (minimum or maximum) edge on the edge
boundary of the given set of nodes.
A return value of ``None`` indicates an empty boundary.
"""
sign = 1 if minimum else -1
minwt = float("inf")
boundary = None
for e in edge_boundary(G, component, data=True):
wt = e[-1].get(weight, 1) * sign
if isnan(wt):
if ignore_nan:
continue
msg = f"NaN found as an edge weight. Edge {e}"
raise ValueError(msg)
if wt < minwt:
minwt = wt
boundary = e
return boundary
# Determine the optimum edge in the edge boundary of each component
# in the forest.
best_edges = (best_edge(component) for component in forest.to_sets())
best_edges = [edge for edge in best_edges if edge is not None]
# If each entry was ``None``, that means the graph was disconnected,
# so we are done generating the forest.
while best_edges:
# Determine the optimum edge in the edge boundary of each
# component in the forest.
#
# This must be a sequence, not an iterator. In this list, the
# same edge may appear twice, in different orientations (but
# that's okay, since a union operation will be called on the
# endpoints the first time it is seen, but not the second time).
#
# Any ``None`` indicates that the edge boundary for that
# component was empty, so that part of the forest has been
# completed.
#
# TODO This can be parallelized, both in the outer loop over
# each component in the forest and in the computation of the
# minimum. (Same goes for the identical lines outside the loop.)
best_edges = (best_edge(component) for component in forest.to_sets())
best_edges = [edge for edge in best_edges if edge is not None]
# Join trees in the forest using the best edges, and yield that
# edge, since it is part of the spanning tree.
#
# TODO This loop can be parallelized, to an extent (the union
# operation must be atomic).
for u, v, d in best_edges:
if forest[u] != forest[v]:
if data:
yield u, v, d
else:
yield u, v
forest.union(u, v)
@hybrid("cpp_kruskal_mst_edges")
def kruskal_mst_edges(G, minimum=True, weight="weight", data=True, ignore_nan=False):
"""Iterate over edges of a Kruskal's algorithm min/max spanning tree.
Parameters
----------
G : EasyGraph Graph
The graph holding the tree of interest.
minimum : bool (default: True)
Find the minimum (True) or maximum (False) spanning tree.
weight : string (default: 'weight')
The name of the edge attribute holding the edge weights.
data : bool (default: True)
Flag for whether to yield edge attribute dicts.
If True, yield edges `(u, v, d)`, where `d` is the attribute dict.
If False, yield edges `(u, v)`.
ignore_nan : bool (default: False)
If a NaN is found as an edge weight normally an exception is raised.
If `ignore_nan is True` then that edge is ignored instead.
"""
subtrees = UnionFind()
edges = []
for u, v, t in G.edges:
edges.append((u, v, t))
def filter_nan_edges(edges=edges, weight=weight):
sign = 1 if minimum else -1
for u, v, d in edges:
wt = d.get(weight, 1) * sign
if isnan(wt):
if ignore_nan:
continue
msg = f"NaN found as an edge weight. Edge {(u, v, d)}"
raise ValueError(msg)
yield wt, u, v, d
edges = sorted(filter_nan_edges(), key=itemgetter(0))
for wt, u, v, d in edges:
if subtrees[u] != subtrees[v]:
if data:
yield (u, v, d)
else:
yield (u, v)
subtrees.union(u, v)
@hybrid("cpp_prim_mst_edges")
def prim_mst_edges(G, minimum=True, weight="weight", data=True, ignore_nan=False):
"""Iterate over edges of Prim's algorithm min/max spanning tree.
Parameters
----------
G : EasyGraph Graph
The graph holding the tree of interest.
minimum : bool (default: True)
Find the minimum (True) or maximum (False) spanning tree.
weight : string (default: 'weight')
The name of the edge attribute holding the edge weights.
data : bool (default: True)
Flag for whether to yield edge attribute dicts.
If True, yield edges `(u, v, d)`, where `d` is the attribute dict.
If False, yield edges `(u, v)`.
ignore_nan : bool (default: False)
If a NaN is found as an edge weight normally an exception is raised.
If `ignore_nan is True` then that edge is ignored instead.
"""
push = heappush
pop = heappop
nodes = set(G)
c = count()
sign = 1 if minimum else -1
while nodes:
u = nodes.pop()
frontier = []
visited = {u}
for v, d in G.adj[u].items():
wt = d.get(weight, 1) * sign
if isnan(wt):
if ignore_nan:
continue
msg = f"NaN found as an edge weight. Edge {(u, v, d)}"
raise ValueError(msg)
push(frontier, (wt, next(c), u, v, d))
while frontier:
W, _, u, v, d = pop(frontier)
if v in visited or v not in nodes:
continue
if data:
yield u, v, d
else:
yield u, v
# update frontier
visited.add(v)
nodes.discard(v)
for w, d2 in G.adj[v].items():
if w in visited:
continue
new_weight = d2.get(weight, 1) * sign
push(frontier, (new_weight, next(c), v, w, d2))
ALGORITHMS = {
"boruvka": boruvka_mst_edges,
"borůvka": boruvka_mst_edges,
"kruskal": kruskal_mst_edges,
"prim": prim_mst_edges,
}
@not_implemented_for("multigraph")
@only_implemented_for_UnDirected_graph
def minimum_spanning_edges(
G, algorithm="kruskal", weight="weight", data=True, ignore_nan=False
):
"""Generate edges in a minimum spanning forest of an undirected
weighted graph.
A minimum spanning tree is a subgraph of the graph (a tree)
with the minimum sum of edge weights. A spanning forest is a
union of the spanning trees for each connected component of the graph.
Parameters
----------
G : undirected Graph
An undirected graph. If `G` is connected, then the algorithm finds a
spanning tree. Otherwise, a spanning forest is found.
algorithm : string
The algorithm to use when finding a minimum spanning tree. Valid
choices are 'kruskal', 'prim', or 'boruvka'. The default is 'kruskal'.
weight : string
Edge data key to use for weight (default 'weight').
data : bool, optional
If True yield the edge data along with the edge.
ignore_nan : bool (default: False)
If a NaN is found as an edge weight normally an exception is raised.
If `ignore_nan is True` then that edge is ignored instead.
Returns
-------
edges : iterator
An iterator over edges in a maximum spanning tree of `G`.
Edges connecting nodes `u` and `v` are represented as tuples:
`(u, v, k, d)` or `(u, v, k)` or `(u, v, d)` or `(u, v)`
Examples
--------
>>> from easygraph.functions.basic import mst
Find minimum spanning edges by Kruskal's algorithm
>>> G.add_edge(0, 3, weight=2)
>>> mst = mst.minimum_spanning_edges(G, algorithm="kruskal", data=False)
>>> edgelist = list(mst)
>>> sorted(sorted(e) for e in edgelist)
[[0, 1], [1, 2], [2, 3]]
Find minimum spanning edges by Prim's algorithm
>>> G.add_edge(0, 3, weight=2)
>>> mst = mst.minimum_spanning_edges(G, algorithm="prim", data=False)
>>> edgelist = list(mst)
>>> sorted(sorted(e) for e in edgelist)
[[0, 1], [1, 2], [2, 3]]
Notes
-----
For Borůvka's algorithm, each edge must have a weight attribute, and
each edge weight must be distinct.
For the other algorithms, if the graph edges do not have a weight
attribute a default weight of 1 will be used.
Modified code from David Eppstein, April 2006
http://www.ics.uci.edu/~eppstein/PADS/
"""
try:
algo = ALGORITHMS[algorithm]
except KeyError as e:
msg = f"{algorithm} is not a valid choice for an algorithm."
raise ValueError(msg) from e
return algo(G, minimum=True, weight=weight, data=data, ignore_nan=ignore_nan)
@not_implemented_for("multigraph")
@only_implemented_for_UnDirected_graph
def maximum_spanning_edges(
G, algorithm="kruskal", weight="weight", data=True, ignore_nan=False
):
"""Generate edges in a maximum spanning forest of an undirected
weighted graph.
A maximum spanning tree is a subgraph of the graph (a tree)
with the maximum possible sum of edge weights. A spanning forest is a
union of the spanning trees for each connected component of the graph.
Parameters
----------
G : undirected Graph
An undirected graph. If `G` is connected, then the algorithm finds a
spanning tree. Otherwise, a spanning forest is found.
algorithm : string
The algorithm to use when finding a maximum spanning tree. Valid
choices are 'kruskal', 'prim', or 'boruvka'. The default is 'kruskal'.
weight : string
Edge data key to use for weight (default 'weight').
data : bool, optional
If True yield the edge data along with the edge.
ignore_nan : bool (default: False)
If a NaN is found as an edge weight normally an exception is raised.
If `ignore_nan is True` then that edge is ignored instead.
Returns
-------
edges : iterator
An iterator over edges in a maximum spanning tree of `G`.
Edges connecting nodes `u` and `v` are represented as tuples:
`(u, v, k, d)` or `(u, v, k)` or `(u, v, d)` or `(u, v)`
Examples
--------
>>> from easygraph.functions.path import mst
Find maximum spanning edges by Kruskal's algorithm
>>> G.add_edge(0, 3, weight=2)
>>> mst = mst.maximum_spanning_edges(G, algorithm="kruskal", data=False)
>>> edgelist = list(mst)
>>> sorted(sorted(e) for e in edgelist)
[[0, 1], [0, 3], [1, 2]]
Find maximum spanning edges by Prim's algorithm
>>> G.add_edge(0, 3, weight=2) # assign weight 2 to edge 0-3
>>> mst = mst.maximum_spanning_edges(G, algorithm="prim", data=False)
>>> edgelist = list(mst)
>>> sorted(sorted(e) for e in edgelist)
[[0, 1], [0, 3], [2, 3]]
Notes
-----
For Borůvka's algorithm, each edge must have a weight attribute, and
each edge weight must be distinct.
For the other algorithms, if the graph edges do not have a weight
attribute a default weight of 1 will be used.
Modified code from David Eppstein, April 2006
http://www.ics.uci.edu/~eppstein/PADS/
"""
try:
algo = ALGORITHMS[algorithm]
except KeyError as e:
msg = f"{algorithm} is not a valid choice for an algorithm."
raise ValueError(msg) from e
return algo(G, minimum=False, weight=weight, data=data, ignore_nan=ignore_nan)
@not_implemented_for("multigraph")
def minimum_spanning_tree(G, weight="weight", algorithm="kruskal", ignore_nan=False):
"""Returns a minimum spanning tree or forest on an undirected graph `G`.
Parameters
----------
G : undirected graph
An undirected graph. If `G` is connected, then the algorithm finds a
spanning tree. Otherwise, a spanning forest is found.
weight : str
Data key to use for edge weights.
algorithm : string
The algorithm to use when finding a minimum spanning tree. Valid
choices are 'kruskal', 'prim', or 'boruvka'. The default is
'kruskal'.
ignore_nan : bool (default: False)
If a NaN is found as an edge weight normally an exception is raised.
If `ignore_nan is True` then that edge is ignored instead.
Returns
-------
G : EasyGraph Graph
A minimum spanning tree or forest.
Examples
--------
>>> G.add_edge(0, 3, weight=2)
>>> T = eg.minimum_spanning_tree(G)
>>> sorted(T.edges(data=True))
[(0, 1, {}), (1, 2, {}), (2, 3, {})]
Notes
-----
For Borůvka's algorithm, each edge must have a weight attribute, and
each edge weight must be distinct.
For the other algorithms, if the graph edges do not have a weight
attribute a default weight of 1 will be used.
Isolated nodes with self-loops are in the tree as edgeless isolated nodes.
"""
edges = list(
minimum_spanning_edges(G, algorithm, weight, data=True, ignore_nan=ignore_nan)
)
T = G.__class__() # Same graph class as G
for i in G.nodes:
T.add_node(i)
for i in edges:
(u, v, t) = i
T.add_edge(u, v, **t)
return T
@not_implemented_for("multigraph")
def maximum_spanning_tree(G, weight="weight", algorithm="kruskal", ignore_nan=False):
"""Returns a maximum spanning tree or forest on an undirected graph `G`.
Parameters
----------
G : undirected graph
An undirected graph. If `G` is connected, then the algorithm finds a
spanning tree. Otherwise, a spanning forest is found.
weight : str
Data key to use for edge weights.
algorithm : string
The algorithm to use when finding a maximum spanning tree. Valid
choices are 'kruskal', 'prim', or 'boruvka'. The default is
'kruskal'.
ignore_nan : bool (default: False)
If a NaN is found as an edge weight normally an exception is raised.
If `ignore_nan is True` then that edge is ignored instead.
Returns
-------
G : EasyGraph Graph
A maximum spanning tree or forest.
Examples
--------
>>> G.add_edge(0, 3, weight=2)
>>> T = eg.maximum_spanning_tree(G)
>>> sorted(T.edges(data=True))
[(0, 1, {}), (0, 3, {'weight': 2}), (1, 2, {})]
Notes
-----
For Borůvka's algorithm, each edge must have a weight attribute, and
each edge weight must be distinct.
For the other algorithms, if the graph edges do not have a weight
attribute a default weight of 1 will be used.
There may be more than one tree with the same minimum or maximum weight.
See :mod:`easygraph.tree.recognition` for more detailed definitions.
Isolated nodes with self-loops are in the tree as edgeless isolated nodes.
"""
edges = list(
maximum_spanning_edges(G, algorithm, weight, data=True, ignore_nan=ignore_nan)
)
T = G.__class__() # Same graph class as G
for i in G.nodes:
T.add_node(i)
for i in edges:
(u, v, t) = i
T.add_edge(u, v, **t)
return T
def edge_boundary(G, nbunch1, nbunch2=None, data=False, default=None):
"""Returns the edge boundary of `nbunch1`.
The *edge boundary* of a set *S* with respect to a set *T* is the
set of edges (*u*, *v*) such that *u* is in *S* and *v* is in *T*.
If *T* is not specified, it is assumed to be the set of all nodes
not in *S*.
Parameters
----------
G : EasyGraph graph
nbunch1 : iterable
Iterable of nodes in the graph representing the set of nodes
whose edge boundary will be returned. (This is the set *S* from
the definition above.)
nbunch2 : iterable
Iterable of nodes representing the target (or "exterior") set of
nodes. (This is the set *T* from the definition above.) If not
specified, this is assumed to be the set of all nodes in `G`
not in `nbunch1`.
data : bool or object
This parameter has the same meaning as in
:meth:`MultiGraph.edges`.
default : object
This parameter has the same meaning as in
:meth:`MultiGraph.edges`.
Returns
-------
iterator
An iterator over the edges in the boundary of `nbunch1` with
respect to `nbunch2`. If `keys`, `data`, or `default`
are specified and `G` is a multigraph, then edges are returned
with keys and/or data, as in :meth:`MultiGraph.edges`.
Notes
-----
Any element of `nbunch` that is not in the graph `G` will be
ignored.
`nbunch1` and `nbunch2` are usually meant to be disjoint, but in
the interest of speed and generality, that is not required here.
"""
nset1 = {v for v in G if v in nbunch1}
# Here we create an iterator over edges incident to nodes in the set
# `nset1`. The `Graph.edges()` method does not provide a guarantee
# on the orientation of the edges, so our algorithm below must
# handle the case in which exactly one orientation, either (u, v) or
# (v, u), appears in this iterable.
edges = G.edges(nset1, data=data, default=default)
# If `nbunch2` is not provided, then it is assumed to be the set
# complement of `nbunch1`. For the sake of efficiency, this is
# implemented by using the `not in` operator, instead of by creating
# an additional set and using the `in` operator.
if nbunch2 is None:
return (e for e in edges if (e[0] in nset1) ^ (e[1] in nset1))
nset2 = set(nbunch2)
return (
e
for e in edges
if (e[0] in nset1 and e[1] in nset2) or (e[1] in nset1 and e[0] in nset2)
)
"""
Union-find data structure.
"""
class UnionFind:
"""Union-find data structure.
Each unionFind instance X maintains a family of disjoint sets of
hashable objects, supporting the following two methods:
- X[item] returns a name for the set containing the given item.
Each set is named by an arbitrarily-chosen one of its members; as
long as the set remains unchanged it will keep the same name. If
the item is not yet part of a set in X, a new singleton set is
created for it.
- X.union(item1, item2, ...) merges the sets containing each item
into a single larger set. If any item is not yet part of a set
in X, it is added to X as one of the members of the merged set.
Union-find data structure. Based on Josiah Carlson's code,
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/215912
with significant additional changes by D. Eppstein.
http://www.ics.uci.edu/~eppstein/PADS/UnionFind.py
"""
def __init__(self, elements=None):
"""Create a new empty union-find structure.
If *elements* is an iterable, this structure will be initialized
with the discrete partition on the given set of elements.
"""
if elements is None:
elements = ()
self.parents = {}
self.weights = {}
for x in elements:
self.weights[x] = 1
self.parents[x] = x
def __getitem__(self, object):
"""Find and return the name of the set containing the object."""
# check for previously unknown object
if object not in self.parents:
self.parents[object] = object
self.weights[object] = 1
return object
# find basic of objects leading to the root
path = [object]
root = self.parents[object]
while root != path[-1]:
path.append(root)
root = self.parents[root]
# compress the basic and return
for ancestor in path:
self.parents[ancestor] = root
return root
def __iter__(self):
"""Iterate through all items ever found or unioned by this structure."""
return iter(self.parents)
def to_sets(self):
"""Iterates over the sets stored in this structure.
For example::
>>> partition = UnionFind("xyz")
>>> sorted(map(sorted, partition.to_sets()))
[['x'], ['y'], ['z']]
>>> partition.union("x", "y")
>>> sorted(map(sorted, partition.to_sets()))
[['x', 'y'], ['z']]
"""
# Ensure fully pruned paths
def groups(parents: dict):
sets = {}
for v, k in parents.items():
if k not in sets:
sets[k] = set()
sets[k].add(v)
return sets
for x in self.parents.keys():
_ = self[x] # Evaluated for side-effect only
yield from groups(self.parents).values()
def union(self, *objects):
"""Find the sets containing the objects and merge them all."""
# Find the heaviest root according to its weight.
roots = iter(sorted({self[x] for x in objects}, key=lambda r: self.weights[r]))
try:
root = next(roots)
except StopIteration:
return
for r in roots:
self.weights[root] += self.weights[r]
self.parents[r] = root
+259
View File
@@ -0,0 +1,259 @@
from easygraph.utils import *
from easygraph.utils.decorators import *
__all__ = [
"Dijkstra",
"Floyd",
"Prim",
"Kruskal",
"Spfa",
"single_source_bfs",
"single_source_dijkstra",
"multi_source_dijkstra",
]
@hybrid("cpp_spfa")
def Spfa(G, node, weight="weight"):
raise EasyGraphError("Please input GraphC or DiGraphC.")
@not_implemented_for("multigraph")
def Dijkstra(G, node, weight="weight"):
"""Returns the length of paths from the certain node to remaining nodes
Parameters
----------
G : graph
weighted graph
node : int
Returns
-------
result_dict : dict
the length of paths from the certain node to remaining nodes
Examples
--------
Returns the length of paths from node 1 to remaining nodes
>>> Dijkstra(G,node=1,weight="weight")
"""
return single_source_dijkstra(G, node, weight=weight)
@not_implemented_for("multigraph")
@only_implemented_for_UnDirected_graph
@hybrid("cpp_Floyd")
def Floyd(G, weight="weight"):
"""Returns the length of paths from all nodes to remaining nodes
Parameters
----------
G : graph
weighted graph
Returns
-------
result_dict : dict
the length of paths from all nodes to remaining nodes
Examples
--------
Returns the length of paths from all nodes to remaining nodes
>>> Floyd(G,weight="weight")
"""
adj = G.adj.copy()
result_dict = {}
for i in G:
result_dict[i] = {}
for i in G:
temp_key = adj[i].keys()
for j in G:
if j in temp_key:
result_dict[i][j] = adj[i][j].get(weight, 1)
else:
result_dict[i][j] = float("inf")
if i == j:
result_dict[i][i] = 0
for k in G:
for i in G:
for j in G:
temp = result_dict[i][k] + result_dict[k][j]
if result_dict[i][j] > temp:
result_dict[i][j] = temp
return result_dict
@not_implemented_for("multigraph")
@only_implemented_for_UnDirected_graph
@hybrid("cpp_Prim")
def Prim(G, weight="weight"):
"""Returns the edges that make up the minimum spanning tree
Parameters
----------
G : graph
weighted graph
Returns
-------
result_dict : dict
the edges that make up the minimum spanning tree
Examples
--------
Returns the edges that make up the minimum spanning tree
>>> Prim(G,weight="weight")
"""
adj = G.adj.copy()
result_dict = {}
for i in G:
result_dict[i] = {}
selected = []
candidate = []
for i in G:
if not selected:
selected.append(i)
else:
candidate.append(i)
while len(candidate):
start = None
end = None
min_weight = float("inf")
for i in selected:
for j in candidate:
if i in G and j in G[i] and adj[i][j].get(weight, 1) < min_weight:
start = i
end = j
min_weight = adj[i][j].get(weight, 1)
if start != None and end != None:
result_dict[start][end] = min_weight
selected.append(end)
candidate.remove(end)
else:
break
return result_dict
@not_implemented_for("multigraph")
@only_implemented_for_UnDirected_graph
@hybrid("cpp_Kruskal")
def Kruskal(G, weight="weight"):
"""Returns the edges that make up the minimum spanning tree
Parameters
----------
G : graph
weighted graph
Returns
-------
result_dict : dict
the edges that make up the minimum spanning tree
Examples
--------
Returns the edges that make up the minimum spanning tree
>>> Kruskal(G,weight="weight")
"""
adj = G.adj.copy()
result_dict = {}
edge_list = []
for i in G:
result_dict[i] = {}
for i in G:
for j in G[i]:
wt = adj[i][j].get(weight, 1)
edge_list.append([i, j, wt])
edge_list.sort(key=lambda a: a[2])
group = [[i] for i in G]
for edge in edge_list:
for i in range(len(group)):
if edge[0] in group[i]:
m = i
if edge[1] in group[i]:
n = i
if m != n:
result_dict[edge[0]][edge[1]] = edge[2]
group[m] = group[m] + group[n]
group[n] = []
return result_dict
@not_implemented_for("multigraph")
def single_source_bfs(G, source, target=None):
nextlevel = {source: 0}
return dict(_single_source_bfs(G.adj, nextlevel, target=target))
def _single_source_bfs(adj, firstlevel, target=None):
seen = {}
level = 0
nextlevel = firstlevel
while nextlevel:
thislevel = nextlevel
nextlevel = {}
for v in thislevel:
if v not in seen:
seen[v] = level
nextlevel.update(adj[v])
yield (v, level)
if v == target:
break
level += 1
del seen
@not_implemented_for("multigraph")
def single_source_dijkstra(G, source, weight="weight", target=None):
from heapq import heappop
from heapq import heappush
push = heappush
pop = heappop
adj = G.adj
dist = {}
seen = {}
from itertools import count
c = count()
Q = []
seen[source] = 0
push(Q, (0, next(c), source))
while Q:
(d, _, v) = pop(Q)
if v in dist:
continue
dist[v] = d
if v == target:
break
for u in adj[v]:
cost = adj[v][u].get(weight, 1)
vu_dist = dist[v] + cost
if u in dist:
if vu_dist < dist[u]:
raise ValueError("Contradictory paths found:", "negative weights?")
elif u not in seen or vu_dist < seen[u]:
seen[u] = vu_dist
push(Q, (vu_dist, next(c), u))
else:
continue
return dist
@not_implemented_for("multigraph")
@hybrid("cpp_dijkstra_multisource")
def multi_source_dijkstra(G, sources, weight="weight", target=None):
return {
source: single_source_dijkstra(G, source, weight, target) for source in sources
}
@@ -0,0 +1,56 @@
import unittest
import easygraph as eg
from easygraph import average_shortest_path_length
from easygraph.utils.exception import EasyGraphError
from easygraph.utils.exception import EasyGraphPointlessConcept
class TestAverageShortestPathLength(unittest.TestCase):
def test_unweighted_path_graph(self):
G = eg.path_graph(5)
result = average_shortest_path_length(G)
self.assertEqual(result, 2.0)
def test_weighted_graph(self):
G = eg.Graph()
G.add_edge(0, 1, weight=1)
G.add_edge(1, 2, weight=2)
G.add_edge(2, 3, weight=3)
result = average_shortest_path_length(G, weight="weight", method="dijkstra")
self.assertAlmostEqual(result, 3.333, places=3)
def test_trivial_graph(self):
G = eg.Graph()
G.add_node(1)
self.assertEqual(average_shortest_path_length(G), 0)
def test_disconnected_graph_undirected(self):
G = eg.Graph([(1, 2), (3, 4)])
with self.assertRaises(EasyGraphError):
average_shortest_path_length(G)
def test_disconnected_graph_directed(self):
G = eg.DiGraph([(0, 1), (2, 3)])
with self.assertRaises(EasyGraphError):
average_shortest_path_length(G)
def test_null_graph(self):
G = eg.Graph()
with self.assertRaises(EasyGraphPointlessConcept):
average_shortest_path_length(G)
def test_directed_strongly_connected(self):
G = eg.DiGraph([(0, 1), (1, 2), (2, 0)])
result = average_shortest_path_length(G)
self.assertEqual(result, 1.5)
def test_unsupported_method(self):
G = eg.path_graph(5)
with self.assertRaises(ValueError):
average_shortest_path_length(G, method="unsupported_method")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,158 @@
import unittest
import easygraph as eg
from easygraph.utils.exception import EasyGraphNotImplemented
class test_bridges(unittest.TestCase):
def setUp(self):
self.g1 = eg.get_graph_karateclub()
# source graph: https://zh.wikipedia.org/zh-cn/%E6%88%B4%E5%85%8B%E6%96%AF%E7%89%B9%E6%8B%89%E7%AE%97%E6%B3%95#/media/File:Dijkstra_Animation.gif
edges = [(1, 2), (1, 3), (1, 6), (2, 3), (2, 4), (3, 4), (3, 6), (4, 5), (5, 6)]
self.g2 = eg.Graph(edges)
self.g2.add_edges(
edges,
edges_attr=[
{"weight": 7},
{"weight": 9},
{"weight": 14},
{"weight": 10},
{"weight": 15},
{"weight": 11},
{"weight": 2},
{"weight": 6},
{"weight": 9},
],
)
# source graph: https://static.javatpoint.com/tutorial/daa/images/dijkstra-algorithm.png
self.g3 = eg.Graph()
edges = [
(0, 1),
(0, 4),
(1, 4),
(1, 2),
(4, 5),
(4, 8),
(2, 3),
(2, 6),
(2, 8),
(5, 6),
(5, 8),
(3, 6),
(3, 7),
(6, 7),
]
self.g3.add_edges(
edges,
edges_attr=[
{"weight": 4},
{"weight": 1},
{"weight": 11},
{"weight": 8},
{"weight": 1},
{"weight": 7},
{"weight": 7},
{"weight": 4},
{"weight": 2},
{"weight": 2},
{"weight": 6},
{"weight": 14},
{"weight": 9},
{"weight": 10},
],
)
self.g4 = eg.Graph()
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3)]
self.g4.add_edges(
edges,
edges_attr=[
{"weight": -1},
{"weight": -2},
{"weight": -3},
{"weight": -4},
{"weight": -5},
{"weight": -6},
],
)
def result(self, g: eg.Graph):
res = eg.bridges(g)
for i in res:
print(i)
def test_bridges(self):
self.result(g=self.g2)
self.result(g=self.g3)
self.result(g=self.g4)
def test_has_bridges(self):
print(eg.has_bridges(self.g2))
def test_empty_graph(self):
g = eg.Graph()
self.assertFalse(eg.has_bridges(g))
self.assertEqual(list(eg.bridges(g)), [])
def test_single_node_graph(self):
g = eg.Graph()
g.add_node(1)
self.assertFalse(eg.has_bridges(g))
self.assertEqual(list(eg.bridges(g)), [])
def test_disconnected_graph(self):
g = eg.Graph()
g.add_edges_from([(0, 1), (2, 3)])
self.assertTrue(eg.has_bridges(g))
self.assertCountEqual(list(eg.bridges(g)), [(0, 1), (2, 3)])
def test_cycle_graph(self):
g = eg.DiGraph([(1, 2), (2, 3), (3, 1)])
self.assertFalse(eg.has_bridges(g))
self.assertEqual(list(eg.bridges(g)), [])
def test_path_graph(self):
g = eg.path_graph(4)
self.assertTrue(eg.has_bridges(g))
self.assertCountEqual(list(eg.bridges(g)), [(0, 1), (1, 2), (2, 3)])
def test_star_graph(self):
g = eg.Graph()
g.add_edges_from([(0, i) for i in range(1, 5)])
expected = [(0, 1), (0, 2), (0, 3), (0, 4)]
self.assertTrue(eg.has_bridges(g))
self.assertCountEqual(list(eg.bridges(g)), expected)
def test_complete_graph(self):
g = eg.complete_graph(5)
self.assertFalse(eg.has_bridges(g))
self.assertEqual(list(eg.bridges(g)), [])
def test_graph_with_invalid_root(self):
g = eg.path_graph(3)
with self.assertRaises(eg.NodeNotFound):
list(eg.bridges(g, root=10))
def test_multigraph_exception(self):
g = eg.MultiGraph()
g.add_edges_from([(0, 1), (1, 2)])
with self.assertRaises(EasyGraphNotImplemented):
list(eg.bridges(g))
with self.assertRaises(EasyGraphNotImplemented):
eg.has_bridges(g)
def test_weighted_graph_should_ignore_weights(self):
g = eg.Graph()
g.add_edges_from(
[(0, 1), (1, 2), (2, 3), (3, 0)],
edges_attr=[{"weight": 10}, {"weight": 20}, {"weight": 30}, {"weight": 40}],
)
self.assertFalse(eg.has_bridges(g))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,143 @@
import unittest
import easygraph as eg
class test_diameter(unittest.TestCase):
def setUp(self):
self.g1 = eg.get_graph_karateclub()
# source graph: https://zh.wikipedia.org/zh-cn/%E6%88%B4%E5%85%8B%E6%96%AF%E7%89%B9%E6%8B%89%E7%AE%97%E6%B3%95#/media/File:Dijkstra_Animation.gif
edges = [(1, 2), (1, 3), (1, 6), (2, 3), (2, 4), (3, 4), (3, 6), (4, 5), (5, 6)]
self.g2 = eg.Graph(edges)
self.g2.add_edges(
edges,
edges_attr=[
{"weight": 7},
{"weight": 9},
{"weight": 14},
{"weight": 10},
{"weight": 15},
{"weight": 11},
{"weight": 2},
{"weight": 6},
{"weight": 9},
],
)
# source graph: https://static.javatpoint.com/tutorial/daa/images/dijkstra-algorithm.png
self.g3 = eg.Graph()
edges = [
(0, 1),
(0, 4),
(1, 4),
(1, 2),
(4, 5),
(4, 8),
(2, 3),
(2, 6),
(2, 8),
(5, 6),
(5, 8),
(3, 6),
(3, 7),
(6, 7),
]
self.g3.add_edges(
edges,
edges_attr=[
{"weight": 4},
{"weight": 1},
{"weight": 11},
{"weight": 8},
{"weight": 1},
{"weight": 7},
{"weight": 7},
{"weight": 4},
{"weight": 2},
{"weight": 2},
{"weight": 6},
{"weight": 14},
{"weight": 9},
{"weight": 10},
],
)
self.g4 = eg.DiGraph()
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3), (1, 0)]
self.g4.add_edges(
edges,
edges_attr=[
{"weight": 1},
{"weight": 2},
{"weight": 3},
{"weight": 4},
{"weight": 5},
{"weight": 6},
{"weight": 11},
],
)
def test_diameter(self):
print(eg.diameter(self.g2))
print(eg.diameter(self.g3))
print(eg.diameter(self.g4))
def test_eccentricity(self):
print(eg.eccentricity(self.g2, list(self.g2.nodes.keys())[0:-1]))
print(eg.eccentricity(self.g3))
print(eg.eccentricity(self.g4))
def test_single_node_graph(self):
G = eg.Graph()
G.add_node(1)
self.assertEqual(eg.eccentricity(G), {1: 0})
self.assertEqual(eg.diameter(G), 0)
def test_two_node_graph(self):
G = eg.Graph([(1, 2)])
self.assertEqual(eg.eccentricity(G), {1: 1, 2: 1})
self.assertEqual(eg.diameter(G), 1)
def test_disconnected_graph(self):
G = eg.Graph()
G.add_nodes_from([1, 2, 3])
G.add_edge(1, 2)
with self.assertRaises(eg.EasyGraphError):
eg.eccentricity(G)
def test_directed_not_strongly_connected(self):
G = eg.DiGraph()
G.add_edges_from([(1, 2), (2, 3)]) # Not strongly connected
with self.assertRaises(eg.EasyGraphError):
eg.eccentricity(G)
def test_eccentricity_with_sp(self):
G = eg.Graph([(1, 2), (2, 3)])
sp = {
1: {1: 0, 2: 1, 3: 2},
2: {2: 0, 1: 1, 3: 1},
3: {3: 0, 2: 1, 1: 2},
}
self.assertEqual(eg.eccentricity(G, sp=sp), {1: 2, 2: 1, 3: 2})
self.assertEqual(eg.diameter(G, e=eg.eccentricity(G, sp=sp)), 2)
def test_eccentricity_single_node_query(self):
G = eg.Graph([(1, 2), (2, 3)])
self.assertEqual(eg.eccentricity(G, v=1), 2)
self.assertEqual(eg.eccentricity(G, v=2), 1)
def test_eccentricity_subset_of_nodes(self):
G = eg.Graph([(1, 2), (2, 3)])
result = eg.eccentricity(G, v=[1, 3])
self.assertEqual(result[1], 2)
self.assertEqual(result[3], 2)
def test_diameter_matches_max_eccentricity(self):
G = eg.Graph([(1, 2), (2, 3)])
ecc = eg.eccentricity(G)
self.assertEqual(eg.diameter(G, e=ecc), max(ecc.values()))
if __name__ == "__main__":
unittest.main()
+176
View File
@@ -0,0 +1,176 @@
import unittest
import easygraph as eg
class test_mst(unittest.TestCase):
def setUp(self):
self.g1 = eg.get_graph_karateclub()
# source graph: https://zh.wikipedia.org/zh-cn/%E6%88%B4%E5%85%8B%E6%96%AF%E7%89%B9%E6%8B%89%E7%AE%97%E6%B3%95#/media/File:Dijkstra_Animation.gif
edges = [(1, 2), (1, 3), (1, 6), (2, 3), (2, 4), (3, 4), (3, 6), (4, 5), (5, 6)]
self.g2 = eg.Graph(edges)
self.g2.add_edges(
edges,
edges_attr=[
{"weight": 7},
{"weight": 9},
{"weight": 14},
{"weight": 10},
{"weight": 15},
{"weight": 11},
{"weight": 2},
{"weight": 6},
{"weight": 9},
],
)
# source graph: https://static.javatpoint.com/tutorial/daa/images/dijkstra-algorithm.png
self.g3 = eg.Graph()
edges = [
(0, 1),
(0, 4),
(1, 4),
(1, 2),
(4, 5),
(4, 8),
(2, 3),
(2, 6),
(2, 8),
(5, 6),
(5, 8),
(3, 6),
(3, 7),
(6, 7),
]
self.g3.add_edges(
edges,
edges_attr=[
{"weight": 4},
{"weight": 1},
{"weight": 11},
{"weight": 8},
{"weight": 1},
{"weight": 7},
{"weight": 7},
{"weight": 4},
{"weight": 2},
{"weight": 2},
{"weight": 6},
{"weight": 14},
{"weight": 9},
{"weight": 10},
],
)
self.g4 = eg.DiGraph()
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3)]
self.g4.add_edges(
edges,
edges_attr=[
{"weight": -1},
{"weight": -2},
{"weight": -3},
{"weight": -4},
{"weight": -5},
{"weight": -6},
],
)
self.nan_graph = eg.Graph()
self.nan_graph.add_edges(
[(0, 1), (1, 2)], edges_attr=[{"weight": float("nan")}, {"weight": 1}]
)
self.no_weight_graph = eg.Graph()
self.no_weight_graph.add_edges([(0, 1), (1, 2)])
self.equal_weight_graph = eg.Graph()
self.equal_weight_graph.add_edges(
[(0, 1), (1, 2), (2, 0)],
edges_attr=[{"weight": 1}, {"weight": 1}, {"weight": 1}],
)
self.negative_weight_graph = eg.Graph()
self.negative_weight_graph.add_edges(
[(0, 1), (1, 2), (2, 3)],
edges_attr=[{"weight": -1}, {"weight": -2}, {"weight": -3}],
)
self.disconnected_graph = eg.Graph()
self.disconnected_graph.add_edges(
[(0, 1), (2, 3)], edges_attr=[{"weight": 1}, {"weight": 2}]
)
self.G = eg.Graph()
self.G.add_edges(
[(0, 1), (1, 2), (2, 3), (3, 0)],
edges_attr=[{"weight": 1}, {"weight": 2}, {"weight": 3}, {"weight": 4}],
)
def helper(self, g: eg.Graph, func):
result = func(g)
if isinstance(result, eg.Graph):
print("nodes: " + str(result.nodes))
print("edges: " + str(result.edges))
else:
for i in result:
print(i)
def test_minimum_spanning_edges(self):
print("test_minimum_spanning_edges")
self.helper(self.g2, eg.minimum_spanning_edges)
self.helper(self.g2, eg.minimum_spanning_edges)
self.helper(self.g4, eg.minimum_spanning_edges)
def test_maximum_spanning_edges(self):
print("test_maximum_spanning_edges")
self.helper(self.g2, eg.maximum_spanning_edges)
self.helper(self.g2, eg.maximum_spanning_edges)
self.helper(self.g4, eg.maximum_spanning_edges)
def test_minimum_spanning_tree(self):
print("test_minimum_spanning_tree")
self.helper(self.g2, eg.minimum_spanning_tree)
self.helper(self.g2, eg.minimum_spanning_tree)
self.helper(self.g4, eg.minimum_spanning_tree)
def test_maximum_spanning_tree(self):
print("test_maximum_spanning_tree")
self.helper(self.g2, eg.maximum_spanning_tree)
self.helper(self.g2, eg.maximum_spanning_tree)
self.helper(self.g4, eg.maximum_spanning_tree)
def test_nan_handling(self):
with self.assertRaises(ValueError):
list(eg.minimum_spanning_edges(self.nan_graph))
edges = list(eg.minimum_spanning_edges(self.nan_graph, ignore_nan=True))
self.assertEqual(len(edges), 1)
def test_missing_weight_defaults_to_one(self):
edges = list(eg.minimum_spanning_edges(self.no_weight_graph))
self.assertEqual(len(edges), 2)
def test_negative_weights(self):
edges = list(eg.minimum_spanning_edges(self.negative_weight_graph))
weights = [attr["weight"] for _, _, attr in edges]
self.assertIn(-3, weights)
self.assertEqual(len(edges), 3)
def test_disconnected_graph(self):
edges = list(eg.minimum_spanning_edges(self.disconnected_graph))
self.assertEqual(len(edges), 2)
def test_maximum_vs_minimum_edges(self):
min_edges = list(eg.minimum_spanning_edges(self.G))
max_edges = list(eg.maximum_spanning_edges(self.G))
min_set = {(min(u, v), max(u, v)) for u, v, _ in min_edges}
max_set = {(min(u, v), max(u, v)) for u, v, _ in max_edges}
self.assertNotEqual(min_set, max_set)
def test_invalid_algorithm_name(self):
with self.assertRaises(ValueError):
list(eg.minimum_spanning_edges(self.G, algorithm="invalid_algo"))
if __name__ == "__main__":
unittest.main()
+200
View File
@@ -0,0 +1,200 @@
import unittest
import easygraph as eg
class test_path(unittest.TestCase):
def setUp(self):
self.g1 = eg.get_graph_karateclub()
# source graph: https://zh.wikipedia.org/zh-cn/%E6%88%B4%E5%85%8B%E6%96%AF%E7%89%B9%E6%8B%89%E7%AE%97%E6%B3%95#/media/File:Dijkstra_Animation.gif
edges = [(1, 2), (1, 3), (1, 6), (2, 3), (2, 4), (3, 4), (3, 6), (4, 5), (5, 6)]
self.g2 = eg.Graph(edges)
self.g2.add_edges(
edges,
edges_attr=[
{"weight": 7},
{"weight": 9},
{"weight": 14},
{"weight": 10},
{"weight": 15},
{"weight": 11},
{"weight": 2},
{"weight": 6},
{"weight": 9},
],
)
# source graph: https://static.javatpoint.com/tutorial/daa/images/dijkstra-algorithm.png
self.g3 = eg.Graph()
edges = [
(0, 1),
(0, 4),
(1, 4),
(1, 2),
(4, 5),
(4, 8),
(2, 3),
(2, 6),
(2, 8),
(5, 6),
(5, 8),
(3, 6),
(3, 7),
(6, 7),
]
self.g3.add_edges(
edges,
edges_attr=[
{"weight": 4},
{"weight": 1},
{"weight": 11},
{"weight": 8},
{"weight": 1},
{"weight": 7},
{"weight": 7},
{"weight": 4},
{"weight": 2},
{"weight": 2},
{"weight": 6},
{"weight": 14},
{"weight": 9},
{"weight": 10},
],
)
self.g4 = eg.Graph()
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3)]
self.g4.add_edges(
edges,
edges_attr=[
{"weight": -1},
{"weight": -2},
{"weight": -3},
{"weight": -4},
{"weight": -5},
{"weight": -6},
],
)
def test_Dijkstra(self):
print("Dijkstra tested:")
print(eg.Dijkstra(self.g2, node=1))
print(eg.Dijkstra(self.g3, node=0))
print()
def test_Floyd(self):
print("Floyd tested:")
print(eg.Floyd(self.g2))
print(eg.Floyd(self.g3))
print(eg.Floyd(self.g4))
# probably need a negative circle detection for input graphs
print()
def test_Prim(self):
print("Prim tested:")
print(eg.Prim(self.g2))
print(eg.Prim(self.g3))
print(eg.Prim(self.g4))
print()
def test_Kruskal(self):
print("Krusal tested:")
print(eg.Kruskal(self.g2))
print(eg.Kruskal(self.g3))
print(eg.Kruskal(self.g4))
print()
def test_Spfa(self):
try:
print(eg.Spfa(self.g2, 1))
except eg.EasyGraphError as e:
print(e)
def test_single_source_bfs(self):
print("single_source_bfs tested:")
print(eg.single_source_bfs(self.g2, 1, target=5))
print(eg.single_source_bfs(self.g3, 0, target=6))
print(eg.single_source_bfs(self.g4, 0, target=3))
print()
def test_single_source_dijkstra(self):
# identical to Dijkstra method
pass
def test_multi_source_dijkstra(self):
print("multi_source_dijkstra tested")
print(eg.multi_source_dijkstra(self.g2, sources=list(self.g2.nodes.keys())))
print(eg.multi_source_dijkstra(self.g3, sources=list(self.g2.nodes.keys())))
try:
print(eg.multi_source_dijkstra(self.g4, sources=list(self.g2.nodes.keys())))
except ValueError as e:
print(e)
print()
def test_dijkstra_negative_weights_raises(self):
with self.assertRaises(ValueError):
eg.Dijkstra(self.g4, node=0)
def test_dijkstra_disconnected_graph(self):
g = eg.Graph()
g.add_edges([(1, 2)], edges_attr=[{"weight": 3}])
g.add_node(3) # disconnected
result = eg.Dijkstra(g, node=1)
self.assertIn(3, g.nodes)
self.assertNotIn(3, result)
def test_floyd_disconnected_graph(self):
g = eg.Graph()
g.add_edges([(1, 2)], edges_attr=[{"weight": 3}])
g.add_node(3)
result = eg.Floyd(g)
self.assertEqual(result[1][3], float("inf"))
self.assertEqual(result[3][3], 0)
def test_prim_disconnected_graph(self):
g = eg.Graph()
g.add_edges([(0, 1), (2, 3)], edges_attr=[{"weight": 1}, {"weight": 1}])
result = eg.Prim(g)
count = sum(len(v) for v in result.values())
self.assertLess(
count, len(g.nodes) - 1
) # not enough edges to connect all nodes
def test_kruskal_disconnected_graph(self):
g = eg.Graph()
g.add_edges([(0, 1), (2, 3)], edges_attr=[{"weight": 1}, {"weight": 1}])
result = eg.Kruskal(g)
count = sum(len(v) for v in result.values())
self.assertLess(count, len(g.nodes) - 1)
def test_spfa_always_errors(self):
with self.assertRaises(eg.EasyGraphError):
eg.Spfa(self.g2, 0)
def test_single_source_bfs_no_target(self):
result = eg.single_source_bfs(self.g2, 1)
self.assertIn(0, result.values()) # BFS level exists
self.assertIsInstance(result, dict)
def test_single_source_bfs_target_not_found(self):
g = eg.Graph()
g.add_edges([(1, 2)], edges_attr=[{"weight": 1}])
g.add_node(99)
result = eg.single_source_bfs(g, 1, target=99)
self.assertNotIn(99, result)
def test_multi_source_dijkstra_empty_sources(self):
result = eg.multi_source_dijkstra(self.g2, sources=[])
self.assertEqual(result, {})
def test_multi_source_dijkstra_matches_single(self):
sources = [1, 2]
multi = eg.multi_source_dijkstra(self.g2, sources)
for s in sources:
single = eg.single_source_dijkstra(self.g2, s)
self.assertEqual(multi[s], single)
if __name__ == "__main__":
unittest.main()