chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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))
|
||||
@@ -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"
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user