about summary refs log tree commit diff
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