chore: import upstream snapshot with attribution

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