chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
from .biconnected import *
|
||||
from .connected import *
|
||||
from .strongly_connected import *
|
||||
from .weakly_connected import *
|
||||
@@ -0,0 +1,247 @@
|
||||
from itertools import chain
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"is_biconnected",
|
||||
"biconnected_components",
|
||||
"generator_biconnected_components_nodes",
|
||||
"generator_biconnected_components_edges",
|
||||
"generator_articulation_points",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph", "directed")
|
||||
def is_biconnected(G):
|
||||
"""Returns whether the graph is biconnected or not.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_biconnected : boolean
|
||||
`True` if the graph is biconnected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> is_biconnected(G)
|
||||
|
||||
"""
|
||||
bc_nodes = list(generator_biconnected_components_nodes(G))
|
||||
if len(bc_nodes) == 1:
|
||||
return len(bc_nodes[0]) == len(
|
||||
G
|
||||
) # avoid situations where there is isolated vertex
|
||||
return False
|
||||
|
||||
|
||||
@not_implemented_for("multigraph", "directed")
|
||||
# TODO: get the subgraph of each biconnected graph
|
||||
def biconnected_components(G):
|
||||
"""Returns a list of biconnected components, each of which denotes the edges set of a biconnected component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
biconnected_components : list of list
|
||||
Each element list is the edges set of a biconnected component.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> connected_components(G)
|
||||
|
||||
"""
|
||||
return list(generator_biconnected_components_edges(G))
|
||||
|
||||
|
||||
@not_implemented_for("multigraph", "directed")
|
||||
def generator_biconnected_components_nodes(G):
|
||||
"""Returns a generator of nodes in each biconnected component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
Yields nodes set of each biconnected component.
|
||||
|
||||
See Also
|
||||
--------
|
||||
generator_biconnected_components_edges
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> generator_biconnected_components_nodes(G)
|
||||
|
||||
|
||||
"""
|
||||
for component in _biconnected_dfs_record_edges(G, need_components=True):
|
||||
# TODO: only one edge = biconnected_component?
|
||||
yield set(chain.from_iterable(component))
|
||||
|
||||
|
||||
@not_implemented_for("multigraph", "directed")
|
||||
def generator_biconnected_components_edges(G):
|
||||
"""Returns a generator of nodes in each biconnected component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
Yields edges set of each biconnected component.
|
||||
|
||||
See Also
|
||||
--------
|
||||
generator_biconnected_components_nodes
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> generator_biconnected_components_edges(G)
|
||||
|
||||
"""
|
||||
yield from _biconnected_dfs_record_edges(G, need_components=True)
|
||||
|
||||
|
||||
@not_implemented_for("multigraph", "directed")
|
||||
def generator_articulation_points(G):
|
||||
"""Returns a generator of articulation points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
Yields the articulation point in *G*.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> generator_articulation_points(G)
|
||||
|
||||
"""
|
||||
seen = set()
|
||||
for cut_vertex in _biconnected_dfs_record_edges(G, need_components=False):
|
||||
if cut_vertex not in seen:
|
||||
seen.add(cut_vertex)
|
||||
yield cut_vertex
|
||||
|
||||
|
||||
@hybrid("cpp_biconnected_dfs_record_edges")
|
||||
def _biconnected_dfs_record_edges(G, need_components=True):
|
||||
"""
|
||||
References
|
||||
----------
|
||||
https://www.cnblogs.com/nullzx/p/7968110.html
|
||||
https://blog.csdn.net/gauss_acm/article/details/43493903
|
||||
"""
|
||||
# record edges of each biconnected component in traversal
|
||||
# Copied version from EasyGraph
|
||||
# depth-first search algorithm to generate articulation points
|
||||
# and biconnected components
|
||||
visited = set()
|
||||
for start in G:
|
||||
if start in visited:
|
||||
continue
|
||||
discovery = {start: 0} # time of first discovery of node during search
|
||||
low = {start: 0}
|
||||
root_children = 0
|
||||
visited.add(start)
|
||||
edge_stack = []
|
||||
stack = [(start, start, iter(G[start]))]
|
||||
while stack:
|
||||
grandparent, parent, children = stack[-1]
|
||||
try:
|
||||
child = next(children)
|
||||
if grandparent == child:
|
||||
continue
|
||||
if child in visited:
|
||||
if discovery[child] <= discovery[parent]: # back edge
|
||||
low[parent] = min(low[parent], discovery[child])
|
||||
if need_components:
|
||||
edge_stack.append((parent, child))
|
||||
else:
|
||||
low[child] = discovery[child] = len(discovery)
|
||||
visited.add(child)
|
||||
stack.append((parent, child, iter(G[child])))
|
||||
if need_components:
|
||||
edge_stack.append((parent, child))
|
||||
except StopIteration:
|
||||
stack.pop()
|
||||
if len(stack) > 1:
|
||||
if low[parent] >= discovery[grandparent]:
|
||||
if need_components:
|
||||
ind = edge_stack.index((grandparent, parent))
|
||||
yield edge_stack[ind:]
|
||||
edge_stack = edge_stack[:ind]
|
||||
else:
|
||||
yield grandparent
|
||||
low[grandparent] = min(low[parent], low[grandparent])
|
||||
elif stack: # length 1 so grandparent is root
|
||||
root_children += 1
|
||||
if need_components:
|
||||
ind = edge_stack.index((grandparent, parent))
|
||||
yield edge_stack[ind:]
|
||||
if not need_components:
|
||||
# root node is articulation point if it has more than 1 child
|
||||
if root_children > 1:
|
||||
yield start
|
||||
|
||||
|
||||
def _biconnected_dfs_record_nodes(G, need_components=True):
|
||||
# record nodes of each biconnected component in traversal
|
||||
# Not used.
|
||||
visited = set()
|
||||
for start in G:
|
||||
if start in visited:
|
||||
continue
|
||||
discovery = {start: 0} # time of first discovery of node during search
|
||||
low = {start: 0}
|
||||
root_children = 0
|
||||
visited.add(start)
|
||||
node_stack = [start]
|
||||
stack = [(start, start, iter(G[start]))]
|
||||
while stack:
|
||||
grandparent, parent, children = stack[-1]
|
||||
try:
|
||||
child = next(children)
|
||||
if grandparent == child:
|
||||
continue
|
||||
if child in visited:
|
||||
if discovery[child] <= discovery[parent]: # back edge
|
||||
low[parent] = min(low[parent], discovery[child])
|
||||
else:
|
||||
low[child] = discovery[child] = len(discovery)
|
||||
visited.add(child)
|
||||
stack.append((parent, child, iter(G[child])))
|
||||
if need_components:
|
||||
node_stack.append(child)
|
||||
except StopIteration:
|
||||
stack.pop()
|
||||
if len(stack) > 1:
|
||||
if low[parent] >= discovery[grandparent]:
|
||||
if need_components:
|
||||
ind = node_stack.index(grandparent)
|
||||
yield node_stack[ind:]
|
||||
node_stack = node_stack[: ind + 1]
|
||||
else:
|
||||
yield grandparent
|
||||
low[grandparent] = min(low[parent], low[grandparent])
|
||||
elif stack: # length 1 so grandparent is root
|
||||
root_children += 1
|
||||
if need_components:
|
||||
ind = node_stack.index(grandparent)
|
||||
yield node_stack[ind:]
|
||||
if not need_components:
|
||||
# root node is articulation point if it has more than 1 child
|
||||
if root_children > 1:
|
||||
yield start
|
||||
@@ -0,0 +1,160 @@
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"is_connected",
|
||||
"number_connected_components",
|
||||
"connected_components",
|
||||
"connected_components_directed",
|
||||
"connected_component_of_node",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def is_connected(G):
|
||||
"""Returns whether the graph is connected or not.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_biconnected : boolean
|
||||
`True` if the graph is connected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> is_connected(G)
|
||||
|
||||
"""
|
||||
assert len(G) != 0, "No node in the graph."
|
||||
arbitrary_node = next(iter(G)) # Pick an arbitrary node to run BFS
|
||||
return len(G) == sum(1 for node in _plain_bfs(G, arbitrary_node))
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def number_connected_components(G):
|
||||
"""Returns the number of connected components.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
number_connected_components : int
|
||||
The number of connected components.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> number_connected_components(G)
|
||||
|
||||
"""
|
||||
return sum(1 for component in _generator_connected_components(G))
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_connected_components_undirected")
|
||||
def connected_components(G):
|
||||
"""Returns a list of connected components, each of which denotes the edges set of a connected component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph
|
||||
Returns
|
||||
-------
|
||||
connected_components : list of list
|
||||
Each element list is the edges set of a connected component.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> connected_components(G)
|
||||
|
||||
"""
|
||||
seen = set()
|
||||
for v in G:
|
||||
if v not in seen:
|
||||
c = set(_plain_bfs(G, v))
|
||||
seen.update(c)
|
||||
yield c
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_connected_components_directed")
|
||||
def connected_components_directed(G):
|
||||
"""Returns a list of connected components, each of which denotes the edges set of a connected component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.DiGraph
|
||||
Returns
|
||||
-------
|
||||
connected_components : list of list
|
||||
Each element list is the edges set of a connected component.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> connected_components(G)
|
||||
|
||||
"""
|
||||
seen = set()
|
||||
for v in G:
|
||||
if v not in seen:
|
||||
c = set(_plain_bfs(G, v))
|
||||
seen.update(c)
|
||||
yield c
|
||||
|
||||
|
||||
def _generator_connected_components(G):
|
||||
seen = set()
|
||||
for v in G:
|
||||
if v not in seen:
|
||||
component = set(_plain_bfs(G, v))
|
||||
yield component
|
||||
seen.update(component)
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def connected_component_of_node(G, node):
|
||||
"""Returns the connected component that *node* belongs to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph
|
||||
|
||||
node : object
|
||||
The target node
|
||||
|
||||
Returns
|
||||
-------
|
||||
connected_component_of_node : set
|
||||
The connected component that *node* belongs to.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Returns the connected component of one node `Jack`.
|
||||
|
||||
>>> connected_component_of_node(G, node='Jack')
|
||||
|
||||
"""
|
||||
return set(_plain_bfs(G, node))
|
||||
|
||||
|
||||
@hybrid("cpp_plain_bfs")
|
||||
def _plain_bfs(G, source):
|
||||
"""
|
||||
A fast BFS node generator
|
||||
"""
|
||||
G_adj = G.adj
|
||||
seen = set()
|
||||
nextlevel = {source}
|
||||
while nextlevel:
|
||||
thislevel = nextlevel
|
||||
nextlevel = set()
|
||||
for v in thislevel:
|
||||
if v not in seen:
|
||||
yield v
|
||||
seen.add(v)
|
||||
nextlevel.update(G_adj[v])
|
||||
@@ -0,0 +1,244 @@
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"number_strongly_connected_components",
|
||||
"strongly_connected_components",
|
||||
"is_strongly_connected",
|
||||
"condensation",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@hybrid("cpp_strongly_connected_components")
|
||||
def strongly_connected_components(G):
|
||||
"""Generate nodes in strongly connected components of graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph
|
||||
A directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
comp : generator of sets
|
||||
A generator of sets of nodes, one for each strongly connected
|
||||
component of G.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Generate a sorted list of strongly connected components, largest first.
|
||||
|
||||
If you only want the largest component, it's more efficient to
|
||||
use max instead of sort.
|
||||
|
||||
>>> largest = max(eg.strongly_connected_components(G), key=len)
|
||||
|
||||
See Also
|
||||
--------
|
||||
connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
Uses Tarjan's algorithm[1]_ with Nuutila's modifications[2]_.
|
||||
Nonrecursive version of algorithm.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Depth-first search and linear graph algorithms, R. Tarjan
|
||||
SIAM Journal of Computing 1(2):146-160, (1972).
|
||||
|
||||
.. [2] On finding the strongly connected components in a directed graph.
|
||||
E. Nuutila and E. Soisalon-Soinen
|
||||
Information Processing Letters 49(1): 9-14, (1994)..
|
||||
|
||||
"""
|
||||
preorder = {}
|
||||
lowlink = {}
|
||||
scc_found = set()
|
||||
scc_queue = []
|
||||
i = 0 # Preorder counter
|
||||
neighbors = {v: iter(G[v]) for v in G}
|
||||
for source in G:
|
||||
if source not in scc_found:
|
||||
queue = [source]
|
||||
while queue:
|
||||
v = queue[-1]
|
||||
if v not in preorder:
|
||||
i = i + 1
|
||||
preorder[v] = i
|
||||
done = True
|
||||
for w in neighbors[v]:
|
||||
if w not in preorder:
|
||||
queue.append(w)
|
||||
done = False
|
||||
break
|
||||
if done:
|
||||
lowlink[v] = preorder[v]
|
||||
for w in G[v]:
|
||||
if w not in scc_found:
|
||||
if preorder[w] > preorder[v]:
|
||||
lowlink[v] = min([lowlink[v], lowlink[w]])
|
||||
else:
|
||||
lowlink[v] = min([lowlink[v], preorder[w]])
|
||||
queue.pop()
|
||||
if lowlink[v] == preorder[v]:
|
||||
scc = {v}
|
||||
while scc_queue and preorder[scc_queue[-1]] > preorder[v]:
|
||||
k = scc_queue.pop()
|
||||
scc.add(k)
|
||||
scc_found.update(scc)
|
||||
yield scc
|
||||
else:
|
||||
scc_queue.append(v)
|
||||
|
||||
|
||||
def number_strongly_connected_components(G):
|
||||
"""Returns number of strongly connected components in graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : Easygraph graph
|
||||
A directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
n : integer
|
||||
Number of strongly connected components
|
||||
|
||||
Raises
|
||||
------
|
||||
EasygraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.DiGraph([(0, 1), (1, 2), (2, 0), (2, 3), (4, 5), (3, 4), (5, 6), (6, 3), (6, 7)])
|
||||
>>> eg.number_strongly_connected_components(G)
|
||||
3
|
||||
|
||||
See Also
|
||||
--------
|
||||
strongly_connected_components
|
||||
number_connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs only.
|
||||
"""
|
||||
return sum(1 for scc in strongly_connected_components(G))
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
def is_strongly_connected(G):
|
||||
"""Test directed graph for strong connectivity.
|
||||
|
||||
A directed graph is strongly connected if and only if every vertex in
|
||||
the graph is reachable from every other vertex.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph
|
||||
A directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
connected : bool
|
||||
True if the graph is strongly connected, False otherwise.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.DiGraph([(0, 1), (1, 2), (2, 3), (3, 0), (2, 4), (4, 2)])
|
||||
>>> eg.is_strongly_connected(G)
|
||||
True
|
||||
>>> G.remove_edge(2, 3)
|
||||
>>> eg.is_strongly_connected(G)
|
||||
False
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
See Also
|
||||
--------
|
||||
is_connected
|
||||
is_biconnected
|
||||
strongly_connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs only.
|
||||
"""
|
||||
if len(G) == 0:
|
||||
raise eg.EasyGraphPointlessConcept(
|
||||
"""Connectivity is undefined for the null graph."""
|
||||
)
|
||||
|
||||
return len(next(strongly_connected_components(G))) == len(G)
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@only_implemented_for_Directed_graph
|
||||
def condensation(G, scc=None):
|
||||
"""Returns the condensation of G.
|
||||
The condensation of G is the graph with each of the strongly connected
|
||||
components contracted into a single node.
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.DiGraph
|
||||
A directed graph.
|
||||
scc: list or generator (optional, default=None)
|
||||
Strongly connected components. If provided, the elements in
|
||||
`scc` must partition the nodes in `G`. If not provided, it will be
|
||||
calculated as scc=strongly_connected_components(G).
|
||||
Returns
|
||||
-------
|
||||
C : easygraph.DiGraph
|
||||
The condensation graph C of G. The node labels are integers
|
||||
corresponding to the index of the component in the list of
|
||||
strongly connected components of G. C has a graph attribute named
|
||||
'mapping' with a dictionary mapping the original nodes to the
|
||||
nodes in C to which they belong. Each node in C also has a node
|
||||
attribute 'members' with the set of original nodes in G that
|
||||
form the SCC that the node in C represents.
|
||||
Examples
|
||||
--------
|
||||
# >>> condensation(G)
|
||||
Notes
|
||||
-----
|
||||
After contracting all strongly connected components to a single node,
|
||||
the resulting graph is a directed acyclic graph.
|
||||
"""
|
||||
if scc is None:
|
||||
scc = strongly_connected_components(G)
|
||||
mapping = {}
|
||||
incoming_info = {}
|
||||
members = {}
|
||||
C = eg.DiGraph()
|
||||
# Add mapping dict as graph attribute
|
||||
C.graph["mapping"] = mapping
|
||||
if len(G) == 0:
|
||||
return C
|
||||
for i, component in enumerate(scc):
|
||||
members[i] = component
|
||||
mapping.update((n, i) for n in component)
|
||||
number_of_components = i + 1
|
||||
for i in range(number_of_components):
|
||||
C.add_node(i, member=members[i], incoming=set())
|
||||
C.add_nodes(range(number_of_components))
|
||||
for edge in G.edges:
|
||||
if mapping[edge[0]] != mapping[edge[1]]:
|
||||
C.add_edge(mapping[edge[0]], mapping[edge[1]])
|
||||
if edge[1] not in incoming_info.keys():
|
||||
incoming_info[edge[1]] = set()
|
||||
incoming_info[edge[1]].add(edge[0])
|
||||
C.graph["incoming_info"] = incoming_info
|
||||
return C
|
||||
@@ -0,0 +1,92 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
from easygraph import biconnected_components
|
||||
from easygraph import generator_articulation_points
|
||||
from easygraph import generator_biconnected_components_edges
|
||||
from easygraph import generator_biconnected_components_nodes
|
||||
from easygraph import is_biconnected
|
||||
|
||||
|
||||
class Test_biconnected(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [(1, 2), (2, 3), ("String", "Bool"), (2, 1), (0, 0), (-99, 256)]
|
||||
self.test_graphs = [eg.Graph(), eg.MultiGraph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
def test_is_biconnected(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.is_biconnected(i))
|
||||
|
||||
def test_biconnected_components(self):
|
||||
for i in self.test_graphs:
|
||||
eg.biconnected_components(i)
|
||||
|
||||
def test_generator_biconnected_components_nodes(self):
|
||||
for i in self.test_graphs:
|
||||
eg.generator_biconnected_components_nodes(i)
|
||||
|
||||
def test_generator_biconnected_components_edges(self):
|
||||
for i in self.test_graphs:
|
||||
eg.generator_biconnected_components_edges(i)
|
||||
|
||||
def test_generator_articulation_points(self):
|
||||
for i in self.test_graphs:
|
||||
eg.generator_articulation_points(i)
|
||||
|
||||
|
||||
class TestBiconnectedFunctions(unittest.TestCase):
|
||||
def test_single_node(self):
|
||||
G = eg.Graph()
|
||||
G.add_node(1)
|
||||
self.assertFalse(is_biconnected(G))
|
||||
self.assertEqual(list(biconnected_components(G)), [])
|
||||
self.assertEqual(list(generator_articulation_points(G)), [])
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
self.assertFalse(is_biconnected(G))
|
||||
self.assertGreaterEqual(len(list(generator_biconnected_components_edges(G))), 1)
|
||||
|
||||
def test_triangle(self):
|
||||
G = eg.Graph([(0, 1), (1, 2), (2, 0)])
|
||||
self.assertTrue(is_biconnected(G))
|
||||
comps = list(biconnected_components(G))
|
||||
self.assertEqual(len(comps), 1)
|
||||
self.assertEqual(set(comps[0]), {(0, 1), (1, 2), (2, 0)})
|
||||
self.assertEqual(list(generator_articulation_points(G)), [])
|
||||
|
||||
def test_with_articulation_point(self):
|
||||
G = eg.Graph([(0, 1), (1, 2), (1, 3)])
|
||||
self.assertFalse(is_biconnected(G))
|
||||
arts = list(generator_articulation_points(G))
|
||||
self.assertIn(1, arts)
|
||||
self.assertEqual(len(arts), 1)
|
||||
|
||||
def test_cycle_plus_leaf(self):
|
||||
G = eg.Graph([(0, 1), (1, 2), (2, 0), (2, 3)])
|
||||
self.assertFalse(is_biconnected(G))
|
||||
arts = list(generator_articulation_points(G))
|
||||
self.assertIn(2, arts)
|
||||
|
||||
def test_multiple_biconnected_components(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3), (3, 1)]) # triangle
|
||||
G.add_edges_from([(3, 4), (4, 5)]) # path
|
||||
components = list(generator_biconnected_components_edges(G))
|
||||
self.assertEqual(len(components), 3)
|
||||
nodes_comps = list(generator_biconnected_components_nodes(G))
|
||||
self.assertTrue(any({1, 2, 3}.issubset(comp) for comp in nodes_comps))
|
||||
self.assertTrue(any({4, 5}.issubset(comp) for comp in nodes_comps))
|
||||
|
||||
def test_articulation_points_multiple(self):
|
||||
G = eg.Graph([(0, 1), (1, 2), (2, 3), (3, 4)])
|
||||
aps = list(generator_articulation_points(G))
|
||||
self.assertEqual(aps, [3, 2, 1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,112 @@
|
||||
import inspect
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph import connected_component_of_node
|
||||
from easygraph import connected_components
|
||||
from easygraph import connected_components_directed
|
||||
from easygraph import is_connected
|
||||
from easygraph import number_connected_components
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class TestConnected(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [(1, 2), (2, 3), (0, 4), (2, 1), (0, 0), (-99, 256)]
|
||||
self.test_graphs = [eg.Graph([(4, -4)]), eg.DiGraph([(4, -4)])]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
def test_is_connected(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.is_connected(i))
|
||||
|
||||
def test_number_connected_components(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.number_connected_components(i))
|
||||
|
||||
def test_connected_components(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.connected_components(i))
|
||||
|
||||
def test_connected_components_directed(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.connected_components_directed(i))
|
||||
|
||||
def test_connected_component_of_node(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.connected_component_of_node(i, 4))
|
||||
|
||||
def test_empty_graph(self):
|
||||
G = eg.Graph()
|
||||
with self.assertRaises(AssertionError):
|
||||
is_connected(G)
|
||||
self.assertEqual(number_connected_components(G), 0)
|
||||
self.assertEqual(list(connected_components(G)), [])
|
||||
|
||||
def test_single_node(self):
|
||||
G = eg.Graph()
|
||||
G.add_node(1)
|
||||
self.assertTrue(is_connected(G))
|
||||
self.assertEqual(number_connected_components(G), 1)
|
||||
self.assertEqual(list(connected_components(G)), [{1}])
|
||||
self.assertEqual(connected_component_of_node(G, 1), {1})
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
self.assertFalse(is_connected(G))
|
||||
self.assertEqual(number_connected_components(G), 2)
|
||||
comps = list(connected_components(G))
|
||||
self.assertTrue({0, 1} in comps and {2, 3} in comps)
|
||||
|
||||
def test_connected_graph(self):
|
||||
G = eg.path_graph(5)
|
||||
self.assertTrue(is_connected(G))
|
||||
self.assertEqual(number_connected_components(G), 1)
|
||||
comps = list(connected_components(G))
|
||||
self.assertEqual(len(comps), 1)
|
||||
self.assertEqual(comps[0], set(range(5)))
|
||||
|
||||
def test_node_component_lookup(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
comp = connected_component_of_node(G, 0)
|
||||
self.assertEqual(comp, {0, 1})
|
||||
with self.assertRaises(KeyError):
|
||||
connected_component_of_node(G, 999) # non-existent node
|
||||
|
||||
def test_undirected_with_self_loops(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 1), (2, 2), (1, 2)])
|
||||
self.assertTrue(is_connected(G))
|
||||
self.assertEqual(number_connected_components(G), 1)
|
||||
self.assertEqual(list(connected_components(G))[0], {1, 2})
|
||||
|
||||
def test_directed_components(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
self.assertEqual(number_connected_components(G), 2)
|
||||
components = list(connected_components_directed(G))
|
||||
self.assertTrue({0, 1} in components and {2, 3} in components)
|
||||
|
||||
def test_directed_strong_vs_weak(self):
|
||||
G = eg.DiGraph([(0, 1), (1, 0), (2, 3)])
|
||||
comps = list(connected_components_directed(G))
|
||||
self.assertTrue({0, 1} in comps)
|
||||
self.assertTrue({2, 3} in comps)
|
||||
|
||||
def test_multigraph_blocked(self):
|
||||
G = eg.MultiGraph([(1, 2), (2, 3)])
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
is_connected(G)
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
number_connected_components(G)
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
list(connected_components(G))
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
connected_component_of_node(G, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,121 @@
|
||||
import inspect
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph import condensation
|
||||
from easygraph import is_strongly_connected
|
||||
from easygraph import number_strongly_connected_components
|
||||
from easygraph import strongly_connected_components
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
from easygraph.utils.exception import EasyGraphPointlessConcept
|
||||
|
||||
|
||||
class Test_strongly_connected(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [(1, 2), (2, 3), ("String", "Bool"), (2, 1), (0, 0), (-99, 256)]
|
||||
self.test_graphs = [eg.Graph([(4, -4)]), eg.DiGraph([(4, False)])]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
def test_empty_graph(self):
|
||||
G = eg.DiGraph()
|
||||
with self.assertRaises(EasyGraphPointlessConcept):
|
||||
is_strongly_connected(G)
|
||||
self.assertEqual(number_strongly_connected_components(G), 0)
|
||||
self.assertEqual(list(strongly_connected_components(G)), [])
|
||||
|
||||
def test_single_node(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_node(1)
|
||||
self.assertTrue(is_strongly_connected(G))
|
||||
self.assertEqual(number_strongly_connected_components(G), 1)
|
||||
scc = list(strongly_connected_components(G))
|
||||
self.assertEqual(scc, [{1}])
|
||||
|
||||
def test_cycle_graph(self):
|
||||
G = eg.DiGraph([(1, 2), (2, 3), (3, 1)])
|
||||
self.assertTrue(is_strongly_connected(G))
|
||||
self.assertEqual(number_strongly_connected_components(G), 1)
|
||||
scc = list(strongly_connected_components(G))
|
||||
self.assertEqual(scc, [{1, 2, 3}])
|
||||
|
||||
def test_disconnected_scc(self):
|
||||
G = eg.DiGraph([(0, 1), (1, 0), (2, 3), (3, 2), (4, 5)])
|
||||
scc = list(strongly_connected_components(G))
|
||||
self.assertEqual(len(scc), 4)
|
||||
self.assertIn({0, 1}, scc)
|
||||
self.assertIn({2, 3}, scc)
|
||||
self.assertIn({4}, scc)
|
||||
self.assertIn({5}, scc)
|
||||
self.assertFalse(is_strongly_connected(G))
|
||||
self.assertEqual(number_strongly_connected_components(G), 4)
|
||||
|
||||
def test_scc_with_self_loops(self):
|
||||
G = eg.DiGraph([(1, 1), (2, 2), (3, 4), (4, 3)])
|
||||
scc = list(strongly_connected_components(G))
|
||||
self.assertEqual(len(scc), 3)
|
||||
self.assertIn({1}, scc)
|
||||
self.assertIn({2}, scc)
|
||||
self.assertIn({3, 4}, scc)
|
||||
|
||||
def test_condensation_structure(self):
|
||||
G = eg.DiGraph(
|
||||
[(0, 1), (1, 2), (2, 0), (2, 3), (4, 5), (3, 4), (5, 6), (6, 3), (6, 7)]
|
||||
)
|
||||
cond = condensation(G)
|
||||
self.assertTrue(cond.is_directed())
|
||||
self.assertIn("mapping", cond.graph)
|
||||
self.assertEqual(len(cond), number_strongly_connected_components(G))
|
||||
|
||||
def has_cycle(G):
|
||||
visited = set()
|
||||
temp_mark = set()
|
||||
|
||||
def visit(node):
|
||||
if node in temp_mark:
|
||||
return True
|
||||
if node in visited:
|
||||
return False
|
||||
temp_mark.add(node)
|
||||
for neighbor in G[node]:
|
||||
if visit(neighbor):
|
||||
return True
|
||||
temp_mark.remove(node)
|
||||
visited.add(node)
|
||||
return False
|
||||
|
||||
return any(visit(v) for v in G)
|
||||
|
||||
self.assertFalse(has_cycle(cond))
|
||||
|
||||
def test_condensation_empty_graph(self):
|
||||
G = eg.DiGraph()
|
||||
C = condensation(G)
|
||||
self.assertEqual(len(C), 0)
|
||||
|
||||
def test_undirected_raises(self):
|
||||
G = eg.Graph([(1, 2), (2, 3)])
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
list(strongly_connected_components(G))
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
is_strongly_connected(G)
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
number_strongly_connected_components(G)
|
||||
|
||||
def test_condensation_on_undirected_graph_raises(self):
|
||||
G = eg.Graph([(1, 2), (2, 3)])
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
condensation(G)
|
||||
|
||||
def test_condensation_manual_scc_input(self):
|
||||
G = eg.DiGraph([(1, 2), (2, 1), (3, 4)])
|
||||
scc = list(strongly_connected_components(G))
|
||||
C = condensation(G, scc=scc)
|
||||
self.assertEqual(len(C.nodes), len(scc))
|
||||
# Check if mapping is consistent
|
||||
all_mapped = set(C.graph["mapping"].keys())
|
||||
self.assertEqual(all_mapped, set(G.nodes))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,82 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph import is_weakly_connected
|
||||
from easygraph import number_weakly_connected_components
|
||||
from easygraph import weakly_connected_components
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
from easygraph.utils.exception import EasyGraphPointlessConcept
|
||||
|
||||
|
||||
class Test_weakly_connected(unittest.TestCase):
|
||||
def test_empty_graph(self):
|
||||
G = eg.DiGraph()
|
||||
with self.assertRaises(EasyGraphPointlessConcept):
|
||||
is_weakly_connected(G)
|
||||
self.assertEqual(number_weakly_connected_components(G), 0)
|
||||
self.assertEqual(list(weakly_connected_components(G)), [])
|
||||
|
||||
def test_single_node(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_node(1)
|
||||
self.assertTrue(is_weakly_connected(G))
|
||||
self.assertEqual(number_weakly_connected_components(G), 1)
|
||||
self.assertEqual(list(weakly_connected_components(G)), [{1}])
|
||||
|
||||
def test_connected_graph(self):
|
||||
G = eg.DiGraph([(1, 2), (2, 3), (3, 4)])
|
||||
self.assertTrue(is_weakly_connected(G))
|
||||
self.assertEqual(number_weakly_connected_components(G), 1)
|
||||
self.assertEqual(list(weakly_connected_components(G)), [{1, 2, 3, 4}])
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
G = eg.DiGraph([(1, 2), (3, 4)])
|
||||
self.assertFalse(is_weakly_connected(G))
|
||||
wcc = list(weakly_connected_components(G))
|
||||
self.assertEqual(len(wcc), 2)
|
||||
self.assertIn({1, 2}, wcc)
|
||||
self.assertIn({3, 4}, wcc)
|
||||
|
||||
def test_self_loops(self):
|
||||
G = eg.DiGraph([(1, 1), (2, 2)])
|
||||
wcc = list(weakly_connected_components(G))
|
||||
self.assertEqual(len(wcc), 2)
|
||||
self.assertIn({1}, wcc)
|
||||
self.assertIn({2}, wcc)
|
||||
self.assertFalse(is_weakly_connected(G))
|
||||
|
||||
def test_multiple_components(self):
|
||||
G = eg.DiGraph([(1, 2), (3, 4), (5, 6), (6, 5)])
|
||||
wcc = list(weakly_connected_components(G))
|
||||
self.assertEqual(number_weakly_connected_components(G), 3)
|
||||
self.assertIn({1, 2}, wcc)
|
||||
self.assertIn({3, 4}, wcc)
|
||||
self.assertIn({5, 6}, wcc)
|
||||
|
||||
def test_unconnected_nodes(self):
|
||||
G = eg.DiGraph([(1, 2), (3, 4)])
|
||||
G.add_node(99) # isolated node
|
||||
wcc = list(weakly_connected_components(G))
|
||||
self.assertEqual(len(wcc), 3)
|
||||
self.assertIn({99}, wcc)
|
||||
|
||||
def test_is_weakly_connected_after_adding_edge(self):
|
||||
G = eg.DiGraph([(0, 1), (2, 1)])
|
||||
G.add_node(3)
|
||||
self.assertFalse(is_weakly_connected(G))
|
||||
G.add_edge(2, 3)
|
||||
self.assertTrue(is_weakly_connected(G))
|
||||
|
||||
def test_undirected_raises(self):
|
||||
G = eg.Graph([(1, 2), (2, 3)])
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
is_weakly_connected(G)
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
number_weakly_connected_components(G)
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
list(weakly_connected_components(G))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Weakly connected components."""
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.decorators import not_implemented_for
|
||||
|
||||
|
||||
__all__ = [
|
||||
"number_weakly_connected_components",
|
||||
"weakly_connected_components",
|
||||
"is_weakly_connected",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
def weakly_connected_components(G):
|
||||
"""Generate weakly connected components of G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph graph
|
||||
A directed graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
comp : generator of sets
|
||||
A generator of sets of nodes, one for each weakly connected
|
||||
component of G.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Generate a sorted list of weakly connected components, largest first.
|
||||
|
||||
>>> G = eg.path_graph(4, create_using=eg.DiGraph())
|
||||
>>> eg.add_path(G, [10, 11, 12])
|
||||
>>> [
|
||||
... len(c)
|
||||
... for c in sorted(eg.weakly_connected_components(G), key=len, reverse=True)
|
||||
... ]
|
||||
[4, 3]
|
||||
|
||||
If you only want the largest component, it's more efficient to
|
||||
use max instead of sort:
|
||||
|
||||
>>> largest_cc = max(eg.weakly_connected_components(G), key=len)
|
||||
|
||||
See Also
|
||||
--------
|
||||
connected_components
|
||||
strongly_connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs only.
|
||||
|
||||
"""
|
||||
seen = set()
|
||||
for v in G:
|
||||
if v not in seen:
|
||||
c = set(_plain_bfs(G, v))
|
||||
seen.update(c)
|
||||
yield c
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
def number_weakly_connected_components(G):
|
||||
"""Returns the number of weakly connected components in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph graph
|
||||
A directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
n : integer
|
||||
Number of weakly connected components
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.DiGraph([(0, 1), (2, 1), (3, 4)])
|
||||
>>> eg.number_weakly_connected_components(G)
|
||||
2
|
||||
|
||||
See Also
|
||||
--------
|
||||
weakly_connected_components
|
||||
number_connected_components
|
||||
number_strongly_connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs only.
|
||||
|
||||
"""
|
||||
return sum(1 for wcc in weakly_connected_components(G))
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
def is_weakly_connected(G):
|
||||
"""Test directed graph for weak connectivity.
|
||||
|
||||
A directed graph is weakly connected if and only if the graph
|
||||
is connected when the direction of the edge between nodes is ignored.
|
||||
|
||||
Note that if a graph is strongly connected (i.e. the graph is connected
|
||||
even when we account for directionality), it is by definition weakly
|
||||
connected as well.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph
|
||||
A directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
connected : bool
|
||||
True if the graph is weakly connected, False otherwise.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.DiGraph([(0, 1), (2, 1)])
|
||||
>>> G.add_node(3)
|
||||
>>> eg.is_weakly_connected(G) # node 3 is not connected to the graph
|
||||
False
|
||||
>>> G.add_edge(2, 3)
|
||||
>>> eg.is_weakly_connected(G)
|
||||
True
|
||||
|
||||
See Also
|
||||
--------
|
||||
is_strongly_connected
|
||||
is_semiconnected
|
||||
is_connected
|
||||
is_biconnected
|
||||
weakly_connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs only.
|
||||
|
||||
"""
|
||||
if len(G) == 0:
|
||||
raise eg.EasyGraphPointlessConcept(
|
||||
"""Connectivity is undefined for the null graph."""
|
||||
)
|
||||
|
||||
return len(next(weakly_connected_components(G))) == len(G)
|
||||
|
||||
|
||||
def _plain_bfs(G, source):
|
||||
"""A fast BFS node generator
|
||||
|
||||
The direction of the edge between nodes is ignored.
|
||||
|
||||
For directed graphs only.
|
||||
|
||||
"""
|
||||
Gsucc = G.adj
|
||||
Gpred = G.pred
|
||||
|
||||
seen = set()
|
||||
nextlevel = {source}
|
||||
while nextlevel:
|
||||
thislevel = nextlevel
|
||||
nextlevel = set()
|
||||
for v in thislevel:
|
||||
if v not in seen:
|
||||
seen.add(v)
|
||||
nextlevel.update(Gsucc[v])
|
||||
nextlevel.update(Gpred[v])
|
||||
yield v
|
||||
Reference in New Issue
Block a user