aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/__init__.py11
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/connectivity.py811
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/cuts.py612
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/disjoint_paths.py408
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_augmentation.py1270
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_kcomponents.py592
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcomponents.py223
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcutsets.py235
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/stoerwagner.py152
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_connectivity.py421
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_cuts.py309
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_disjoint_paths.py249
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_edge_augmentation.py502
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_edge_kcomponents.py488
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_kcomponents.py296
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_kcutsets.py273
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_stoer_wagner.py102
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/utils.py88
19 files changed, 7042 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/__init__.py
new file mode 100644
index 00000000..d08a3606
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/__init__.py
@@ -0,0 +1,11 @@
+"""Connectivity and cut algorithms"""
+
+from .connectivity import *
+from .cuts import *
+from .edge_augmentation import *
+from .edge_kcomponents import *
+from .disjoint_paths import *
+from .kcomponents import *
+from .kcutsets import *
+from .stoerwagner import *
+from .utils import *
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/connectivity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/connectivity.py
new file mode 100644
index 00000000..2f85c865
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/connectivity.py
@@ -0,0 +1,811 @@
+"""
+Flow based connectivity algorithms
+"""
+
+import itertools
+from operator import itemgetter
+
+import networkx as nx
+
+# Define the default maximum flow function to use in all flow based
+# connectivity algorithms.
+from networkx.algorithms.flow import (
+ boykov_kolmogorov,
+ build_residual_network,
+ dinitz,
+ edmonds_karp,
+ preflow_push,
+ shortest_augmenting_path,
+)
+
+default_flow_func = edmonds_karp
+
+from .utils import build_auxiliary_edge_connectivity, build_auxiliary_node_connectivity
+
+__all__ = [
+ "average_node_connectivity",
+ "local_node_connectivity",
+ "node_connectivity",
+ "local_edge_connectivity",
+ "edge_connectivity",
+ "all_pairs_node_connectivity",
+]
+
+
+@nx._dispatchable(graphs={"G": 0, "auxiliary?": 4}, preserve_graph_attrs={"auxiliary"})
+def local_node_connectivity(
+ G, s, t, flow_func=None, auxiliary=None, residual=None, cutoff=None
+):
+ r"""Computes local node connectivity for nodes s and t.
+
+ Local node connectivity for two non adjacent nodes s and t is the
+ minimum number of nodes that must be removed (along with their incident
+ edges) to disconnect them.
+
+ This is a flow based implementation of node connectivity. We compute the
+ maximum flow on an auxiliary digraph build from the original input
+ graph (see below for details).
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Undirected graph
+
+ s : node
+ Source node
+
+ t : node
+ Target node
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See below for details. The choice
+ of the default function may change from version to version and
+ should not be relied on. Default value: None.
+
+ auxiliary : NetworkX DiGraph
+ Auxiliary digraph to compute flow based node connectivity. It has
+ to have a graph attribute called mapping with a dictionary mapping
+ node names in G and in the auxiliary digraph. If provided
+ it will be reused instead of recreated. Default value: None.
+
+ residual : NetworkX DiGraph
+ Residual network to compute maximum flow. If provided it will be
+ reused instead of recreated. Default value: None.
+
+ cutoff : integer, float, or None (default: None)
+ If specified, the maximum flow algorithm will terminate when the
+ flow value reaches or exceeds the cutoff. This only works for flows
+ that support the cutoff parameter (most do) and is ignored otherwise.
+
+ Returns
+ -------
+ K : integer
+ local node connectivity for nodes s and t
+
+ Examples
+ --------
+ This function is not imported in the base NetworkX namespace, so you
+ have to explicitly import it from the connectivity package:
+
+ >>> from networkx.algorithms.connectivity import local_node_connectivity
+
+ We use in this example the platonic icosahedral graph, which has node
+ connectivity 5.
+
+ >>> G = nx.icosahedral_graph()
+ >>> local_node_connectivity(G, 0, 6)
+ 5
+
+ If you need to compute local connectivity on several pairs of
+ nodes in the same graph, it is recommended that you reuse the
+ data structures that NetworkX uses in the computation: the
+ auxiliary digraph for node connectivity, and the residual
+ network for the underlying maximum flow computation.
+
+ Example of how to compute local node connectivity among
+ all pairs of nodes of the platonic icosahedral graph reusing
+ the data structures.
+
+ >>> import itertools
+ >>> # You also have to explicitly import the function for
+ >>> # building the auxiliary digraph from the connectivity package
+ >>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity
+ >>> H = build_auxiliary_node_connectivity(G)
+ >>> # And the function for building the residual network from the
+ >>> # flow package
+ >>> from networkx.algorithms.flow import build_residual_network
+ >>> # Note that the auxiliary digraph has an edge attribute named capacity
+ >>> R = build_residual_network(H, "capacity")
+ >>> result = dict.fromkeys(G, dict())
+ >>> # Reuse the auxiliary digraph and the residual network by passing them
+ >>> # as parameters
+ >>> for u, v in itertools.combinations(G, 2):
+ ... k = local_node_connectivity(G, u, v, auxiliary=H, residual=R)
+ ... result[u][v] = k
+ >>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
+ True
+
+ You can also use alternative flow algorithms for computing node
+ connectivity. For instance, in dense networks the algorithm
+ :meth:`shortest_augmenting_path` will usually perform better than
+ the default :meth:`edmonds_karp` which is faster for sparse
+ networks with highly skewed degree distributions. Alternative flow
+ functions have to be explicitly imported from the flow package.
+
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
+ >>> local_node_connectivity(G, 0, 6, flow_func=shortest_augmenting_path)
+ 5
+
+ Notes
+ -----
+ This is a flow based implementation of node connectivity. We compute the
+ maximum flow using, by default, the :meth:`edmonds_karp` algorithm (see:
+ :meth:`maximum_flow`) on an auxiliary digraph build from the original
+ input graph:
+
+ For an undirected graph G having `n` nodes and `m` edges we derive a
+ directed graph H with `2n` nodes and `2m+n` arcs by replacing each
+ original node `v` with two nodes `v_A`, `v_B` linked by an (internal)
+ arc in H. Then for each edge (`u`, `v`) in G we add two arcs
+ (`u_B`, `v_A`) and (`v_B`, `u_A`) in H. Finally we set the attribute
+ capacity = 1 for each arc in H [1]_ .
+
+ For a directed graph G having `n` nodes and `m` arcs we derive a
+ directed graph H with `2n` nodes and `m+n` arcs by replacing each
+ original node `v` with two nodes `v_A`, `v_B` linked by an (internal)
+ arc (`v_A`, `v_B`) in H. Then for each arc (`u`, `v`) in G we add one arc
+ (`u_B`, `v_A`) in H. Finally we set the attribute capacity = 1 for
+ each arc in H.
+
+ This is equal to the local node connectivity because the value of
+ a maximum s-t-flow is equal to the capacity of a minimum s-t-cut.
+
+ See also
+ --------
+ :meth:`local_edge_connectivity`
+ :meth:`node_connectivity`
+ :meth:`minimum_node_cut`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ References
+ ----------
+ .. [1] Kammer, Frank and Hanjo Taubig. Graph Connectivity. in Brandes and
+ Erlebach, 'Network Analysis: Methodological Foundations', Lecture
+ Notes in Computer Science, Volume 3418, Springer-Verlag, 2005.
+ http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf
+
+ """
+ if flow_func is None:
+ flow_func = default_flow_func
+
+ if auxiliary is None:
+ H = build_auxiliary_node_connectivity(G)
+ else:
+ H = auxiliary
+
+ mapping = H.graph.get("mapping", None)
+ if mapping is None:
+ raise nx.NetworkXError("Invalid auxiliary digraph.")
+
+ kwargs = {"flow_func": flow_func, "residual": residual}
+
+ if flow_func is not preflow_push:
+ kwargs["cutoff"] = cutoff
+
+ if flow_func is shortest_augmenting_path:
+ kwargs["two_phase"] = True
+
+ return nx.maximum_flow_value(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs)
+
+
+@nx._dispatchable
+def node_connectivity(G, s=None, t=None, flow_func=None):
+ r"""Returns node connectivity for a graph or digraph G.
+
+ Node connectivity is equal to the minimum number of nodes that
+ must be removed to disconnect G or render it trivial. If source
+ and target nodes are provided, this function returns the local node
+ connectivity: the minimum number of nodes that must be removed to break
+ all paths from source to target in G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Undirected graph
+
+ s : node
+ Source node. Optional. Default value: None.
+
+ t : node
+ Target node. Optional. Default value: None.
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See below for details. The
+ choice of the default function may change from version
+ to version and should not be relied on. Default value: None.
+
+ Returns
+ -------
+ K : integer
+ Node connectivity of G, or local node connectivity if source
+ and target are provided.
+
+ Examples
+ --------
+ >>> # Platonic icosahedral graph is 5-node-connected
+ >>> G = nx.icosahedral_graph()
+ >>> nx.node_connectivity(G)
+ 5
+
+ You can use alternative flow algorithms for the underlying maximum
+ flow computation. In dense networks the algorithm
+ :meth:`shortest_augmenting_path` will usually perform better
+ than the default :meth:`edmonds_karp`, which is faster for
+ sparse networks with highly skewed degree distributions. Alternative
+ flow functions have to be explicitly imported from the flow package.
+
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
+ >>> nx.node_connectivity(G, flow_func=shortest_augmenting_path)
+ 5
+
+ If you specify a pair of nodes (source and target) as parameters,
+ this function returns the value of local node connectivity.
+
+ >>> nx.node_connectivity(G, 3, 7)
+ 5
+
+ If you need to perform several local computations among different
+ pairs of nodes on the same graph, it is recommended that you reuse
+ the data structures used in the maximum flow computations. See
+ :meth:`local_node_connectivity` for details.
+
+ Notes
+ -----
+ This is a flow based implementation of node connectivity. The
+ algorithm works by solving $O((n-\delta-1+\delta(\delta-1)/2))$
+ maximum flow problems on an auxiliary digraph. Where $\delta$
+ is the minimum degree of G. For details about the auxiliary
+ digraph and the computation of local node connectivity see
+ :meth:`local_node_connectivity`. This implementation is based
+ on algorithm 11 in [1]_.
+
+ See also
+ --------
+ :meth:`local_node_connectivity`
+ :meth:`edge_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ References
+ ----------
+ .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
+ http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
+
+ """
+ if (s is not None and t is None) or (s is None and t is not None):
+ raise nx.NetworkXError("Both source and target must be specified.")
+
+ # Local node connectivity
+ if s is not None and t is not None:
+ if s not in G:
+ raise nx.NetworkXError(f"node {s} not in graph")
+ if t not in G:
+ raise nx.NetworkXError(f"node {t} not in graph")
+ return local_node_connectivity(G, s, t, flow_func=flow_func)
+
+ # Global node connectivity
+ if G.is_directed():
+ if not nx.is_weakly_connected(G):
+ return 0
+ iter_func = itertools.permutations
+ # It is necessary to consider both predecessors
+ # and successors for directed graphs
+
+ def neighbors(v):
+ return itertools.chain.from_iterable([G.predecessors(v), G.successors(v)])
+
+ else:
+ if not nx.is_connected(G):
+ return 0
+ iter_func = itertools.combinations
+ neighbors = G.neighbors
+
+ # Reuse the auxiliary digraph and the residual network
+ H = build_auxiliary_node_connectivity(G)
+ R = build_residual_network(H, "capacity")
+ kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R}
+
+ # Pick a node with minimum degree
+ # Node connectivity is bounded by degree.
+ v, K = min(G.degree(), key=itemgetter(1))
+ # compute local node connectivity with all its non-neighbors nodes
+ for w in set(G) - set(neighbors(v)) - {v}:
+ kwargs["cutoff"] = K
+ K = min(K, local_node_connectivity(G, v, w, **kwargs))
+ # Also for non adjacent pairs of neighbors of v
+ for x, y in iter_func(neighbors(v), 2):
+ if y in G[x]:
+ continue
+ kwargs["cutoff"] = K
+ K = min(K, local_node_connectivity(G, x, y, **kwargs))
+
+ return K
+
+
+@nx._dispatchable
+def average_node_connectivity(G, flow_func=None):
+ r"""Returns the average connectivity of a graph G.
+
+ The average connectivity `\bar{\kappa}` of a graph G is the average
+ of local node connectivity over all pairs of nodes of G [1]_ .
+
+ .. math::
+
+ \bar{\kappa}(G) = \frac{\sum_{u,v} \kappa_{G}(u,v)}{{n \choose 2}}
+
+ Parameters
+ ----------
+
+ G : NetworkX graph
+ Undirected graph
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See :meth:`local_node_connectivity`
+ for details. The choice of the default function may change from
+ version to version and should not be relied on. Default value: None.
+
+ Returns
+ -------
+ K : float
+ Average node connectivity
+
+ See also
+ --------
+ :meth:`local_node_connectivity`
+ :meth:`node_connectivity`
+ :meth:`edge_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ References
+ ----------
+ .. [1] Beineke, L., O. Oellermann, and R. Pippert (2002). The average
+ connectivity of a graph. Discrete mathematics 252(1-3), 31-45.
+ http://www.sciencedirect.com/science/article/pii/S0012365X01001807
+
+ """
+ if G.is_directed():
+ iter_func = itertools.permutations
+ else:
+ iter_func = itertools.combinations
+
+ # Reuse the auxiliary digraph and the residual network
+ H = build_auxiliary_node_connectivity(G)
+ R = build_residual_network(H, "capacity")
+ kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R}
+
+ num, den = 0, 0
+ for u, v in iter_func(G, 2):
+ num += local_node_connectivity(G, u, v, **kwargs)
+ den += 1
+
+ if den == 0: # Null Graph
+ return 0
+ return num / den
+
+
+@nx._dispatchable
+def all_pairs_node_connectivity(G, nbunch=None, flow_func=None):
+ """Compute node connectivity between all pairs of nodes of G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Undirected graph
+
+ nbunch: container
+ Container of nodes. If provided node connectivity will be computed
+ only over pairs of nodes in nbunch.
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See below for details. The
+ choice of the default function may change from version
+ to version and should not be relied on. Default value: None.
+
+ Returns
+ -------
+ all_pairs : dict
+ A dictionary with node connectivity between all pairs of nodes
+ in G, or in nbunch if provided.
+
+ See also
+ --------
+ :meth:`local_node_connectivity`
+ :meth:`edge_connectivity`
+ :meth:`local_edge_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ """
+ if nbunch is None:
+ nbunch = G
+ else:
+ nbunch = set(nbunch)
+
+ directed = G.is_directed()
+ if directed:
+ iter_func = itertools.permutations
+ else:
+ iter_func = itertools.combinations
+
+ all_pairs = {n: {} for n in nbunch}
+
+ # Reuse auxiliary digraph and residual network
+ H = build_auxiliary_node_connectivity(G)
+ mapping = H.graph["mapping"]
+ R = build_residual_network(H, "capacity")
+ kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R}
+
+ for u, v in iter_func(nbunch, 2):
+ K = local_node_connectivity(G, u, v, **kwargs)
+ all_pairs[u][v] = K
+ if not directed:
+ all_pairs[v][u] = K
+
+ return all_pairs
+
+
+@nx._dispatchable(graphs={"G": 0, "auxiliary?": 4})
+def local_edge_connectivity(
+ G, s, t, flow_func=None, auxiliary=None, residual=None, cutoff=None
+):
+ r"""Returns local edge connectivity for nodes s and t in G.
+
+ Local edge connectivity for two nodes s and t is the minimum number
+ of edges that must be removed to disconnect them.
+
+ This is a flow based implementation of edge connectivity. We compute the
+ maximum flow on an auxiliary digraph build from the original
+ network (see below for details). This is equal to the local edge
+ connectivity because the value of a maximum s-t-flow is equal to the
+ capacity of a minimum s-t-cut (Ford and Fulkerson theorem) [1]_ .
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Undirected or directed graph
+
+ s : node
+ Source node
+
+ t : node
+ Target node
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See below for details. The
+ choice of the default function may change from version
+ to version and should not be relied on. Default value: None.
+
+ auxiliary : NetworkX DiGraph
+ Auxiliary digraph for computing flow based edge connectivity. If
+ provided it will be reused instead of recreated. Default value: None.
+
+ residual : NetworkX DiGraph
+ Residual network to compute maximum flow. If provided it will be
+ reused instead of recreated. Default value: None.
+
+ cutoff : integer, float, or None (default: None)
+ If specified, the maximum flow algorithm will terminate when the
+ flow value reaches or exceeds the cutoff. This only works for flows
+ that support the cutoff parameter (most do) and is ignored otherwise.
+
+ Returns
+ -------
+ K : integer
+ local edge connectivity for nodes s and t.
+
+ Examples
+ --------
+ This function is not imported in the base NetworkX namespace, so you
+ have to explicitly import it from the connectivity package:
+
+ >>> from networkx.algorithms.connectivity import local_edge_connectivity
+
+ We use in this example the platonic icosahedral graph, which has edge
+ connectivity 5.
+
+ >>> G = nx.icosahedral_graph()
+ >>> local_edge_connectivity(G, 0, 6)
+ 5
+
+ If you need to compute local connectivity on several pairs of
+ nodes in the same graph, it is recommended that you reuse the
+ data structures that NetworkX uses in the computation: the
+ auxiliary digraph for edge connectivity, and the residual
+ network for the underlying maximum flow computation.
+
+ Example of how to compute local edge connectivity among
+ all pairs of nodes of the platonic icosahedral graph reusing
+ the data structures.
+
+ >>> import itertools
+ >>> # You also have to explicitly import the function for
+ >>> # building the auxiliary digraph from the connectivity package
+ >>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity
+ >>> H = build_auxiliary_edge_connectivity(G)
+ >>> # And the function for building the residual network from the
+ >>> # flow package
+ >>> from networkx.algorithms.flow import build_residual_network
+ >>> # Note that the auxiliary digraph has an edge attribute named capacity
+ >>> R = build_residual_network(H, "capacity")
+ >>> result = dict.fromkeys(G, dict())
+ >>> # Reuse the auxiliary digraph and the residual network by passing them
+ >>> # as parameters
+ >>> for u, v in itertools.combinations(G, 2):
+ ... k = local_edge_connectivity(G, u, v, auxiliary=H, residual=R)
+ ... result[u][v] = k
+ >>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
+ True
+
+ You can also use alternative flow algorithms for computing edge
+ connectivity. For instance, in dense networks the algorithm
+ :meth:`shortest_augmenting_path` will usually perform better than
+ the default :meth:`edmonds_karp` which is faster for sparse
+ networks with highly skewed degree distributions. Alternative flow
+ functions have to be explicitly imported from the flow package.
+
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
+ >>> local_edge_connectivity(G, 0, 6, flow_func=shortest_augmenting_path)
+ 5
+
+ Notes
+ -----
+ This is a flow based implementation of edge connectivity. We compute the
+ maximum flow using, by default, the :meth:`edmonds_karp` algorithm on an
+ auxiliary digraph build from the original input graph:
+
+ If the input graph is undirected, we replace each edge (`u`,`v`) with
+ two reciprocal arcs (`u`, `v`) and (`v`, `u`) and then we set the attribute
+ 'capacity' for each arc to 1. If the input graph is directed we simply
+ add the 'capacity' attribute. This is an implementation of algorithm 1
+ in [1]_.
+
+ The maximum flow in the auxiliary network is equal to the local edge
+ connectivity because the value of a maximum s-t-flow is equal to the
+ capacity of a minimum s-t-cut (Ford and Fulkerson theorem).
+
+ See also
+ --------
+ :meth:`edge_connectivity`
+ :meth:`local_node_connectivity`
+ :meth:`node_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ References
+ ----------
+ .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
+ http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
+
+ """
+ if flow_func is None:
+ flow_func = default_flow_func
+
+ if auxiliary is None:
+ H = build_auxiliary_edge_connectivity(G)
+ else:
+ H = auxiliary
+
+ kwargs = {"flow_func": flow_func, "residual": residual}
+
+ if flow_func is not preflow_push:
+ kwargs["cutoff"] = cutoff
+
+ if flow_func is shortest_augmenting_path:
+ kwargs["two_phase"] = True
+
+ return nx.maximum_flow_value(H, s, t, **kwargs)
+
+
+@nx._dispatchable
+def edge_connectivity(G, s=None, t=None, flow_func=None, cutoff=None):
+ r"""Returns the edge connectivity of the graph or digraph G.
+
+ The edge connectivity is equal to the minimum number of edges that
+ must be removed to disconnect G or render it trivial. If source
+ and target nodes are provided, this function returns the local edge
+ connectivity: the minimum number of edges that must be removed to
+ break all paths from source to target in G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Undirected or directed graph
+
+ s : node
+ Source node. Optional. Default value: None.
+
+ t : node
+ Target node. Optional. Default value: None.
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See below for details. The
+ choice of the default function may change from version
+ to version and should not be relied on. Default value: None.
+
+ cutoff : integer, float, or None (default: None)
+ If specified, the maximum flow algorithm will terminate when the
+ flow value reaches or exceeds the cutoff. This only works for flows
+ that support the cutoff parameter (most do) and is ignored otherwise.
+
+ Returns
+ -------
+ K : integer
+ Edge connectivity for G, or local edge connectivity if source
+ and target were provided
+
+ Examples
+ --------
+ >>> # Platonic icosahedral graph is 5-edge-connected
+ >>> G = nx.icosahedral_graph()
+ >>> nx.edge_connectivity(G)
+ 5
+
+ You can use alternative flow algorithms for the underlying
+ maximum flow computation. In dense networks the algorithm
+ :meth:`shortest_augmenting_path` will usually perform better
+ than the default :meth:`edmonds_karp`, which is faster for
+ sparse networks with highly skewed degree distributions.
+ Alternative flow functions have to be explicitly imported
+ from the flow package.
+
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
+ >>> nx.edge_connectivity(G, flow_func=shortest_augmenting_path)
+ 5
+
+ If you specify a pair of nodes (source and target) as parameters,
+ this function returns the value of local edge connectivity.
+
+ >>> nx.edge_connectivity(G, 3, 7)
+ 5
+
+ If you need to perform several local computations among different
+ pairs of nodes on the same graph, it is recommended that you reuse
+ the data structures used in the maximum flow computations. See
+ :meth:`local_edge_connectivity` for details.
+
+ Notes
+ -----
+ This is a flow based implementation of global edge connectivity.
+ For undirected graphs the algorithm works by finding a 'small'
+ dominating set of nodes of G (see algorithm 7 in [1]_ ) and
+ computing local maximum flow (see :meth:`local_edge_connectivity`)
+ between an arbitrary node in the dominating set and the rest of
+ nodes in it. This is an implementation of algorithm 6 in [1]_ .
+ For directed graphs, the algorithm does n calls to the maximum
+ flow function. This is an implementation of algorithm 8 in [1]_ .
+
+ See also
+ --------
+ :meth:`local_edge_connectivity`
+ :meth:`local_node_connectivity`
+ :meth:`node_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+ :meth:`k_edge_components`
+ :meth:`k_edge_subgraphs`
+
+ References
+ ----------
+ .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
+ http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
+
+ """
+ if (s is not None and t is None) or (s is None and t is not None):
+ raise nx.NetworkXError("Both source and target must be specified.")
+
+ # Local edge connectivity
+ if s is not None and t is not None:
+ if s not in G:
+ raise nx.NetworkXError(f"node {s} not in graph")
+ if t not in G:
+ raise nx.NetworkXError(f"node {t} not in graph")
+ return local_edge_connectivity(G, s, t, flow_func=flow_func, cutoff=cutoff)
+
+ # Global edge connectivity
+ # reuse auxiliary digraph and residual network
+ H = build_auxiliary_edge_connectivity(G)
+ R = build_residual_network(H, "capacity")
+ kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R}
+
+ if G.is_directed():
+ # Algorithm 8 in [1]
+ if not nx.is_weakly_connected(G):
+ return 0
+
+ # initial value for \lambda is minimum degree
+ L = min(d for n, d in G.degree())
+ nodes = list(G)
+ n = len(nodes)
+
+ if cutoff is not None:
+ L = min(cutoff, L)
+
+ for i in range(n):
+ kwargs["cutoff"] = L
+ try:
+ L = min(L, local_edge_connectivity(G, nodes[i], nodes[i + 1], **kwargs))
+ except IndexError: # last node!
+ L = min(L, local_edge_connectivity(G, nodes[i], nodes[0], **kwargs))
+ return L
+ else: # undirected
+ # Algorithm 6 in [1]
+ if not nx.is_connected(G):
+ return 0
+
+ # initial value for \lambda is minimum degree
+ L = min(d for n, d in G.degree())
+
+ if cutoff is not None:
+ L = min(cutoff, L)
+
+ # A dominating set is \lambda-covering
+ # We need a dominating set with at least two nodes
+ for node in G:
+ D = nx.dominating_set(G, start_with=node)
+ v = D.pop()
+ if D:
+ break
+ else:
+ # in complete graphs the dominating sets will always be of one node
+ # thus we return min degree
+ return L
+
+ for w in D:
+ kwargs["cutoff"] = L
+ L = min(L, local_edge_connectivity(G, v, w, **kwargs))
+
+ return L
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/cuts.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/cuts.py
new file mode 100644
index 00000000..27124e1b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/cuts.py
@@ -0,0 +1,612 @@
+"""
+Flow based cut algorithms
+"""
+
+import itertools
+
+import networkx as nx
+
+# Define the default maximum flow function to use in all flow based
+# cut algorithms.
+from networkx.algorithms.flow import build_residual_network, edmonds_karp
+
+default_flow_func = edmonds_karp
+
+from .utils import build_auxiliary_edge_connectivity, build_auxiliary_node_connectivity
+
+__all__ = [
+ "minimum_st_node_cut",
+ "minimum_node_cut",
+ "minimum_st_edge_cut",
+ "minimum_edge_cut",
+]
+
+
+@nx._dispatchable(
+ graphs={"G": 0, "auxiliary?": 4},
+ preserve_edge_attrs={"auxiliary": {"capacity": float("inf")}},
+ preserve_graph_attrs={"auxiliary"},
+)
+def minimum_st_edge_cut(G, s, t, flow_func=None, auxiliary=None, residual=None):
+ """Returns the edges of the cut-set of a minimum (s, t)-cut.
+
+ This function returns the set of edges of minimum cardinality that,
+ if removed, would destroy all paths among source and target in G.
+ Edge weights are not considered. See :meth:`minimum_cut` for
+ computing minimum cuts considering edge weights.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ s : node
+ Source node for the flow.
+
+ t : node
+ Sink node for the flow.
+
+ auxiliary : NetworkX DiGraph
+ Auxiliary digraph to compute flow based node connectivity. It has
+ to have a graph attribute called mapping with a dictionary mapping
+ node names in G and in the auxiliary digraph. If provided
+ it will be reused instead of recreated. Default value: None.
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See :meth:`node_connectivity` for
+ details. The choice of the default function may change from version
+ to version and should not be relied on. Default value: None.
+
+ residual : NetworkX DiGraph
+ Residual network to compute maximum flow. If provided it will be
+ reused instead of recreated. Default value: None.
+
+ Returns
+ -------
+ cutset : set
+ Set of edges that, if removed from the graph, will disconnect it.
+
+ See also
+ --------
+ :meth:`minimum_cut`
+ :meth:`minimum_node_cut`
+ :meth:`minimum_edge_cut`
+ :meth:`stoer_wagner`
+ :meth:`node_connectivity`
+ :meth:`edge_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ Examples
+ --------
+ This function is not imported in the base NetworkX namespace, so you
+ have to explicitly import it from the connectivity package:
+
+ >>> from networkx.algorithms.connectivity import minimum_st_edge_cut
+
+ We use in this example the platonic icosahedral graph, which has edge
+ connectivity 5.
+
+ >>> G = nx.icosahedral_graph()
+ >>> len(minimum_st_edge_cut(G, 0, 6))
+ 5
+
+ If you need to compute local edge cuts on several pairs of
+ nodes in the same graph, it is recommended that you reuse the
+ data structures that NetworkX uses in the computation: the
+ auxiliary digraph for edge connectivity, and the residual
+ network for the underlying maximum flow computation.
+
+ Example of how to compute local edge cuts among all pairs of
+ nodes of the platonic icosahedral graph reusing the data
+ structures.
+
+ >>> import itertools
+ >>> # You also have to explicitly import the function for
+ >>> # building the auxiliary digraph from the connectivity package
+ >>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity
+ >>> H = build_auxiliary_edge_connectivity(G)
+ >>> # And the function for building the residual network from the
+ >>> # flow package
+ >>> from networkx.algorithms.flow import build_residual_network
+ >>> # Note that the auxiliary digraph has an edge attribute named capacity
+ >>> R = build_residual_network(H, "capacity")
+ >>> result = dict.fromkeys(G, dict())
+ >>> # Reuse the auxiliary digraph and the residual network by passing them
+ >>> # as parameters
+ >>> for u, v in itertools.combinations(G, 2):
+ ... k = len(minimum_st_edge_cut(G, u, v, auxiliary=H, residual=R))
+ ... result[u][v] = k
+ >>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
+ True
+
+ You can also use alternative flow algorithms for computing edge
+ cuts. For instance, in dense networks the algorithm
+ :meth:`shortest_augmenting_path` will usually perform better than
+ the default :meth:`edmonds_karp` which is faster for sparse
+ networks with highly skewed degree distributions. Alternative flow
+ functions have to be explicitly imported from the flow package.
+
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
+ >>> len(minimum_st_edge_cut(G, 0, 6, flow_func=shortest_augmenting_path))
+ 5
+
+ """
+ if flow_func is None:
+ flow_func = default_flow_func
+
+ if auxiliary is None:
+ H = build_auxiliary_edge_connectivity(G)
+ else:
+ H = auxiliary
+
+ kwargs = {"capacity": "capacity", "flow_func": flow_func, "residual": residual}
+
+ cut_value, partition = nx.minimum_cut(H, s, t, **kwargs)
+ reachable, non_reachable = partition
+ # Any edge in the original graph linking the two sets in the
+ # partition is part of the edge cutset
+ cutset = set()
+ for u, nbrs in ((n, G[n]) for n in reachable):
+ cutset.update((u, v) for v in nbrs if v in non_reachable)
+
+ return cutset
+
+
+@nx._dispatchable(
+ graphs={"G": 0, "auxiliary?": 4},
+ preserve_node_attrs={"auxiliary": {"id": None}},
+ preserve_graph_attrs={"auxiliary"},
+)
+def minimum_st_node_cut(G, s, t, flow_func=None, auxiliary=None, residual=None):
+ r"""Returns a set of nodes of minimum cardinality that disconnect source
+ from target in G.
+
+ This function returns the set of nodes of minimum cardinality that,
+ if removed, would destroy all paths among source and target in G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ s : node
+ Source node.
+
+ t : node
+ Target node.
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See below for details. The choice
+ of the default function may change from version to version and
+ should not be relied on. Default value: None.
+
+ auxiliary : NetworkX DiGraph
+ Auxiliary digraph to compute flow based node connectivity. It has
+ to have a graph attribute called mapping with a dictionary mapping
+ node names in G and in the auxiliary digraph. If provided
+ it will be reused instead of recreated. Default value: None.
+
+ residual : NetworkX DiGraph
+ Residual network to compute maximum flow. If provided it will be
+ reused instead of recreated. Default value: None.
+
+ Returns
+ -------
+ cutset : set
+ Set of nodes that, if removed, would destroy all paths between
+ source and target in G.
+
+ Examples
+ --------
+ This function is not imported in the base NetworkX namespace, so you
+ have to explicitly import it from the connectivity package:
+
+ >>> from networkx.algorithms.connectivity import minimum_st_node_cut
+
+ We use in this example the platonic icosahedral graph, which has node
+ connectivity 5.
+
+ >>> G = nx.icosahedral_graph()
+ >>> len(minimum_st_node_cut(G, 0, 6))
+ 5
+
+ If you need to compute local st cuts between several pairs of
+ nodes in the same graph, it is recommended that you reuse the
+ data structures that NetworkX uses in the computation: the
+ auxiliary digraph for node connectivity and node cuts, and the
+ residual network for the underlying maximum flow computation.
+
+ Example of how to compute local st node cuts reusing the data
+ structures:
+
+ >>> # You also have to explicitly import the function for
+ >>> # building the auxiliary digraph from the connectivity package
+ >>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity
+ >>> H = build_auxiliary_node_connectivity(G)
+ >>> # And the function for building the residual network from the
+ >>> # flow package
+ >>> from networkx.algorithms.flow import build_residual_network
+ >>> # Note that the auxiliary digraph has an edge attribute named capacity
+ >>> R = build_residual_network(H, "capacity")
+ >>> # Reuse the auxiliary digraph and the residual network by passing them
+ >>> # as parameters
+ >>> len(minimum_st_node_cut(G, 0, 6, auxiliary=H, residual=R))
+ 5
+
+ You can also use alternative flow algorithms for computing minimum st
+ node cuts. For instance, in dense networks the algorithm
+ :meth:`shortest_augmenting_path` will usually perform better than
+ the default :meth:`edmonds_karp` which is faster for sparse
+ networks with highly skewed degree distributions. Alternative flow
+ functions have to be explicitly imported from the flow package.
+
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
+ >>> len(minimum_st_node_cut(G, 0, 6, flow_func=shortest_augmenting_path))
+ 5
+
+ Notes
+ -----
+ This is a flow based implementation of minimum node cut. The algorithm
+ is based in solving a number of maximum flow computations to determine
+ the capacity of the minimum cut on an auxiliary directed network that
+ corresponds to the minimum node cut of G. It handles both directed
+ and undirected graphs. This implementation is based on algorithm 11
+ in [1]_.
+
+ See also
+ --------
+ :meth:`minimum_node_cut`
+ :meth:`minimum_edge_cut`
+ :meth:`stoer_wagner`
+ :meth:`node_connectivity`
+ :meth:`edge_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ References
+ ----------
+ .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
+ http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
+
+ """
+ if auxiliary is None:
+ H = build_auxiliary_node_connectivity(G)
+ else:
+ H = auxiliary
+
+ mapping = H.graph.get("mapping", None)
+ if mapping is None:
+ raise nx.NetworkXError("Invalid auxiliary digraph.")
+ if G.has_edge(s, t) or G.has_edge(t, s):
+ return {}
+ kwargs = {"flow_func": flow_func, "residual": residual, "auxiliary": H}
+
+ # The edge cut in the auxiliary digraph corresponds to the node cut in the
+ # original graph.
+ edge_cut = minimum_st_edge_cut(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs)
+ # Each node in the original graph maps to two nodes of the auxiliary graph
+ node_cut = {H.nodes[node]["id"] for edge in edge_cut for node in edge}
+ return node_cut - {s, t}
+
+
+@nx._dispatchable
+def minimum_node_cut(G, s=None, t=None, flow_func=None):
+ r"""Returns a set of nodes of minimum cardinality that disconnects G.
+
+ If source and target nodes are provided, this function returns the
+ set of nodes of minimum cardinality that, if removed, would destroy
+ all paths among source and target in G. If not, it returns a set
+ of nodes of minimum cardinality that disconnects G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ s : node
+ Source node. Optional. Default value: None.
+
+ t : node
+ Target node. Optional. Default value: None.
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See below for details. The
+ choice of the default function may change from version
+ to version and should not be relied on. Default value: None.
+
+ Returns
+ -------
+ cutset : set
+ Set of nodes that, if removed, would disconnect G. If source
+ and target nodes are provided, the set contains the nodes that
+ if removed, would destroy all paths between source and target.
+
+ Examples
+ --------
+ >>> # Platonic icosahedral graph has node connectivity 5
+ >>> G = nx.icosahedral_graph()
+ >>> node_cut = nx.minimum_node_cut(G)
+ >>> len(node_cut)
+ 5
+
+ You can use alternative flow algorithms for the underlying maximum
+ flow computation. In dense networks the algorithm
+ :meth:`shortest_augmenting_path` will usually perform better
+ than the default :meth:`edmonds_karp`, which is faster for
+ sparse networks with highly skewed degree distributions. Alternative
+ flow functions have to be explicitly imported from the flow package.
+
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
+ >>> node_cut == nx.minimum_node_cut(G, flow_func=shortest_augmenting_path)
+ True
+
+ If you specify a pair of nodes (source and target) as parameters,
+ this function returns a local st node cut.
+
+ >>> len(nx.minimum_node_cut(G, 3, 7))
+ 5
+
+ If you need to perform several local st cuts among different
+ pairs of nodes on the same graph, it is recommended that you reuse
+ the data structures used in the maximum flow computations. See
+ :meth:`minimum_st_node_cut` for details.
+
+ Notes
+ -----
+ This is a flow based implementation of minimum node cut. The algorithm
+ is based in solving a number of maximum flow computations to determine
+ the capacity of the minimum cut on an auxiliary directed network that
+ corresponds to the minimum node cut of G. It handles both directed
+ and undirected graphs. This implementation is based on algorithm 11
+ in [1]_.
+
+ See also
+ --------
+ :meth:`minimum_st_node_cut`
+ :meth:`minimum_cut`
+ :meth:`minimum_edge_cut`
+ :meth:`stoer_wagner`
+ :meth:`node_connectivity`
+ :meth:`edge_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ References
+ ----------
+ .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
+ http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
+
+ """
+ if (s is not None and t is None) or (s is None and t is not None):
+ raise nx.NetworkXError("Both source and target must be specified.")
+
+ # Local minimum node cut.
+ if s is not None and t is not None:
+ if s not in G:
+ raise nx.NetworkXError(f"node {s} not in graph")
+ if t not in G:
+ raise nx.NetworkXError(f"node {t} not in graph")
+ return minimum_st_node_cut(G, s, t, flow_func=flow_func)
+
+ # Global minimum node cut.
+ # Analog to the algorithm 11 for global node connectivity in [1].
+ if G.is_directed():
+ if not nx.is_weakly_connected(G):
+ raise nx.NetworkXError("Input graph is not connected")
+ iter_func = itertools.permutations
+
+ def neighbors(v):
+ return itertools.chain.from_iterable([G.predecessors(v), G.successors(v)])
+
+ else:
+ if not nx.is_connected(G):
+ raise nx.NetworkXError("Input graph is not connected")
+ iter_func = itertools.combinations
+ neighbors = G.neighbors
+
+ # Reuse the auxiliary digraph and the residual network.
+ H = build_auxiliary_node_connectivity(G)
+ R = build_residual_network(H, "capacity")
+ kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R}
+
+ # Choose a node with minimum degree.
+ v = min(G, key=G.degree)
+ # Initial node cutset is all neighbors of the node with minimum degree.
+ min_cut = set(G[v])
+ # Compute st node cuts between v and all its non-neighbors nodes in G.
+ for w in set(G) - set(neighbors(v)) - {v}:
+ this_cut = minimum_st_node_cut(G, v, w, **kwargs)
+ if len(min_cut) >= len(this_cut):
+ min_cut = this_cut
+ # Also for non adjacent pairs of neighbors of v.
+ for x, y in iter_func(neighbors(v), 2):
+ if y in G[x]:
+ continue
+ this_cut = minimum_st_node_cut(G, x, y, **kwargs)
+ if len(min_cut) >= len(this_cut):
+ min_cut = this_cut
+
+ return min_cut
+
+
+@nx._dispatchable
+def minimum_edge_cut(G, s=None, t=None, flow_func=None):
+ r"""Returns a set of edges of minimum cardinality that disconnects G.
+
+ If source and target nodes are provided, this function returns the
+ set of edges of minimum cardinality that, if removed, would break
+ all paths among source and target in G. If not, it returns a set of
+ edges of minimum cardinality that disconnects G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ s : node
+ Source node. Optional. Default value: None.
+
+ t : node
+ Target node. Optional. Default value: None.
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See below for details. The
+ choice of the default function may change from version
+ to version and should not be relied on. Default value: None.
+
+ Returns
+ -------
+ cutset : set
+ Set of edges that, if removed, would disconnect G. If source
+ and target nodes are provided, the set contains the edges that
+ if removed, would destroy all paths between source and target.
+
+ Examples
+ --------
+ >>> # Platonic icosahedral graph has edge connectivity 5
+ >>> G = nx.icosahedral_graph()
+ >>> len(nx.minimum_edge_cut(G))
+ 5
+
+ You can use alternative flow algorithms for the underlying
+ maximum flow computation. In dense networks the algorithm
+ :meth:`shortest_augmenting_path` will usually perform better
+ than the default :meth:`edmonds_karp`, which is faster for
+ sparse networks with highly skewed degree distributions.
+ Alternative flow functions have to be explicitly imported
+ from the flow package.
+
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
+ >>> len(nx.minimum_edge_cut(G, flow_func=shortest_augmenting_path))
+ 5
+
+ If you specify a pair of nodes (source and target) as parameters,
+ this function returns the value of local edge connectivity.
+
+ >>> nx.edge_connectivity(G, 3, 7)
+ 5
+
+ If you need to perform several local computations among different
+ pairs of nodes on the same graph, it is recommended that you reuse
+ the data structures used in the maximum flow computations. See
+ :meth:`local_edge_connectivity` for details.
+
+ Notes
+ -----
+ This is a flow based implementation of minimum edge cut. For
+ undirected graphs the algorithm works by finding a 'small' dominating
+ set of nodes of G (see algorithm 7 in [1]_) and computing the maximum
+ flow between an arbitrary node in the dominating set and the rest of
+ nodes in it. This is an implementation of algorithm 6 in [1]_. For
+ directed graphs, the algorithm does n calls to the max flow function.
+ The function raises an error if the directed graph is not weakly
+ connected and returns an empty set if it is weakly connected.
+ It is an implementation of algorithm 8 in [1]_.
+
+ See also
+ --------
+ :meth:`minimum_st_edge_cut`
+ :meth:`minimum_node_cut`
+ :meth:`stoer_wagner`
+ :meth:`node_connectivity`
+ :meth:`edge_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ References
+ ----------
+ .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
+ http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
+
+ """
+ if (s is not None and t is None) or (s is None and t is not None):
+ raise nx.NetworkXError("Both source and target must be specified.")
+
+ # reuse auxiliary digraph and residual network
+ H = build_auxiliary_edge_connectivity(G)
+ R = build_residual_network(H, "capacity")
+ kwargs = {"flow_func": flow_func, "residual": R, "auxiliary": H}
+
+ # Local minimum edge cut if s and t are not None
+ if s is not None and t is not None:
+ if s not in G:
+ raise nx.NetworkXError(f"node {s} not in graph")
+ if t not in G:
+ raise nx.NetworkXError(f"node {t} not in graph")
+ return minimum_st_edge_cut(H, s, t, **kwargs)
+
+ # Global minimum edge cut
+ # Analog to the algorithm for global edge connectivity
+ if G.is_directed():
+ # Based on algorithm 8 in [1]
+ if not nx.is_weakly_connected(G):
+ raise nx.NetworkXError("Input graph is not connected")
+
+ # Initial cutset is all edges of a node with minimum degree
+ node = min(G, key=G.degree)
+ min_cut = set(G.edges(node))
+ nodes = list(G)
+ n = len(nodes)
+ for i in range(n):
+ try:
+ this_cut = minimum_st_edge_cut(H, nodes[i], nodes[i + 1], **kwargs)
+ if len(this_cut) <= len(min_cut):
+ min_cut = this_cut
+ except IndexError: # Last node!
+ this_cut = minimum_st_edge_cut(H, nodes[i], nodes[0], **kwargs)
+ if len(this_cut) <= len(min_cut):
+ min_cut = this_cut
+
+ return min_cut
+
+ else: # undirected
+ # Based on algorithm 6 in [1]
+ if not nx.is_connected(G):
+ raise nx.NetworkXError("Input graph is not connected")
+
+ # Initial cutset is all edges of a node with minimum degree
+ node = min(G, key=G.degree)
+ min_cut = set(G.edges(node))
+ # A dominating set is \lambda-covering
+ # We need a dominating set with at least two nodes
+ for node in G:
+ D = nx.dominating_set(G, start_with=node)
+ v = D.pop()
+ if D:
+ break
+ else:
+ # in complete graphs the dominating set will always be of one node
+ # thus we return min_cut, which now contains the edges of a node
+ # with minimum degree
+ return min_cut
+ for w in D:
+ this_cut = minimum_st_edge_cut(H, v, w, **kwargs)
+ if len(this_cut) <= len(min_cut):
+ min_cut = this_cut
+
+ return min_cut
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/disjoint_paths.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/disjoint_paths.py
new file mode 100644
index 00000000..00616492
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/disjoint_paths.py
@@ -0,0 +1,408 @@
+"""Flow based node and edge disjoint paths."""
+
+import networkx as nx
+
+# Define the default maximum flow function to use for the underlying
+# maximum flow computations
+from networkx.algorithms.flow import (
+ edmonds_karp,
+ preflow_push,
+ shortest_augmenting_path,
+)
+from networkx.exception import NetworkXNoPath
+
+default_flow_func = edmonds_karp
+from itertools import filterfalse as _filterfalse
+
+# Functions to build auxiliary data structures.
+from .utils import build_auxiliary_edge_connectivity, build_auxiliary_node_connectivity
+
+__all__ = ["edge_disjoint_paths", "node_disjoint_paths"]
+
+
+@nx._dispatchable(
+ graphs={"G": 0, "auxiliary?": 5},
+ preserve_edge_attrs={"auxiliary": {"capacity": float("inf")}},
+)
+def edge_disjoint_paths(
+ G, s, t, flow_func=None, cutoff=None, auxiliary=None, residual=None
+):
+ """Returns the edges disjoint paths between source and target.
+
+ Edge disjoint paths are paths that do not share any edge. The
+ number of edge disjoint paths between source and target is equal
+ to their edge connectivity.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ s : node
+ Source node for the flow.
+
+ t : node
+ Sink node for the flow.
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. The choice of the default function
+ may change from version to version and should not be relied on.
+ Default value: None.
+
+ cutoff : integer or None (default: None)
+ Maximum number of paths to yield. If specified, the maximum flow
+ algorithm will terminate when the flow value reaches or exceeds the
+ cutoff. This only works for flows that support the cutoff parameter
+ (most do) and is ignored otherwise.
+
+ auxiliary : NetworkX DiGraph
+ Auxiliary digraph to compute flow based edge connectivity. It has
+ to have a graph attribute called mapping with a dictionary mapping
+ node names in G and in the auxiliary digraph. If provided
+ it will be reused instead of recreated. Default value: None.
+
+ residual : NetworkX DiGraph
+ Residual network to compute maximum flow. If provided it will be
+ reused instead of recreated. Default value: None.
+
+ Returns
+ -------
+ paths : generator
+ A generator of edge independent paths.
+
+ Raises
+ ------
+ NetworkXNoPath
+ If there is no path between source and target.
+
+ NetworkXError
+ If source or target are not in the graph G.
+
+ See also
+ --------
+ :meth:`node_disjoint_paths`
+ :meth:`edge_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ Examples
+ --------
+ We use in this example the platonic icosahedral graph, which has node
+ edge connectivity 5, thus there are 5 edge disjoint paths between any
+ pair of nodes.
+
+ >>> G = nx.icosahedral_graph()
+ >>> len(list(nx.edge_disjoint_paths(G, 0, 6)))
+ 5
+
+
+ If you need to compute edge disjoint paths on several pairs of
+ nodes in the same graph, it is recommended that you reuse the
+ data structures that NetworkX uses in the computation: the
+ auxiliary digraph for edge connectivity, and the residual
+ network for the underlying maximum flow computation.
+
+ Example of how to compute edge disjoint paths among all pairs of
+ nodes of the platonic icosahedral graph reusing the data
+ structures.
+
+ >>> import itertools
+ >>> # You also have to explicitly import the function for
+ >>> # building the auxiliary digraph from the connectivity package
+ >>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity
+ >>> H = build_auxiliary_edge_connectivity(G)
+ >>> # And the function for building the residual network from the
+ >>> # flow package
+ >>> from networkx.algorithms.flow import build_residual_network
+ >>> # Note that the auxiliary digraph has an edge attribute named capacity
+ >>> R = build_residual_network(H, "capacity")
+ >>> result = {n: {} for n in G}
+ >>> # Reuse the auxiliary digraph and the residual network by passing them
+ >>> # as arguments
+ >>> for u, v in itertools.combinations(G, 2):
+ ... k = len(list(nx.edge_disjoint_paths(G, u, v, auxiliary=H, residual=R)))
+ ... result[u][v] = k
+ >>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
+ True
+
+ You can also use alternative flow algorithms for computing edge disjoint
+ paths. For instance, in dense networks the algorithm
+ :meth:`shortest_augmenting_path` will usually perform better than
+ the default :meth:`edmonds_karp` which is faster for sparse
+ networks with highly skewed degree distributions. Alternative flow
+ functions have to be explicitly imported from the flow package.
+
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
+ >>> len(list(nx.edge_disjoint_paths(G, 0, 6, flow_func=shortest_augmenting_path)))
+ 5
+
+ Notes
+ -----
+ This is a flow based implementation of edge disjoint paths. We compute
+ the maximum flow between source and target on an auxiliary directed
+ network. The saturated edges in the residual network after running the
+ maximum flow algorithm correspond to edge disjoint paths between source
+ and target in the original network. This function handles both directed
+ and undirected graphs, and can use all flow algorithms from NetworkX flow
+ package.
+
+ """
+ if s not in G:
+ raise nx.NetworkXError(f"node {s} not in graph")
+ if t not in G:
+ raise nx.NetworkXError(f"node {t} not in graph")
+
+ if flow_func is None:
+ flow_func = default_flow_func
+
+ if auxiliary is None:
+ H = build_auxiliary_edge_connectivity(G)
+ else:
+ H = auxiliary
+
+ # Maximum possible edge disjoint paths
+ possible = min(H.out_degree(s), H.in_degree(t))
+ if not possible:
+ raise NetworkXNoPath
+
+ if cutoff is None:
+ cutoff = possible
+ else:
+ cutoff = min(cutoff, possible)
+
+ # Compute maximum flow between source and target. Flow functions in
+ # NetworkX return a residual network.
+ kwargs = {
+ "capacity": "capacity",
+ "residual": residual,
+ "cutoff": cutoff,
+ "value_only": True,
+ }
+ if flow_func is preflow_push:
+ del kwargs["cutoff"]
+ if flow_func is shortest_augmenting_path:
+ kwargs["two_phase"] = True
+ R = flow_func(H, s, t, **kwargs)
+
+ if R.graph["flow_value"] == 0:
+ raise NetworkXNoPath
+
+ # Saturated edges in the residual network form the edge disjoint paths
+ # between source and target
+ cutset = [
+ (u, v)
+ for u, v, d in R.edges(data=True)
+ if d["capacity"] == d["flow"] and d["flow"] > 0
+ ]
+ # This is equivalent of what flow.utils.build_flow_dict returns, but
+ # only for the nodes with saturated edges and without reporting 0 flows.
+ flow_dict = {n: {} for edge in cutset for n in edge}
+ for u, v in cutset:
+ flow_dict[u][v] = 1
+
+ # Rebuild the edge disjoint paths from the flow dictionary.
+ paths_found = 0
+ for v in list(flow_dict[s]):
+ if paths_found >= cutoff:
+ # preflow_push does not support cutoff: we have to
+ # keep track of the paths founds and stop at cutoff.
+ break
+ path = [s]
+ if v == t:
+ path.append(v)
+ yield path
+ continue
+ u = v
+ while u != t:
+ path.append(u)
+ try:
+ u, _ = flow_dict[u].popitem()
+ except KeyError:
+ break
+ else:
+ path.append(t)
+ yield path
+ paths_found += 1
+
+
+@nx._dispatchable(
+ graphs={"G": 0, "auxiliary?": 5},
+ preserve_node_attrs={"auxiliary": {"id": None}},
+ preserve_graph_attrs={"auxiliary"},
+)
+def node_disjoint_paths(
+ G, s, t, flow_func=None, cutoff=None, auxiliary=None, residual=None
+):
+ r"""Computes node disjoint paths between source and target.
+
+ Node disjoint paths are paths that only share their first and last
+ nodes. The number of node independent paths between two nodes is
+ equal to their local node connectivity.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ s : node
+ Source node.
+
+ t : node
+ Target node.
+
+ flow_func : function
+ A function for computing the maximum flow among a pair of nodes.
+ The function has to accept at least three parameters: a Digraph,
+ a source node, and a target node. And return a residual network
+ that follows NetworkX conventions (see :meth:`maximum_flow` for
+ details). If flow_func is None, the default maximum flow function
+ (:meth:`edmonds_karp`) is used. See below for details. The choice
+ of the default function may change from version to version and
+ should not be relied on. Default value: None.
+
+ cutoff : integer or None (default: None)
+ Maximum number of paths to yield. If specified, the maximum flow
+ algorithm will terminate when the flow value reaches or exceeds the
+ cutoff. This only works for flows that support the cutoff parameter
+ (most do) and is ignored otherwise.
+
+ auxiliary : NetworkX DiGraph
+ Auxiliary digraph to compute flow based node connectivity. It has
+ to have a graph attribute called mapping with a dictionary mapping
+ node names in G and in the auxiliary digraph. If provided
+ it will be reused instead of recreated. Default value: None.
+
+ residual : NetworkX DiGraph
+ Residual network to compute maximum flow. If provided it will be
+ reused instead of recreated. Default value: None.
+
+ Returns
+ -------
+ paths : generator
+ Generator of node disjoint paths.
+
+ Raises
+ ------
+ NetworkXNoPath
+ If there is no path between source and target.
+
+ NetworkXError
+ If source or target are not in the graph G.
+
+ Examples
+ --------
+ We use in this example the platonic icosahedral graph, which has node
+ connectivity 5, thus there are 5 node disjoint paths between any pair
+ of non neighbor nodes.
+
+ >>> G = nx.icosahedral_graph()
+ >>> len(list(nx.node_disjoint_paths(G, 0, 6)))
+ 5
+
+ If you need to compute node disjoint paths between several pairs of
+ nodes in the same graph, it is recommended that you reuse the
+ data structures that NetworkX uses in the computation: the
+ auxiliary digraph for node connectivity and node cuts, and the
+ residual network for the underlying maximum flow computation.
+
+ Example of how to compute node disjoint paths reusing the data
+ structures:
+
+ >>> # You also have to explicitly import the function for
+ >>> # building the auxiliary digraph from the connectivity package
+ >>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity
+ >>> H = build_auxiliary_node_connectivity(G)
+ >>> # And the function for building the residual network from the
+ >>> # flow package
+ >>> from networkx.algorithms.flow import build_residual_network
+ >>> # Note that the auxiliary digraph has an edge attribute named capacity
+ >>> R = build_residual_network(H, "capacity")
+ >>> # Reuse the auxiliary digraph and the residual network by passing them
+ >>> # as arguments
+ >>> len(list(nx.node_disjoint_paths(G, 0, 6, auxiliary=H, residual=R)))
+ 5
+
+ You can also use alternative flow algorithms for computing node disjoint
+ paths. For instance, in dense networks the algorithm
+ :meth:`shortest_augmenting_path` will usually perform better than
+ the default :meth:`edmonds_karp` which is faster for sparse
+ networks with highly skewed degree distributions. Alternative flow
+ functions have to be explicitly imported from the flow package.
+
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
+ >>> len(list(nx.node_disjoint_paths(G, 0, 6, flow_func=shortest_augmenting_path)))
+ 5
+
+ Notes
+ -----
+ This is a flow based implementation of node disjoint paths. We compute
+ the maximum flow between source and target on an auxiliary directed
+ network. The saturated edges in the residual network after running the
+ maximum flow algorithm correspond to node disjoint paths between source
+ and target in the original network. This function handles both directed
+ and undirected graphs, and can use all flow algorithms from NetworkX flow
+ package.
+
+ See also
+ --------
+ :meth:`edge_disjoint_paths`
+ :meth:`node_connectivity`
+ :meth:`maximum_flow`
+ :meth:`edmonds_karp`
+ :meth:`preflow_push`
+ :meth:`shortest_augmenting_path`
+
+ """
+ if s not in G:
+ raise nx.NetworkXError(f"node {s} not in graph")
+ if t not in G:
+ raise nx.NetworkXError(f"node {t} not in graph")
+
+ if auxiliary is None:
+ H = build_auxiliary_node_connectivity(G)
+ else:
+ H = auxiliary
+
+ mapping = H.graph.get("mapping", None)
+ if mapping is None:
+ raise nx.NetworkXError("Invalid auxiliary digraph.")
+
+ # Maximum possible edge disjoint paths
+ possible = min(H.out_degree(f"{mapping[s]}B"), H.in_degree(f"{mapping[t]}A"))
+ if not possible:
+ raise NetworkXNoPath
+
+ if cutoff is None:
+ cutoff = possible
+ else:
+ cutoff = min(cutoff, possible)
+
+ kwargs = {
+ "flow_func": flow_func,
+ "residual": residual,
+ "auxiliary": H,
+ "cutoff": cutoff,
+ }
+
+ # The edge disjoint paths in the auxiliary digraph correspond to the node
+ # disjoint paths in the original graph.
+ paths_edges = edge_disjoint_paths(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs)
+ for path in paths_edges:
+ # Each node in the original graph maps to two nodes in auxiliary graph
+ yield list(_unique_everseen(H.nodes[node]["id"] for node in path))
+
+
+def _unique_everseen(iterable):
+ # Adapted from https://docs.python.org/3/library/itertools.html examples
+ "List unique elements, preserving order. Remember all elements ever seen."
+ # unique_everseen('AAAABBBCCDAABBB') --> A B C D
+ seen = set()
+ seen_add = seen.add
+ for element in _filterfalse(seen.__contains__, iterable):
+ seen_add(element)
+ yield element
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_augmentation.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_augmentation.py
new file mode 100644
index 00000000..278a8e36
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_augmentation.py
@@ -0,0 +1,1270 @@
+"""
+Algorithms for finding k-edge-augmentations
+
+A k-edge-augmentation is a set of edges, that once added to a graph, ensures
+that the graph is k-edge-connected; i.e. the graph cannot be disconnected
+unless k or more edges are removed. Typically, the goal is to find the
+augmentation with minimum weight. In general, it is not guaranteed that a
+k-edge-augmentation exists.
+
+See Also
+--------
+:mod:`edge_kcomponents` : algorithms for finding k-edge-connected components
+:mod:`connectivity` : algorithms for determining edge connectivity.
+"""
+
+import itertools as it
+import math
+from collections import defaultdict, namedtuple
+
+import networkx as nx
+from networkx.utils import not_implemented_for, py_random_state
+
+__all__ = ["k_edge_augmentation", "is_k_edge_connected", "is_locally_k_edge_connected"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def is_k_edge_connected(G, k):
+ """Tests to see if a graph is k-edge-connected.
+
+ Is it impossible to disconnect the graph by removing fewer than k edges?
+ If so, then G is k-edge-connected.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ k : integer
+ edge connectivity to test for
+
+ Returns
+ -------
+ boolean
+ True if G is k-edge-connected.
+
+ See Also
+ --------
+ :func:`is_locally_k_edge_connected`
+
+ Examples
+ --------
+ >>> G = nx.barbell_graph(10, 0)
+ >>> nx.is_k_edge_connected(G, k=1)
+ True
+ >>> nx.is_k_edge_connected(G, k=2)
+ False
+ """
+ if k < 1:
+ raise ValueError(f"k must be positive, not {k}")
+ # First try to quickly determine if G is not k-edge-connected
+ if G.number_of_nodes() < k + 1:
+ return False
+ elif any(d < k for n, d in G.degree()):
+ return False
+ else:
+ # Otherwise perform the full check
+ if k == 1:
+ return nx.is_connected(G)
+ elif k == 2:
+ return nx.is_connected(G) and not nx.has_bridges(G)
+ else:
+ return nx.edge_connectivity(G, cutoff=k) >= k
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def is_locally_k_edge_connected(G, s, t, k):
+ """Tests to see if an edge in a graph is locally k-edge-connected.
+
+ Is it impossible to disconnect s and t by removing fewer than k edges?
+ If so, then s and t are locally k-edge-connected in G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ s : node
+ Source node
+
+ t : node
+ Target node
+
+ k : integer
+ local edge connectivity for nodes s and t
+
+ Returns
+ -------
+ boolean
+ True if s and t are locally k-edge-connected in G.
+
+ See Also
+ --------
+ :func:`is_k_edge_connected`
+
+ Examples
+ --------
+ >>> from networkx.algorithms.connectivity import is_locally_k_edge_connected
+ >>> G = nx.barbell_graph(10, 0)
+ >>> is_locally_k_edge_connected(G, 5, 15, k=1)
+ True
+ >>> is_locally_k_edge_connected(G, 5, 15, k=2)
+ False
+ >>> is_locally_k_edge_connected(G, 1, 5, k=2)
+ True
+ """
+ if k < 1:
+ raise ValueError(f"k must be positive, not {k}")
+
+ # First try to quickly determine s, t is not k-locally-edge-connected in G
+ if G.degree(s) < k or G.degree(t) < k:
+ return False
+ else:
+ # Otherwise perform the full check
+ if k == 1:
+ return nx.has_path(G, s, t)
+ else:
+ localk = nx.connectivity.local_edge_connectivity(G, s, t, cutoff=k)
+ return localk >= k
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def k_edge_augmentation(G, k, avail=None, weight=None, partial=False):
+ """Finds set of edges to k-edge-connect G.
+
+ Adding edges from the augmentation to G make it impossible to disconnect G
+ unless k or more edges are removed. This function uses the most efficient
+ function available (depending on the value of k and if the problem is
+ weighted or unweighted) to search for a minimum weight subset of available
+ edges that k-edge-connects G. In general, finding a k-edge-augmentation is
+ NP-hard, so solutions are not guaranteed to be minimal. Furthermore, a
+ k-edge-augmentation may not exist.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ k : integer
+ Desired edge connectivity
+
+ avail : dict or a set of 2 or 3 tuples
+ The available edges that can be used in the augmentation.
+
+ If unspecified, then all edges in the complement of G are available.
+ Otherwise, each item is an available edge (with an optional weight).
+
+ In the unweighted case, each item is an edge ``(u, v)``.
+
+ In the weighted case, each item is a 3-tuple ``(u, v, d)`` or a dict
+ with items ``(u, v): d``. The third item, ``d``, can be a dictionary
+ or a real number. If ``d`` is a dictionary ``d[weight]``
+ correspondings to the weight.
+
+ weight : string
+ key to use to find weights if ``avail`` is a set of 3-tuples where the
+ third item in each tuple is a dictionary.
+
+ partial : boolean
+ If partial is True and no feasible k-edge-augmentation exists, then all
+ a partial k-edge-augmentation is generated. Adding the edges in a
+ partial augmentation to G, minimizes the number of k-edge-connected
+ components and maximizes the edge connectivity between those
+ components. For details, see :func:`partial_k_edge_augmentation`.
+
+ Yields
+ ------
+ edge : tuple
+ Edges that, once added to G, would cause G to become k-edge-connected.
+ If partial is False, an error is raised if this is not possible.
+ Otherwise, generated edges form a partial augmentation, which
+ k-edge-connects any part of G where it is possible, and maximally
+ connects the remaining parts.
+
+ Raises
+ ------
+ NetworkXUnfeasible
+ If partial is False and no k-edge-augmentation exists.
+
+ NetworkXNotImplemented
+ If the input graph is directed or a multigraph.
+
+ ValueError:
+ If k is less than 1
+
+ Notes
+ -----
+ When k=1 this returns an optimal solution.
+
+ When k=2 and ``avail`` is None, this returns an optimal solution.
+ Otherwise when k=2, this returns a 2-approximation of the optimal solution.
+
+ For k>3, this problem is NP-hard and this uses a randomized algorithm that
+ produces a feasible solution, but provides no guarantees on the
+ solution weight.
+
+ Examples
+ --------
+ >>> # Unweighted cases
+ >>> G = nx.path_graph((1, 2, 3, 4))
+ >>> G.add_node(5)
+ >>> sorted(nx.k_edge_augmentation(G, k=1))
+ [(1, 5)]
+ >>> sorted(nx.k_edge_augmentation(G, k=2))
+ [(1, 5), (5, 4)]
+ >>> sorted(nx.k_edge_augmentation(G, k=3))
+ [(1, 4), (1, 5), (2, 5), (3, 5), (4, 5)]
+ >>> complement = list(nx.k_edge_augmentation(G, k=5, partial=True))
+ >>> G.add_edges_from(complement)
+ >>> nx.edge_connectivity(G)
+ 4
+
+ >>> # Weighted cases
+ >>> G = nx.path_graph((1, 2, 3, 4))
+ >>> G.add_node(5)
+ >>> # avail can be a tuple with a dict
+ >>> avail = [(1, 5, {"weight": 11}), (2, 5, {"weight": 10})]
+ >>> sorted(nx.k_edge_augmentation(G, k=1, avail=avail, weight="weight"))
+ [(2, 5)]
+ >>> # or avail can be a 3-tuple with a real number
+ >>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 51)]
+ >>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail))
+ [(1, 5), (2, 5), (4, 5)]
+ >>> # or avail can be a dict
+ >>> avail = {(1, 5): 11, (2, 5): 10, (4, 3): 1, (4, 5): 51}
+ >>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail))
+ [(1, 5), (2, 5), (4, 5)]
+ >>> # If augmentation is infeasible, then a partial solution can be found
+ >>> avail = {(1, 5): 11}
+ >>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail, partial=True))
+ [(1, 5)]
+ """
+ try:
+ if k <= 0:
+ raise ValueError(f"k must be a positive integer, not {k}")
+ elif G.number_of_nodes() < k + 1:
+ msg = f"impossible to {k} connect in graph with less than {k + 1} nodes"
+ raise nx.NetworkXUnfeasible(msg)
+ elif avail is not None and len(avail) == 0:
+ if not nx.is_k_edge_connected(G, k):
+ raise nx.NetworkXUnfeasible("no available edges")
+ aug_edges = []
+ elif k == 1:
+ aug_edges = one_edge_augmentation(
+ G, avail=avail, weight=weight, partial=partial
+ )
+ elif k == 2:
+ aug_edges = bridge_augmentation(G, avail=avail, weight=weight)
+ else:
+ # raise NotImplementedError(f'not implemented for k>2. k={k}')
+ aug_edges = greedy_k_edge_augmentation(
+ G, k=k, avail=avail, weight=weight, seed=0
+ )
+ # Do eager evaluation so we can catch any exceptions
+ # Before executing partial code.
+ yield from list(aug_edges)
+ except nx.NetworkXUnfeasible:
+ if partial:
+ # Return all available edges
+ if avail is None:
+ aug_edges = complement_edges(G)
+ else:
+ # If we can't k-edge-connect the entire graph, try to
+ # k-edge-connect as much as possible
+ aug_edges = partial_k_edge_augmentation(
+ G, k=k, avail=avail, weight=weight
+ )
+ yield from aug_edges
+ else:
+ raise
+
+
+@nx._dispatchable
+def partial_k_edge_augmentation(G, k, avail, weight=None):
+ """Finds augmentation that k-edge-connects as much of the graph as possible.
+
+ When a k-edge-augmentation is not possible, we can still try to find a
+ small set of edges that partially k-edge-connects as much of the graph as
+ possible. All possible edges are generated between remaining parts.
+ This minimizes the number of k-edge-connected subgraphs in the resulting
+ graph and maximizes the edge connectivity between those subgraphs.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ k : integer
+ Desired edge connectivity
+
+ avail : dict or a set of 2 or 3 tuples
+ For more details, see :func:`k_edge_augmentation`.
+
+ weight : string
+ key to use to find weights if ``avail`` is a set of 3-tuples.
+ For more details, see :func:`k_edge_augmentation`.
+
+ Yields
+ ------
+ edge : tuple
+ Edges in the partial augmentation of G. These edges k-edge-connect any
+ part of G where it is possible, and maximally connects the remaining
+ parts. In other words, all edges from avail are generated except for
+ those within subgraphs that have already become k-edge-connected.
+
+ Notes
+ -----
+ Construct H that augments G with all edges in avail.
+ Find the k-edge-subgraphs of H.
+ For each k-edge-subgraph, if the number of nodes is more than k, then find
+ the k-edge-augmentation of that graph and add it to the solution. Then add
+ all edges in avail between k-edge subgraphs to the solution.
+
+ See Also
+ --------
+ :func:`k_edge_augmentation`
+
+ Examples
+ --------
+ >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7))
+ >>> G.add_node(8)
+ >>> avail = [(1, 3), (1, 4), (1, 5), (2, 4), (2, 5), (3, 5), (1, 8)]
+ >>> sorted(partial_k_edge_augmentation(G, k=2, avail=avail))
+ [(1, 5), (1, 8)]
+ """
+
+ def _edges_between_disjoint(H, only1, only2):
+ """finds edges between disjoint nodes"""
+ only1_adj = {u: set(H.adj[u]) for u in only1}
+ for u, neighbs in only1_adj.items():
+ # Find the neighbors of u in only1 that are also in only2
+ neighbs12 = neighbs.intersection(only2)
+ for v in neighbs12:
+ yield (u, v)
+
+ avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G)
+
+ # Find which parts of the graph can be k-edge-connected
+ H = G.copy()
+ H.add_edges_from(
+ (
+ (u, v, {"weight": w, "generator": (u, v)})
+ for (u, v), w in zip(avail, avail_w)
+ )
+ )
+ k_edge_subgraphs = list(nx.k_edge_subgraphs(H, k=k))
+
+ # Generate edges to k-edge-connect internal subgraphs
+ for nodes in k_edge_subgraphs:
+ if len(nodes) > 1:
+ # Get the k-edge-connected subgraph
+ C = H.subgraph(nodes).copy()
+ # Find the internal edges that were available
+ sub_avail = {
+ d["generator"]: d["weight"]
+ for (u, v, d) in C.edges(data=True)
+ if "generator" in d
+ }
+ # Remove potential augmenting edges
+ C.remove_edges_from(sub_avail.keys())
+ # Find a subset of these edges that makes the component
+ # k-edge-connected and ignore the rest
+ yield from nx.k_edge_augmentation(C, k=k, avail=sub_avail)
+
+ # Generate all edges between CCs that could not be k-edge-connected
+ for cc1, cc2 in it.combinations(k_edge_subgraphs, 2):
+ for u, v in _edges_between_disjoint(H, cc1, cc2):
+ d = H.get_edge_data(u, v)
+ edge = d.get("generator", None)
+ if edge is not None:
+ yield edge
+
+
+@not_implemented_for("multigraph")
+@not_implemented_for("directed")
+@nx._dispatchable
+def one_edge_augmentation(G, avail=None, weight=None, partial=False):
+ """Finds minimum weight set of edges to connect G.
+
+ Equivalent to :func:`k_edge_augmentation` when k=1. Adding the resulting
+ edges to G will make it 1-edge-connected. The solution is optimal for both
+ weighted and non-weighted variants.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ avail : dict or a set of 2 or 3 tuples
+ For more details, see :func:`k_edge_augmentation`.
+
+ weight : string
+ key to use to find weights if ``avail`` is a set of 3-tuples.
+ For more details, see :func:`k_edge_augmentation`.
+
+ partial : boolean
+ If partial is True and no feasible k-edge-augmentation exists, then the
+ augmenting edges minimize the number of connected components.
+
+ Yields
+ ------
+ edge : tuple
+ Edges in the one-augmentation of G
+
+ Raises
+ ------
+ NetworkXUnfeasible
+ If partial is False and no one-edge-augmentation exists.
+
+ Notes
+ -----
+ Uses either :func:`unconstrained_one_edge_augmentation` or
+ :func:`weighted_one_edge_augmentation` depending on whether ``avail`` is
+ specified. Both algorithms are based on finding a minimum spanning tree.
+ As such both algorithms find optimal solutions and run in linear time.
+
+ See Also
+ --------
+ :func:`k_edge_augmentation`
+ """
+ if avail is None:
+ return unconstrained_one_edge_augmentation(G)
+ else:
+ return weighted_one_edge_augmentation(
+ G, avail=avail, weight=weight, partial=partial
+ )
+
+
+@not_implemented_for("multigraph")
+@not_implemented_for("directed")
+@nx._dispatchable
+def bridge_augmentation(G, avail=None, weight=None):
+ """Finds the a set of edges that bridge connects G.
+
+ Equivalent to :func:`k_edge_augmentation` when k=2, and partial=False.
+ Adding the resulting edges to G will make it 2-edge-connected. If no
+ constraints are specified the returned set of edges is minimum an optimal,
+ otherwise the solution is approximated.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ avail : dict or a set of 2 or 3 tuples
+ For more details, see :func:`k_edge_augmentation`.
+
+ weight : string
+ key to use to find weights if ``avail`` is a set of 3-tuples.
+ For more details, see :func:`k_edge_augmentation`.
+
+ Yields
+ ------
+ edge : tuple
+ Edges in the bridge-augmentation of G
+
+ Raises
+ ------
+ NetworkXUnfeasible
+ If no bridge-augmentation exists.
+
+ Notes
+ -----
+ If there are no constraints the solution can be computed in linear time
+ using :func:`unconstrained_bridge_augmentation`. Otherwise, the problem
+ becomes NP-hard and is the solution is approximated by
+ :func:`weighted_bridge_augmentation`.
+
+ See Also
+ --------
+ :func:`k_edge_augmentation`
+ """
+ if G.number_of_nodes() < 3:
+ raise nx.NetworkXUnfeasible("impossible to bridge connect less than 3 nodes")
+ if avail is None:
+ return unconstrained_bridge_augmentation(G)
+ else:
+ return weighted_bridge_augmentation(G, avail, weight=weight)
+
+
+# --- Algorithms and Helpers ---
+
+
+def _ordered(u, v):
+ """Returns the nodes in an undirected edge in lower-triangular order"""
+ return (u, v) if u < v else (v, u)
+
+
+def _unpack_available_edges(avail, weight=None, G=None):
+ """Helper to separate avail into edges and corresponding weights"""
+ if weight is None:
+ weight = "weight"
+ if isinstance(avail, dict):
+ avail_uv = list(avail.keys())
+ avail_w = list(avail.values())
+ else:
+
+ def _try_getitem(d):
+ try:
+ return d[weight]
+ except TypeError:
+ return d
+
+ avail_uv = [tup[0:2] for tup in avail]
+ avail_w = [1 if len(tup) == 2 else _try_getitem(tup[-1]) for tup in avail]
+
+ if G is not None:
+ # Edges already in the graph are filtered
+ flags = [not G.has_edge(u, v) for u, v in avail_uv]
+ avail_uv = list(it.compress(avail_uv, flags))
+ avail_w = list(it.compress(avail_w, flags))
+ return avail_uv, avail_w
+
+
+MetaEdge = namedtuple("MetaEdge", ("meta_uv", "uv", "w"))
+
+
+def _lightest_meta_edges(mapping, avail_uv, avail_w):
+ """Maps available edges in the original graph to edges in the metagraph.
+
+ Parameters
+ ----------
+ mapping : dict
+ mapping produced by :func:`collapse`, that maps each node in the
+ original graph to a node in the meta graph
+
+ avail_uv : list
+ list of edges
+
+ avail_w : list
+ list of edge weights
+
+ Notes
+ -----
+ Each node in the metagraph is a k-edge-connected component in the original
+ graph. We don't care about any edge within the same k-edge-connected
+ component, so we ignore self edges. We also are only interested in the
+ minimum weight edge bridging each k-edge-connected component so, we group
+ the edges by meta-edge and take the lightest in each group.
+
+ Examples
+ --------
+ >>> # Each group represents a meta-node
+ >>> groups = ([1, 2, 3], [4, 5], [6])
+ >>> mapping = {n: meta_n for meta_n, ns in enumerate(groups) for n in ns}
+ >>> avail_uv = [(1, 2), (3, 6), (1, 4), (5, 2), (6, 1), (2, 6), (3, 1)]
+ >>> avail_w = [20, 99, 20, 15, 50, 99, 20]
+ >>> sorted(_lightest_meta_edges(mapping, avail_uv, avail_w))
+ [MetaEdge(meta_uv=(0, 1), uv=(5, 2), w=15), MetaEdge(meta_uv=(0, 2), uv=(6, 1), w=50)]
+ """
+ grouped_wuv = defaultdict(list)
+ for w, (u, v) in zip(avail_w, avail_uv):
+ # Order the meta-edge so it can be used as a dict key
+ meta_uv = _ordered(mapping[u], mapping[v])
+ # Group each available edge using the meta-edge as a key
+ grouped_wuv[meta_uv].append((w, u, v))
+
+ # Now that all available edges are grouped, choose one per group
+ for (mu, mv), choices_wuv in grouped_wuv.items():
+ # Ignore available edges within the same meta-node
+ if mu != mv:
+ # Choose the lightest available edge belonging to each meta-edge
+ w, u, v = min(choices_wuv)
+ yield MetaEdge((mu, mv), (u, v), w)
+
+
+@nx._dispatchable
+def unconstrained_one_edge_augmentation(G):
+ """Finds the smallest set of edges to connect G.
+
+ This is a variant of the unweighted MST problem.
+ If G is not empty, a feasible solution always exists.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ Yields
+ ------
+ edge : tuple
+ Edges in the one-edge-augmentation of G
+
+ See Also
+ --------
+ :func:`one_edge_augmentation`
+ :func:`k_edge_augmentation`
+
+ Examples
+ --------
+ >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)])
+ >>> G.add_nodes_from([6, 7, 8])
+ >>> sorted(unconstrained_one_edge_augmentation(G))
+ [(1, 4), (4, 6), (6, 7), (7, 8)]
+ """
+ ccs1 = list(nx.connected_components(G))
+ C = collapse(G, ccs1)
+ # When we are not constrained, we can just make a meta graph tree.
+ meta_nodes = list(C.nodes())
+ # build a path in the metagraph
+ meta_aug = list(zip(meta_nodes, meta_nodes[1:]))
+ # map that path to the original graph
+ inverse = defaultdict(list)
+ for k, v in C.graph["mapping"].items():
+ inverse[v].append(k)
+ for mu, mv in meta_aug:
+ yield (inverse[mu][0], inverse[mv][0])
+
+
+@nx._dispatchable
+def weighted_one_edge_augmentation(G, avail, weight=None, partial=False):
+ """Finds the minimum weight set of edges to connect G if one exists.
+
+ This is a variant of the weighted MST problem.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ avail : dict or a set of 2 or 3 tuples
+ For more details, see :func:`k_edge_augmentation`.
+
+ weight : string
+ key to use to find weights if ``avail`` is a set of 3-tuples.
+ For more details, see :func:`k_edge_augmentation`.
+
+ partial : boolean
+ If partial is True and no feasible k-edge-augmentation exists, then the
+ augmenting edges minimize the number of connected components.
+
+ Yields
+ ------
+ edge : tuple
+ Edges in the subset of avail chosen to connect G.
+
+ See Also
+ --------
+ :func:`one_edge_augmentation`
+ :func:`k_edge_augmentation`
+
+ Examples
+ --------
+ >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)])
+ >>> G.add_nodes_from([6, 7, 8])
+ >>> # any edge not in avail has an implicit weight of infinity
+ >>> avail = [(1, 3), (1, 5), (4, 7), (4, 8), (6, 1), (8, 1), (8, 2)]
+ >>> sorted(weighted_one_edge_augmentation(G, avail))
+ [(1, 5), (4, 7), (6, 1), (8, 1)]
+ >>> # find another solution by giving large weights to edges in the
+ >>> # previous solution (note some of the old edges must be used)
+ >>> avail = [(1, 3), (1, 5, 99), (4, 7, 9), (6, 1, 99), (8, 1, 99), (8, 2)]
+ >>> sorted(weighted_one_edge_augmentation(G, avail))
+ [(1, 5), (4, 7), (6, 1), (8, 2)]
+ """
+ avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G)
+ # Collapse CCs in the original graph into nodes in a metagraph
+ # Then find an MST of the metagraph instead of the original graph
+ C = collapse(G, nx.connected_components(G))
+ mapping = C.graph["mapping"]
+ # Assign each available edge to an edge in the metagraph
+ candidate_mapping = _lightest_meta_edges(mapping, avail_uv, avail_w)
+ # nx.set_edge_attributes(C, name='weight', values=0)
+ C.add_edges_from(
+ (mu, mv, {"weight": w, "generator": uv})
+ for (mu, mv), uv, w in candidate_mapping
+ )
+ # Find MST of the meta graph
+ meta_mst = nx.minimum_spanning_tree(C)
+ if not partial and not nx.is_connected(meta_mst):
+ raise nx.NetworkXUnfeasible("Not possible to connect G with available edges")
+ # Yield the edge that generated the meta-edge
+ for mu, mv, d in meta_mst.edges(data=True):
+ if "generator" in d:
+ edge = d["generator"]
+ yield edge
+
+
+@nx._dispatchable
+def unconstrained_bridge_augmentation(G):
+ """Finds an optimal 2-edge-augmentation of G using the fewest edges.
+
+ This is an implementation of the algorithm detailed in [1]_.
+ The basic idea is to construct a meta-graph of bridge-ccs, connect leaf
+ nodes of the trees to connect the entire graph, and finally connect the
+ leafs of the tree in dfs-preorder to bridge connect the entire graph.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ Yields
+ ------
+ edge : tuple
+ Edges in the bridge augmentation of G
+
+ Notes
+ -----
+ Input: a graph G.
+ First find the bridge components of G and collapse each bridge-cc into a
+ node of a metagraph graph C, which is guaranteed to be a forest of trees.
+
+ C contains p "leafs" --- nodes with exactly one incident edge.
+ C contains q "isolated nodes" --- nodes with no incident edges.
+
+ Theorem: If p + q > 1, then at least :math:`ceil(p / 2) + q` edges are
+ needed to bridge connect C. This algorithm achieves this min number.
+
+ The method first adds enough edges to make G into a tree and then pairs
+ leafs in a simple fashion.
+
+ Let n be the number of trees in C. Let v(i) be an isolated vertex in the
+ i-th tree if one exists, otherwise it is a pair of distinct leafs nodes
+ in the i-th tree. Alternating edges from these sets (i.e. adding edges
+ A1 = [(v(i)[0], v(i + 1)[1]), v(i + 1)[0], v(i + 2)[1])...]) connects C
+ into a tree T. This tree has p' = p + 2q - 2(n -1) leafs and no isolated
+ vertices. A1 has n - 1 edges. The next step finds ceil(p' / 2) edges to
+ biconnect any tree with p' leafs.
+
+ Convert T into an arborescence T' by picking an arbitrary root node with
+ degree >= 2 and directing all edges away from the root. Note the
+ implementation implicitly constructs T'.
+
+ The leafs of T are the nodes with no existing edges in T'.
+ Order the leafs of T' by DFS preorder. Then break this list in half
+ and add the zipped pairs to A2.
+
+ The set A = A1 + A2 is the minimum augmentation in the metagraph.
+
+ To convert this to edges in the original graph
+
+ References
+ ----------
+ .. [1] Eswaran, Kapali P., and R. Endre Tarjan. (1975) Augmentation problems.
+ http://epubs.siam.org/doi/abs/10.1137/0205044
+
+ See Also
+ --------
+ :func:`bridge_augmentation`
+ :func:`k_edge_augmentation`
+
+ Examples
+ --------
+ >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7))
+ >>> sorted(unconstrained_bridge_augmentation(G))
+ [(1, 7)]
+ >>> G = nx.path_graph((1, 2, 3, 2, 4, 5, 6, 7))
+ >>> sorted(unconstrained_bridge_augmentation(G))
+ [(1, 3), (3, 7)]
+ >>> G = nx.Graph([(0, 1), (0, 2), (1, 2)])
+ >>> G.add_node(4)
+ >>> sorted(unconstrained_bridge_augmentation(G))
+ [(1, 4), (4, 0)]
+ """
+ # -----
+ # Mapping of terms from (Eswaran and Tarjan):
+ # G = G_0 - the input graph
+ # C = G_0' - the bridge condensation of G. (This is a forest of trees)
+ # A1 = A_1 - the edges to connect the forest into a tree
+ # leaf = pendant - a node with degree of 1
+
+ # alpha(v) = maps the node v in G to its meta-node in C
+ # beta(x) = maps the meta-node x in C to any node in the bridge
+ # component of G corresponding to x.
+
+ # find the 2-edge-connected components of G
+ bridge_ccs = list(nx.connectivity.bridge_components(G))
+ # condense G into an forest C
+ C = collapse(G, bridge_ccs)
+
+ # Choose pairs of distinct leaf nodes in each tree. If this is not
+ # possible then make a pair using the single isolated node in the tree.
+ vset1 = [
+ tuple(cc) * 2 # case1: an isolated node
+ if len(cc) == 1
+ else sorted(cc, key=C.degree)[0:2] # case2: pair of leaf nodes
+ for cc in nx.connected_components(C)
+ ]
+ if len(vset1) > 1:
+ # Use this set to construct edges that connect C into a tree.
+ nodes1 = [vs[0] for vs in vset1]
+ nodes2 = [vs[1] for vs in vset1]
+ A1 = list(zip(nodes1[1:], nodes2))
+ else:
+ A1 = []
+ # Connect each tree in the forest to construct an arborescence
+ T = C.copy()
+ T.add_edges_from(A1)
+
+ # If there are only two leaf nodes, we simply connect them.
+ leafs = [n for n, d in T.degree() if d == 1]
+ if len(leafs) == 1:
+ A2 = []
+ if len(leafs) == 2:
+ A2 = [tuple(leafs)]
+ else:
+ # Choose an arbitrary non-leaf root
+ try:
+ root = next(n for n, d in T.degree() if d > 1)
+ except StopIteration: # no nodes found with degree > 1
+ return
+ # order the leaves of C by (induced directed) preorder
+ v2 = [n for n in nx.dfs_preorder_nodes(T, root) if T.degree(n) == 1]
+ # connecting first half of the leafs in pre-order to the second
+ # half will bridge connect the tree with the fewest edges.
+ half = math.ceil(len(v2) / 2)
+ A2 = list(zip(v2[:half], v2[-half:]))
+
+ # collect the edges used to augment the original forest
+ aug_tree_edges = A1 + A2
+
+ # Construct the mapping (beta) from meta-nodes to regular nodes
+ inverse = defaultdict(list)
+ for k, v in C.graph["mapping"].items():
+ inverse[v].append(k)
+ # sort so we choose minimum degree nodes first
+ inverse = {
+ mu: sorted(mapped, key=lambda u: (G.degree(u), u))
+ for mu, mapped in inverse.items()
+ }
+
+ # For each meta-edge, map back to an arbitrary pair in the original graph
+ G2 = G.copy()
+ for mu, mv in aug_tree_edges:
+ # Find the first available edge that doesn't exist and return it
+ for u, v in it.product(inverse[mu], inverse[mv]):
+ if not G2.has_edge(u, v):
+ G2.add_edge(u, v)
+ yield u, v
+ break
+
+
+@nx._dispatchable
+def weighted_bridge_augmentation(G, avail, weight=None):
+ """Finds an approximate min-weight 2-edge-augmentation of G.
+
+ This is an implementation of the approximation algorithm detailed in [1]_.
+ It chooses a set of edges from avail to add to G that renders it
+ 2-edge-connected if such a subset exists. This is done by finding a
+ minimum spanning arborescence of a specially constructed metagraph.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ avail : set of 2 or 3 tuples.
+ candidate edges (with optional weights) to choose from
+
+ weight : string
+ key to use to find weights if avail is a set of 3-tuples where the
+ third item in each tuple is a dictionary.
+
+ Yields
+ ------
+ edge : tuple
+ Edges in the subset of avail chosen to bridge augment G.
+
+ Notes
+ -----
+ Finding a weighted 2-edge-augmentation is NP-hard.
+ Any edge not in ``avail`` is considered to have a weight of infinity.
+ The approximation factor is 2 if ``G`` is connected and 3 if it is not.
+ Runs in :math:`O(m + n log(n))` time
+
+ References
+ ----------
+ .. [1] Khuller, Samir, and Ramakrishna Thurimella. (1993) Approximation
+ algorithms for graph augmentation.
+ http://www.sciencedirect.com/science/article/pii/S0196677483710102
+
+ See Also
+ --------
+ :func:`bridge_augmentation`
+ :func:`k_edge_augmentation`
+
+ Examples
+ --------
+ >>> G = nx.path_graph((1, 2, 3, 4))
+ >>> # When the weights are equal, (1, 4) is the best
+ >>> avail = [(1, 4, 1), (1, 3, 1), (2, 4, 1)]
+ >>> sorted(weighted_bridge_augmentation(G, avail))
+ [(1, 4)]
+ >>> # Giving (1, 4) a high weight makes the two edge solution the best.
+ >>> avail = [(1, 4, 1000), (1, 3, 1), (2, 4, 1)]
+ >>> sorted(weighted_bridge_augmentation(G, avail))
+ [(1, 3), (2, 4)]
+ >>> # ------
+ >>> G = nx.path_graph((1, 2, 3, 4))
+ >>> G.add_node(5)
+ >>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 1)]
+ >>> sorted(weighted_bridge_augmentation(G, avail=avail))
+ [(1, 5), (4, 5)]
+ >>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 51)]
+ >>> sorted(weighted_bridge_augmentation(G, avail=avail))
+ [(1, 5), (2, 5), (4, 5)]
+ """
+
+ if weight is None:
+ weight = "weight"
+
+ # If input G is not connected the approximation factor increases to 3
+ if not nx.is_connected(G):
+ H = G.copy()
+ connectors = list(one_edge_augmentation(H, avail=avail, weight=weight))
+ H.add_edges_from(connectors)
+
+ yield from connectors
+ else:
+ connectors = []
+ H = G
+
+ if len(avail) == 0:
+ if nx.has_bridges(H):
+ raise nx.NetworkXUnfeasible("no augmentation possible")
+
+ avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=H)
+
+ # Collapse input into a metagraph. Meta nodes are bridge-ccs
+ bridge_ccs = nx.connectivity.bridge_components(H)
+ C = collapse(H, bridge_ccs)
+
+ # Use the meta graph to shrink avail to a small feasible subset
+ mapping = C.graph["mapping"]
+ # Choose the minimum weight feasible edge in each group
+ meta_to_wuv = {
+ (mu, mv): (w, uv)
+ for (mu, mv), uv, w in _lightest_meta_edges(mapping, avail_uv, avail_w)
+ }
+
+ # Mapping of terms from (Khuller and Thurimella):
+ # C : G_0 = (V, E^0)
+ # This is the metagraph where each node is a 2-edge-cc in G.
+ # The edges in C represent bridges in the original graph.
+ # (mu, mv) : E - E^0 # they group both avail and given edges in E
+ # T : \Gamma
+ # D : G^D = (V, E_D)
+
+ # The paper uses ancestor because children point to parents, which is
+ # contrary to networkx standards. So, we actually need to run
+ # nx.least_common_ancestor on the reversed Tree.
+
+ # Pick an arbitrary leaf from C as the root
+ try:
+ root = next(n for n, d in C.degree() if d == 1)
+ except StopIteration: # no nodes found with degree == 1
+ return
+ # Root C into a tree TR by directing all edges away from the root
+ # Note in their paper T directs edges towards the root
+ TR = nx.dfs_tree(C, root)
+
+ # Add to D the directed edges of T and set their weight to zero
+ # This indicates that it costs nothing to use edges that were given.
+ D = nx.reverse(TR).copy()
+
+ nx.set_edge_attributes(D, name="weight", values=0)
+
+ # The LCA of mu and mv in T is the shared ancestor of mu and mv that is
+ # located farthest from the root.
+ lca_gen = nx.tree_all_pairs_lowest_common_ancestor(
+ TR, root=root, pairs=meta_to_wuv.keys()
+ )
+
+ for (mu, mv), lca in lca_gen:
+ w, uv = meta_to_wuv[(mu, mv)]
+ if lca == mu:
+ # If u is an ancestor of v in TR, then add edge u->v to D
+ D.add_edge(lca, mv, weight=w, generator=uv)
+ elif lca == mv:
+ # If v is an ancestor of u in TR, then add edge v->u to D
+ D.add_edge(lca, mu, weight=w, generator=uv)
+ else:
+ # If neither u nor v is a ancestor of the other in TR
+ # let t = lca(TR, u, v) and add edges t->u and t->v
+ # Track the original edge that GENERATED these edges.
+ D.add_edge(lca, mu, weight=w, generator=uv)
+ D.add_edge(lca, mv, weight=w, generator=uv)
+
+ # Then compute a minimum rooted branching
+ try:
+ # Note the original edges must be directed towards to root for the
+ # branching to give us a bridge-augmentation.
+ A = _minimum_rooted_branching(D, root)
+ except nx.NetworkXException as err:
+ # If there is no branching then augmentation is not possible
+ raise nx.NetworkXUnfeasible("no 2-edge-augmentation possible") from err
+
+ # For each edge e, in the branching that did not belong to the directed
+ # tree T, add the corresponding edge that **GENERATED** it (this is not
+ # necessarily e itself!)
+
+ # ensure the third case does not generate edges twice
+ bridge_connectors = set()
+ for mu, mv in A.edges():
+ data = D.get_edge_data(mu, mv)
+ if "generator" in data:
+ # Add the avail edge that generated the branching edge.
+ edge = data["generator"]
+ bridge_connectors.add(edge)
+
+ yield from bridge_connectors
+
+
+def _minimum_rooted_branching(D, root):
+ """Helper function to compute a minimum rooted branching (aka rooted
+ arborescence)
+
+ Before the branching can be computed, the directed graph must be rooted by
+ removing the predecessors of root.
+
+ A branching / arborescence of rooted graph G is a subgraph that contains a
+ directed path from the root to every other vertex. It is the directed
+ analog of the minimum spanning tree problem.
+
+ References
+ ----------
+ [1] Khuller, Samir (2002) Advanced Algorithms Lecture 24 Notes.
+ https://web.archive.org/web/20121030033722/https://www.cs.umd.edu/class/spring2011/cmsc651/lec07.pdf
+ """
+ rooted = D.copy()
+ # root the graph by removing all predecessors to `root`.
+ rooted.remove_edges_from([(u, root) for u in D.predecessors(root)])
+ # Then compute the branching / arborescence.
+ A = nx.minimum_spanning_arborescence(rooted)
+ return A
+
+
+@nx._dispatchable(returns_graph=True)
+def collapse(G, grouped_nodes):
+ """Collapses each group of nodes into a single node.
+
+ This is similar to condensation, but works on undirected graphs.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ grouped_nodes: list or generator
+ Grouping of nodes to collapse. The grouping must be disjoint.
+ If grouped_nodes are strongly_connected_components then this is
+ equivalent to :func:`condensation`.
+
+ Returns
+ -------
+ C : NetworkX Graph
+ The collapsed graph C of G with respect to the node grouping. The node
+ labels are integers corresponding to the index of the component in the
+ list of grouped_nodes. 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 group that the node in C
+ represents.
+
+ Examples
+ --------
+ >>> # Collapses a graph using disjoint groups, but not necessarily connected
+ >>> G = nx.Graph([(1, 0), (2, 3), (3, 1), (3, 4), (4, 5), (5, 6), (5, 7)])
+ >>> G.add_node("A")
+ >>> grouped_nodes = [{0, 1, 2, 3}, {5, 6, 7}]
+ >>> C = collapse(G, grouped_nodes)
+ >>> members = nx.get_node_attributes(C, "members")
+ >>> sorted(members.keys())
+ [0, 1, 2, 3]
+ >>> member_values = set(map(frozenset, members.values()))
+ >>> assert {0, 1, 2, 3} in member_values
+ >>> assert {4} in member_values
+ >>> assert {5, 6, 7} in member_values
+ >>> assert {"A"} in member_values
+ """
+ mapping = {}
+ members = {}
+ C = G.__class__()
+ i = 0 # required if G is empty
+ remaining = set(G.nodes())
+ for i, group in enumerate(grouped_nodes):
+ group = set(group)
+ assert remaining.issuperset(
+ group
+ ), "grouped nodes must exist in G and be disjoint"
+ remaining.difference_update(group)
+ members[i] = group
+ mapping.update((n, i) for n in group)
+ # remaining nodes are in their own group
+ for i, node in enumerate(remaining, start=i + 1):
+ group = {node}
+ members[i] = group
+ mapping.update((n, i) for n in group)
+ number_of_groups = i + 1
+ C.add_nodes_from(range(number_of_groups))
+ C.add_edges_from(
+ (mapping[u], mapping[v]) for u, v in G.edges() if mapping[u] != mapping[v]
+ )
+ # Add a list of members (ie original nodes) to each node (ie scc) in C.
+ nx.set_node_attributes(C, name="members", values=members)
+ # Add mapping dict as graph attribute
+ C.graph["mapping"] = mapping
+ return C
+
+
+@nx._dispatchable
+def complement_edges(G):
+ """Returns only the edges in the complement of G
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ Yields
+ ------
+ edge : tuple
+ Edges in the complement of G
+
+ Examples
+ --------
+ >>> G = nx.path_graph((1, 2, 3, 4))
+ >>> sorted(complement_edges(G))
+ [(1, 3), (1, 4), (2, 4)]
+ >>> G = nx.path_graph((1, 2, 3, 4), nx.DiGraph())
+ >>> sorted(complement_edges(G))
+ [(1, 3), (1, 4), (2, 1), (2, 4), (3, 1), (3, 2), (4, 1), (4, 2), (4, 3)]
+ >>> G = nx.complete_graph(1000)
+ >>> sorted(complement_edges(G))
+ []
+ """
+ G_adj = G._adj # Store as a variable to eliminate attribute lookup
+ if G.is_directed():
+ for u, v in it.combinations(G.nodes(), 2):
+ if v not in G_adj[u]:
+ yield (u, v)
+ if u not in G_adj[v]:
+ yield (v, u)
+ else:
+ for u, v in it.combinations(G.nodes(), 2):
+ if v not in G_adj[u]:
+ yield (u, v)
+
+
+def _compat_shuffle(rng, input):
+ """wrapper around rng.shuffle for python 2 compatibility reasons"""
+ rng.shuffle(input)
+
+
+@not_implemented_for("multigraph")
+@not_implemented_for("directed")
+@py_random_state(4)
+@nx._dispatchable
+def greedy_k_edge_augmentation(G, k, avail=None, weight=None, seed=None):
+ """Greedy algorithm for finding a k-edge-augmentation
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ k : integer
+ Desired edge connectivity
+
+ avail : dict or a set of 2 or 3 tuples
+ For more details, see :func:`k_edge_augmentation`.
+
+ weight : string
+ key to use to find weights if ``avail`` is a set of 3-tuples.
+ For more details, see :func:`k_edge_augmentation`.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Yields
+ ------
+ edge : tuple
+ Edges in the greedy augmentation of G
+
+ Notes
+ -----
+ The algorithm is simple. Edges are incrementally added between parts of the
+ graph that are not yet locally k-edge-connected. Then edges are from the
+ augmenting set are pruned as long as local-edge-connectivity is not broken.
+
+ This algorithm is greedy and does not provide optimality guarantees. It
+ exists only to provide :func:`k_edge_augmentation` with the ability to
+ generate a feasible solution for arbitrary k.
+
+ See Also
+ --------
+ :func:`k_edge_augmentation`
+
+ Examples
+ --------
+ >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7))
+ >>> sorted(greedy_k_edge_augmentation(G, k=2))
+ [(1, 7)]
+ >>> sorted(greedy_k_edge_augmentation(G, k=1, avail=[]))
+ []
+ >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7))
+ >>> avail = {(u, v): 1 for (u, v) in complement_edges(G)}
+ >>> # randomized pruning process can produce different solutions
+ >>> sorted(greedy_k_edge_augmentation(G, k=4, avail=avail, seed=2))
+ [(1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (2, 4), (2, 6), (3, 7), (5, 7)]
+ >>> sorted(greedy_k_edge_augmentation(G, k=4, avail=avail, seed=3))
+ [(1, 3), (1, 5), (1, 6), (2, 4), (2, 6), (3, 7), (4, 7), (5, 7)]
+ """
+ # Result set
+ aug_edges = []
+
+ done = is_k_edge_connected(G, k)
+ if done:
+ return
+ if avail is None:
+ # all edges are available
+ avail_uv = list(complement_edges(G))
+ avail_w = [1] * len(avail_uv)
+ else:
+ # Get the unique set of unweighted edges
+ avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G)
+
+ # Greedy: order lightest edges. Use degree sum to tie-break
+ tiebreaker = [sum(map(G.degree, uv)) for uv in avail_uv]
+ avail_wduv = sorted(zip(avail_w, tiebreaker, avail_uv))
+ avail_uv = [uv for w, d, uv in avail_wduv]
+
+ # Incrementally add edges in until we are k-connected
+ H = G.copy()
+ for u, v in avail_uv:
+ done = False
+ if not is_locally_k_edge_connected(H, u, v, k=k):
+ # Only add edges in parts that are not yet locally k-edge-connected
+ aug_edges.append((u, v))
+ H.add_edge(u, v)
+ # Did adding this edge help?
+ if H.degree(u) >= k and H.degree(v) >= k:
+ done = is_k_edge_connected(H, k)
+ if done:
+ break
+
+ # Check for feasibility
+ if not done:
+ raise nx.NetworkXUnfeasible("not able to k-edge-connect with available edges")
+
+ # Randomized attempt to reduce the size of the solution
+ _compat_shuffle(seed, aug_edges)
+ for u, v in list(aug_edges):
+ # Don't remove if we know it would break connectivity
+ if H.degree(u) <= k or H.degree(v) <= k:
+ continue
+ H.remove_edge(u, v)
+ aug_edges.remove((u, v))
+ if not is_k_edge_connected(H, k=k):
+ # If removing this edge breaks feasibility, undo
+ H.add_edge(u, v)
+ aug_edges.append((u, v))
+
+ # Generate results
+ yield from aug_edges
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_kcomponents.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_kcomponents.py
new file mode 100644
index 00000000..96886f2b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_kcomponents.py
@@ -0,0 +1,592 @@
+"""
+Algorithms for finding k-edge-connected components and subgraphs.
+
+A k-edge-connected component (k-edge-cc) is a maximal set of nodes in G, such
+that all pairs of node have an edge-connectivity of at least k.
+
+A k-edge-connected subgraph (k-edge-subgraph) is a maximal set of nodes in G,
+such that the subgraph of G defined by the nodes has an edge-connectivity at
+least k.
+"""
+
+import itertools as it
+from functools import partial
+
+import networkx as nx
+from networkx.utils import arbitrary_element, not_implemented_for
+
+__all__ = [
+ "k_edge_components",
+ "k_edge_subgraphs",
+ "bridge_components",
+ "EdgeComponentAuxGraph",
+]
+
+
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def k_edge_components(G, k):
+ """Generates nodes in each maximal k-edge-connected component in G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ k : Integer
+ Desired edge connectivity
+
+ Returns
+ -------
+ k_edge_components : a generator of k-edge-ccs. Each set of returned nodes
+ will have k-edge-connectivity in the graph G.
+
+ See Also
+ --------
+ :func:`local_edge_connectivity`
+ :func:`k_edge_subgraphs` : similar to this function, but the subgraph
+ defined by the nodes must also have k-edge-connectivity.
+ :func:`k_components` : similar to this function, but uses node-connectivity
+ instead of edge-connectivity
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If the input graph is a multigraph.
+
+ ValueError:
+ If k is less than 1
+
+ Notes
+ -----
+ Attempts to use the most efficient implementation available based on k.
+ If k=1, this is simply connected components for directed graphs and
+ connected components for undirected graphs.
+ If k=2 on an efficient bridge connected component algorithm from _[1] is
+ run based on the chain decomposition.
+ Otherwise, the algorithm from _[2] is used.
+
+ Examples
+ --------
+ >>> import itertools as it
+ >>> from networkx.utils import pairwise
+ >>> paths = [
+ ... (1, 2, 4, 3, 1, 4),
+ ... (5, 6, 7, 8, 5, 7, 8, 6),
+ ... ]
+ >>> G = nx.Graph()
+ >>> G.add_nodes_from(it.chain(*paths))
+ >>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
+ >>> # note this returns {1, 4} unlike k_edge_subgraphs
+ >>> sorted(map(sorted, nx.k_edge_components(G, k=3)))
+ [[1, 4], [2], [3], [5, 6, 7, 8]]
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Bridge_%28graph_theory%29
+ .. [2] Wang, Tianhao, et al. (2015) A simple algorithm for finding all
+ k-edge-connected components.
+ http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
+ """
+ # Compute k-edge-ccs using the most efficient algorithms available.
+ if k < 1:
+ raise ValueError("k cannot be less than 1")
+ if G.is_directed():
+ if k == 1:
+ return nx.strongly_connected_components(G)
+ else:
+ # TODO: investigate https://arxiv.org/abs/1412.6466 for k=2
+ aux_graph = EdgeComponentAuxGraph.construct(G)
+ return aux_graph.k_edge_components(k)
+ else:
+ if k == 1:
+ return nx.connected_components(G)
+ elif k == 2:
+ return bridge_components(G)
+ else:
+ aux_graph = EdgeComponentAuxGraph.construct(G)
+ return aux_graph.k_edge_components(k)
+
+
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def k_edge_subgraphs(G, k):
+ """Generates nodes in each maximal k-edge-connected subgraph in G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ k : Integer
+ Desired edge connectivity
+
+ Returns
+ -------
+ k_edge_subgraphs : a generator of k-edge-subgraphs
+ Each k-edge-subgraph is a maximal set of nodes that defines a subgraph
+ of G that is k-edge-connected.
+
+ See Also
+ --------
+ :func:`edge_connectivity`
+ :func:`k_edge_components` : similar to this function, but nodes only
+ need to have k-edge-connectivity within the graph G and the subgraphs
+ might not be k-edge-connected.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If the input graph is a multigraph.
+
+ ValueError:
+ If k is less than 1
+
+ Notes
+ -----
+ Attempts to use the most efficient implementation available based on k.
+ If k=1, or k=2 and the graph is undirected, then this simply calls
+ `k_edge_components`. Otherwise the algorithm from _[1] is used.
+
+ Examples
+ --------
+ >>> import itertools as it
+ >>> from networkx.utils import pairwise
+ >>> paths = [
+ ... (1, 2, 4, 3, 1, 4),
+ ... (5, 6, 7, 8, 5, 7, 8, 6),
+ ... ]
+ >>> G = nx.Graph()
+ >>> G.add_nodes_from(it.chain(*paths))
+ >>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
+ >>> # note this does not return {1, 4} unlike k_edge_components
+ >>> sorted(map(sorted, nx.k_edge_subgraphs(G, k=3)))
+ [[1], [2], [3], [4], [5, 6, 7, 8]]
+
+ References
+ ----------
+ .. [1] Zhou, Liu, et al. (2012) Finding maximal k-edge-connected subgraphs
+ from a large graph. ACM International Conference on Extending Database
+ Technology 2012 480-–491.
+ https://openproceedings.org/2012/conf/edbt/ZhouLYLCL12.pdf
+ """
+ if k < 1:
+ raise ValueError("k cannot be less than 1")
+ if G.is_directed():
+ if k <= 1:
+ # For directed graphs ,
+ # When k == 1, k-edge-ccs and k-edge-subgraphs are the same
+ return k_edge_components(G, k)
+ else:
+ return _k_edge_subgraphs_nodes(G, k)
+ else:
+ if k <= 2:
+ # For undirected graphs,
+ # when k <= 2, k-edge-ccs and k-edge-subgraphs are the same
+ return k_edge_components(G, k)
+ else:
+ return _k_edge_subgraphs_nodes(G, k)
+
+
+def _k_edge_subgraphs_nodes(G, k):
+ """Helper to get the nodes from the subgraphs.
+
+ This allows k_edge_subgraphs to return a generator.
+ """
+ for C in general_k_edge_subgraphs(G, k):
+ yield set(C.nodes())
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def bridge_components(G):
+ """Finds all bridge-connected components G.
+
+ Parameters
+ ----------
+ G : NetworkX undirected graph
+
+ Returns
+ -------
+ bridge_components : a generator of 2-edge-connected components
+
+
+ See Also
+ --------
+ :func:`k_edge_subgraphs` : this function is a special case for an
+ undirected graph where k=2.
+ :func:`biconnected_components` : similar to this function, but is defined
+ using 2-node-connectivity instead of 2-edge-connectivity.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If the input graph is directed or a multigraph.
+
+ Notes
+ -----
+ Bridge-connected components are also known as 2-edge-connected components.
+
+ Examples
+ --------
+ >>> # The barbell graph with parameter zero has a single bridge
+ >>> G = nx.barbell_graph(5, 0)
+ >>> from networkx.algorithms.connectivity.edge_kcomponents import bridge_components
+ >>> sorted(map(sorted, bridge_components(G)))
+ [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
+ """
+ H = G.copy()
+ H.remove_edges_from(nx.bridges(G))
+ yield from nx.connected_components(H)
+
+
+class EdgeComponentAuxGraph:
+ r"""A simple algorithm to find all k-edge-connected components in a graph.
+
+ Constructing the auxiliary graph (which may take some time) allows for the
+ k-edge-ccs to be found in linear time for arbitrary k.
+
+ Notes
+ -----
+ This implementation is based on [1]_. The idea is to construct an auxiliary
+ graph from which the k-edge-ccs can be extracted in linear time. The
+ auxiliary graph is constructed in $O(|V|\cdot F)$ operations, where F is the
+ complexity of max flow. Querying the components takes an additional $O(|V|)$
+ operations. This algorithm can be slow for large graphs, but it handles an
+ arbitrary k and works for both directed and undirected inputs.
+
+ The undirected case for k=1 is exactly connected components.
+ The undirected case for k=2 is exactly bridge connected components.
+ The directed case for k=1 is exactly strongly connected components.
+
+ References
+ ----------
+ .. [1] Wang, Tianhao, et al. (2015) A simple algorithm for finding all
+ k-edge-connected components.
+ http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
+
+ Examples
+ --------
+ >>> import itertools as it
+ >>> from networkx.utils import pairwise
+ >>> from networkx.algorithms.connectivity import EdgeComponentAuxGraph
+ >>> # Build an interesting graph with multiple levels of k-edge-ccs
+ >>> paths = [
+ ... (1, 2, 3, 4, 1, 3, 4, 2), # a 3-edge-cc (a 4 clique)
+ ... (5, 6, 7, 5), # a 2-edge-cc (a 3 clique)
+ ... (1, 5), # combine first two ccs into a 1-edge-cc
+ ... (0,), # add an additional disconnected 1-edge-cc
+ ... ]
+ >>> G = nx.Graph()
+ >>> G.add_nodes_from(it.chain(*paths))
+ >>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
+ >>> # Constructing the AuxGraph takes about O(n ** 4)
+ >>> aux_graph = EdgeComponentAuxGraph.construct(G)
+ >>> # Once constructed, querying takes O(n)
+ >>> sorted(map(sorted, aux_graph.k_edge_components(k=1)))
+ [[0], [1, 2, 3, 4, 5, 6, 7]]
+ >>> sorted(map(sorted, aux_graph.k_edge_components(k=2)))
+ [[0], [1, 2, 3, 4], [5, 6, 7]]
+ >>> sorted(map(sorted, aux_graph.k_edge_components(k=3)))
+ [[0], [1, 2, 3, 4], [5], [6], [7]]
+ >>> sorted(map(sorted, aux_graph.k_edge_components(k=4)))
+ [[0], [1], [2], [3], [4], [5], [6], [7]]
+
+ The auxiliary graph is primarily used for k-edge-ccs but it
+ can also speed up the queries of k-edge-subgraphs by refining the
+ search space.
+
+ >>> import itertools as it
+ >>> from networkx.utils import pairwise
+ >>> from networkx.algorithms.connectivity import EdgeComponentAuxGraph
+ >>> paths = [
+ ... (1, 2, 4, 3, 1, 4),
+ ... ]
+ >>> G = nx.Graph()
+ >>> G.add_nodes_from(it.chain(*paths))
+ >>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
+ >>> aux_graph = EdgeComponentAuxGraph.construct(G)
+ >>> sorted(map(sorted, aux_graph.k_edge_subgraphs(k=3)))
+ [[1], [2], [3], [4]]
+ >>> sorted(map(sorted, aux_graph.k_edge_components(k=3)))
+ [[1, 4], [2], [3]]
+ """
+
+ # @not_implemented_for('multigraph') # TODO: fix decor for classmethods
+ @classmethod
+ def construct(EdgeComponentAuxGraph, G):
+ """Builds an auxiliary graph encoding edge-connectivity between nodes.
+
+ Notes
+ -----
+ Given G=(V, E), initialize an empty auxiliary graph A.
+ Choose an arbitrary source node s. Initialize a set N of available
+ nodes (that can be used as the sink). The algorithm picks an
+ arbitrary node t from N - {s}, and then computes the minimum st-cut
+ (S, T) with value w. If G is directed the minimum of the st-cut or
+ the ts-cut is used instead. Then, the edge (s, t) is added to the
+ auxiliary graph with weight w. The algorithm is called recursively
+ first using S as the available nodes and s as the source, and then
+ using T and t. Recursion stops when the source is the only available
+ node.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ """
+ # workaround for classmethod decorator
+ not_implemented_for("multigraph")(lambda G: G)(G)
+
+ def _recursive_build(H, A, source, avail):
+ # Terminate once the flow has been compute to every node.
+ if {source} == avail:
+ return
+ # pick an arbitrary node as the sink
+ sink = arbitrary_element(avail - {source})
+ # find the minimum cut and its weight
+ value, (S, T) = nx.minimum_cut(H, source, sink)
+ if H.is_directed():
+ # check if the reverse direction has a smaller cut
+ value_, (T_, S_) = nx.minimum_cut(H, sink, source)
+ if value_ < value:
+ value, S, T = value_, S_, T_
+ # add edge with weight of cut to the aux graph
+ A.add_edge(source, sink, weight=value)
+ # recursively call until all but one node is used
+ _recursive_build(H, A, source, avail.intersection(S))
+ _recursive_build(H, A, sink, avail.intersection(T))
+
+ # Copy input to ensure all edges have unit capacity
+ H = G.__class__()
+ H.add_nodes_from(G.nodes())
+ H.add_edges_from(G.edges(), capacity=1)
+
+ # A is the auxiliary graph to be constructed
+ # It is a weighted undirected tree
+ A = nx.Graph()
+
+ # Pick an arbitrary node as the source
+ if H.number_of_nodes() > 0:
+ source = arbitrary_element(H.nodes())
+ # Initialize a set of elements that can be chosen as the sink
+ avail = set(H.nodes())
+
+ # This constructs A
+ _recursive_build(H, A, source, avail)
+
+ # This class is a container the holds the auxiliary graph A and
+ # provides access the k_edge_components function.
+ self = EdgeComponentAuxGraph()
+ self.A = A
+ self.H = H
+ return self
+
+ def k_edge_components(self, k):
+ """Queries the auxiliary graph for k-edge-connected components.
+
+ Parameters
+ ----------
+ k : Integer
+ Desired edge connectivity
+
+ Returns
+ -------
+ k_edge_components : a generator of k-edge-ccs
+
+ Notes
+ -----
+ Given the auxiliary graph, the k-edge-connected components can be
+ determined in linear time by removing all edges with weights less than
+ k from the auxiliary graph. The resulting connected components are the
+ k-edge-ccs in the original graph.
+ """
+ if k < 1:
+ raise ValueError("k cannot be less than 1")
+ A = self.A
+ # "traverse the auxiliary graph A and delete all edges with weights less
+ # than k"
+ aux_weights = nx.get_edge_attributes(A, "weight")
+ # Create a relevant graph with the auxiliary edges with weights >= k
+ R = nx.Graph()
+ R.add_nodes_from(A.nodes())
+ R.add_edges_from(e for e, w in aux_weights.items() if w >= k)
+
+ # Return the nodes that are k-edge-connected in the original graph
+ yield from nx.connected_components(R)
+
+ def k_edge_subgraphs(self, k):
+ """Queries the auxiliary graph for k-edge-connected subgraphs.
+
+ Parameters
+ ----------
+ k : Integer
+ Desired edge connectivity
+
+ Returns
+ -------
+ k_edge_subgraphs : a generator of k-edge-subgraphs
+
+ Notes
+ -----
+ Refines the k-edge-ccs into k-edge-subgraphs. The running time is more
+ than $O(|V|)$.
+
+ For single values of k it is faster to use `nx.k_edge_subgraphs`.
+ But for multiple values of k, it can be faster to build AuxGraph and
+ then use this method.
+ """
+ if k < 1:
+ raise ValueError("k cannot be less than 1")
+ H = self.H
+ A = self.A
+ # "traverse the auxiliary graph A and delete all edges with weights less
+ # than k"
+ aux_weights = nx.get_edge_attributes(A, "weight")
+ # Create a relevant graph with the auxiliary edges with weights >= k
+ R = nx.Graph()
+ R.add_nodes_from(A.nodes())
+ R.add_edges_from(e for e, w in aux_weights.items() if w >= k)
+
+ # Return the components whose subgraphs are k-edge-connected
+ for cc in nx.connected_components(R):
+ if len(cc) < k:
+ # Early return optimization
+ for node in cc:
+ yield {node}
+ else:
+ # Call subgraph solution to refine the results
+ C = H.subgraph(cc)
+ yield from k_edge_subgraphs(C, k)
+
+
+def _low_degree_nodes(G, k, nbunch=None):
+ """Helper for finding nodes with degree less than k."""
+ # Nodes with degree less than k cannot be k-edge-connected.
+ if G.is_directed():
+ # Consider both in and out degree in the directed case
+ seen = set()
+ for node, degree in G.out_degree(nbunch):
+ if degree < k:
+ seen.add(node)
+ yield node
+ for node, degree in G.in_degree(nbunch):
+ if node not in seen and degree < k:
+ seen.add(node)
+ yield node
+ else:
+ # Only the degree matters in the undirected case
+ for node, degree in G.degree(nbunch):
+ if degree < k:
+ yield node
+
+
+def _high_degree_components(G, k):
+ """Helper for filtering components that can't be k-edge-connected.
+
+ Removes and generates each node with degree less than k. Then generates
+ remaining components where all nodes have degree at least k.
+ """
+ # Iteratively remove parts of the graph that are not k-edge-connected
+ H = G.copy()
+ singletons = set(_low_degree_nodes(H, k))
+ while singletons:
+ # Only search neighbors of removed nodes
+ nbunch = set(it.chain.from_iterable(map(H.neighbors, singletons)))
+ nbunch.difference_update(singletons)
+ H.remove_nodes_from(singletons)
+ for node in singletons:
+ yield {node}
+ singletons = set(_low_degree_nodes(H, k, nbunch))
+
+ # Note: remaining connected components may not be k-edge-connected
+ if G.is_directed():
+ yield from nx.strongly_connected_components(H)
+ else:
+ yield from nx.connected_components(H)
+
+
+@nx._dispatchable(returns_graph=True)
+def general_k_edge_subgraphs(G, k):
+ """General algorithm to find all maximal k-edge-connected subgraphs in `G`.
+
+ Parameters
+ ----------
+ G : nx.Graph
+ Graph in which all maximal k-edge-connected subgraphs will be found.
+
+ k : int
+
+ Yields
+ ------
+ k_edge_subgraphs : Graph instances that are k-edge-subgraphs
+ Each k-edge-subgraph contains a maximal set of nodes that defines a
+ subgraph of `G` that is k-edge-connected.
+
+ Notes
+ -----
+ Implementation of the basic algorithm from [1]_. The basic idea is to find
+ a global minimum cut of the graph. If the cut value is at least k, then the
+ graph is a k-edge-connected subgraph and can be added to the results.
+ Otherwise, the cut is used to split the graph in two and the procedure is
+ applied recursively. If the graph is just a single node, then it is also
+ added to the results. At the end, each result is either guaranteed to be
+ a single node or a subgraph of G that is k-edge-connected.
+
+ This implementation contains optimizations for reducing the number of calls
+ to max-flow, but there are other optimizations in [1]_ that could be
+ implemented.
+
+ References
+ ----------
+ .. [1] Zhou, Liu, et al. (2012) Finding maximal k-edge-connected subgraphs
+ from a large graph. ACM International Conference on Extending Database
+ Technology 2012 480-–491.
+ https://openproceedings.org/2012/conf/edbt/ZhouLYLCL12.pdf
+
+ Examples
+ --------
+ >>> from networkx.utils import pairwise
+ >>> paths = [
+ ... (11, 12, 13, 14, 11, 13, 14, 12), # a 4-clique
+ ... (21, 22, 23, 24, 21, 23, 24, 22), # another 4-clique
+ ... # connect the cliques with high degree but low connectivity
+ ... (50, 13),
+ ... (12, 50, 22),
+ ... (13, 102, 23),
+ ... (14, 101, 24),
+ ... ]
+ >>> G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
+ >>> sorted(len(k_sg) for k_sg in k_edge_subgraphs(G, k=3))
+ [1, 1, 1, 4, 4]
+ """
+ if k < 1:
+ raise ValueError("k cannot be less than 1")
+
+ # Node pruning optimization (incorporates early return)
+ # find_ccs is either connected_components/strongly_connected_components
+ find_ccs = partial(_high_degree_components, k=k)
+
+ # Quick return optimization
+ if G.number_of_nodes() < k:
+ for node in G.nodes():
+ yield G.subgraph([node]).copy()
+ return
+
+ # Intermediate results
+ R0 = {G.subgraph(cc).copy() for cc in find_ccs(G)}
+ # Subdivide CCs in the intermediate results until they are k-conn
+ while R0:
+ G1 = R0.pop()
+ if G1.number_of_nodes() == 1:
+ yield G1
+ else:
+ # Find a global minimum cut
+ cut_edges = nx.minimum_edge_cut(G1)
+ cut_value = len(cut_edges)
+ if cut_value < k:
+ # G1 is not k-edge-connected, so subdivide it
+ G1.remove_edges_from(cut_edges)
+ for cc in find_ccs(G1):
+ R0.add(G1.subgraph(cc).copy())
+ else:
+ # Otherwise we found a k-edge-connected subgraph
+ yield G1
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcomponents.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcomponents.py
new file mode 100644
index 00000000..e2f1ba28
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcomponents.py
@@ -0,0 +1,223 @@
+"""
+Moody and White algorithm for k-components
+"""
+
+from collections import defaultdict
+from itertools import combinations
+from operator import itemgetter
+
+import networkx as nx
+
+# Define the default maximum flow function.
+from networkx.algorithms.flow import edmonds_karp
+from networkx.utils import not_implemented_for
+
+default_flow_func = edmonds_karp
+
+__all__ = ["k_components"]
+
+
+@not_implemented_for("directed")
+@nx._dispatchable
+def k_components(G, flow_func=None):
+ r"""Returns the k-component structure of a graph G.
+
+ A `k`-component is a maximal subgraph of a graph G that has, at least,
+ node connectivity `k`: we need to remove at least `k` nodes to break it
+ into more components. `k`-components have an inherent hierarchical
+ structure because they are nested in terms of connectivity: a connected
+ graph can contain several 2-components, each of which can contain
+ one or more 3-components, and so forth.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ flow_func : function
+ Function to perform the underlying flow computations. Default value
+ :meth:`edmonds_karp`. This function performs better in sparse graphs with
+ right tailed degree distributions. :meth:`shortest_augmenting_path` will
+ perform better in denser graphs.
+
+ Returns
+ -------
+ k_components : dict
+ Dictionary with all connectivity levels `k` in the input Graph as keys
+ and a list of sets of nodes that form a k-component of level `k` as
+ values.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If the input graph is directed.
+
+ Examples
+ --------
+ >>> # Petersen graph has 10 nodes and it is triconnected, thus all
+ >>> # nodes are in a single component on all three connectivity levels
+ >>> G = nx.petersen_graph()
+ >>> k_components = nx.k_components(G)
+
+ Notes
+ -----
+ Moody and White [1]_ (appendix A) provide an algorithm for identifying
+ k-components in a graph, which is based on Kanevsky's algorithm [2]_
+ for finding all minimum-size node cut-sets of a graph (implemented in
+ :meth:`all_node_cuts` function):
+
+ 1. Compute node connectivity, k, of the input graph G.
+
+ 2. Identify all k-cutsets at the current level of connectivity using
+ Kanevsky's algorithm.
+
+ 3. Generate new graph components based on the removal of
+ these cutsets. Nodes in a cutset belong to both sides
+ of the induced cut.
+
+ 4. If the graph is neither complete nor trivial, return to 1;
+ else end.
+
+ This implementation also uses some heuristics (see [3]_ for details)
+ to speed up the computation.
+
+ See also
+ --------
+ node_connectivity
+ all_node_cuts
+ biconnected_components : special case of this function when k=2
+ k_edge_components : similar to this function, but uses edge-connectivity
+ instead of node-connectivity
+
+ References
+ ----------
+ .. [1] Moody, J. and D. White (2003). Social cohesion and embeddedness:
+ A hierarchical conception of social groups.
+ American Sociological Review 68(1), 103--28.
+ http://www2.asanet.org/journals/ASRFeb03MoodyWhite.pdf
+
+ .. [2] Kanevsky, A. (1993). Finding all minimum-size separating vertex
+ sets in a graph. Networks 23(6), 533--541.
+ http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract
+
+ .. [3] Torrents, J. and F. Ferraro (2015). Structural Cohesion:
+ Visualization and Heuristics for Fast Computation.
+ https://arxiv.org/pdf/1503.04476v1
+
+ """
+ # Dictionary with connectivity level (k) as keys and a list of
+ # sets of nodes that form a k-component as values. Note that
+ # k-components can overlap (but only k - 1 nodes).
+ k_components = defaultdict(list)
+ # Define default flow function
+ if flow_func is None:
+ flow_func = default_flow_func
+ # Bicomponents as a base to check for higher order k-components
+ for component in nx.connected_components(G):
+ # isolated nodes have connectivity 0
+ comp = set(component)
+ if len(comp) > 1:
+ k_components[1].append(comp)
+ bicomponents = [G.subgraph(c) for c in nx.biconnected_components(G)]
+ for bicomponent in bicomponents:
+ bicomp = set(bicomponent)
+ # avoid considering dyads as bicomponents
+ if len(bicomp) > 2:
+ k_components[2].append(bicomp)
+ for B in bicomponents:
+ if len(B) <= 2:
+ continue
+ k = nx.node_connectivity(B, flow_func=flow_func)
+ if k > 2:
+ k_components[k].append(set(B))
+ # Perform cuts in a DFS like order.
+ cuts = list(nx.all_node_cuts(B, k=k, flow_func=flow_func))
+ stack = [(k, _generate_partition(B, cuts, k))]
+ while stack:
+ (parent_k, partition) = stack[-1]
+ try:
+ nodes = next(partition)
+ C = B.subgraph(nodes)
+ this_k = nx.node_connectivity(C, flow_func=flow_func)
+ if this_k > parent_k and this_k > 2:
+ k_components[this_k].append(set(C))
+ cuts = list(nx.all_node_cuts(C, k=this_k, flow_func=flow_func))
+ if cuts:
+ stack.append((this_k, _generate_partition(C, cuts, this_k)))
+ except StopIteration:
+ stack.pop()
+
+ # This is necessary because k-components may only be reported at their
+ # maximum k level. But we want to return a dictionary in which keys are
+ # connectivity levels and values list of sets of components, without
+ # skipping any connectivity level. Also, it's possible that subsets of
+ # an already detected k-component appear at a level k. Checking for this
+ # in the while loop above penalizes the common case. Thus we also have to
+ # _consolidate all connectivity levels in _reconstruct_k_components.
+ return _reconstruct_k_components(k_components)
+
+
+def _consolidate(sets, k):
+ """Merge sets that share k or more elements.
+
+ See: http://rosettacode.org/wiki/Set_consolidation
+
+ The iterative python implementation posted there is
+ faster than this because of the overhead of building a
+ Graph and calling nx.connected_components, but it's not
+ clear for us if we can use it in NetworkX because there
+ is no licence for the code.
+
+ """
+ G = nx.Graph()
+ nodes = dict(enumerate(sets))
+ G.add_nodes_from(nodes)
+ G.add_edges_from(
+ (u, v) for u, v in combinations(nodes, 2) if len(nodes[u] & nodes[v]) >= k
+ )
+ for component in nx.connected_components(G):
+ yield set.union(*[nodes[n] for n in component])
+
+
+def _generate_partition(G, cuts, k):
+ def has_nbrs_in_partition(G, node, partition):
+ return any(n in partition for n in G[node])
+
+ components = []
+ nodes = {n for n, d in G.degree() if d > k} - {n for cut in cuts for n in cut}
+ H = G.subgraph(nodes)
+ for cc in nx.connected_components(H):
+ component = set(cc)
+ for cut in cuts:
+ for node in cut:
+ if has_nbrs_in_partition(G, node, cc):
+ component.add(node)
+ if len(component) < G.order():
+ components.append(component)
+ yield from _consolidate(components, k + 1)
+
+
+def _reconstruct_k_components(k_comps):
+ result = {}
+ max_k = max(k_comps)
+ for k in reversed(range(1, max_k + 1)):
+ if k == max_k:
+ result[k] = list(_consolidate(k_comps[k], k))
+ elif k not in k_comps:
+ result[k] = list(_consolidate(result[k + 1], k))
+ else:
+ nodes_at_k = set.union(*k_comps[k])
+ to_add = [c for c in result[k + 1] if any(n not in nodes_at_k for n in c)]
+ if to_add:
+ result[k] = list(_consolidate(k_comps[k] + to_add, k))
+ else:
+ result[k] = list(_consolidate(k_comps[k], k))
+ return result
+
+
+def build_k_number_dict(kcomps):
+ result = {}
+ for k, comps in sorted(kcomps.items(), key=itemgetter(0)):
+ for comp in comps:
+ for node in comp:
+ result[node] = k
+ return result
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcutsets.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcutsets.py
new file mode 100644
index 00000000..de26f4c5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcutsets.py
@@ -0,0 +1,235 @@
+"""
+Kanevsky all minimum node k cutsets algorithm.
+"""
+
+import copy
+from collections import defaultdict
+from itertools import combinations
+from operator import itemgetter
+
+import networkx as nx
+from networkx.algorithms.flow import (
+ build_residual_network,
+ edmonds_karp,
+ shortest_augmenting_path,
+)
+
+from .utils import build_auxiliary_node_connectivity
+
+default_flow_func = edmonds_karp
+
+
+__all__ = ["all_node_cuts"]
+
+
+@nx._dispatchable
+def all_node_cuts(G, k=None, flow_func=None):
+ r"""Returns all minimum k cutsets of an undirected graph G.
+
+ This implementation is based on Kanevsky's algorithm [1]_ for finding all
+ minimum-size node cut-sets of an undirected graph G; ie the set (or sets)
+ of nodes of cardinality equal to the node connectivity of G. Thus if
+ removed, would break G into two or more connected components.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Undirected graph
+
+ k : Integer
+ Node connectivity of the input graph. If k is None, then it is
+ computed. Default value: None.
+
+ flow_func : function
+ Function to perform the underlying flow computations. Default value is
+ :func:`~networkx.algorithms.flow.edmonds_karp`. This function performs
+ better in sparse graphs with right tailed degree distributions.
+ :func:`~networkx.algorithms.flow.shortest_augmenting_path` will
+ perform better in denser graphs.
+
+
+ Returns
+ -------
+ cuts : a generator of node cutsets
+ Each node cutset has cardinality equal to the node connectivity of
+ the input graph.
+
+ Examples
+ --------
+ >>> # A two-dimensional grid graph has 4 cutsets of cardinality 2
+ >>> G = nx.grid_2d_graph(5, 5)
+ >>> cutsets = list(nx.all_node_cuts(G))
+ >>> len(cutsets)
+ 4
+ >>> all(2 == len(cutset) for cutset in cutsets)
+ True
+ >>> nx.node_connectivity(G)
+ 2
+
+ Notes
+ -----
+ This implementation is based on the sequential algorithm for finding all
+ minimum-size separating vertex sets in a graph [1]_. The main idea is to
+ compute minimum cuts using local maximum flow computations among a set
+ of nodes of highest degree and all other non-adjacent nodes in the Graph.
+ Once we find a minimum cut, we add an edge between the high degree
+ node and the target node of the local maximum flow computation to make
+ sure that we will not find that minimum cut again.
+
+ See also
+ --------
+ node_connectivity
+ edmonds_karp
+ shortest_augmenting_path
+
+ References
+ ----------
+ .. [1] Kanevsky, A. (1993). Finding all minimum-size separating vertex
+ sets in a graph. Networks 23(6), 533--541.
+ http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract
+
+ """
+ if not nx.is_connected(G):
+ raise nx.NetworkXError("Input graph is disconnected.")
+
+ # Address some corner cases first.
+ # For complete Graphs
+
+ if nx.density(G) == 1:
+ yield from ()
+ return
+
+ # Initialize data structures.
+ # Keep track of the cuts already computed so we do not repeat them.
+ seen = []
+ # Even-Tarjan reduction is what we call auxiliary digraph
+ # for node connectivity.
+ H = build_auxiliary_node_connectivity(G)
+ H_nodes = H.nodes # for speed
+ mapping = H.graph["mapping"]
+ # Keep a copy of original predecessors, H will be modified later.
+ # Shallow copy is enough.
+ original_H_pred = copy.copy(H._pred)
+ R = build_residual_network(H, "capacity")
+ kwargs = {"capacity": "capacity", "residual": R}
+ # Define default flow function
+ if flow_func is None:
+ flow_func = default_flow_func
+ if flow_func is shortest_augmenting_path:
+ kwargs["two_phase"] = True
+ # Begin the actual algorithm
+ # step 1: Find node connectivity k of G
+ if k is None:
+ k = nx.node_connectivity(G, flow_func=flow_func)
+ # step 2:
+ # Find k nodes with top degree, call it X:
+ X = {n for n, d in sorted(G.degree(), key=itemgetter(1), reverse=True)[:k]}
+ # Check if X is a k-node-cutset
+ if _is_separating_set(G, X):
+ seen.append(X)
+ yield X
+
+ for x in X:
+ # step 3: Compute local connectivity flow of x with all other
+ # non adjacent nodes in G
+ non_adjacent = set(G) - {x} - set(G[x])
+ for v in non_adjacent:
+ # step 4: compute maximum flow in an Even-Tarjan reduction H of G
+ # and step 5: build the associated residual network R
+ R = flow_func(H, f"{mapping[x]}B", f"{mapping[v]}A", **kwargs)
+ flow_value = R.graph["flow_value"]
+
+ if flow_value == k:
+ # Find the nodes incident to the flow.
+ E1 = flowed_edges = [
+ (u, w) for (u, w, d) in R.edges(data=True) if d["flow"] != 0
+ ]
+ VE1 = incident_nodes = {n for edge in E1 for n in edge}
+ # Remove saturated edges form the residual network.
+ # Note that reversed edges are introduced with capacity 0
+ # in the residual graph and they need to be removed too.
+ saturated_edges = [
+ (u, w, d)
+ for (u, w, d) in R.edges(data=True)
+ if d["capacity"] == d["flow"] or d["capacity"] == 0
+ ]
+ R.remove_edges_from(saturated_edges)
+ R_closure = nx.transitive_closure(R)
+ # step 6: shrink the strongly connected components of
+ # residual flow network R and call it L.
+ L = nx.condensation(R)
+ cmap = L.graph["mapping"]
+ inv_cmap = defaultdict(list)
+ for n, scc in cmap.items():
+ inv_cmap[scc].append(n)
+ # Find the incident nodes in the condensed graph.
+ VE1 = {cmap[n] for n in VE1}
+ # step 7: Compute all antichains of L;
+ # they map to closed sets in H.
+ # Any edge in H that links a closed set is part of a cutset.
+ for antichain in nx.antichains(L):
+ # Only antichains that are subsets of incident nodes counts.
+ # Lemma 8 in reference.
+ if not set(antichain).issubset(VE1):
+ continue
+ # Nodes in an antichain of the condensation graph of
+ # the residual network map to a closed set of nodes that
+ # define a node partition of the auxiliary digraph H
+ # through taking all of antichain's predecessors in the
+ # transitive closure.
+ S = set()
+ for scc in antichain:
+ S.update(inv_cmap[scc])
+ S_ancestors = set()
+ for n in S:
+ S_ancestors.update(R_closure._pred[n])
+ S.update(S_ancestors)
+ if f"{mapping[x]}B" not in S or f"{mapping[v]}A" in S:
+ continue
+ # Find the cutset that links the node partition (S,~S) in H
+ cutset = set()
+ for u in S:
+ cutset.update((u, w) for w in original_H_pred[u] if w not in S)
+ # The edges in H that form the cutset are internal edges
+ # (ie edges that represent a node of the original graph G)
+ if any(H_nodes[u]["id"] != H_nodes[w]["id"] for u, w in cutset):
+ continue
+ node_cut = {H_nodes[u]["id"] for u, _ in cutset}
+
+ if len(node_cut) == k:
+ # The cut is invalid if it includes internal edges of
+ # end nodes. The other half of Lemma 8 in ref.
+ if x in node_cut or v in node_cut:
+ continue
+ if node_cut not in seen:
+ yield node_cut
+ seen.append(node_cut)
+
+ # Add an edge (x, v) to make sure that we do not
+ # find this cutset again. This is equivalent
+ # of adding the edge in the input graph
+ # G.add_edge(x, v) and then regenerate H and R:
+ # Add edges to the auxiliary digraph.
+ # See build_residual_network for convention we used
+ # in residual graphs.
+ H.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1)
+ H.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1)
+ # Add edges to the residual network.
+ R.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1)
+ R.add_edge(f"{mapping[v]}A", f"{mapping[x]}B", capacity=0)
+ R.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1)
+ R.add_edge(f"{mapping[x]}A", f"{mapping[v]}B", capacity=0)
+
+ # Add again the saturated edges to reuse the residual network
+ R.add_edges_from(saturated_edges)
+
+
+def _is_separating_set(G, cut):
+ """Assumes that the input graph is connected"""
+ if len(cut) == len(G) - 1:
+ return True
+
+ H = nx.restricted_view(G, cut, [])
+ if nx.is_connected(H):
+ return False
+ return True
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/stoerwagner.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/stoerwagner.py
new file mode 100644
index 00000000..29604b14
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/stoerwagner.py
@@ -0,0 +1,152 @@
+"""
+Stoer-Wagner minimum cut algorithm.
+"""
+
+from itertools import islice
+
+import networkx as nx
+
+from ...utils import BinaryHeap, arbitrary_element, not_implemented_for
+
+__all__ = ["stoer_wagner"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable(edge_attrs="weight")
+def stoer_wagner(G, weight="weight", heap=BinaryHeap):
+ r"""Returns the weighted minimum edge cut using the Stoer-Wagner algorithm.
+
+ Determine the minimum edge cut of a connected graph using the
+ Stoer-Wagner algorithm. In weighted cases, all weights must be
+ nonnegative.
+
+ The running time of the algorithm depends on the type of heaps used:
+
+ ============== =============================================
+ Type of heap Running time
+ ============== =============================================
+ Binary heap $O(n (m + n) \log n)$
+ Fibonacci heap $O(nm + n^2 \log n)$
+ Pairing heap $O(2^{2 \sqrt{\log \log n}} nm + n^2 \log n)$
+ ============== =============================================
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Edges of the graph are expected to have an attribute named by the
+ weight parameter below. If this attribute is not present, the edge is
+ considered to have unit weight.
+
+ weight : string
+ Name of the weight attribute of the edges. If the attribute is not
+ present, unit weight is assumed. Default value: 'weight'.
+
+ heap : class
+ Type of heap to be used in the algorithm. It should be a subclass of
+ :class:`MinHeap` or implement a compatible interface.
+
+ If a stock heap implementation is to be used, :class:`BinaryHeap` is
+ recommended over :class:`PairingHeap` for Python implementations without
+ optimized attribute accesses (e.g., CPython) despite a slower
+ asymptotic running time. For Python implementations with optimized
+ attribute accesses (e.g., PyPy), :class:`PairingHeap` provides better
+ performance. Default value: :class:`BinaryHeap`.
+
+ Returns
+ -------
+ cut_value : integer or float
+ The sum of weights of edges in a minimum cut.
+
+ partition : pair of node lists
+ A partitioning of the nodes that defines a minimum cut.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If the graph is directed or a multigraph.
+
+ NetworkXError
+ If the graph has less than two nodes, is not connected or has a
+ negative-weighted edge.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> G.add_edge("x", "a", weight=3)
+ >>> G.add_edge("x", "b", weight=1)
+ >>> G.add_edge("a", "c", weight=3)
+ >>> G.add_edge("b", "c", weight=5)
+ >>> G.add_edge("b", "d", weight=4)
+ >>> G.add_edge("d", "e", weight=2)
+ >>> G.add_edge("c", "y", weight=2)
+ >>> G.add_edge("e", "y", weight=3)
+ >>> cut_value, partition = nx.stoer_wagner(G)
+ >>> cut_value
+ 4
+ """
+ n = len(G)
+ if n < 2:
+ raise nx.NetworkXError("graph has less than two nodes.")
+ if not nx.is_connected(G):
+ raise nx.NetworkXError("graph is not connected.")
+
+ # Make a copy of the graph for internal use.
+ G = nx.Graph(
+ (u, v, {"weight": e.get(weight, 1)}) for u, v, e in G.edges(data=True) if u != v
+ )
+ G.__networkx_cache__ = None # Disable caching
+
+ for u, v, e in G.edges(data=True):
+ if e["weight"] < 0:
+ raise nx.NetworkXError("graph has a negative-weighted edge.")
+
+ cut_value = float("inf")
+ nodes = set(G)
+ contractions = [] # contracted node pairs
+
+ # Repeatedly pick a pair of nodes to contract until only one node is left.
+ for i in range(n - 1):
+ # Pick an arbitrary node u and create a set A = {u}.
+ u = arbitrary_element(G)
+ A = {u}
+ # Repeatedly pick the node "most tightly connected" to A and add it to
+ # A. The tightness of connectivity of a node not in A is defined by the
+ # of edges connecting it to nodes in A.
+ h = heap() # min-heap emulating a max-heap
+ for v, e in G[u].items():
+ h.insert(v, -e["weight"])
+ # Repeat until all but one node has been added to A.
+ for j in range(n - i - 2):
+ u = h.pop()[0]
+ A.add(u)
+ for v, e in G[u].items():
+ if v not in A:
+ h.insert(v, h.get(v, 0) - e["weight"])
+ # A and the remaining node v define a "cut of the phase". There is a
+ # minimum cut of the original graph that is also a cut of the phase.
+ # Due to contractions in earlier phases, v may in fact represent
+ # multiple nodes in the original graph.
+ v, w = h.min()
+ w = -w
+ if w < cut_value:
+ cut_value = w
+ best_phase = i
+ # Contract v and the last node added to A.
+ contractions.append((u, v))
+ for w, e in G[v].items():
+ if w != u:
+ if w not in G[u]:
+ G.add_edge(u, w, weight=e["weight"])
+ else:
+ G[u][w]["weight"] += e["weight"]
+ G.remove_node(v)
+
+ # Recover the optimal partitioning from the contractions.
+ G = nx.Graph(islice(contractions, best_phase))
+ v = contractions[best_phase][1]
+ G.add_node(v)
+ reachable = set(nx.single_source_shortest_path_length(G, v))
+ partition = (list(reachable), list(nodes - reachable))
+
+ return cut_value, partition
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_connectivity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_connectivity.py
new file mode 100644
index 00000000..7aef2477
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_connectivity.py
@@ -0,0 +1,421 @@
+import itertools
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms import flow
+from networkx.algorithms.connectivity import (
+ local_edge_connectivity,
+ local_node_connectivity,
+)
+
+flow_funcs = [
+ flow.boykov_kolmogorov,
+ flow.dinitz,
+ flow.edmonds_karp,
+ flow.preflow_push,
+ flow.shortest_augmenting_path,
+]
+
+
+# helper functions for tests
+
+
+def _generate_no_biconnected(max_attempts=50):
+ attempts = 0
+ while True:
+ G = nx.fast_gnp_random_graph(100, 0.0575, seed=42)
+ if nx.is_connected(G) and not nx.is_biconnected(G):
+ attempts = 0
+ yield G
+ else:
+ if attempts >= max_attempts:
+ msg = f"Tried {max_attempts} times: no suitable Graph."
+ raise Exception(msg)
+ else:
+ attempts += 1
+
+
+def test_average_connectivity():
+ # figure 1 from:
+ # Beineke, L., O. Oellermann, and R. Pippert (2002). The average
+ # connectivity of a graph. Discrete mathematics 252(1-3), 31-45
+ # http://www.sciencedirect.com/science/article/pii/S0012365X01001807
+ G1 = nx.path_graph(3)
+ G1.add_edges_from([(1, 3), (1, 4)])
+ G2 = nx.path_graph(3)
+ G2.add_edges_from([(1, 3), (1, 4), (0, 3), (0, 4), (3, 4)])
+ G3 = nx.Graph()
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert nx.average_node_connectivity(G1, **kwargs) == 1, errmsg
+ assert nx.average_node_connectivity(G2, **kwargs) == 2.2, errmsg
+ assert nx.average_node_connectivity(G3, **kwargs) == 0, errmsg
+
+
+def test_average_connectivity_directed():
+ G = nx.DiGraph([(1, 3), (1, 4), (1, 5)])
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert nx.average_node_connectivity(G) == 0.25, errmsg
+
+
+def test_articulation_points():
+ Ggen = _generate_no_biconnected()
+ for flow_func in flow_funcs:
+ for i in range(3):
+ G = next(Ggen)
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert nx.node_connectivity(G, flow_func=flow_func) == 1, errmsg
+
+
+def test_brandes_erlebach():
+ # Figure 1 chapter 7: Connectivity
+ # http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf
+ G = nx.Graph()
+ G.add_edges_from(
+ [
+ (1, 2),
+ (1, 3),
+ (1, 4),
+ (1, 5),
+ (2, 3),
+ (2, 6),
+ (3, 4),
+ (3, 6),
+ (4, 6),
+ (4, 7),
+ (5, 7),
+ (6, 8),
+ (6, 9),
+ (7, 8),
+ (7, 10),
+ (8, 11),
+ (9, 10),
+ (9, 11),
+ (10, 11),
+ ]
+ )
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert 3 == local_edge_connectivity(G, 1, 11, **kwargs), errmsg
+ assert 3 == nx.edge_connectivity(G, 1, 11, **kwargs), errmsg
+ assert 2 == local_node_connectivity(G, 1, 11, **kwargs), errmsg
+ assert 2 == nx.node_connectivity(G, 1, 11, **kwargs), errmsg
+ assert 2 == nx.edge_connectivity(G, **kwargs), errmsg
+ assert 2 == nx.node_connectivity(G, **kwargs), errmsg
+ if flow_func is flow.preflow_push:
+ assert 3 == nx.edge_connectivity(G, 1, 11, cutoff=2, **kwargs), errmsg
+ else:
+ assert 2 == nx.edge_connectivity(G, 1, 11, cutoff=2, **kwargs), errmsg
+
+
+def test_white_harary_1():
+ # Figure 1b white and harary (2001)
+ # https://doi.org/10.1111/0081-1750.00098
+ # A graph with high adhesion (edge connectivity) and low cohesion
+ # (vertex connectivity)
+ G = nx.disjoint_union(nx.complete_graph(4), nx.complete_graph(4))
+ G.remove_node(7)
+ for i in range(4, 7):
+ G.add_edge(0, i)
+ G = nx.disjoint_union(G, nx.complete_graph(4))
+ G.remove_node(G.order() - 1)
+ for i in range(7, 10):
+ G.add_edge(0, i)
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert 1 == nx.node_connectivity(G, flow_func=flow_func), errmsg
+ assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
+
+
+def test_white_harary_2():
+ # Figure 8 white and harary (2001)
+ # https://doi.org/10.1111/0081-1750.00098
+ G = nx.disjoint_union(nx.complete_graph(4), nx.complete_graph(4))
+ G.add_edge(0, 4)
+ # kappa <= lambda <= delta
+ assert 3 == min(nx.core_number(G).values())
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert 1 == nx.node_connectivity(G, flow_func=flow_func), errmsg
+ assert 1 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
+
+
+def test_complete_graphs():
+ for n in range(5, 20, 5):
+ for flow_func in flow_funcs:
+ G = nx.complete_graph(n)
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert n - 1 == nx.node_connectivity(G, flow_func=flow_func), errmsg
+ assert n - 1 == nx.node_connectivity(
+ G.to_directed(), flow_func=flow_func
+ ), errmsg
+ assert n - 1 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
+ assert n - 1 == nx.edge_connectivity(
+ G.to_directed(), flow_func=flow_func
+ ), errmsg
+
+
+def test_empty_graphs():
+ for k in range(5, 25, 5):
+ G = nx.empty_graph(k)
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert 0 == nx.node_connectivity(G, flow_func=flow_func), errmsg
+ assert 0 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
+
+
+def test_petersen():
+ G = nx.petersen_graph()
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert 3 == nx.node_connectivity(G, flow_func=flow_func), errmsg
+ assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
+
+
+def test_tutte():
+ G = nx.tutte_graph()
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert 3 == nx.node_connectivity(G, flow_func=flow_func), errmsg
+ assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
+
+
+def test_dodecahedral():
+ G = nx.dodecahedral_graph()
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert 3 == nx.node_connectivity(G, flow_func=flow_func), errmsg
+ assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
+
+
+def test_octahedral():
+ G = nx.octahedral_graph()
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert 4 == nx.node_connectivity(G, flow_func=flow_func), errmsg
+ assert 4 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
+
+
+def test_icosahedral():
+ G = nx.icosahedral_graph()
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert 5 == nx.node_connectivity(G, flow_func=flow_func), errmsg
+ assert 5 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
+
+
+def test_missing_source():
+ G = nx.path_graph(4)
+ for flow_func in flow_funcs:
+ pytest.raises(
+ nx.NetworkXError, nx.node_connectivity, G, 10, 1, flow_func=flow_func
+ )
+
+
+def test_missing_target():
+ G = nx.path_graph(4)
+ for flow_func in flow_funcs:
+ pytest.raises(
+ nx.NetworkXError, nx.node_connectivity, G, 1, 10, flow_func=flow_func
+ )
+
+
+def test_edge_missing_source():
+ G = nx.path_graph(4)
+ for flow_func in flow_funcs:
+ pytest.raises(
+ nx.NetworkXError, nx.edge_connectivity, G, 10, 1, flow_func=flow_func
+ )
+
+
+def test_edge_missing_target():
+ G = nx.path_graph(4)
+ for flow_func in flow_funcs:
+ pytest.raises(
+ nx.NetworkXError, nx.edge_connectivity, G, 1, 10, flow_func=flow_func
+ )
+
+
+def test_not_weakly_connected():
+ G = nx.DiGraph()
+ nx.add_path(G, [1, 2, 3])
+ nx.add_path(G, [4, 5])
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert nx.node_connectivity(G) == 0, errmsg
+ assert nx.edge_connectivity(G) == 0, errmsg
+
+
+def test_not_connected():
+ G = nx.Graph()
+ nx.add_path(G, [1, 2, 3])
+ nx.add_path(G, [4, 5])
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert nx.node_connectivity(G) == 0, errmsg
+ assert nx.edge_connectivity(G) == 0, errmsg
+
+
+def test_directed_edge_connectivity():
+ G = nx.cycle_graph(10, create_using=nx.DiGraph()) # only one direction
+ D = nx.cycle_graph(10).to_directed() # 2 reciprocal edges
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ assert 1 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
+ assert 1 == local_edge_connectivity(G, 1, 4, flow_func=flow_func), errmsg
+ assert 1 == nx.edge_connectivity(G, 1, 4, flow_func=flow_func), errmsg
+ assert 2 == nx.edge_connectivity(D, flow_func=flow_func), errmsg
+ assert 2 == local_edge_connectivity(D, 1, 4, flow_func=flow_func), errmsg
+ assert 2 == nx.edge_connectivity(D, 1, 4, flow_func=flow_func), errmsg
+
+
+def test_cutoff():
+ G = nx.complete_graph(5)
+ for local_func in [local_edge_connectivity, local_node_connectivity]:
+ for flow_func in flow_funcs:
+ if flow_func is flow.preflow_push:
+ # cutoff is not supported by preflow_push
+ continue
+ for cutoff in [3, 2, 1]:
+ result = local_func(G, 0, 4, flow_func=flow_func, cutoff=cutoff)
+ assert cutoff == result, f"cutoff error in {flow_func.__name__}"
+
+
+def test_invalid_auxiliary():
+ G = nx.complete_graph(5)
+ pytest.raises(nx.NetworkXError, local_node_connectivity, G, 0, 3, auxiliary=G)
+
+
+def test_interface_only_source():
+ G = nx.complete_graph(5)
+ for interface_func in [nx.node_connectivity, nx.edge_connectivity]:
+ pytest.raises(nx.NetworkXError, interface_func, G, s=0)
+
+
+def test_interface_only_target():
+ G = nx.complete_graph(5)
+ for interface_func in [nx.node_connectivity, nx.edge_connectivity]:
+ pytest.raises(nx.NetworkXError, interface_func, G, t=3)
+
+
+def test_edge_connectivity_flow_vs_stoer_wagner():
+ graph_funcs = [nx.icosahedral_graph, nx.octahedral_graph, nx.dodecahedral_graph]
+ for graph_func in graph_funcs:
+ G = graph_func()
+ assert nx.stoer_wagner(G)[0] == nx.edge_connectivity(G)
+
+
+class TestAllPairsNodeConnectivity:
+ @classmethod
+ def setup_class(cls):
+ cls.path = nx.path_graph(7)
+ cls.directed_path = nx.path_graph(7, create_using=nx.DiGraph())
+ cls.cycle = nx.cycle_graph(7)
+ cls.directed_cycle = nx.cycle_graph(7, create_using=nx.DiGraph())
+ cls.gnp = nx.gnp_random_graph(30, 0.1, seed=42)
+ cls.directed_gnp = nx.gnp_random_graph(30, 0.1, directed=True, seed=42)
+ cls.K20 = nx.complete_graph(20)
+ cls.K10 = nx.complete_graph(10)
+ cls.K5 = nx.complete_graph(5)
+ cls.G_list = [
+ cls.path,
+ cls.directed_path,
+ cls.cycle,
+ cls.directed_cycle,
+ cls.gnp,
+ cls.directed_gnp,
+ cls.K10,
+ cls.K5,
+ cls.K20,
+ ]
+
+ def test_cycles(self):
+ K_undir = nx.all_pairs_node_connectivity(self.cycle)
+ for source in K_undir:
+ for target, k in K_undir[source].items():
+ assert k == 2
+ K_dir = nx.all_pairs_node_connectivity(self.directed_cycle)
+ for source in K_dir:
+ for target, k in K_dir[source].items():
+ assert k == 1
+
+ def test_complete(self):
+ for G in [self.K10, self.K5, self.K20]:
+ K = nx.all_pairs_node_connectivity(G)
+ for source in K:
+ for target, k in K[source].items():
+ assert k == len(G) - 1
+
+ def test_paths(self):
+ K_undir = nx.all_pairs_node_connectivity(self.path)
+ for source in K_undir:
+ for target, k in K_undir[source].items():
+ assert k == 1
+ K_dir = nx.all_pairs_node_connectivity(self.directed_path)
+ for source in K_dir:
+ for target, k in K_dir[source].items():
+ if source < target:
+ assert k == 1
+ else:
+ assert k == 0
+
+ def test_all_pairs_connectivity_nbunch(self):
+ G = nx.complete_graph(5)
+ nbunch = [0, 2, 3]
+ C = nx.all_pairs_node_connectivity(G, nbunch=nbunch)
+ assert len(C) == len(nbunch)
+
+ def test_all_pairs_connectivity_icosahedral(self):
+ G = nx.icosahedral_graph()
+ C = nx.all_pairs_node_connectivity(G)
+ assert all(5 == C[u][v] for u, v in itertools.combinations(G, 2))
+
+ def test_all_pairs_connectivity(self):
+ G = nx.Graph()
+ nodes = [0, 1, 2, 3]
+ nx.add_path(G, nodes)
+ A = {n: {} for n in G}
+ for u, v in itertools.combinations(nodes, 2):
+ A[u][v] = A[v][u] = nx.node_connectivity(G, u, v)
+ C = nx.all_pairs_node_connectivity(G)
+ assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
+ (k, sorted(v)) for k, v in C.items()
+ )
+
+ def test_all_pairs_connectivity_directed(self):
+ G = nx.DiGraph()
+ nodes = [0, 1, 2, 3]
+ nx.add_path(G, nodes)
+ A = {n: {} for n in G}
+ for u, v in itertools.permutations(nodes, 2):
+ A[u][v] = nx.node_connectivity(G, u, v)
+ C = nx.all_pairs_node_connectivity(G)
+ assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
+ (k, sorted(v)) for k, v in C.items()
+ )
+
+ def test_all_pairs_connectivity_nbunch_combinations(self):
+ G = nx.complete_graph(5)
+ nbunch = [0, 2, 3]
+ A = {n: {} for n in nbunch}
+ for u, v in itertools.combinations(nbunch, 2):
+ A[u][v] = A[v][u] = nx.node_connectivity(G, u, v)
+ C = nx.all_pairs_node_connectivity(G, nbunch=nbunch)
+ assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
+ (k, sorted(v)) for k, v in C.items()
+ )
+
+ def test_all_pairs_connectivity_nbunch_iter(self):
+ G = nx.complete_graph(5)
+ nbunch = [0, 2, 3]
+ A = {n: {} for n in nbunch}
+ for u, v in itertools.combinations(nbunch, 2):
+ A[u][v] = A[v][u] = nx.node_connectivity(G, u, v)
+ C = nx.all_pairs_node_connectivity(G, nbunch=iter(nbunch))
+ assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
+ (k, sorted(v)) for k, v in C.items()
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_cuts.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_cuts.py
new file mode 100644
index 00000000..7a485be3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_cuts.py
@@ -0,0 +1,309 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms import flow
+from networkx.algorithms.connectivity import minimum_st_edge_cut, minimum_st_node_cut
+from networkx.utils import arbitrary_element
+
+flow_funcs = [
+ flow.boykov_kolmogorov,
+ flow.dinitz,
+ flow.edmonds_karp,
+ flow.preflow_push,
+ flow.shortest_augmenting_path,
+]
+
+# Tests for node and edge cutsets
+
+
+def _generate_no_biconnected(max_attempts=50):
+ attempts = 0
+ while True:
+ G = nx.fast_gnp_random_graph(100, 0.0575, seed=42)
+ if nx.is_connected(G) and not nx.is_biconnected(G):
+ attempts = 0
+ yield G
+ else:
+ if attempts >= max_attempts:
+ msg = f"Tried {attempts} times: no suitable Graph."
+ raise Exception(msg)
+ else:
+ attempts += 1
+
+
+def test_articulation_points():
+ Ggen = _generate_no_biconnected()
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ for i in range(1): # change 1 to 3 or more for more realizations.
+ G = next(Ggen)
+ cut = nx.minimum_node_cut(G, flow_func=flow_func)
+ assert len(cut) == 1, errmsg
+ assert cut.pop() in set(nx.articulation_points(G)), errmsg
+
+
+def test_brandes_erlebach_book():
+ # Figure 1 chapter 7: Connectivity
+ # http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf
+ G = nx.Graph()
+ G.add_edges_from(
+ [
+ (1, 2),
+ (1, 3),
+ (1, 4),
+ (1, 5),
+ (2, 3),
+ (2, 6),
+ (3, 4),
+ (3, 6),
+ (4, 6),
+ (4, 7),
+ (5, 7),
+ (6, 8),
+ (6, 9),
+ (7, 8),
+ (7, 10),
+ (8, 11),
+ (9, 10),
+ (9, 11),
+ (10, 11),
+ ]
+ )
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge cutsets
+ assert 3 == len(nx.minimum_edge_cut(G, 1, 11, **kwargs)), errmsg
+ edge_cut = nx.minimum_edge_cut(G, **kwargs)
+ # Node 5 has only two edges
+ assert 2 == len(edge_cut), errmsg
+ H = G.copy()
+ H.remove_edges_from(edge_cut)
+ assert not nx.is_connected(H), errmsg
+ # node cuts
+ assert {6, 7} == minimum_st_node_cut(G, 1, 11, **kwargs), errmsg
+ assert {6, 7} == nx.minimum_node_cut(G, 1, 11, **kwargs), errmsg
+ node_cut = nx.minimum_node_cut(G, **kwargs)
+ assert 2 == len(node_cut), errmsg
+ H = G.copy()
+ H.remove_nodes_from(node_cut)
+ assert not nx.is_connected(H), errmsg
+
+
+def test_white_harary_paper():
+ # Figure 1b white and harary (2001)
+ # https://doi.org/10.1111/0081-1750.00098
+ # A graph with high adhesion (edge connectivity) and low cohesion
+ # (node connectivity)
+ G = nx.disjoint_union(nx.complete_graph(4), nx.complete_graph(4))
+ G.remove_node(7)
+ for i in range(4, 7):
+ G.add_edge(0, i)
+ G = nx.disjoint_union(G, nx.complete_graph(4))
+ G.remove_node(G.order() - 1)
+ for i in range(7, 10):
+ G.add_edge(0, i)
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge cuts
+ edge_cut = nx.minimum_edge_cut(G, **kwargs)
+ assert 3 == len(edge_cut), errmsg
+ H = G.copy()
+ H.remove_edges_from(edge_cut)
+ assert not nx.is_connected(H), errmsg
+ # node cuts
+ node_cut = nx.minimum_node_cut(G, **kwargs)
+ assert {0} == node_cut, errmsg
+ H = G.copy()
+ H.remove_nodes_from(node_cut)
+ assert not nx.is_connected(H), errmsg
+
+
+def test_petersen_cutset():
+ G = nx.petersen_graph()
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge cuts
+ edge_cut = nx.minimum_edge_cut(G, **kwargs)
+ assert 3 == len(edge_cut), errmsg
+ H = G.copy()
+ H.remove_edges_from(edge_cut)
+ assert not nx.is_connected(H), errmsg
+ # node cuts
+ node_cut = nx.minimum_node_cut(G, **kwargs)
+ assert 3 == len(node_cut), errmsg
+ H = G.copy()
+ H.remove_nodes_from(node_cut)
+ assert not nx.is_connected(H), errmsg
+
+
+def test_octahedral_cutset():
+ G = nx.octahedral_graph()
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge cuts
+ edge_cut = nx.minimum_edge_cut(G, **kwargs)
+ assert 4 == len(edge_cut), errmsg
+ H = G.copy()
+ H.remove_edges_from(edge_cut)
+ assert not nx.is_connected(H), errmsg
+ # node cuts
+ node_cut = nx.minimum_node_cut(G, **kwargs)
+ assert 4 == len(node_cut), errmsg
+ H = G.copy()
+ H.remove_nodes_from(node_cut)
+ assert not nx.is_connected(H), errmsg
+
+
+def test_icosahedral_cutset():
+ G = nx.icosahedral_graph()
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge cuts
+ edge_cut = nx.minimum_edge_cut(G, **kwargs)
+ assert 5 == len(edge_cut), errmsg
+ H = G.copy()
+ H.remove_edges_from(edge_cut)
+ assert not nx.is_connected(H), errmsg
+ # node cuts
+ node_cut = nx.minimum_node_cut(G, **kwargs)
+ assert 5 == len(node_cut), errmsg
+ H = G.copy()
+ H.remove_nodes_from(node_cut)
+ assert not nx.is_connected(H), errmsg
+
+
+def test_node_cutset_exception():
+ G = nx.Graph()
+ G.add_edges_from([(1, 2), (3, 4)])
+ for flow_func in flow_funcs:
+ pytest.raises(nx.NetworkXError, nx.minimum_node_cut, G, flow_func=flow_func)
+
+
+def test_node_cutset_random_graphs():
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ for i in range(3):
+ G = nx.fast_gnp_random_graph(50, 0.25, seed=42)
+ if not nx.is_connected(G):
+ ccs = iter(nx.connected_components(G))
+ start = arbitrary_element(next(ccs))
+ G.add_edges_from((start, arbitrary_element(c)) for c in ccs)
+ cutset = nx.minimum_node_cut(G, flow_func=flow_func)
+ assert nx.node_connectivity(G) == len(cutset), errmsg
+ G.remove_nodes_from(cutset)
+ assert not nx.is_connected(G), errmsg
+
+
+def test_edge_cutset_random_graphs():
+ for flow_func in flow_funcs:
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ for i in range(3):
+ G = nx.fast_gnp_random_graph(50, 0.25, seed=42)
+ if not nx.is_connected(G):
+ ccs = iter(nx.connected_components(G))
+ start = arbitrary_element(next(ccs))
+ G.add_edges_from((start, arbitrary_element(c)) for c in ccs)
+ cutset = nx.minimum_edge_cut(G, flow_func=flow_func)
+ assert nx.edge_connectivity(G) == len(cutset), errmsg
+ G.remove_edges_from(cutset)
+ assert not nx.is_connected(G), errmsg
+
+
+def test_empty_graphs():
+ G = nx.Graph()
+ D = nx.DiGraph()
+ for interface_func in [nx.minimum_node_cut, nx.minimum_edge_cut]:
+ for flow_func in flow_funcs:
+ pytest.raises(
+ nx.NetworkXPointlessConcept, interface_func, G, flow_func=flow_func
+ )
+ pytest.raises(
+ nx.NetworkXPointlessConcept, interface_func, D, flow_func=flow_func
+ )
+
+
+def test_unbounded():
+ G = nx.complete_graph(5)
+ for flow_func in flow_funcs:
+ assert 4 == len(minimum_st_edge_cut(G, 1, 4, flow_func=flow_func))
+
+
+def test_missing_source():
+ G = nx.path_graph(4)
+ for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
+ for flow_func in flow_funcs:
+ pytest.raises(
+ nx.NetworkXError, interface_func, G, 10, 1, flow_func=flow_func
+ )
+
+
+def test_missing_target():
+ G = nx.path_graph(4)
+ for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
+ for flow_func in flow_funcs:
+ pytest.raises(
+ nx.NetworkXError, interface_func, G, 1, 10, flow_func=flow_func
+ )
+
+
+def test_not_weakly_connected():
+ G = nx.DiGraph()
+ nx.add_path(G, [1, 2, 3])
+ nx.add_path(G, [4, 5])
+ for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
+ for flow_func in flow_funcs:
+ pytest.raises(nx.NetworkXError, interface_func, G, flow_func=flow_func)
+
+
+def test_not_connected():
+ G = nx.Graph()
+ nx.add_path(G, [1, 2, 3])
+ nx.add_path(G, [4, 5])
+ for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
+ for flow_func in flow_funcs:
+ pytest.raises(nx.NetworkXError, interface_func, G, flow_func=flow_func)
+
+
+def tests_min_cut_complete():
+ G = nx.complete_graph(5)
+ for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
+ for flow_func in flow_funcs:
+ assert 4 == len(interface_func(G, flow_func=flow_func))
+
+
+def tests_min_cut_complete_directed():
+ G = nx.complete_graph(5)
+ G = G.to_directed()
+ for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
+ for flow_func in flow_funcs:
+ assert 4 == len(interface_func(G, flow_func=flow_func))
+
+
+def tests_minimum_st_node_cut():
+ G = nx.Graph()
+ G.add_nodes_from([0, 1, 2, 3, 7, 8, 11, 12])
+ G.add_edges_from([(7, 11), (1, 11), (1, 12), (12, 8), (0, 1)])
+ nodelist = minimum_st_node_cut(G, 7, 11)
+ assert nodelist == {}
+
+
+def test_invalid_auxiliary():
+ G = nx.complete_graph(5)
+ pytest.raises(nx.NetworkXError, minimum_st_node_cut, G, 0, 3, auxiliary=G)
+
+
+def test_interface_only_source():
+ G = nx.complete_graph(5)
+ for interface_func in [nx.minimum_node_cut, nx.minimum_edge_cut]:
+ pytest.raises(nx.NetworkXError, interface_func, G, s=0)
+
+
+def test_interface_only_target():
+ G = nx.complete_graph(5)
+ for interface_func in [nx.minimum_node_cut, nx.minimum_edge_cut]:
+ pytest.raises(nx.NetworkXError, interface_func, G, t=3)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_disjoint_paths.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_disjoint_paths.py
new file mode 100644
index 00000000..0c0fad9f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_disjoint_paths.py
@@ -0,0 +1,249 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms import flow
+from networkx.utils import pairwise
+
+flow_funcs = [
+ flow.boykov_kolmogorov,
+ flow.edmonds_karp,
+ flow.dinitz,
+ flow.preflow_push,
+ flow.shortest_augmenting_path,
+]
+
+
+def is_path(G, path):
+ return all(v in G[u] for u, v in pairwise(path))
+
+
+def are_edge_disjoint_paths(G, paths):
+ if not paths:
+ return False
+ for path in paths:
+ assert is_path(G, path)
+ paths_edges = [list(pairwise(p)) for p in paths]
+ num_of_edges = sum(len(e) for e in paths_edges)
+ num_unique_edges = len(set.union(*[set(es) for es in paths_edges]))
+ if num_of_edges == num_unique_edges:
+ return True
+ return False
+
+
+def are_node_disjoint_paths(G, paths):
+ if not paths:
+ return False
+ for path in paths:
+ assert is_path(G, path)
+ # first and last nodes are source and target
+ st = {paths[0][0], paths[0][-1]}
+ num_of_nodes = len([n for path in paths for n in path if n not in st])
+ num_unique_nodes = len({n for path in paths for n in path if n not in st})
+ if num_of_nodes == num_unique_nodes:
+ return True
+ return False
+
+
+def test_graph_from_pr_2053():
+ G = nx.Graph()
+ G.add_edges_from(
+ [
+ ("A", "B"),
+ ("A", "D"),
+ ("A", "F"),
+ ("A", "G"),
+ ("B", "C"),
+ ("B", "D"),
+ ("B", "G"),
+ ("C", "D"),
+ ("C", "E"),
+ ("C", "Z"),
+ ("D", "E"),
+ ("D", "F"),
+ ("E", "F"),
+ ("E", "Z"),
+ ("F", "Z"),
+ ("G", "Z"),
+ ]
+ )
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge disjoint paths
+ edge_paths = list(nx.edge_disjoint_paths(G, "A", "Z", **kwargs))
+ assert are_edge_disjoint_paths(G, edge_paths), errmsg
+ assert nx.edge_connectivity(G, "A", "Z") == len(edge_paths), errmsg
+ # node disjoint paths
+ node_paths = list(nx.node_disjoint_paths(G, "A", "Z", **kwargs))
+ assert are_node_disjoint_paths(G, node_paths), errmsg
+ assert nx.node_connectivity(G, "A", "Z") == len(node_paths), errmsg
+
+
+def test_florentine_families():
+ G = nx.florentine_families_graph()
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge disjoint paths
+ edge_dpaths = list(nx.edge_disjoint_paths(G, "Medici", "Strozzi", **kwargs))
+ assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
+ assert nx.edge_connectivity(G, "Medici", "Strozzi") == len(edge_dpaths), errmsg
+ # node disjoint paths
+ node_dpaths = list(nx.node_disjoint_paths(G, "Medici", "Strozzi", **kwargs))
+ assert are_node_disjoint_paths(G, node_dpaths), errmsg
+ assert nx.node_connectivity(G, "Medici", "Strozzi") == len(node_dpaths), errmsg
+
+
+def test_karate():
+ G = nx.karate_club_graph()
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge disjoint paths
+ edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 33, **kwargs))
+ assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
+ assert nx.edge_connectivity(G, 0, 33) == len(edge_dpaths), errmsg
+ # node disjoint paths
+ node_dpaths = list(nx.node_disjoint_paths(G, 0, 33, **kwargs))
+ assert are_node_disjoint_paths(G, node_dpaths), errmsg
+ assert nx.node_connectivity(G, 0, 33) == len(node_dpaths), errmsg
+
+
+def test_petersen_disjoint_paths():
+ G = nx.petersen_graph()
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge disjoint paths
+ edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 6, **kwargs))
+ assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
+ assert 3 == len(edge_dpaths), errmsg
+ # node disjoint paths
+ node_dpaths = list(nx.node_disjoint_paths(G, 0, 6, **kwargs))
+ assert are_node_disjoint_paths(G, node_dpaths), errmsg
+ assert 3 == len(node_dpaths), errmsg
+
+
+def test_octahedral_disjoint_paths():
+ G = nx.octahedral_graph()
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge disjoint paths
+ edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 5, **kwargs))
+ assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
+ assert 4 == len(edge_dpaths), errmsg
+ # node disjoint paths
+ node_dpaths = list(nx.node_disjoint_paths(G, 0, 5, **kwargs))
+ assert are_node_disjoint_paths(G, node_dpaths), errmsg
+ assert 4 == len(node_dpaths), errmsg
+
+
+def test_icosahedral_disjoint_paths():
+ G = nx.icosahedral_graph()
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ # edge disjoint paths
+ edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 6, **kwargs))
+ assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
+ assert 5 == len(edge_dpaths), errmsg
+ # node disjoint paths
+ node_dpaths = list(nx.node_disjoint_paths(G, 0, 6, **kwargs))
+ assert are_node_disjoint_paths(G, node_dpaths), errmsg
+ assert 5 == len(node_dpaths), errmsg
+
+
+def test_cutoff_disjoint_paths():
+ G = nx.icosahedral_graph()
+ for flow_func in flow_funcs:
+ kwargs = {"flow_func": flow_func}
+ errmsg = f"Assertion failed in function: {flow_func.__name__}"
+ for cutoff in [2, 4]:
+ kwargs["cutoff"] = cutoff
+ # edge disjoint paths
+ edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 6, **kwargs))
+ assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
+ assert cutoff == len(edge_dpaths), errmsg
+ # node disjoint paths
+ node_dpaths = list(nx.node_disjoint_paths(G, 0, 6, **kwargs))
+ assert are_node_disjoint_paths(G, node_dpaths), errmsg
+ assert cutoff == len(node_dpaths), errmsg
+
+
+def test_missing_source_edge_paths():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.path_graph(4)
+ list(nx.edge_disjoint_paths(G, 10, 1))
+
+
+def test_missing_source_node_paths():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.path_graph(4)
+ list(nx.node_disjoint_paths(G, 10, 1))
+
+
+def test_missing_target_edge_paths():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.path_graph(4)
+ list(nx.edge_disjoint_paths(G, 1, 10))
+
+
+def test_missing_target_node_paths():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.path_graph(4)
+ list(nx.node_disjoint_paths(G, 1, 10))
+
+
+def test_not_weakly_connected_edges():
+ with pytest.raises(nx.NetworkXNoPath):
+ G = nx.DiGraph()
+ nx.add_path(G, [1, 2, 3])
+ nx.add_path(G, [4, 5])
+ list(nx.edge_disjoint_paths(G, 1, 5))
+
+
+def test_not_weakly_connected_nodes():
+ with pytest.raises(nx.NetworkXNoPath):
+ G = nx.DiGraph()
+ nx.add_path(G, [1, 2, 3])
+ nx.add_path(G, [4, 5])
+ list(nx.node_disjoint_paths(G, 1, 5))
+
+
+def test_not_connected_edges():
+ with pytest.raises(nx.NetworkXNoPath):
+ G = nx.Graph()
+ nx.add_path(G, [1, 2, 3])
+ nx.add_path(G, [4, 5])
+ list(nx.edge_disjoint_paths(G, 1, 5))
+
+
+def test_not_connected_nodes():
+ with pytest.raises(nx.NetworkXNoPath):
+ G = nx.Graph()
+ nx.add_path(G, [1, 2, 3])
+ nx.add_path(G, [4, 5])
+ list(nx.node_disjoint_paths(G, 1, 5))
+
+
+def test_isolated_edges():
+ with pytest.raises(nx.NetworkXNoPath):
+ G = nx.Graph()
+ G.add_node(1)
+ nx.add_path(G, [4, 5])
+ list(nx.edge_disjoint_paths(G, 1, 5))
+
+
+def test_isolated_nodes():
+ with pytest.raises(nx.NetworkXNoPath):
+ G = nx.Graph()
+ G.add_node(1)
+ nx.add_path(G, [4, 5])
+ list(nx.node_disjoint_paths(G, 1, 5))
+
+
+def test_invalid_auxiliary():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.complete_graph(5)
+ list(nx.node_disjoint_paths(G, 0, 3, auxiliary=G))
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_edge_augmentation.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_edge_augmentation.py
new file mode 100644
index 00000000..e1d92d99
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_edge_augmentation.py
@@ -0,0 +1,502 @@
+import itertools as it
+import random
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.connectivity import k_edge_augmentation
+from networkx.algorithms.connectivity.edge_augmentation import (
+ _unpack_available_edges,
+ collapse,
+ complement_edges,
+ is_k_edge_connected,
+ is_locally_k_edge_connected,
+)
+from networkx.utils import pairwise
+
+# This should be set to the largest k for which an efficient algorithm is
+# explicitly defined.
+MAX_EFFICIENT_K = 2
+
+
+def tarjan_bridge_graph():
+ # graph from tarjan paper
+ # RE Tarjan - "A note on finding the bridges of a graph"
+ # Information Processing Letters, 1974 - Elsevier
+ # doi:10.1016/0020-0190(74)90003-9.
+ # define 2-connected components and bridges
+ ccs = [
+ (1, 2, 4, 3, 1, 4),
+ (5, 6, 7, 5),
+ (8, 9, 10, 8),
+ (17, 18, 16, 15, 17),
+ (11, 12, 14, 13, 11, 14),
+ ]
+ bridges = [(4, 8), (3, 5), (3, 17)]
+ G = nx.Graph(it.chain(*(pairwise(path) for path in ccs + bridges)))
+ return G
+
+
+def test_weight_key():
+ G = nx.Graph()
+ G.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8, 9])
+ G.add_edges_from([(3, 8), (1, 2), (2, 3)])
+ impossible = {(3, 6), (3, 9)}
+ rng = random.Random(0)
+ avail_uv = list(set(complement_edges(G)) - impossible)
+ avail = [(u, v, {"cost": rng.random()}) for u, v in avail_uv]
+
+ _augment_and_check(G, k=1)
+ _augment_and_check(G, k=1, avail=avail_uv)
+ _augment_and_check(G, k=1, avail=avail, weight="cost")
+
+ _check_augmentations(G, avail, weight="cost")
+
+
+def test_is_locally_k_edge_connected_exceptions():
+ pytest.raises(nx.NetworkXNotImplemented, is_k_edge_connected, nx.DiGraph(), k=0)
+ pytest.raises(nx.NetworkXNotImplemented, is_k_edge_connected, nx.MultiGraph(), k=0)
+ pytest.raises(ValueError, is_k_edge_connected, nx.Graph(), k=0)
+
+
+def test_is_k_edge_connected():
+ G = nx.barbell_graph(10, 0)
+ assert is_k_edge_connected(G, k=1)
+ assert not is_k_edge_connected(G, k=2)
+
+ G = nx.Graph()
+ G.add_nodes_from([5, 15])
+ assert not is_k_edge_connected(G, k=1)
+ assert not is_k_edge_connected(G, k=2)
+
+ G = nx.complete_graph(5)
+ assert is_k_edge_connected(G, k=1)
+ assert is_k_edge_connected(G, k=2)
+ assert is_k_edge_connected(G, k=3)
+ assert is_k_edge_connected(G, k=4)
+
+ G = nx.compose(nx.complete_graph([0, 1, 2]), nx.complete_graph([3, 4, 5]))
+ assert not is_k_edge_connected(G, k=1)
+ assert not is_k_edge_connected(G, k=2)
+ assert not is_k_edge_connected(G, k=3)
+
+
+def test_is_k_edge_connected_exceptions():
+ pytest.raises(
+ nx.NetworkXNotImplemented, is_locally_k_edge_connected, nx.DiGraph(), 1, 2, k=0
+ )
+ pytest.raises(
+ nx.NetworkXNotImplemented,
+ is_locally_k_edge_connected,
+ nx.MultiGraph(),
+ 1,
+ 2,
+ k=0,
+ )
+ pytest.raises(ValueError, is_locally_k_edge_connected, nx.Graph(), 1, 2, k=0)
+
+
+def test_is_locally_k_edge_connected():
+ G = nx.barbell_graph(10, 0)
+ assert is_locally_k_edge_connected(G, 5, 15, k=1)
+ assert not is_locally_k_edge_connected(G, 5, 15, k=2)
+
+ G = nx.Graph()
+ G.add_nodes_from([5, 15])
+ assert not is_locally_k_edge_connected(G, 5, 15, k=2)
+
+
+def test_null_graph():
+ G = nx.Graph()
+ _check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
+
+
+def test_cliques():
+ for n in range(1, 10):
+ G = nx.complete_graph(n)
+ _check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
+
+
+def test_clique_and_node():
+ for n in range(1, 10):
+ G = nx.complete_graph(n)
+ G.add_node(n + 1)
+ _check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
+
+
+def test_point_graph():
+ G = nx.Graph()
+ G.add_node(1)
+ _check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
+
+
+def test_edgeless_graph():
+ G = nx.Graph()
+ G.add_nodes_from([1, 2, 3, 4])
+ _check_augmentations(G)
+
+
+def test_invalid_k():
+ G = nx.Graph()
+ pytest.raises(ValueError, list, k_edge_augmentation(G, k=-1))
+ pytest.raises(ValueError, list, k_edge_augmentation(G, k=0))
+
+
+def test_unfeasible():
+ G = tarjan_bridge_graph()
+ pytest.raises(nx.NetworkXUnfeasible, list, k_edge_augmentation(G, k=1, avail=[]))
+
+ pytest.raises(nx.NetworkXUnfeasible, list, k_edge_augmentation(G, k=2, avail=[]))
+
+ pytest.raises(
+ nx.NetworkXUnfeasible, list, k_edge_augmentation(G, k=2, avail=[(7, 9)])
+ )
+
+ # partial solutions should not error if real solutions are infeasible
+ aug_edges = list(k_edge_augmentation(G, k=2, avail=[(7, 9)], partial=True))
+ assert aug_edges == [(7, 9)]
+
+ _check_augmentations(G, avail=[], max_k=MAX_EFFICIENT_K + 2)
+
+ _check_augmentations(G, avail=[(7, 9)], max_k=MAX_EFFICIENT_K + 2)
+
+
+def test_tarjan():
+ G = tarjan_bridge_graph()
+
+ aug_edges = set(_augment_and_check(G, k=2)[0])
+ print(f"aug_edges = {aug_edges!r}")
+ # can't assert edge exactly equality due to non-determinant edge order
+ # but we do know the size of the solution must be 3
+ assert len(aug_edges) == 3
+
+ avail = [
+ (9, 7),
+ (8, 5),
+ (2, 10),
+ (6, 13),
+ (11, 18),
+ (1, 17),
+ (2, 3),
+ (16, 17),
+ (18, 14),
+ (15, 14),
+ ]
+ aug_edges = set(_augment_and_check(G, avail=avail, k=2)[0])
+
+ # Can't assert exact length since approximation depends on the order of a
+ # dict traversal.
+ assert len(aug_edges) <= 3 * 2
+
+ _check_augmentations(G, avail)
+
+
+def test_configuration():
+ # seeds = [2718183590, 2470619828, 1694705158, 3001036531, 2401251497]
+ seeds = [1001, 1002, 1003, 1004]
+ for seed in seeds:
+ deg_seq = nx.random_powerlaw_tree_sequence(20, seed=seed, tries=5000)
+ G = nx.Graph(nx.configuration_model(deg_seq, seed=seed))
+ G.remove_edges_from(nx.selfloop_edges(G))
+ _check_augmentations(G)
+
+
+def test_shell():
+ # seeds = [2057382236, 3331169846, 1840105863, 476020778, 2247498425]
+ seeds = [18]
+ for seed in seeds:
+ constructor = [(12, 70, 0.8), (15, 40, 0.6)]
+ G = nx.random_shell_graph(constructor, seed=seed)
+ _check_augmentations(G)
+
+
+def test_karate():
+ G = nx.karate_club_graph()
+ _check_augmentations(G)
+
+
+def test_star():
+ G = nx.star_graph(3)
+ _check_augmentations(G)
+
+ G = nx.star_graph(5)
+ _check_augmentations(G)
+
+ G = nx.star_graph(10)
+ _check_augmentations(G)
+
+
+def test_barbell():
+ G = nx.barbell_graph(5, 0)
+ _check_augmentations(G)
+
+ G = nx.barbell_graph(5, 2)
+ _check_augmentations(G)
+
+ G = nx.barbell_graph(5, 3)
+ _check_augmentations(G)
+
+ G = nx.barbell_graph(5, 4)
+ _check_augmentations(G)
+
+
+def test_bridge():
+ G = nx.Graph([(2393, 2257), (2393, 2685), (2685, 2257), (1758, 2257)])
+ _check_augmentations(G)
+
+
+def test_gnp_augmentation():
+ rng = random.Random(0)
+ G = nx.gnp_random_graph(30, 0.005, seed=0)
+ # Randomly make edges available
+ avail = {
+ (u, v): 1 + rng.random() for u, v in complement_edges(G) if rng.random() < 0.25
+ }
+ _check_augmentations(G, avail)
+
+
+def _assert_solution_properties(G, aug_edges, avail_dict=None):
+ """Checks that aug_edges are consistently formatted"""
+ if avail_dict is not None:
+ assert all(
+ e in avail_dict for e in aug_edges
+ ), "when avail is specified aug-edges should be in avail"
+
+ unique_aug = set(map(tuple, map(sorted, aug_edges)))
+ unique_aug = list(map(tuple, map(sorted, aug_edges)))
+ assert len(aug_edges) == len(unique_aug), "edges should be unique"
+
+ assert not any(u == v for u, v in unique_aug), "should be no self-edges"
+
+ assert not any(
+ G.has_edge(u, v) for u, v in unique_aug
+ ), "aug edges and G.edges should be disjoint"
+
+
+def _augment_and_check(
+ G, k, avail=None, weight=None, verbose=False, orig_k=None, max_aug_k=None
+):
+ """
+ Does one specific augmentation and checks for properties of the result
+ """
+ if orig_k is None:
+ try:
+ orig_k = nx.edge_connectivity(G)
+ except nx.NetworkXPointlessConcept:
+ orig_k = 0
+ info = {}
+ try:
+ if avail is not None:
+ # ensure avail is in dict form
+ avail_dict = dict(zip(*_unpack_available_edges(avail, weight=weight)))
+ else:
+ avail_dict = None
+ try:
+ # Find the augmentation if possible
+ generator = nx.k_edge_augmentation(G, k=k, weight=weight, avail=avail)
+ assert not isinstance(generator, list), "should always return an iter"
+ aug_edges = []
+ for edge in generator:
+ aug_edges.append(edge)
+ except nx.NetworkXUnfeasible:
+ infeasible = True
+ info["infeasible"] = True
+ assert len(aug_edges) == 0, "should not generate anything if unfeasible"
+
+ if avail is None:
+ n_nodes = G.number_of_nodes()
+ assert n_nodes <= k, (
+ "unconstrained cases are only unfeasible if |V| <= k. "
+ f"Got |V|={n_nodes} and k={k}"
+ )
+ else:
+ if max_aug_k is None:
+ G_aug_all = G.copy()
+ G_aug_all.add_edges_from(avail_dict.keys())
+ try:
+ max_aug_k = nx.edge_connectivity(G_aug_all)
+ except nx.NetworkXPointlessConcept:
+ max_aug_k = 0
+
+ assert max_aug_k < k, (
+ "avail should only be unfeasible if using all edges "
+ "does not achieve k-edge-connectivity"
+ )
+
+ # Test for a partial solution
+ partial_edges = list(
+ nx.k_edge_augmentation(G, k=k, weight=weight, partial=True, avail=avail)
+ )
+
+ info["n_partial_edges"] = len(partial_edges)
+
+ if avail_dict is None:
+ assert set(partial_edges) == set(
+ complement_edges(G)
+ ), "unweighted partial solutions should be the complement"
+ elif len(avail_dict) > 0:
+ H = G.copy()
+
+ # Find the partial / full augmented connectivity
+ H.add_edges_from(partial_edges)
+ partial_conn = nx.edge_connectivity(H)
+
+ H.add_edges_from(set(avail_dict.keys()))
+ full_conn = nx.edge_connectivity(H)
+
+ # Full connectivity should be no better than our partial
+ # solution.
+ assert (
+ partial_conn == full_conn
+ ), "adding more edges should not increase k-conn"
+
+ # Find the new edge-connectivity after adding the augmenting edges
+ aug_edges = partial_edges
+ else:
+ infeasible = False
+
+ # Find the weight of the augmentation
+ num_edges = len(aug_edges)
+ if avail is not None:
+ total_weight = sum(avail_dict[e] for e in aug_edges)
+ else:
+ total_weight = num_edges
+
+ info["total_weight"] = total_weight
+ info["num_edges"] = num_edges
+
+ # Find the new edge-connectivity after adding the augmenting edges
+ G_aug = G.copy()
+ G_aug.add_edges_from(aug_edges)
+ try:
+ aug_k = nx.edge_connectivity(G_aug)
+ except nx.NetworkXPointlessConcept:
+ aug_k = 0
+ info["aug_k"] = aug_k
+
+ # Do checks
+ if not infeasible and orig_k < k:
+ assert info["aug_k"] >= k, f"connectivity should increase to k={k} or more"
+
+ assert info["aug_k"] >= orig_k, "augmenting should never reduce connectivity"
+
+ _assert_solution_properties(G, aug_edges, avail_dict)
+
+ except Exception:
+ info["failed"] = True
+ print(f"edges = {list(G.edges())}")
+ print(f"nodes = {list(G.nodes())}")
+ print(f"aug_edges = {list(aug_edges)}")
+ print(f"info = {info}")
+ raise
+ else:
+ if verbose:
+ print(f"info = {info}")
+
+ if infeasible:
+ aug_edges = None
+ return aug_edges, info
+
+
+def _check_augmentations(G, avail=None, max_k=None, weight=None, verbose=False):
+ """Helper to check weighted/unweighted cases with multiple values of k"""
+ # Using all available edges, find the maximum edge-connectivity
+ try:
+ orig_k = nx.edge_connectivity(G)
+ except nx.NetworkXPointlessConcept:
+ orig_k = 0
+
+ if avail is not None:
+ all_aug_edges = _unpack_available_edges(avail, weight=weight)[0]
+ G_aug_all = G.copy()
+ G_aug_all.add_edges_from(all_aug_edges)
+ try:
+ max_aug_k = nx.edge_connectivity(G_aug_all)
+ except nx.NetworkXPointlessConcept:
+ max_aug_k = 0
+ else:
+ max_aug_k = G.number_of_nodes() - 1
+
+ if max_k is None:
+ max_k = min(4, max_aug_k)
+
+ avail_uniform = {e: 1 for e in complement_edges(G)}
+
+ if verbose:
+ print("\n=== CHECK_AUGMENTATION ===")
+ print(f"G.number_of_nodes = {G.number_of_nodes()!r}")
+ print(f"G.number_of_edges = {G.number_of_edges()!r}")
+ print(f"max_k = {max_k!r}")
+ print(f"max_aug_k = {max_aug_k!r}")
+ print(f"orig_k = {orig_k!r}")
+
+ # check augmentation for multiple values of k
+ for k in range(1, max_k + 1):
+ if verbose:
+ print("---------------")
+ print(f"Checking k = {k}")
+
+ # Check the unweighted version
+ if verbose:
+ print("unweighted case")
+ aug_edges1, info1 = _augment_and_check(G, k=k, verbose=verbose, orig_k=orig_k)
+
+ # Check that the weighted version with all available edges and uniform
+ # weights gives a similar solution to the unweighted case.
+ if verbose:
+ print("weighted uniform case")
+ aug_edges2, info2 = _augment_and_check(
+ G,
+ k=k,
+ avail=avail_uniform,
+ verbose=verbose,
+ orig_k=orig_k,
+ max_aug_k=G.number_of_nodes() - 1,
+ )
+
+ # Check the weighted version
+ if avail is not None:
+ if verbose:
+ print("weighted case")
+ aug_edges3, info3 = _augment_and_check(
+ G,
+ k=k,
+ avail=avail,
+ weight=weight,
+ verbose=verbose,
+ max_aug_k=max_aug_k,
+ orig_k=orig_k,
+ )
+
+ if aug_edges1 is not None:
+ # Check approximation ratios
+ if k == 1:
+ # when k=1, both solutions should be optimal
+ assert info2["total_weight"] == info1["total_weight"]
+ if k == 2:
+ # when k=2, the weighted version is an approximation
+ if orig_k == 0:
+ # the approximation ratio is 3 if G is not connected
+ assert info2["total_weight"] <= info1["total_weight"] * 3
+ else:
+ # the approximation ratio is 2 if G is was connected
+ assert info2["total_weight"] <= info1["total_weight"] * 2
+ _check_unconstrained_bridge_property(G, info1)
+
+
+def _check_unconstrained_bridge_property(G, info1):
+ # Check Theorem 5 from Eswaran and Tarjan. (1975) Augmentation problems
+ import math
+
+ bridge_ccs = list(nx.connectivity.bridge_components(G))
+ # condense G into an forest C
+ C = collapse(G, bridge_ccs)
+
+ p = len([n for n, d in C.degree() if d == 1]) # leafs
+ q = len([n for n, d in C.degree() if d == 0]) # isolated
+ if p + q > 1:
+ size_target = math.ceil(p / 2) + q
+ size_aug = info1["num_edges"]
+ assert (
+ size_aug == size_target
+ ), "augmentation size is different from what theory predicts"
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_edge_kcomponents.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_edge_kcomponents.py
new file mode 100644
index 00000000..4a1f681a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_edge_kcomponents.py
@@ -0,0 +1,488 @@
+import itertools as it
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.connectivity import EdgeComponentAuxGraph, bridge_components
+from networkx.algorithms.connectivity.edge_kcomponents import general_k_edge_subgraphs
+from networkx.utils import pairwise
+
+# ----------------
+# Helper functions
+# ----------------
+
+
+def fset(list_of_sets):
+ """allows == to be used for list of sets"""
+ return set(map(frozenset, list_of_sets))
+
+
+def _assert_subgraph_edge_connectivity(G, ccs_subgraph, k):
+ """
+ tests properties of k-edge-connected subgraphs
+
+ the actual edge connectivity should be no less than k unless the cc is a
+ single node.
+ """
+ for cc in ccs_subgraph:
+ C = G.subgraph(cc)
+ if len(cc) > 1:
+ connectivity = nx.edge_connectivity(C)
+ assert connectivity >= k
+
+
+def _memo_connectivity(G, u, v, memo):
+ edge = (u, v)
+ if edge in memo:
+ return memo[edge]
+ if not G.is_directed():
+ redge = (v, u)
+ if redge in memo:
+ return memo[redge]
+ memo[edge] = nx.edge_connectivity(G, *edge)
+ return memo[edge]
+
+
+def _all_pairs_connectivity(G, cc, k, memo):
+ # Brute force check
+ for u, v in it.combinations(cc, 2):
+ # Use a memoization dict to save on computation
+ connectivity = _memo_connectivity(G, u, v, memo)
+ if G.is_directed():
+ connectivity = min(connectivity, _memo_connectivity(G, v, u, memo))
+ assert connectivity >= k
+
+
+def _assert_local_cc_edge_connectivity(G, ccs_local, k, memo):
+ """
+ tests properties of k-edge-connected components
+
+ the local edge connectivity between each pair of nodes in the original
+ graph should be no less than k unless the cc is a single node.
+ """
+ for cc in ccs_local:
+ if len(cc) > 1:
+ # Strategy for testing a bit faster: If the subgraph has high edge
+ # connectivity then it must have local connectivity
+ C = G.subgraph(cc)
+ connectivity = nx.edge_connectivity(C)
+ if connectivity < k:
+ # Otherwise do the brute force (with memoization) check
+ _all_pairs_connectivity(G, cc, k, memo)
+
+
+# Helper function
+def _check_edge_connectivity(G):
+ """
+ Helper - generates all k-edge-components using the aux graph. Checks the
+ both local and subgraph edge connectivity of each cc. Also checks that
+ alternate methods of computing the k-edge-ccs generate the same result.
+ """
+ # Construct the auxiliary graph that can be used to make each k-cc or k-sub
+ aux_graph = EdgeComponentAuxGraph.construct(G)
+
+ # memoize the local connectivity in this graph
+ memo = {}
+
+ for k in it.count(1):
+ # Test "local" k-edge-components and k-edge-subgraphs
+ ccs_local = fset(aux_graph.k_edge_components(k))
+ ccs_subgraph = fset(aux_graph.k_edge_subgraphs(k))
+
+ # Check connectivity properties that should be guaranteed by the
+ # algorithms.
+ _assert_local_cc_edge_connectivity(G, ccs_local, k, memo)
+ _assert_subgraph_edge_connectivity(G, ccs_subgraph, k)
+
+ if k == 1 or k == 2 and not G.is_directed():
+ assert (
+ ccs_local == ccs_subgraph
+ ), "Subgraphs and components should be the same when k == 1 or (k == 2 and not G.directed())"
+
+ if G.is_directed():
+ # Test special case methods are the same as the aux graph
+ if k == 1:
+ alt_sccs = fset(nx.strongly_connected_components(G))
+ assert alt_sccs == ccs_local, "k=1 failed alt"
+ assert alt_sccs == ccs_subgraph, "k=1 failed alt"
+ else:
+ # Test special case methods are the same as the aux graph
+ if k == 1:
+ alt_ccs = fset(nx.connected_components(G))
+ assert alt_ccs == ccs_local, "k=1 failed alt"
+ assert alt_ccs == ccs_subgraph, "k=1 failed alt"
+ elif k == 2:
+ alt_bridge_ccs = fset(bridge_components(G))
+ assert alt_bridge_ccs == ccs_local, "k=2 failed alt"
+ assert alt_bridge_ccs == ccs_subgraph, "k=2 failed alt"
+ # if new methods for k == 3 or k == 4 are implemented add them here
+
+ # Check the general subgraph method works by itself
+ alt_subgraph_ccs = fset(
+ [set(C.nodes()) for C in general_k_edge_subgraphs(G, k=k)]
+ )
+ assert alt_subgraph_ccs == ccs_subgraph, "alt subgraph method failed"
+
+ # Stop once k is larger than all special case methods
+ # and we cannot break down ccs any further.
+ if k > 2 and all(len(cc) == 1 for cc in ccs_local):
+ break
+
+
+# ----------------
+# Misc tests
+# ----------------
+
+
+def test_zero_k_exception():
+ G = nx.Graph()
+ # functions that return generators error immediately
+ pytest.raises(ValueError, nx.k_edge_components, G, k=0)
+ pytest.raises(ValueError, nx.k_edge_subgraphs, G, k=0)
+
+ # actual generators only error when you get the first item
+ aux_graph = EdgeComponentAuxGraph.construct(G)
+ pytest.raises(ValueError, list, aux_graph.k_edge_components(k=0))
+ pytest.raises(ValueError, list, aux_graph.k_edge_subgraphs(k=0))
+
+ pytest.raises(ValueError, list, general_k_edge_subgraphs(G, k=0))
+
+
+def test_empty_input():
+ G = nx.Graph()
+ assert [] == list(nx.k_edge_components(G, k=5))
+ assert [] == list(nx.k_edge_subgraphs(G, k=5))
+
+ G = nx.DiGraph()
+ assert [] == list(nx.k_edge_components(G, k=5))
+ assert [] == list(nx.k_edge_subgraphs(G, k=5))
+
+
+def test_not_implemented():
+ G = nx.MultiGraph()
+ pytest.raises(nx.NetworkXNotImplemented, EdgeComponentAuxGraph.construct, G)
+ pytest.raises(nx.NetworkXNotImplemented, nx.k_edge_components, G, k=2)
+ pytest.raises(nx.NetworkXNotImplemented, nx.k_edge_subgraphs, G, k=2)
+ with pytest.raises(nx.NetworkXNotImplemented):
+ next(bridge_components(G))
+ with pytest.raises(nx.NetworkXNotImplemented):
+ next(bridge_components(nx.DiGraph()))
+
+
+def test_general_k_edge_subgraph_quick_return():
+ # tests quick return optimization
+ G = nx.Graph()
+ G.add_node(0)
+ subgraphs = list(general_k_edge_subgraphs(G, k=1))
+ assert len(subgraphs) == 1
+ for subgraph in subgraphs:
+ assert subgraph.number_of_nodes() == 1
+
+ G.add_node(1)
+ subgraphs = list(general_k_edge_subgraphs(G, k=1))
+ assert len(subgraphs) == 2
+ for subgraph in subgraphs:
+ assert subgraph.number_of_nodes() == 1
+
+
+# ----------------
+# Undirected tests
+# ----------------
+
+
+def test_random_gnp():
+ # seeds = [1550709854, 1309423156, 4208992358, 2785630813, 1915069929]
+ seeds = [12, 13]
+
+ for seed in seeds:
+ G = nx.gnp_random_graph(20, 0.2, seed=seed)
+ _check_edge_connectivity(G)
+
+
+def test_configuration():
+ # seeds = [2718183590, 2470619828, 1694705158, 3001036531, 2401251497]
+ seeds = [14, 15]
+ for seed in seeds:
+ deg_seq = nx.random_powerlaw_tree_sequence(20, seed=seed, tries=5000)
+ G = nx.Graph(nx.configuration_model(deg_seq, seed=seed))
+ G.remove_edges_from(nx.selfloop_edges(G))
+ _check_edge_connectivity(G)
+
+
+def test_shell():
+ # seeds = [2057382236, 3331169846, 1840105863, 476020778, 2247498425]
+ seeds = [20]
+ for seed in seeds:
+ constructor = [(12, 70, 0.8), (15, 40, 0.6)]
+ G = nx.random_shell_graph(constructor, seed=seed)
+ _check_edge_connectivity(G)
+
+
+def test_karate():
+ G = nx.karate_club_graph()
+ _check_edge_connectivity(G)
+
+
+def test_tarjan_bridge():
+ # graph from tarjan paper
+ # RE Tarjan - "A note on finding the bridges of a graph"
+ # Information Processing Letters, 1974 - Elsevier
+ # doi:10.1016/0020-0190(74)90003-9.
+ # define 2-connected components and bridges
+ ccs = [
+ (1, 2, 4, 3, 1, 4),
+ (5, 6, 7, 5),
+ (8, 9, 10, 8),
+ (17, 18, 16, 15, 17),
+ (11, 12, 14, 13, 11, 14),
+ ]
+ bridges = [(4, 8), (3, 5), (3, 17)]
+ G = nx.Graph(it.chain(*(pairwise(path) for path in ccs + bridges)))
+ _check_edge_connectivity(G)
+
+
+def test_bridge_cc():
+ # define 2-connected components and bridges
+ cc2 = [(1, 2, 4, 3, 1, 4), (8, 9, 10, 8), (11, 12, 13, 11)]
+ bridges = [(4, 8), (3, 5), (20, 21), (22, 23, 24)]
+ G = nx.Graph(it.chain(*(pairwise(path) for path in cc2 + bridges)))
+ bridge_ccs = fset(bridge_components(G))
+ target_ccs = fset(
+ [{1, 2, 3, 4}, {5}, {8, 9, 10}, {11, 12, 13}, {20}, {21}, {22}, {23}, {24}]
+ )
+ assert bridge_ccs == target_ccs
+ _check_edge_connectivity(G)
+
+
+def test_undirected_aux_graph():
+ # Graph similar to the one in
+ # http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
+ a, b, c, d, e, f, g, h, i = "abcdefghi"
+ paths = [
+ (a, d, b, f, c),
+ (a, e, b),
+ (a, e, b, c, g, b, a),
+ (c, b),
+ (f, g, f),
+ (h, i),
+ ]
+ G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
+ aux_graph = EdgeComponentAuxGraph.construct(G)
+
+ components_1 = fset(aux_graph.k_edge_subgraphs(k=1))
+ target_1 = fset([{a, b, c, d, e, f, g}, {h, i}])
+ assert target_1 == components_1
+
+ # Check that the undirected case for k=1 agrees with CCs
+ alt_1 = fset(nx.k_edge_subgraphs(G, k=1))
+ assert alt_1 == components_1
+
+ components_2 = fset(aux_graph.k_edge_subgraphs(k=2))
+ target_2 = fset([{a, b, c, d, e, f, g}, {h}, {i}])
+ assert target_2 == components_2
+
+ # Check that the undirected case for k=2 agrees with bridge components
+ alt_2 = fset(nx.k_edge_subgraphs(G, k=2))
+ assert alt_2 == components_2
+
+ components_3 = fset(aux_graph.k_edge_subgraphs(k=3))
+ target_3 = fset([{a}, {b, c, f, g}, {d}, {e}, {h}, {i}])
+ assert target_3 == components_3
+
+ components_4 = fset(aux_graph.k_edge_subgraphs(k=4))
+ target_4 = fset([{a}, {b}, {c}, {d}, {e}, {f}, {g}, {h}, {i}])
+ assert target_4 == components_4
+
+ _check_edge_connectivity(G)
+
+
+def test_local_subgraph_difference():
+ paths = [
+ (11, 12, 13, 14, 11, 13, 14, 12), # first 4-clique
+ (21, 22, 23, 24, 21, 23, 24, 22), # second 4-clique
+ # paths connecting each node of the 4 cliques
+ (11, 101, 21),
+ (12, 102, 22),
+ (13, 103, 23),
+ (14, 104, 24),
+ ]
+ G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
+ aux_graph = EdgeComponentAuxGraph.construct(G)
+
+ # Each clique is returned separately in k-edge-subgraphs
+ subgraph_ccs = fset(aux_graph.k_edge_subgraphs(3))
+ subgraph_target = fset(
+ [{101}, {102}, {103}, {104}, {21, 22, 23, 24}, {11, 12, 13, 14}]
+ )
+ assert subgraph_ccs == subgraph_target
+
+ # But in k-edge-ccs they are returned together
+ # because they are locally 3-edge-connected
+ local_ccs = fset(aux_graph.k_edge_components(3))
+ local_target = fset([{101}, {102}, {103}, {104}, {11, 12, 13, 14, 21, 22, 23, 24}])
+ assert local_ccs == local_target
+
+
+def test_local_subgraph_difference_directed():
+ dipaths = [(1, 2, 3, 4, 1), (1, 3, 1)]
+ G = nx.DiGraph(it.chain(*[pairwise(path) for path in dipaths]))
+
+ assert fset(nx.k_edge_components(G, k=1)) == fset(nx.k_edge_subgraphs(G, k=1))
+
+ # Unlike undirected graphs, when k=2, for directed graphs there is a case
+ # where the k-edge-ccs are not the same as the k-edge-subgraphs.
+ # (in directed graphs ccs and subgraphs are the same when k=2)
+ assert fset(nx.k_edge_components(G, k=2)) != fset(nx.k_edge_subgraphs(G, k=2))
+
+ assert fset(nx.k_edge_components(G, k=3)) == fset(nx.k_edge_subgraphs(G, k=3))
+
+ _check_edge_connectivity(G)
+
+
+def test_triangles():
+ paths = [
+ (11, 12, 13, 11), # first 3-clique
+ (21, 22, 23, 21), # second 3-clique
+ (11, 21), # connected by an edge
+ ]
+ G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
+
+ # subgraph and ccs are the same in all cases here
+ assert fset(nx.k_edge_components(G, k=1)) == fset(nx.k_edge_subgraphs(G, k=1))
+
+ assert fset(nx.k_edge_components(G, k=2)) == fset(nx.k_edge_subgraphs(G, k=2))
+
+ assert fset(nx.k_edge_components(G, k=3)) == fset(nx.k_edge_subgraphs(G, k=3))
+
+ _check_edge_connectivity(G)
+
+
+def test_four_clique():
+ paths = [
+ (11, 12, 13, 14, 11, 13, 14, 12), # first 4-clique
+ (21, 22, 23, 24, 21, 23, 24, 22), # second 4-clique
+ # paths connecting the 4 cliques such that they are
+ # 3-connected in G, but not in the subgraph.
+ # Case where the nodes bridging them do not have degree less than 3.
+ (100, 13),
+ (12, 100, 22),
+ (13, 200, 23),
+ (14, 300, 24),
+ ]
+ G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
+
+ # The subgraphs and ccs are different for k=3
+ local_ccs = fset(nx.k_edge_components(G, k=3))
+ subgraphs = fset(nx.k_edge_subgraphs(G, k=3))
+ assert local_ccs != subgraphs
+
+ # The cliques ares in the same cc
+ clique1 = frozenset(paths[0])
+ clique2 = frozenset(paths[1])
+ assert clique1.union(clique2).union({100}) in local_ccs
+
+ # but different subgraphs
+ assert clique1 in subgraphs
+ assert clique2 in subgraphs
+
+ assert G.degree(100) == 3
+
+ _check_edge_connectivity(G)
+
+
+def test_five_clique():
+ # Make a graph that can be disconnected less than 4 edges, but no node has
+ # degree less than 4.
+ G = nx.disjoint_union(nx.complete_graph(5), nx.complete_graph(5))
+ paths = [
+ # add aux-connections
+ (1, 100, 6),
+ (2, 100, 7),
+ (3, 200, 8),
+ (4, 200, 100),
+ ]
+ G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
+ assert min(dict(nx.degree(G)).values()) == 4
+
+ # For k=3 they are the same
+ assert fset(nx.k_edge_components(G, k=3)) == fset(nx.k_edge_subgraphs(G, k=3))
+
+ # For k=4 they are the different
+ # the aux nodes are in the same CC as clique 1 but no the same subgraph
+ assert fset(nx.k_edge_components(G, k=4)) != fset(nx.k_edge_subgraphs(G, k=4))
+
+ # For k=5 they are not the same
+ assert fset(nx.k_edge_components(G, k=5)) != fset(nx.k_edge_subgraphs(G, k=5))
+
+ # For k=6 they are the same
+ assert fset(nx.k_edge_components(G, k=6)) == fset(nx.k_edge_subgraphs(G, k=6))
+ _check_edge_connectivity(G)
+
+
+# ----------------
+# Undirected tests
+# ----------------
+
+
+def test_directed_aux_graph():
+ # Graph similar to the one in
+ # http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
+ a, b, c, d, e, f, g, h, i = "abcdefghi"
+ dipaths = [
+ (a, d, b, f, c),
+ (a, e, b),
+ (a, e, b, c, g, b, a),
+ (c, b),
+ (f, g, f),
+ (h, i),
+ ]
+ G = nx.DiGraph(it.chain(*[pairwise(path) for path in dipaths]))
+ aux_graph = EdgeComponentAuxGraph.construct(G)
+
+ components_1 = fset(aux_graph.k_edge_subgraphs(k=1))
+ target_1 = fset([{a, b, c, d, e, f, g}, {h}, {i}])
+ assert target_1 == components_1
+
+ # Check that the directed case for k=1 agrees with SCCs
+ alt_1 = fset(nx.strongly_connected_components(G))
+ assert alt_1 == components_1
+
+ components_2 = fset(aux_graph.k_edge_subgraphs(k=2))
+ target_2 = fset([{i}, {e}, {d}, {b, c, f, g}, {h}, {a}])
+ assert target_2 == components_2
+
+ components_3 = fset(aux_graph.k_edge_subgraphs(k=3))
+ target_3 = fset([{a}, {b}, {c}, {d}, {e}, {f}, {g}, {h}, {i}])
+ assert target_3 == components_3
+
+
+def test_random_gnp_directed():
+ # seeds = [3894723670, 500186844, 267231174, 2181982262, 1116750056]
+ seeds = [21]
+ for seed in seeds:
+ G = nx.gnp_random_graph(20, 0.2, directed=True, seed=seed)
+ _check_edge_connectivity(G)
+
+
+def test_configuration_directed():
+ # seeds = [671221681, 2403749451, 124433910, 672335939, 1193127215]
+ seeds = [67]
+ for seed in seeds:
+ deg_seq = nx.random_powerlaw_tree_sequence(20, seed=seed, tries=5000)
+ G = nx.DiGraph(nx.configuration_model(deg_seq, seed=seed))
+ G.remove_edges_from(nx.selfloop_edges(G))
+ _check_edge_connectivity(G)
+
+
+def test_shell_directed():
+ # seeds = [3134027055, 4079264063, 1350769518, 1405643020, 530038094]
+ seeds = [31]
+ for seed in seeds:
+ constructor = [(12, 70, 0.8), (15, 40, 0.6)]
+ G = nx.random_shell_graph(constructor, seed=seed).to_directed()
+ _check_edge_connectivity(G)
+
+
+def test_karate_directed():
+ G = nx.karate_club_graph().to_directed()
+ _check_edge_connectivity(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_kcomponents.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_kcomponents.py
new file mode 100644
index 00000000..f4436acd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_kcomponents.py
@@ -0,0 +1,296 @@
+# Test for Moody and White k-components algorithm
+import pytest
+
+import networkx as nx
+from networkx.algorithms.connectivity.kcomponents import (
+ _consolidate,
+ build_k_number_dict,
+)
+
+##
+# A nice synthetic graph
+##
+
+
+def torrents_and_ferraro_graph():
+ # Graph from https://arxiv.org/pdf/1503.04476v1 p.26
+ G = nx.convert_node_labels_to_integers(
+ nx.grid_graph([5, 5]), label_attribute="labels"
+ )
+ rlabels = nx.get_node_attributes(G, "labels")
+ labels = {v: k for k, v in rlabels.items()}
+
+ for nodes in [(labels[(0, 4)], labels[(1, 4)]), (labels[(3, 4)], labels[(4, 4)])]:
+ new_node = G.order() + 1
+ # Petersen graph is triconnected
+ P = nx.petersen_graph()
+ G = nx.disjoint_union(G, P)
+ # Add two edges between the grid and P
+ G.add_edge(new_node + 1, nodes[0])
+ G.add_edge(new_node, nodes[1])
+ # K5 is 4-connected
+ K = nx.complete_graph(5)
+ G = nx.disjoint_union(G, K)
+ # Add three edges between P and K5
+ G.add_edge(new_node + 2, new_node + 11)
+ G.add_edge(new_node + 3, new_node + 12)
+ G.add_edge(new_node + 4, new_node + 13)
+ # Add another K5 sharing a node
+ G = nx.disjoint_union(G, K)
+ nbrs = G[new_node + 10]
+ G.remove_node(new_node + 10)
+ for nbr in nbrs:
+ G.add_edge(new_node + 17, nbr)
+ # This edge makes the graph biconnected; it's
+ # needed because K5s share only one node.
+ G.add_edge(new_node + 16, new_node + 8)
+
+ for nodes in [(labels[(0, 0)], labels[(1, 0)]), (labels[(3, 0)], labels[(4, 0)])]:
+ new_node = G.order() + 1
+ # Petersen graph is triconnected
+ P = nx.petersen_graph()
+ G = nx.disjoint_union(G, P)
+ # Add two edges between the grid and P
+ G.add_edge(new_node + 1, nodes[0])
+ G.add_edge(new_node, nodes[1])
+ # K5 is 4-connected
+ K = nx.complete_graph(5)
+ G = nx.disjoint_union(G, K)
+ # Add three edges between P and K5
+ G.add_edge(new_node + 2, new_node + 11)
+ G.add_edge(new_node + 3, new_node + 12)
+ G.add_edge(new_node + 4, new_node + 13)
+ # Add another K5 sharing two nodes
+ G = nx.disjoint_union(G, K)
+ nbrs = G[new_node + 10]
+ G.remove_node(new_node + 10)
+ for nbr in nbrs:
+ G.add_edge(new_node + 17, nbr)
+ nbrs2 = G[new_node + 9]
+ G.remove_node(new_node + 9)
+ for nbr in nbrs2:
+ G.add_edge(new_node + 18, nbr)
+ return G
+
+
+def test_directed():
+ with pytest.raises(nx.NetworkXNotImplemented):
+ G = nx.gnp_random_graph(10, 0.2, directed=True, seed=42)
+ nx.k_components(G)
+
+
+# Helper function
+def _check_connectivity(G, k_components):
+ for k, components in k_components.items():
+ if k < 3:
+ continue
+ # check that k-components have node connectivity >= k.
+ for component in components:
+ C = G.subgraph(component)
+ K = nx.node_connectivity(C)
+ assert K >= k
+
+
+@pytest.mark.slow
+def test_torrents_and_ferraro_graph():
+ G = torrents_and_ferraro_graph()
+ result = nx.k_components(G)
+ _check_connectivity(G, result)
+
+ # In this example graph there are 8 3-components, 4 with 15 nodes
+ # and 4 with 5 nodes.
+ assert len(result[3]) == 8
+ assert len([c for c in result[3] if len(c) == 15]) == 4
+ assert len([c for c in result[3] if len(c) == 5]) == 4
+ # There are also 8 4-components all with 5 nodes.
+ assert len(result[4]) == 8
+ assert all(len(c) == 5 for c in result[4])
+
+
+@pytest.mark.slow
+def test_random_gnp():
+ G = nx.gnp_random_graph(50, 0.2, seed=42)
+ result = nx.k_components(G)
+ _check_connectivity(G, result)
+
+
+@pytest.mark.slow
+def test_shell():
+ constructor = [(20, 80, 0.8), (80, 180, 0.6)]
+ G = nx.random_shell_graph(constructor, seed=42)
+ result = nx.k_components(G)
+ _check_connectivity(G, result)
+
+
+def test_configuration():
+ deg_seq = nx.random_powerlaw_tree_sequence(100, tries=5, seed=72)
+ G = nx.Graph(nx.configuration_model(deg_seq))
+ G.remove_edges_from(nx.selfloop_edges(G))
+ result = nx.k_components(G)
+ _check_connectivity(G, result)
+
+
+def test_karate():
+ G = nx.karate_club_graph()
+ result = nx.k_components(G)
+ _check_connectivity(G, result)
+
+
+def test_karate_component_number():
+ karate_k_num = {
+ 0: 4,
+ 1: 4,
+ 2: 4,
+ 3: 4,
+ 4: 3,
+ 5: 3,
+ 6: 3,
+ 7: 4,
+ 8: 4,
+ 9: 2,
+ 10: 3,
+ 11: 1,
+ 12: 2,
+ 13: 4,
+ 14: 2,
+ 15: 2,
+ 16: 2,
+ 17: 2,
+ 18: 2,
+ 19: 3,
+ 20: 2,
+ 21: 2,
+ 22: 2,
+ 23: 3,
+ 24: 3,
+ 25: 3,
+ 26: 2,
+ 27: 3,
+ 28: 3,
+ 29: 3,
+ 30: 4,
+ 31: 3,
+ 32: 4,
+ 33: 4,
+ }
+ G = nx.karate_club_graph()
+ k_components = nx.k_components(G)
+ k_num = build_k_number_dict(k_components)
+ assert karate_k_num == k_num
+
+
+def test_davis_southern_women():
+ G = nx.davis_southern_women_graph()
+ result = nx.k_components(G)
+ _check_connectivity(G, result)
+
+
+def test_davis_southern_women_detail_3_and_4():
+ solution = {
+ 3: [
+ {
+ "Nora Fayette",
+ "E10",
+ "Myra Liddel",
+ "E12",
+ "E14",
+ "Frances Anderson",
+ "Evelyn Jefferson",
+ "Ruth DeSand",
+ "Helen Lloyd",
+ "Eleanor Nye",
+ "E9",
+ "E8",
+ "E5",
+ "E4",
+ "E7",
+ "E6",
+ "E1",
+ "Verne Sanderson",
+ "E3",
+ "E2",
+ "Theresa Anderson",
+ "Pearl Oglethorpe",
+ "Katherina Rogers",
+ "Brenda Rogers",
+ "E13",
+ "Charlotte McDowd",
+ "Sylvia Avondale",
+ "Laura Mandeville",
+ }
+ ],
+ 4: [
+ {
+ "Nora Fayette",
+ "E10",
+ "Verne Sanderson",
+ "E12",
+ "Frances Anderson",
+ "Evelyn Jefferson",
+ "Ruth DeSand",
+ "Helen Lloyd",
+ "Eleanor Nye",
+ "E9",
+ "E8",
+ "E5",
+ "E4",
+ "E7",
+ "E6",
+ "Myra Liddel",
+ "E3",
+ "Theresa Anderson",
+ "Katherina Rogers",
+ "Brenda Rogers",
+ "Charlotte McDowd",
+ "Sylvia Avondale",
+ "Laura Mandeville",
+ }
+ ],
+ }
+ G = nx.davis_southern_women_graph()
+ result = nx.k_components(G)
+ for k, components in result.items():
+ if k < 3:
+ continue
+ assert len(components) == len(solution[k])
+ for component in components:
+ assert component in solution[k]
+
+
+def test_set_consolidation_rosettacode():
+ # Tests from http://rosettacode.org/wiki/Set_consolidation
+ def list_of_sets_equal(result, solution):
+ assert {frozenset(s) for s in result} == {frozenset(s) for s in solution}
+
+ question = [{"A", "B"}, {"C", "D"}]
+ solution = [{"A", "B"}, {"C", "D"}]
+ list_of_sets_equal(_consolidate(question, 1), solution)
+ question = [{"A", "B"}, {"B", "C"}]
+ solution = [{"A", "B", "C"}]
+ list_of_sets_equal(_consolidate(question, 1), solution)
+ question = [{"A", "B"}, {"C", "D"}, {"D", "B"}]
+ solution = [{"A", "C", "B", "D"}]
+ list_of_sets_equal(_consolidate(question, 1), solution)
+ question = [{"H", "I", "K"}, {"A", "B"}, {"C", "D"}, {"D", "B"}, {"F", "G", "H"}]
+ solution = [{"A", "C", "B", "D"}, {"G", "F", "I", "H", "K"}]
+ list_of_sets_equal(_consolidate(question, 1), solution)
+ question = [
+ {"A", "H"},
+ {"H", "I", "K"},
+ {"A", "B"},
+ {"C", "D"},
+ {"D", "B"},
+ {"F", "G", "H"},
+ ]
+ solution = [{"A", "C", "B", "D", "G", "F", "I", "H", "K"}]
+ list_of_sets_equal(_consolidate(question, 1), solution)
+ question = [
+ {"H", "I", "K"},
+ {"A", "B"},
+ {"C", "D"},
+ {"D", "B"},
+ {"F", "G", "H"},
+ {"A", "H"},
+ ]
+ solution = [{"A", "C", "B", "D", "G", "F", "I", "H", "K"}]
+ list_of_sets_equal(_consolidate(question, 1), solution)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_kcutsets.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_kcutsets.py
new file mode 100644
index 00000000..4b4b5494
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_kcutsets.py
@@ -0,0 +1,273 @@
+# Jordi Torrents
+# Test for k-cutsets
+import itertools
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms import flow
+from networkx.algorithms.connectivity.kcutsets import _is_separating_set
+
+MAX_CUTSETS_TO_TEST = 4 # originally 100. cut to decrease testing time
+
+flow_funcs = [
+ flow.boykov_kolmogorov,
+ flow.dinitz,
+ flow.edmonds_karp,
+ flow.preflow_push,
+ flow.shortest_augmenting_path,
+]
+
+
+##
+# Some nice synthetic graphs
+##
+def graph_example_1():
+ G = nx.convert_node_labels_to_integers(
+ nx.grid_graph([5, 5]), label_attribute="labels"
+ )
+ rlabels = nx.get_node_attributes(G, "labels")
+ labels = {v: k for k, v in rlabels.items()}
+
+ for nodes in [
+ (labels[(0, 0)], labels[(1, 0)]),
+ (labels[(0, 4)], labels[(1, 4)]),
+ (labels[(3, 0)], labels[(4, 0)]),
+ (labels[(3, 4)], labels[(4, 4)]),
+ ]:
+ new_node = G.order() + 1
+ # Petersen graph is triconnected
+ P = nx.petersen_graph()
+ G = nx.disjoint_union(G, P)
+ # Add two edges between the grid and P
+ G.add_edge(new_node + 1, nodes[0])
+ G.add_edge(new_node, nodes[1])
+ # K5 is 4-connected
+ K = nx.complete_graph(5)
+ G = nx.disjoint_union(G, K)
+ # Add three edges between P and K5
+ G.add_edge(new_node + 2, new_node + 11)
+ G.add_edge(new_node + 3, new_node + 12)
+ G.add_edge(new_node + 4, new_node + 13)
+ # Add another K5 sharing a node
+ G = nx.disjoint_union(G, K)
+ nbrs = G[new_node + 10]
+ G.remove_node(new_node + 10)
+ for nbr in nbrs:
+ G.add_edge(new_node + 17, nbr)
+ G.add_edge(new_node + 16, new_node + 5)
+ return G
+
+
+def torrents_and_ferraro_graph():
+ G = nx.convert_node_labels_to_integers(
+ nx.grid_graph([5, 5]), label_attribute="labels"
+ )
+ rlabels = nx.get_node_attributes(G, "labels")
+ labels = {v: k for k, v in rlabels.items()}
+
+ for nodes in [(labels[(0, 4)], labels[(1, 4)]), (labels[(3, 4)], labels[(4, 4)])]:
+ new_node = G.order() + 1
+ # Petersen graph is triconnected
+ P = nx.petersen_graph()
+ G = nx.disjoint_union(G, P)
+ # Add two edges between the grid and P
+ G.add_edge(new_node + 1, nodes[0])
+ G.add_edge(new_node, nodes[1])
+ # K5 is 4-connected
+ K = nx.complete_graph(5)
+ G = nx.disjoint_union(G, K)
+ # Add three edges between P and K5
+ G.add_edge(new_node + 2, new_node + 11)
+ G.add_edge(new_node + 3, new_node + 12)
+ G.add_edge(new_node + 4, new_node + 13)
+ # Add another K5 sharing a node
+ G = nx.disjoint_union(G, K)
+ nbrs = G[new_node + 10]
+ G.remove_node(new_node + 10)
+ for nbr in nbrs:
+ G.add_edge(new_node + 17, nbr)
+ # Commenting this makes the graph not biconnected !!
+ # This stupid mistake make one reviewer very angry :P
+ G.add_edge(new_node + 16, new_node + 8)
+
+ for nodes in [(labels[(0, 0)], labels[(1, 0)]), (labels[(3, 0)], labels[(4, 0)])]:
+ new_node = G.order() + 1
+ # Petersen graph is triconnected
+ P = nx.petersen_graph()
+ G = nx.disjoint_union(G, P)
+ # Add two edges between the grid and P
+ G.add_edge(new_node + 1, nodes[0])
+ G.add_edge(new_node, nodes[1])
+ # K5 is 4-connected
+ K = nx.complete_graph(5)
+ G = nx.disjoint_union(G, K)
+ # Add three edges between P and K5
+ G.add_edge(new_node + 2, new_node + 11)
+ G.add_edge(new_node + 3, new_node + 12)
+ G.add_edge(new_node + 4, new_node + 13)
+ # Add another K5 sharing two nodes
+ G = nx.disjoint_union(G, K)
+ nbrs = G[new_node + 10]
+ G.remove_node(new_node + 10)
+ for nbr in nbrs:
+ G.add_edge(new_node + 17, nbr)
+ nbrs2 = G[new_node + 9]
+ G.remove_node(new_node + 9)
+ for nbr in nbrs2:
+ G.add_edge(new_node + 18, nbr)
+ return G
+
+
+# Helper function
+def _check_separating_sets(G):
+ for cc in nx.connected_components(G):
+ if len(cc) < 3:
+ continue
+ Gc = G.subgraph(cc)
+ node_conn = nx.node_connectivity(Gc)
+ all_cuts = nx.all_node_cuts(Gc)
+ # Only test a limited number of cut sets to reduce test time.
+ for cut in itertools.islice(all_cuts, MAX_CUTSETS_TO_TEST):
+ assert node_conn == len(cut)
+ assert not nx.is_connected(nx.restricted_view(G, cut, []))
+
+
+@pytest.mark.slow
+def test_torrents_and_ferraro_graph():
+ G = torrents_and_ferraro_graph()
+ _check_separating_sets(G)
+
+
+def test_example_1():
+ G = graph_example_1()
+ _check_separating_sets(G)
+
+
+def test_random_gnp():
+ G = nx.gnp_random_graph(100, 0.1, seed=42)
+ _check_separating_sets(G)
+
+
+def test_shell():
+ constructor = [(20, 80, 0.8), (80, 180, 0.6)]
+ G = nx.random_shell_graph(constructor, seed=42)
+ _check_separating_sets(G)
+
+
+def test_configuration():
+ deg_seq = nx.random_powerlaw_tree_sequence(100, tries=5, seed=72)
+ G = nx.Graph(nx.configuration_model(deg_seq))
+ G.remove_edges_from(nx.selfloop_edges(G))
+ _check_separating_sets(G)
+
+
+def test_karate():
+ G = nx.karate_club_graph()
+ _check_separating_sets(G)
+
+
+def _generate_no_biconnected(max_attempts=50):
+ attempts = 0
+ while True:
+ G = nx.fast_gnp_random_graph(100, 0.0575, seed=42)
+ if nx.is_connected(G) and not nx.is_biconnected(G):
+ attempts = 0
+ yield G
+ else:
+ if attempts >= max_attempts:
+ msg = f"Tried {attempts} times: no suitable Graph."
+ raise Exception(msg)
+ else:
+ attempts += 1
+
+
+def test_articulation_points():
+ Ggen = _generate_no_biconnected()
+ for i in range(1): # change 1 to 3 or more for more realizations.
+ G = next(Ggen)
+ articulation_points = [{a} for a in nx.articulation_points(G)]
+ for cut in nx.all_node_cuts(G):
+ assert cut in articulation_points
+
+
+def test_grid_2d_graph():
+ # All minimum node cuts of a 2d grid
+ # are the four pairs of nodes that are
+ # neighbors of the four corner nodes.
+ G = nx.grid_2d_graph(5, 5)
+ solution = [{(0, 1), (1, 0)}, {(3, 0), (4, 1)}, {(3, 4), (4, 3)}, {(0, 3), (1, 4)}]
+ for cut in nx.all_node_cuts(G):
+ assert cut in solution
+
+
+def test_disconnected_graph():
+ G = nx.fast_gnp_random_graph(100, 0.01, seed=42)
+ cuts = nx.all_node_cuts(G)
+ pytest.raises(nx.NetworkXError, next, cuts)
+
+
+@pytest.mark.slow
+def test_alternative_flow_functions():
+ graphs = [nx.grid_2d_graph(4, 4), nx.cycle_graph(5)]
+ for G in graphs:
+ node_conn = nx.node_connectivity(G)
+ for flow_func in flow_funcs:
+ all_cuts = nx.all_node_cuts(G, flow_func=flow_func)
+ # Only test a limited number of cut sets to reduce test time.
+ for cut in itertools.islice(all_cuts, MAX_CUTSETS_TO_TEST):
+ assert node_conn == len(cut)
+ assert not nx.is_connected(nx.restricted_view(G, cut, []))
+
+
+def test_is_separating_set_complete_graph():
+ G = nx.complete_graph(5)
+ assert _is_separating_set(G, {0, 1, 2, 3})
+
+
+def test_is_separating_set():
+ for i in [5, 10, 15]:
+ G = nx.star_graph(i)
+ max_degree_node = max(G, key=G.degree)
+ assert _is_separating_set(G, {max_degree_node})
+
+
+def test_non_repeated_cuts():
+ # The algorithm was repeating the cut {0, 1} for the giant biconnected
+ # component of the Karate club graph.
+ K = nx.karate_club_graph()
+ bcc = max(list(nx.biconnected_components(K)), key=len)
+ G = K.subgraph(bcc)
+ solution = [{32, 33}, {2, 33}, {0, 3}, {0, 1}, {29, 33}]
+ cuts = list(nx.all_node_cuts(G))
+ if len(solution) != len(cuts):
+ print(f"Solution: {solution}")
+ print(f"Result: {cuts}")
+ assert len(solution) == len(cuts)
+ for cut in cuts:
+ assert cut in solution
+
+
+def test_cycle_graph():
+ G = nx.cycle_graph(5)
+ solution = [{0, 2}, {0, 3}, {1, 3}, {1, 4}, {2, 4}]
+ cuts = list(nx.all_node_cuts(G))
+ assert len(solution) == len(cuts)
+ for cut in cuts:
+ assert cut in solution
+
+
+def test_complete_graph():
+ G = nx.complete_graph(5)
+ assert nx.node_connectivity(G) == 4
+ assert list(nx.all_node_cuts(G)) == []
+
+
+def test_all_node_cuts_simple_case():
+ G = nx.complete_graph(5)
+ G.remove_edges_from([(0, 1), (3, 4)])
+ expected = [{0, 1, 2}, {2, 3, 4}]
+ actual = list(nx.all_node_cuts(G))
+ assert len(actual) == len(expected)
+ for cut in actual:
+ assert cut in expected
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_stoer_wagner.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_stoer_wagner.py
new file mode 100644
index 00000000..2b9e2bab
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/tests/test_stoer_wagner.py
@@ -0,0 +1,102 @@
+from itertools import chain
+
+import pytest
+
+import networkx as nx
+
+
+def _check_partition(G, cut_value, partition, weight):
+ assert isinstance(partition, tuple)
+ assert len(partition) == 2
+ assert isinstance(partition[0], list)
+ assert isinstance(partition[1], list)
+ assert len(partition[0]) > 0
+ assert len(partition[1]) > 0
+ assert sum(map(len, partition)) == len(G)
+ assert set(chain.from_iterable(partition)) == set(G)
+ partition = tuple(map(set, partition))
+ w = 0
+ for u, v, e in G.edges(data=True):
+ if (u in partition[0]) == (v in partition[1]):
+ w += e.get(weight, 1)
+ assert w == cut_value
+
+
+def _test_stoer_wagner(G, answer, weight="weight"):
+ cut_value, partition = nx.stoer_wagner(G, weight, heap=nx.utils.PairingHeap)
+ assert cut_value == answer
+ _check_partition(G, cut_value, partition, weight)
+ cut_value, partition = nx.stoer_wagner(G, weight, heap=nx.utils.BinaryHeap)
+ assert cut_value == answer
+ _check_partition(G, cut_value, partition, weight)
+
+
+def test_graph1():
+ G = nx.Graph()
+ G.add_edge("x", "a", weight=3)
+ G.add_edge("x", "b", weight=1)
+ G.add_edge("a", "c", weight=3)
+ G.add_edge("b", "c", weight=5)
+ G.add_edge("b", "d", weight=4)
+ G.add_edge("d", "e", weight=2)
+ G.add_edge("c", "y", weight=2)
+ G.add_edge("e", "y", weight=3)
+ _test_stoer_wagner(G, 4)
+
+
+def test_graph2():
+ G = nx.Graph()
+ G.add_edge("x", "a")
+ G.add_edge("x", "b")
+ G.add_edge("a", "c")
+ G.add_edge("b", "c")
+ G.add_edge("b", "d")
+ G.add_edge("d", "e")
+ G.add_edge("c", "y")
+ G.add_edge("e", "y")
+ _test_stoer_wagner(G, 2)
+
+
+def test_graph3():
+ # Source:
+ # Stoer, M. and Wagner, F. (1997). "A simple min-cut algorithm". Journal of
+ # the ACM 44 (4), 585-591.
+ G = nx.Graph()
+ G.add_edge(1, 2, weight=2)
+ G.add_edge(1, 5, weight=3)
+ G.add_edge(2, 3, weight=3)
+ G.add_edge(2, 5, weight=2)
+ G.add_edge(2, 6, weight=2)
+ G.add_edge(3, 4, weight=4)
+ G.add_edge(3, 7, weight=2)
+ G.add_edge(4, 7, weight=2)
+ G.add_edge(4, 8, weight=2)
+ G.add_edge(5, 6, weight=3)
+ G.add_edge(6, 7, weight=1)
+ G.add_edge(7, 8, weight=3)
+ _test_stoer_wagner(G, 4)
+
+
+def test_weight_name():
+ G = nx.Graph()
+ G.add_edge(1, 2, weight=1, cost=8)
+ G.add_edge(1, 3, cost=2)
+ G.add_edge(2, 3, cost=4)
+ _test_stoer_wagner(G, 6, weight="cost")
+
+
+def test_exceptions():
+ G = nx.Graph()
+ pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
+ G.add_node(1)
+ pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
+ G.add_node(2)
+ pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
+ G.add_edge(1, 2, weight=-2)
+ pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
+ G = nx.DiGraph()
+ pytest.raises(nx.NetworkXNotImplemented, nx.stoer_wagner, G)
+ G = nx.MultiGraph()
+ pytest.raises(nx.NetworkXNotImplemented, nx.stoer_wagner, G)
+ G = nx.MultiDiGraph()
+ pytest.raises(nx.NetworkXNotImplemented, nx.stoer_wagner, G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/utils.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/utils.py
new file mode 100644
index 00000000..7bf99945
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/connectivity/utils.py
@@ -0,0 +1,88 @@
+"""
+Utilities for connectivity package
+"""
+
+import networkx as nx
+
+__all__ = ["build_auxiliary_node_connectivity", "build_auxiliary_edge_connectivity"]
+
+
+@nx._dispatchable(returns_graph=True)
+def build_auxiliary_node_connectivity(G):
+ r"""Creates a directed graph D from an undirected graph G to compute flow
+ based node connectivity.
+
+ For an undirected graph G having `n` nodes and `m` edges we derive a
+ directed graph D with `2n` nodes and `2m+n` arcs by replacing each
+ original node `v` with two nodes `vA`, `vB` linked by an (internal)
+ arc in D. Then for each edge (`u`, `v`) in G we add two arcs (`uB`, `vA`)
+ and (`vB`, `uA`) in D. Finally we set the attribute capacity = 1 for each
+ arc in D [1]_.
+
+ For a directed graph having `n` nodes and `m` arcs we derive a
+ directed graph D with `2n` nodes and `m+n` arcs by replacing each
+ original node `v` with two nodes `vA`, `vB` linked by an (internal)
+ arc (`vA`, `vB`) in D. Then for each arc (`u`, `v`) in G we add one
+ arc (`uB`, `vA`) in D. Finally we set the attribute capacity = 1 for
+ each arc in D.
+
+ A dictionary with a mapping between nodes in the original graph and the
+ auxiliary digraph is stored as a graph attribute: D.graph['mapping'].
+
+ References
+ ----------
+ .. [1] Kammer, Frank and Hanjo Taubig. Graph Connectivity. in Brandes and
+ Erlebach, 'Network Analysis: Methodological Foundations', Lecture
+ Notes in Computer Science, Volume 3418, Springer-Verlag, 2005.
+ https://doi.org/10.1007/978-3-540-31955-9_7
+
+ """
+ directed = G.is_directed()
+
+ mapping = {}
+ H = nx.DiGraph()
+
+ for i, node in enumerate(G):
+ mapping[node] = i
+ H.add_node(f"{i}A", id=node)
+ H.add_node(f"{i}B", id=node)
+ H.add_edge(f"{i}A", f"{i}B", capacity=1)
+
+ edges = []
+ for source, target in G.edges():
+ edges.append((f"{mapping[source]}B", f"{mapping[target]}A"))
+ if not directed:
+ edges.append((f"{mapping[target]}B", f"{mapping[source]}A"))
+ H.add_edges_from(edges, capacity=1)
+
+ # Store mapping as graph attribute
+ H.graph["mapping"] = mapping
+ return H
+
+
+@nx._dispatchable(returns_graph=True)
+def build_auxiliary_edge_connectivity(G):
+ """Auxiliary digraph for computing flow based edge connectivity
+
+ If the input graph is undirected, we replace each edge (`u`,`v`) with
+ two reciprocal arcs (`u`, `v`) and (`v`, `u`) and then we set the attribute
+ 'capacity' for each arc to 1. If the input graph is directed we simply
+ add the 'capacity' attribute. Part of algorithm 1 in [1]_ .
+
+ References
+ ----------
+ .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms. (this is a
+ chapter, look for the reference of the book).
+ http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
+ """
+ if G.is_directed():
+ H = nx.DiGraph()
+ H.add_nodes_from(G.nodes())
+ H.add_edges_from(G.edges(), capacity=1)
+ return H
+ else:
+ H = nx.DiGraph()
+ H.add_nodes_from(G.nodes())
+ for source, target in G.edges():
+ H.add_edges_from([(source, target), (target, source)], capacity=1)
+ return H