about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/networkx/algorithms/approximation
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-4a52a71956a8d46fcb7294ac71734504bb09bcc2.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/approximation')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/__init__.py25
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/clique.py259
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/clustering_coefficient.py71
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/connectivity.py412
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/distance_measures.py150
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/dominating_set.py149
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/kcomponents.py369
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/matching.py44
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/maxcut.py143
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/ramsey.py53
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/steinertree.py231
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_approx_clust_coeff.py41
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_clique.py112
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_connectivity.py199
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_distance_measures.py59
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_dominating_set.py78
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_kcomponents.py303
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_matching.py8
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_maxcut.py94
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_ramsey.py31
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_steinertree.py265
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_traveling_salesman.py977
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_treewidth.py280
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_vertex_cover.py68
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py1501
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/treewidth.py252
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/vertex_cover.py83
28 files changed, 6257 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/__init__.py
new file mode 100644
index 00000000..d12a8f8b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/__init__.py
@@ -0,0 +1,25 @@
+"""Approximations of graph properties and Heuristic methods for optimization.
+
+The functions in this class are not imported into the top-level ``networkx``
+namespace so the easiest way to use them is with::
+
+    >>> from networkx.algorithms import approximation
+
+Another option is to import the specific function with
+``from networkx.algorithms.approximation import function_name``.
+
+"""
+
+from networkx.algorithms.approximation.clustering_coefficient import *
+from networkx.algorithms.approximation.clique import *
+from networkx.algorithms.approximation.connectivity import *
+from networkx.algorithms.approximation.distance_measures import *
+from networkx.algorithms.approximation.dominating_set import *
+from networkx.algorithms.approximation.kcomponents import *
+from networkx.algorithms.approximation.matching import *
+from networkx.algorithms.approximation.ramsey import *
+from networkx.algorithms.approximation.steinertree import *
+from networkx.algorithms.approximation.traveling_salesman import *
+from networkx.algorithms.approximation.treewidth import *
+from networkx.algorithms.approximation.vertex_cover import *
+from networkx.algorithms.approximation.maxcut import *
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/clique.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/clique.py
new file mode 100644
index 00000000..ed0f3506
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/clique.py
@@ -0,0 +1,259 @@
+"""Functions for computing large cliques and maximum independent sets."""
+
+import networkx as nx
+from networkx.algorithms.approximation import ramsey
+from networkx.utils import not_implemented_for
+
+__all__ = [
+    "clique_removal",
+    "max_clique",
+    "large_clique_size",
+    "maximum_independent_set",
+]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def maximum_independent_set(G):
+    """Returns an approximate maximum independent set.
+
+    Independent set or stable set is a set of vertices in a graph, no two of
+    which are adjacent. That is, it is a set I of vertices such that for every
+    two vertices in I, there is no edge connecting the two. Equivalently, each
+    edge in the graph has at most one endpoint in I. The size of an independent
+    set is the number of vertices it contains [1]_.
+
+    A maximum independent set is a largest independent set for a given graph G
+    and its size is denoted $\\alpha(G)$. The problem of finding such a set is called
+    the maximum independent set problem and is an NP-hard optimization problem.
+    As such, it is unlikely that there exists an efficient algorithm for finding
+    a maximum independent set of a graph.
+
+    The Independent Set algorithm is based on [2]_.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        Undirected graph
+
+    Returns
+    -------
+    iset : Set
+        The apx-maximum independent set
+
+    Examples
+    --------
+    >>> G = nx.path_graph(10)
+    >>> nx.approximation.maximum_independent_set(G)
+    {0, 2, 4, 6, 9}
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If the graph is directed or is a multigraph.
+
+    Notes
+    -----
+    Finds the $O(|V|/(log|V|)^2)$ apx of independent set in the worst case.
+
+    References
+    ----------
+    .. [1] `Wikipedia: Independent set
+        <https://en.wikipedia.org/wiki/Independent_set_(graph_theory)>`_
+    .. [2] Boppana, R., & Halldórsson, M. M. (1992).
+       Approximating maximum independent sets by excluding subgraphs.
+       BIT Numerical Mathematics, 32(2), 180–196. Springer.
+    """
+    iset, _ = clique_removal(G)
+    return iset
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def max_clique(G):
+    r"""Find the Maximum Clique
+
+    Finds the $O(|V|/(log|V|)^2)$ apx of maximum clique/independent set
+    in the worst case.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        Undirected graph
+
+    Returns
+    -------
+    clique : set
+        The apx-maximum clique of the graph
+
+    Examples
+    --------
+    >>> G = nx.path_graph(10)
+    >>> nx.approximation.max_clique(G)
+    {8, 9}
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If the graph is directed or is a multigraph.
+
+    Notes
+    -----
+    A clique in an undirected graph G = (V, E) is a subset of the vertex set
+    `C \subseteq V` such that for every two vertices in C there exists an edge
+    connecting the two. This is equivalent to saying that the subgraph
+    induced by C is complete (in some cases, the term clique may also refer
+    to the subgraph).
+
+    A maximum clique is a clique of the largest possible size in a given graph.
+    The clique number `\omega(G)` of a graph G is the number of
+    vertices in a maximum clique in G. The intersection number of
+    G is the smallest number of cliques that together cover all edges of G.
+
+    https://en.wikipedia.org/wiki/Maximum_clique
+
+    References
+    ----------
+    .. [1] Boppana, R., & Halldórsson, M. M. (1992).
+        Approximating maximum independent sets by excluding subgraphs.
+        BIT Numerical Mathematics, 32(2), 180–196. Springer.
+        doi:10.1007/BF01994876
+    """
+    # finding the maximum clique in a graph is equivalent to finding
+    # the independent set in the complementary graph
+    cgraph = nx.complement(G)
+    iset, _ = clique_removal(cgraph)
+    return iset
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def clique_removal(G):
+    r"""Repeatedly remove cliques from the graph.
+
+    Results in a $O(|V|/(\log |V|)^2)$ approximation of maximum clique
+    and independent set. Returns the largest independent set found, along
+    with found maximal cliques.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        Undirected graph
+
+    Returns
+    -------
+    max_ind_cliques : (set, list) tuple
+        2-tuple of Maximal Independent Set and list of maximal cliques (sets).
+
+    Examples
+    --------
+    >>> G = nx.path_graph(10)
+    >>> nx.approximation.clique_removal(G)
+    ({0, 2, 4, 6, 9}, [{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}])
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If the graph is directed or is a multigraph.
+
+    References
+    ----------
+    .. [1] Boppana, R., & Halldórsson, M. M. (1992).
+        Approximating maximum independent sets by excluding subgraphs.
+        BIT Numerical Mathematics, 32(2), 180–196. Springer.
+    """
+    graph = G.copy()
+    c_i, i_i = ramsey.ramsey_R2(graph)
+    cliques = [c_i]
+    isets = [i_i]
+    while graph:
+        graph.remove_nodes_from(c_i)
+        c_i, i_i = ramsey.ramsey_R2(graph)
+        if c_i:
+            cliques.append(c_i)
+        if i_i:
+            isets.append(i_i)
+    # Determine the largest independent set as measured by cardinality.
+    maxiset = max(isets, key=len)
+    return maxiset, cliques
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def large_clique_size(G):
+    """Find the size of a large clique in a graph.
+
+    A *clique* is a subset of nodes in which each pair of nodes is
+    adjacent. This function is a heuristic for finding the size of a
+    large clique in the graph.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    Returns
+    -------
+    k: integer
+       The size of a large clique in the graph.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(10)
+    >>> nx.approximation.large_clique_size(G)
+    2
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If the graph is directed or is a multigraph.
+
+    Notes
+    -----
+    This implementation is from [1]_. Its worst case time complexity is
+    :math:`O(n d^2)`, where *n* is the number of nodes in the graph and
+    *d* is the maximum degree.
+
+    This function is a heuristic, which means it may work well in
+    practice, but there is no rigorous mathematical guarantee on the
+    ratio between the returned number and the actual largest clique size
+    in the graph.
+
+    References
+    ----------
+    .. [1] Pattabiraman, Bharath, et al.
+       "Fast Algorithms for the Maximum Clique Problem on Massive Graphs
+       with Applications to Overlapping Community Detection."
+       *Internet Mathematics* 11.4-5 (2015): 421--448.
+       <https://doi.org/10.1080/15427951.2014.986778>
+
+    See also
+    --------
+
+    :func:`networkx.algorithms.approximation.clique.max_clique`
+        A function that returns an approximate maximum clique with a
+        guarantee on the approximation ratio.
+
+    :mod:`networkx.algorithms.clique`
+        Functions for finding the exact maximum clique in a graph.
+
+    """
+    degrees = G.degree
+
+    def _clique_heuristic(G, U, size, best_size):
+        if not U:
+            return max(best_size, size)
+        u = max(U, key=degrees)
+        U.remove(u)
+        N_prime = {v for v in G[u] if degrees[v] >= best_size}
+        return _clique_heuristic(G, U & N_prime, size + 1, best_size)
+
+    best_size = 0
+    nodes = (u for u in G if degrees[u] >= best_size)
+    for u in nodes:
+        neighbors = {v for v in G[u] if degrees[v] >= best_size}
+        best_size = _clique_heuristic(G, neighbors, 1, best_size)
+    return best_size
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/clustering_coefficient.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/clustering_coefficient.py
new file mode 100644
index 00000000..545fc655
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/clustering_coefficient.py
@@ -0,0 +1,71 @@
+import networkx as nx
+from networkx.utils import not_implemented_for, py_random_state
+
+__all__ = ["average_clustering"]
+
+
+@not_implemented_for("directed")
+@py_random_state(2)
+@nx._dispatchable(name="approximate_average_clustering")
+def average_clustering(G, trials=1000, seed=None):
+    r"""Estimates the average clustering coefficient of G.
+
+    The local clustering of each node in `G` is the fraction of triangles
+    that actually exist over all possible triangles in its neighborhood.
+    The average clustering coefficient of a graph `G` is the mean of
+    local clusterings.
+
+    This function finds an approximate average clustering coefficient
+    for G by repeating `n` times (defined in `trials`) the following
+    experiment: choose a node at random, choose two of its neighbors
+    at random, and check if they are connected. The approximate
+    coefficient is the fraction of triangles found over the number
+    of trials [1]_.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    trials : integer
+        Number of trials to perform (default 1000).
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    Returns
+    -------
+    c : float
+        Approximated average clustering coefficient.
+
+    Examples
+    --------
+    >>> from networkx.algorithms import approximation
+    >>> G = nx.erdos_renyi_graph(10, 0.2, seed=10)
+    >>> approximation.average_clustering(G, trials=1000, seed=10)
+    0.214
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If G is directed.
+
+    References
+    ----------
+    .. [1] Schank, Thomas, and Dorothea Wagner. Approximating clustering
+       coefficient and transitivity. Universität Karlsruhe, Fakultät für
+       Informatik, 2004.
+       https://doi.org/10.5445/IR/1000001239
+
+    """
+    n = len(G)
+    triangles = 0
+    nodes = list(G)
+    for i in [int(seed.random() * n) for i in range(trials)]:
+        nbrs = list(G[nodes[i]])
+        if len(nbrs) < 2:
+            continue
+        u, v = seed.sample(nbrs, 2)
+        if u in G[v]:
+            triangles += 1
+    return triangles / trials
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/connectivity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/connectivity.py
new file mode 100644
index 00000000..0b596fdf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/connectivity.py
@@ -0,0 +1,412 @@
+"""Fast approximation for node connectivity"""
+
+import itertools
+from operator import itemgetter
+
+import networkx as nx
+
+__all__ = [
+    "local_node_connectivity",
+    "node_connectivity",
+    "all_pairs_node_connectivity",
+]
+
+
+@nx._dispatchable(name="approximate_local_node_connectivity")
+def local_node_connectivity(G, source, target, cutoff=None):
+    """Compute node connectivity between source and target.
+
+    Pairwise or local node connectivity between two distinct and nonadjacent
+    nodes is the minimum number of nodes that must be removed (minimum
+    separating cutset) to disconnect them. By Menger's theorem, this is equal
+    to the number of node independent paths (paths that share no nodes other
+    than source and target). Which is what we compute in this function.
+
+    This algorithm is a fast approximation that gives an strict lower
+    bound on the actual number of node independent paths between two nodes [1]_.
+    It works for both directed and undirected graphs.
+
+    Parameters
+    ----------
+
+    G : NetworkX graph
+
+    source : node
+        Starting node for node connectivity
+
+    target : node
+        Ending node for node connectivity
+
+    cutoff : integer
+        Maximum node connectivity to consider. If None, the minimum degree
+        of source or target is used as a cutoff. Default value None.
+
+    Returns
+    -------
+    k: integer
+       pairwise node connectivity
+
+    Examples
+    --------
+    >>> # Platonic octahedral graph has node connectivity 4
+    >>> # for each non adjacent node pair
+    >>> from networkx.algorithms import approximation as approx
+    >>> G = nx.octahedral_graph()
+    >>> approx.local_node_connectivity(G, 0, 5)
+    4
+
+    Notes
+    -----
+    This algorithm [1]_ finds node independents paths between two nodes by
+    computing their shortest path using BFS, marking the nodes of the path
+    found as 'used' and then searching other shortest paths excluding the
+    nodes marked as used until no more paths exist. It is not exact because
+    a shortest path could use nodes that, if the path were longer, may belong
+    to two different node independent paths. Thus it only guarantees an
+    strict lower bound on node connectivity.
+
+    Note that the authors propose a further refinement, losing accuracy and
+    gaining speed, which is not implemented yet.
+
+    See also
+    --------
+    all_pairs_node_connectivity
+    node_connectivity
+
+    References
+    ----------
+    .. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
+        Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
+        http://eclectic.ss.uci.edu/~drwhite/working.pdf
+
+    """
+    if target == source:
+        raise nx.NetworkXError("source and target have to be different nodes.")
+
+    # Maximum possible node independent paths
+    if G.is_directed():
+        possible = min(G.out_degree(source), G.in_degree(target))
+    else:
+        possible = min(G.degree(source), G.degree(target))
+
+    K = 0
+    if not possible:
+        return K
+
+    if cutoff is None:
+        cutoff = float("inf")
+
+    exclude = set()
+    for i in range(min(possible, cutoff)):
+        try:
+            path = _bidirectional_shortest_path(G, source, target, exclude)
+            exclude.update(set(path))
+            K += 1
+        except nx.NetworkXNoPath:
+            break
+
+    return K
+
+
+@nx._dispatchable(name="approximate_node_connectivity")
+def node_connectivity(G, s=None, t=None):
+    r"""Returns an approximation for 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. By Menger's theorem,
+    this is equal to the number of node independent paths (paths that
+    share no nodes other than source and target).
+
+    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.
+
+    This algorithm is based on a fast approximation that gives an strict lower
+    bound on the actual number of node independent paths between two nodes [1]_.
+    It works for both directed and undirected graphs.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        Undirected graph
+
+    s : node
+        Source node. Optional. Default value: None.
+
+    t : node
+        Target node. Optional. Default value: None.
+
+    Returns
+    -------
+    K : integer
+        Node connectivity of G, or local node connectivity if source
+        and target are provided.
+
+    Examples
+    --------
+    >>> # Platonic octahedral graph is 4-node-connected
+    >>> from networkx.algorithms import approximation as approx
+    >>> G = nx.octahedral_graph()
+    >>> approx.node_connectivity(G)
+    4
+
+    Notes
+    -----
+    This algorithm [1]_ finds node independents paths between two nodes by
+    computing their shortest path using BFS, marking the nodes of the path
+    found as 'used' and then searching other shortest paths excluding the
+    nodes marked as used until no more paths exist. It is not exact because
+    a shortest path could use nodes that, if the path were longer, may belong
+    to two different node independent paths. Thus it only guarantees an
+    strict lower bound on node connectivity.
+
+    See also
+    --------
+    all_pairs_node_connectivity
+    local_node_connectivity
+
+    References
+    ----------
+    .. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
+        Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
+        http://eclectic.ss.uci.edu/~drwhite/working.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)
+
+    # Global node connectivity
+    if G.is_directed():
+        connected_func = nx.is_weakly_connected
+        iter_func = itertools.permutations
+
+        def neighbors(v):
+            return itertools.chain(G.predecessors(v), G.successors(v))
+
+    else:
+        connected_func = nx.is_connected
+        iter_func = itertools.combinations
+        neighbors = G.neighbors
+
+    if not connected_func(G):
+        return 0
+
+    # Choose a node with minimum degree
+    v, minimum_degree = min(G.degree(), key=itemgetter(1))
+    # Node connectivity is bounded by minimum degree
+    K = minimum_degree
+    # compute local node connectivity with all non-neighbors nodes
+    # and store the minimum
+    for w in set(G) - set(neighbors(v)) - {v}:
+        K = min(K, local_node_connectivity(G, v, w, cutoff=K))
+    # Same for non adjacent pairs of neighbors of v
+    for x, y in iter_func(neighbors(v), 2):
+        if y not in G[x] and x != y:
+            K = min(K, local_node_connectivity(G, x, y, cutoff=K))
+    return K
+
+
+@nx._dispatchable(name="approximate_all_pairs_node_connectivity")
+def all_pairs_node_connectivity(G, nbunch=None, cutoff=None):
+    """Compute node connectivity between all pairs of nodes.
+
+    Pairwise or local node connectivity between two distinct and nonadjacent
+    nodes is the minimum number of nodes that must be removed (minimum
+    separating cutset) to disconnect them. By Menger's theorem, this is equal
+    to the number of node independent paths (paths that share no nodes other
+    than source and target). Which is what we compute in this function.
+
+    This algorithm is a fast approximation that gives an strict lower
+    bound on the actual number of node independent paths between two nodes [1]_.
+    It works for both directed and undirected graphs.
+
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    nbunch: container
+        Container of nodes. If provided node connectivity will be computed
+        only over pairs of nodes in nbunch.
+
+    cutoff : integer
+        Maximum node connectivity to consider. If None, the minimum degree
+        of source or target is used as a cutoff in each pair of nodes.
+        Default value None.
+
+    Returns
+    -------
+    K : dictionary
+        Dictionary, keyed by source and target, of pairwise node connectivity
+
+    Examples
+    --------
+    A 3 node cycle with one extra node attached has connectivity 2 between all
+    nodes in the cycle and connectivity 1 between the extra node and the rest:
+
+    >>> G = nx.cycle_graph(3)
+    >>> G.add_edge(2, 3)
+    >>> import pprint  # for nice dictionary formatting
+    >>> pprint.pprint(nx.all_pairs_node_connectivity(G))
+    {0: {1: 2, 2: 2, 3: 1},
+     1: {0: 2, 2: 2, 3: 1},
+     2: {0: 2, 1: 2, 3: 1},
+     3: {0: 1, 1: 1, 2: 1}}
+
+    See Also
+    --------
+    local_node_connectivity
+    node_connectivity
+
+    References
+    ----------
+    .. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
+        Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
+        http://eclectic.ss.uci.edu/~drwhite/working.pdf
+    """
+    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}
+
+    for u, v in iter_func(nbunch, 2):
+        k = local_node_connectivity(G, u, v, cutoff=cutoff)
+        all_pairs[u][v] = k
+        if not directed:
+            all_pairs[v][u] = k
+
+    return all_pairs
+
+
+def _bidirectional_shortest_path(G, source, target, exclude):
+    """Returns shortest path between source and target ignoring nodes in the
+    container 'exclude'.
+
+    Parameters
+    ----------
+
+    G : NetworkX graph
+
+    source : node
+        Starting node for path
+
+    target : node
+        Ending node for path
+
+    exclude: container
+        Container for nodes to exclude from the search for shortest paths
+
+    Returns
+    -------
+    path: list
+        Shortest path between source and target ignoring nodes in 'exclude'
+
+    Raises
+    ------
+    NetworkXNoPath
+        If there is no path or if nodes are adjacent and have only one path
+        between them
+
+    Notes
+    -----
+    This function and its helper are originally from
+    networkx.algorithms.shortest_paths.unweighted and are modified to
+    accept the extra parameter 'exclude', which is a container for nodes
+    already used in other paths that should be ignored.
+
+    References
+    ----------
+    .. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
+        Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
+        http://eclectic.ss.uci.edu/~drwhite/working.pdf
+
+    """
+    # call helper to do the real work
+    results = _bidirectional_pred_succ(G, source, target, exclude)
+    pred, succ, w = results
+
+    # build path from pred+w+succ
+    path = []
+    # from source to w
+    while w is not None:
+        path.append(w)
+        w = pred[w]
+    path.reverse()
+    # from w to target
+    w = succ[path[-1]]
+    while w is not None:
+        path.append(w)
+        w = succ[w]
+
+    return path
+
+
+def _bidirectional_pred_succ(G, source, target, exclude):
+    # does BFS from both source and target and meets in the middle
+    # excludes nodes in the container "exclude" from the search
+
+    # handle either directed or undirected
+    if G.is_directed():
+        Gpred = G.predecessors
+        Gsucc = G.successors
+    else:
+        Gpred = G.neighbors
+        Gsucc = G.neighbors
+
+    # predecessor and successors in search
+    pred = {source: None}
+    succ = {target: None}
+
+    # initialize fringes, start with forward
+    forward_fringe = [source]
+    reverse_fringe = [target]
+
+    level = 0
+
+    while forward_fringe and reverse_fringe:
+        # Make sure that we iterate one step forward and one step backwards
+        # thus source and target will only trigger "found path" when they are
+        # adjacent and then they can be safely included in the container 'exclude'
+        level += 1
+        if level % 2 != 0:
+            this_level = forward_fringe
+            forward_fringe = []
+            for v in this_level:
+                for w in Gsucc(v):
+                    if w in exclude:
+                        continue
+                    if w not in pred:
+                        forward_fringe.append(w)
+                        pred[w] = v
+                    if w in succ:
+                        return pred, succ, w  # found path
+        else:
+            this_level = reverse_fringe
+            reverse_fringe = []
+            for v in this_level:
+                for w in Gpred(v):
+                    if w in exclude:
+                        continue
+                    if w not in succ:
+                        succ[w] = v
+                        reverse_fringe.append(w)
+                    if w in pred:
+                        return pred, succ, w  # found path
+
+    raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/distance_measures.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/distance_measures.py
new file mode 100644
index 00000000..d5847e65
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/distance_measures.py
@@ -0,0 +1,150 @@
+"""Distance measures approximated metrics."""
+
+import networkx as nx
+from networkx.utils.decorators import py_random_state
+
+__all__ = ["diameter"]
+
+
+@py_random_state(1)
+@nx._dispatchable(name="approximate_diameter")
+def diameter(G, seed=None):
+    """Returns a lower bound on the diameter of the graph G.
+
+    The function computes a lower bound on the diameter (i.e., the maximum eccentricity)
+    of a directed or undirected graph G. The procedure used varies depending on the graph
+    being directed or not.
+
+    If G is an `undirected` graph, then the function uses the `2-sweep` algorithm [1]_.
+    The main idea is to pick the farthest node from a random node and return its eccentricity.
+
+    Otherwise, if G is a `directed` graph, the function uses the `2-dSweep` algorithm [2]_,
+    The procedure starts by selecting a random source node $s$ from which it performs a
+    forward and a backward BFS. Let $a_1$ and $a_2$ be the farthest nodes in the forward and
+    backward cases, respectively. Then, it computes the backward eccentricity of $a_1$ using
+    a backward BFS and the forward eccentricity of $a_2$ using a forward BFS.
+    Finally, it returns the best lower bound between the two.
+
+    In both cases, the time complexity is linear with respect to the size of G.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    Returns
+    -------
+    d : integer
+       Lower Bound on the Diameter of G
+
+    Examples
+    --------
+    >>> G = nx.path_graph(10)  # undirected graph
+    >>> nx.diameter(G)
+    9
+    >>> G = nx.cycle_graph(3, create_using=nx.DiGraph)  # directed graph
+    >>> nx.diameter(G)
+    2
+
+    Raises
+    ------
+    NetworkXError
+        If the graph is empty or
+        If the graph is undirected and not connected or
+        If the graph is directed and not strongly connected.
+
+    See Also
+    --------
+    networkx.algorithms.distance_measures.diameter
+
+    References
+    ----------
+    .. [1] Magnien, Clémence, Matthieu Latapy, and Michel Habib.
+       *Fast computation of empirically tight bounds for the diameter of massive graphs.*
+       Journal of Experimental Algorithmics (JEA), 2009.
+       https://arxiv.org/pdf/0904.2728.pdf
+    .. [2] Crescenzi, Pierluigi, Roberto Grossi, Leonardo Lanzi, and Andrea Marino.
+       *On computing the diameter of real-world directed (weighted) graphs.*
+       International Symposium on Experimental Algorithms. Springer, Berlin, Heidelberg, 2012.
+       https://courses.cs.ut.ee/MTAT.03.238/2014_fall/uploads/Main/diameter.pdf
+    """
+    # if G is empty
+    if not G:
+        raise nx.NetworkXError("Expected non-empty NetworkX graph!")
+    # if there's only a node
+    if G.number_of_nodes() == 1:
+        return 0
+    # if G is directed
+    if G.is_directed():
+        return _two_sweep_directed(G, seed)
+    # else if G is undirected
+    return _two_sweep_undirected(G, seed)
+
+
+def _two_sweep_undirected(G, seed):
+    """Helper function for finding a lower bound on the diameter
+        for undirected Graphs.
+
+        The idea is to pick the farthest node from a random node
+        and return its eccentricity.
+
+        ``G`` is a NetworkX undirected graph.
+
+    .. note::
+
+        ``seed`` is a random.Random or numpy.random.RandomState instance
+    """
+    # select a random source node
+    source = seed.choice(list(G))
+    # get the distances to the other nodes
+    distances = nx.shortest_path_length(G, source)
+    # if some nodes have not been visited, then the graph is not connected
+    if len(distances) != len(G):
+        raise nx.NetworkXError("Graph not connected.")
+    # take a node that is (one of) the farthest nodes from the source
+    *_, node = distances
+    # return the eccentricity of the node
+    return nx.eccentricity(G, node)
+
+
+def _two_sweep_directed(G, seed):
+    """Helper function for finding a lower bound on the diameter
+        for directed Graphs.
+
+        It implements 2-dSweep, the directed version of the 2-sweep algorithm.
+        The algorithm follows the following steps.
+        1. Select a source node $s$ at random.
+        2. Perform a forward BFS from $s$ to select a node $a_1$ at the maximum
+        distance from the source, and compute $LB_1$, the backward eccentricity of $a_1$.
+        3. Perform a backward BFS from $s$ to select a node $a_2$ at the maximum
+        distance from the source, and compute $LB_2$, the forward eccentricity of $a_2$.
+        4. Return the maximum between $LB_1$ and $LB_2$.
+
+        ``G`` is a NetworkX directed graph.
+
+    .. note::
+
+        ``seed`` is a random.Random or numpy.random.RandomState instance
+    """
+    # get a new digraph G' with the edges reversed in the opposite direction
+    G_reversed = G.reverse()
+    # select a random source node
+    source = seed.choice(list(G))
+    # compute forward distances from source
+    forward_distances = nx.shortest_path_length(G, source)
+    # compute backward distances  from source
+    backward_distances = nx.shortest_path_length(G_reversed, source)
+    # if either the source can't reach every node or not every node
+    # can reach the source, then the graph is not strongly connected
+    n = len(G)
+    if len(forward_distances) != n or len(backward_distances) != n:
+        raise nx.NetworkXError("DiGraph not strongly connected.")
+    # take a node a_1 at the maximum distance from the source in G
+    *_, a_1 = forward_distances
+    # take a node a_2 at the maximum distance from the source in G_reversed
+    *_, a_2 = backward_distances
+    # return the max between the backward eccentricity of a_1 and the forward eccentricity of a_2
+    return max(nx.eccentricity(G_reversed, a_1), nx.eccentricity(G, a_2))
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/dominating_set.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/dominating_set.py
new file mode 100644
index 00000000..e568a827
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/dominating_set.py
@@ -0,0 +1,149 @@
+"""Functions for finding node and edge dominating sets.
+
+A `dominating set`_ for an undirected graph *G* with vertex set *V*
+and edge set *E* is a subset *D* of *V* such that every vertex not in
+*D* is adjacent to at least one member of *D*. An `edge dominating set`_
+is a subset *F* of *E* such that every edge not in *F* is
+incident to an endpoint of at least one edge in *F*.
+
+.. _dominating set: https://en.wikipedia.org/wiki/Dominating_set
+.. _edge dominating set: https://en.wikipedia.org/wiki/Edge_dominating_set
+
+"""
+
+import networkx as nx
+
+from ...utils import not_implemented_for
+from ..matching import maximal_matching
+
+__all__ = ["min_weighted_dominating_set", "min_edge_dominating_set"]
+
+
+# TODO Why doesn't this algorithm work for directed graphs?
+@not_implemented_for("directed")
+@nx._dispatchable(node_attrs="weight")
+def min_weighted_dominating_set(G, weight=None):
+    r"""Returns a dominating set that approximates the minimum weight node
+    dominating set.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        Undirected graph.
+
+    weight : string
+        The node attribute storing the weight of an node. If provided,
+        the node attribute with this key must be a number for each
+        node. If not provided, each node is assumed to have weight one.
+
+    Returns
+    -------
+    min_weight_dominating_set : set
+        A set of nodes, the sum of whose weights is no more than `(\log
+        w(V)) w(V^*)`, where `w(V)` denotes the sum of the weights of
+        each node in the graph and `w(V^*)` denotes the sum of the
+        weights of each node in the minimum weight dominating set.
+
+    Examples
+    --------
+    >>> G = nx.Graph([(0, 1), (0, 4), (1, 4), (1, 2), (2, 3), (3, 4), (2, 5)])
+    >>> nx.approximation.min_weighted_dominating_set(G)
+    {1, 2, 4}
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If G is directed.
+
+    Notes
+    -----
+    This algorithm computes an approximate minimum weighted dominating
+    set for the graph `G`. The returned solution has weight `(\log
+    w(V)) w(V^*)`, where `w(V)` denotes the sum of the weights of each
+    node in the graph and `w(V^*)` denotes the sum of the weights of
+    each node in the minimum weight dominating set for the graph.
+
+    This implementation of the algorithm runs in $O(m)$ time, where $m$
+    is the number of edges in the graph.
+
+    References
+    ----------
+    .. [1] Vazirani, Vijay V.
+           *Approximation Algorithms*.
+           Springer Science & Business Media, 2001.
+
+    """
+    # The unique dominating set for the null graph is the empty set.
+    if len(G) == 0:
+        return set()
+
+    # This is the dominating set that will eventually be returned.
+    dom_set = set()
+
+    def _cost(node_and_neighborhood):
+        """Returns the cost-effectiveness of greedily choosing the given
+        node.
+
+        `node_and_neighborhood` is a two-tuple comprising a node and its
+        closed neighborhood.
+
+        """
+        v, neighborhood = node_and_neighborhood
+        return G.nodes[v].get(weight, 1) / len(neighborhood - dom_set)
+
+    # This is a set of all vertices not already covered by the
+    # dominating set.
+    vertices = set(G)
+    # This is a dictionary mapping each node to the closed neighborhood
+    # of that node.
+    neighborhoods = {v: {v} | set(G[v]) for v in G}
+
+    # Continue until all vertices are adjacent to some node in the
+    # dominating set.
+    while vertices:
+        # Find the most cost-effective node to add, along with its
+        # closed neighborhood.
+        dom_node, min_set = min(neighborhoods.items(), key=_cost)
+        # Add the node to the dominating set and reduce the remaining
+        # set of nodes to cover.
+        dom_set.add(dom_node)
+        del neighborhoods[dom_node]
+        vertices -= min_set
+
+    return dom_set
+
+
+@nx._dispatchable
+def min_edge_dominating_set(G):
+    r"""Returns minimum cardinality edge dominating set.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+      Undirected graph
+
+    Returns
+    -------
+    min_edge_dominating_set : set
+      Returns a set of dominating edges whose size is no more than 2 * OPT.
+
+    Examples
+    --------
+    >>> G = nx.petersen_graph()
+    >>> nx.approximation.min_edge_dominating_set(G)
+    {(0, 1), (4, 9), (6, 8), (5, 7), (2, 3)}
+
+    Raises
+    ------
+    ValueError
+        If the input graph `G` is empty.
+
+    Notes
+    -----
+    The algorithm computes an approximate solution to the edge dominating set
+    problem. The result is no more than 2 * OPT in terms of size of the set.
+    Runtime of the algorithm is $O(|E|)$.
+    """
+    if not G:
+        raise ValueError("Expected non-empty NetworkX graph!")
+    return maximal_matching(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/kcomponents.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/kcomponents.py
new file mode 100644
index 00000000..f726a4e6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/kcomponents.py
@@ -0,0 +1,369 @@
+"""Fast approximation for k-component structure"""
+
+import itertools
+from collections import defaultdict
+from collections.abc import Mapping
+from functools import cached_property
+
+import networkx as nx
+from networkx.algorithms.approximation import local_node_connectivity
+from networkx.exception import NetworkXError
+from networkx.utils import not_implemented_for
+
+__all__ = ["k_components"]
+
+
+@not_implemented_for("directed")
+@nx._dispatchable(name="approximate_k_components")
+def k_components(G, min_density=0.95):
+    r"""Returns the approximate 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.
+
+    This implementation is based on the fast heuristics to approximate
+    the `k`-component structure of a graph [1]_. Which, in turn, it is based on
+    a fast approximation algorithm for finding good lower bounds of the number
+    of node independent paths between two nodes [2]_.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        Undirected graph
+
+    min_density : Float
+        Density relaxation threshold. Default value 0.95
+
+    Returns
+    -------
+    k_components : dict
+        Dictionary with connectivity level `k` as key and a list of
+        sets of nodes that form a k-component of level `k` as values.
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If G 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
+    >>> from networkx.algorithms import approximation as apxa
+    >>> G = nx.petersen_graph()
+    >>> k_components = apxa.k_components(G)
+
+    Notes
+    -----
+    The logic of the approximation algorithm for computing the `k`-component
+    structure [1]_ is based on repeatedly applying simple and fast algorithms
+    for `k`-cores and biconnected components in order to narrow down the
+    number of pairs of nodes over which we have to compute White and Newman's
+    approximation algorithm for finding node independent paths [2]_. More
+    formally, this algorithm is based on Whitney's theorem, which states
+    an inclusion relation among node connectivity, edge connectivity, and
+    minimum degree for any graph G. This theorem implies that every
+    `k`-component is nested inside a `k`-edge-component, which in turn,
+    is contained in a `k`-core. Thus, this algorithm computes node independent
+    paths among pairs of nodes in each biconnected part of each `k`-core,
+    and repeats this procedure for each `k` from 3 to the maximal core number
+    of a node in the input graph.
+
+    Because, in practice, many nodes of the core of level `k` inside a
+    bicomponent actually are part of a component of level k, the auxiliary
+    graph needed for the algorithm is likely to be very dense. Thus, we use
+    a complement graph data structure (see `AntiGraph`) to save memory.
+    AntiGraph only stores information of the edges that are *not* present
+    in the actual auxiliary graph. When applying algorithms to this
+    complement graph data structure, it behaves as if it were the dense
+    version.
+
+    See also
+    --------
+    k_components
+
+    References
+    ----------
+    .. [1]  Torrents, J. and F. Ferraro (2015) Structural Cohesion:
+            Visualization and Heuristics for Fast Computation.
+            https://arxiv.org/pdf/1503.04476v1
+
+    .. [2]  White, Douglas R., and Mark Newman (2001) A Fast Algorithm for
+            Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
+            https://www.santafe.edu/research/results/working-papers/fast-approximation-algorithms-for-finding-node-ind
+
+    .. [3]  Moody, J. and D. White (2003). Social cohesion and embeddedness:
+            A hierarchical conception of social groups.
+            American Sociological Review 68(1), 103--28.
+            https://doi.org/10.2307/3088904
+
+    """
+    # Dictionary with connectivity level (k) as keys and a list of
+    # sets of nodes that form a k-component as values
+    k_components = defaultdict(list)
+    # make a few functions local for speed
+    node_connectivity = local_node_connectivity
+    k_core = nx.k_core
+    core_number = nx.core_number
+    biconnected_components = nx.biconnected_components
+    combinations = itertools.combinations
+    # Exact solution for k = {1,2}
+    # There is a linear time algorithm for triconnectivity, if we had an
+    # implementation available we could start from k = 4.
+    for component in nx.connected_components(G):
+        # isolated nodes have connectivity 0
+        comp = set(component)
+        if len(comp) > 1:
+            k_components[1].append(comp)
+    for bicomponent in nx.biconnected_components(G):
+        # avoid considering dyads as bicomponents
+        bicomp = set(bicomponent)
+        if len(bicomp) > 2:
+            k_components[2].append(bicomp)
+    # There is no k-component of k > maximum core number
+    # \kappa(G) <= \lambda(G) <= \delta(G)
+    g_cnumber = core_number(G)
+    max_core = max(g_cnumber.values())
+    for k in range(3, max_core + 1):
+        C = k_core(G, k, core_number=g_cnumber)
+        for nodes in biconnected_components(C):
+            # Build a subgraph SG induced by the nodes that are part of
+            # each biconnected component of the k-core subgraph C.
+            if len(nodes) < k:
+                continue
+            SG = G.subgraph(nodes)
+            # Build auxiliary graph
+            H = _AntiGraph()
+            H.add_nodes_from(SG.nodes())
+            for u, v in combinations(SG, 2):
+                K = node_connectivity(SG, u, v, cutoff=k)
+                if k > K:
+                    H.add_edge(u, v)
+            for h_nodes in biconnected_components(H):
+                if len(h_nodes) <= k:
+                    continue
+                SH = H.subgraph(h_nodes)
+                for Gc in _cliques_heuristic(SG, SH, k, min_density):
+                    for k_nodes in biconnected_components(Gc):
+                        Gk = nx.k_core(SG.subgraph(k_nodes), k)
+                        if len(Gk) <= k:
+                            continue
+                        k_components[k].append(set(Gk))
+    return k_components
+
+
+def _cliques_heuristic(G, H, k, min_density):
+    h_cnumber = nx.core_number(H)
+    for i, c_value in enumerate(sorted(set(h_cnumber.values()), reverse=True)):
+        cands = {n for n, c in h_cnumber.items() if c == c_value}
+        # Skip checking for overlap for the highest core value
+        if i == 0:
+            overlap = False
+        else:
+            overlap = set.intersection(
+                *[{x for x in H[n] if x not in cands} for n in cands]
+            )
+        if overlap and len(overlap) < k:
+            SH = H.subgraph(cands | overlap)
+        else:
+            SH = H.subgraph(cands)
+        sh_cnumber = nx.core_number(SH)
+        SG = nx.k_core(G.subgraph(SH), k)
+        while not (_same(sh_cnumber) and nx.density(SH) >= min_density):
+            # This subgraph must be writable => .copy()
+            SH = H.subgraph(SG).copy()
+            if len(SH) <= k:
+                break
+            sh_cnumber = nx.core_number(SH)
+            sh_deg = dict(SH.degree())
+            min_deg = min(sh_deg.values())
+            SH.remove_nodes_from(n for n, d in sh_deg.items() if d == min_deg)
+            SG = nx.k_core(G.subgraph(SH), k)
+        else:
+            yield SG
+
+
+def _same(measure, tol=0):
+    vals = set(measure.values())
+    if (max(vals) - min(vals)) <= tol:
+        return True
+    return False
+
+
+class _AntiGraph(nx.Graph):
+    """
+    Class for complement graphs.
+
+    The main goal is to be able to work with big and dense graphs with
+    a low memory footprint.
+
+    In this class you add the edges that *do not exist* in the dense graph,
+    the report methods of the class return the neighbors, the edges and
+    the degree as if it was the dense graph. Thus it's possible to use
+    an instance of this class with some of NetworkX functions. In this
+    case we only use k-core, connected_components, and biconnected_components.
+    """
+
+    all_edge_dict = {"weight": 1}
+
+    def single_edge_dict(self):
+        return self.all_edge_dict
+
+    edge_attr_dict_factory = single_edge_dict  # type: ignore[assignment]
+
+    def __getitem__(self, n):
+        """Returns a dict of neighbors of node n in the dense graph.
+
+        Parameters
+        ----------
+        n : node
+           A node in the graph.
+
+        Returns
+        -------
+        adj_dict : dictionary
+           The adjacency dictionary for nodes connected to n.
+
+        """
+        all_edge_dict = self.all_edge_dict
+        return {
+            node: all_edge_dict for node in set(self._adj) - set(self._adj[n]) - {n}
+        }
+
+    def neighbors(self, n):
+        """Returns an iterator over all neighbors of node n in the
+        dense graph.
+        """
+        try:
+            return iter(set(self._adj) - set(self._adj[n]) - {n})
+        except KeyError as err:
+            raise NetworkXError(f"The node {n} is not in the graph.") from err
+
+    class AntiAtlasView(Mapping):
+        """An adjacency inner dict for AntiGraph"""
+
+        def __init__(self, graph, node):
+            self._graph = graph
+            self._atlas = graph._adj[node]
+            self._node = node
+
+        def __len__(self):
+            return len(self._graph) - len(self._atlas) - 1
+
+        def __iter__(self):
+            return (n for n in self._graph if n not in self._atlas and n != self._node)
+
+        def __getitem__(self, nbr):
+            nbrs = set(self._graph._adj) - set(self._atlas) - {self._node}
+            if nbr in nbrs:
+                return self._graph.all_edge_dict
+            raise KeyError(nbr)
+
+    class AntiAdjacencyView(AntiAtlasView):
+        """An adjacency outer dict for AntiGraph"""
+
+        def __init__(self, graph):
+            self._graph = graph
+            self._atlas = graph._adj
+
+        def __len__(self):
+            return len(self._atlas)
+
+        def __iter__(self):
+            return iter(self._graph)
+
+        def __getitem__(self, node):
+            if node not in self._graph:
+                raise KeyError(node)
+            return self._graph.AntiAtlasView(self._graph, node)
+
+    @cached_property
+    def adj(self):
+        return self.AntiAdjacencyView(self)
+
+    def subgraph(self, nodes):
+        """This subgraph method returns a full AntiGraph. Not a View"""
+        nodes = set(nodes)
+        G = _AntiGraph()
+        G.add_nodes_from(nodes)
+        for n in G:
+            Gnbrs = G.adjlist_inner_dict_factory()
+            G._adj[n] = Gnbrs
+            for nbr, d in self._adj[n].items():
+                if nbr in G._adj:
+                    Gnbrs[nbr] = d
+                    G._adj[nbr][n] = d
+        G.graph = self.graph
+        return G
+
+    class AntiDegreeView(nx.reportviews.DegreeView):
+        def __iter__(self):
+            all_nodes = set(self._succ)
+            for n in self._nodes:
+                nbrs = all_nodes - set(self._succ[n]) - {n}
+                yield (n, len(nbrs))
+
+        def __getitem__(self, n):
+            nbrs = set(self._succ) - set(self._succ[n]) - {n}
+            # AntiGraph is a ThinGraph so all edges have weight 1
+            return len(nbrs) + (n in nbrs)
+
+    @cached_property
+    def degree(self):
+        """Returns an iterator for (node, degree) and degree for single node.
+
+        The node degree is the number of edges adjacent to the node.
+
+        Parameters
+        ----------
+        nbunch : iterable container, optional (default=all nodes)
+            A container of nodes.  The container will be iterated
+            through once.
+
+        weight : string or None, optional (default=None)
+           The edge attribute that holds the numerical value used
+           as a weight.  If None, then each edge has weight 1.
+           The degree is the sum of the edge weights adjacent to the node.
+
+        Returns
+        -------
+        deg:
+            Degree of the node, if a single node is passed as argument.
+        nd_iter : an iterator
+            The iterator returns two-tuples of (node, degree).
+
+        See Also
+        --------
+        degree
+
+        Examples
+        --------
+        >>> G = nx.path_graph(4)
+        >>> G.degree(0)  # node 0 with degree 1
+        1
+        >>> list(G.degree([0, 1]))
+        [(0, 1), (1, 2)]
+
+        """
+        return self.AntiDegreeView(self)
+
+    def adjacency(self):
+        """Returns an iterator of (node, adjacency set) tuples for all nodes
+           in the dense graph.
+
+        This is the fastest way to look at every edge.
+        For directed graphs, only outgoing adjacencies are included.
+
+        Returns
+        -------
+        adj_iter : iterator
+           An iterator of (node, adjacency set) for all nodes in
+           the graph.
+
+        """
+        for n in self._adj:
+            yield (n, set(self._adj) - set(self._adj[n]) - {n})
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/matching.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/matching.py
new file mode 100644
index 00000000..dc089194
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/matching.py
@@ -0,0 +1,44 @@
+"""
+**************
+Graph Matching
+**************
+
+Given a graph G = (V,E), a matching M in G is a set of pairwise non-adjacent
+edges; that is, no two edges share a common vertex.
+
+`Wikipedia: Matching <https://en.wikipedia.org/wiki/Matching_(graph_theory)>`_
+"""
+
+import networkx as nx
+
+__all__ = ["min_maximal_matching"]
+
+
+@nx._dispatchable
+def min_maximal_matching(G):
+    r"""Returns the minimum maximal matching of G. That is, out of all maximal
+    matchings of the graph G, the smallest is returned.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+      Undirected graph
+
+    Returns
+    -------
+    min_maximal_matching : set
+      Returns a set of edges such that no two edges share a common endpoint
+      and every edge not in the set shares some common endpoint in the set.
+      Cardinality will be 2*OPT in the worst case.
+
+    Notes
+    -----
+    The algorithm computes an approximate solution for the minimum maximal
+    cardinality matching problem. The solution is no more than 2 * OPT in size.
+    Runtime is $O(|E|)$.
+
+    References
+    ----------
+    .. [1] Vazirani, Vijay Approximation Algorithms (2001)
+    """
+    return nx.maximal_matching(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/maxcut.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/maxcut.py
new file mode 100644
index 00000000..f4e1da87
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/maxcut.py
@@ -0,0 +1,143 @@
+import networkx as nx
+from networkx.utils.decorators import not_implemented_for, py_random_state
+
+__all__ = ["randomized_partitioning", "one_exchange"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@py_random_state(1)
+@nx._dispatchable(edge_attrs="weight")
+def randomized_partitioning(G, seed=None, p=0.5, weight=None):
+    """Compute a random partitioning of the graph nodes and its cut value.
+
+    A partitioning is calculated by observing each node
+    and deciding to add it to the partition with probability `p`,
+    returning a random cut and its corresponding value (the
+    sum of weights of edges connecting different partitions).
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    p : scalar
+        Probability for each node to be part of the first partition.
+        Should be in [0,1]
+
+    weight : object
+        Edge attribute key to use as weight. If not specified, edges
+        have weight one.
+
+    Returns
+    -------
+    cut_size : scalar
+        Value of the minimum cut.
+
+    partition : pair of node sets
+        A partitioning of the nodes that defines a minimum cut.
+
+    Examples
+    --------
+    >>> G = nx.complete_graph(5)
+    >>> cut_size, partition = nx.approximation.randomized_partitioning(G, seed=1)
+    >>> cut_size
+    6
+    >>> partition
+    ({0, 3, 4}, {1, 2})
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If the graph is directed or is a multigraph.
+    """
+    cut = {node for node in G.nodes() if seed.random() < p}
+    cut_size = nx.algorithms.cut_size(G, cut, weight=weight)
+    partition = (cut, G.nodes - cut)
+    return cut_size, partition
+
+
+def _swap_node_partition(cut, node):
+    return cut - {node} if node in cut else cut.union({node})
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@py_random_state(2)
+@nx._dispatchable(edge_attrs="weight")
+def one_exchange(G, initial_cut=None, seed=None, weight=None):
+    """Compute a partitioning of the graphs nodes and the corresponding cut value.
+
+    Use a greedy one exchange strategy to find a locally maximal cut
+    and its value, it works by finding the best node (one that gives
+    the highest gain to the cut value) to add to the current cut
+    and repeats this process until no improvement can be made.
+
+    Parameters
+    ----------
+    G : networkx Graph
+        Graph to find a maximum cut for.
+
+    initial_cut : set
+        Cut to use as a starting point. If not supplied the algorithm
+        starts with an empty cut.
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    weight : object
+        Edge attribute key to use as weight. If not specified, edges
+        have weight one.
+
+    Returns
+    -------
+    cut_value : scalar
+        Value of the maximum cut.
+
+    partition : pair of node sets
+        A partitioning of the nodes that defines a maximum cut.
+
+    Examples
+    --------
+    >>> G = nx.complete_graph(5)
+    >>> curr_cut_size, partition = nx.approximation.one_exchange(G, seed=1)
+    >>> curr_cut_size
+    6
+    >>> partition
+    ({0, 2}, {1, 3, 4})
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If the graph is directed or is a multigraph.
+    """
+    if initial_cut is None:
+        initial_cut = set()
+    cut = set(initial_cut)
+    current_cut_size = nx.algorithms.cut_size(G, cut, weight=weight)
+    while True:
+        nodes = list(G.nodes())
+        # Shuffling the nodes ensures random tie-breaks in the following call to max
+        seed.shuffle(nodes)
+        best_node_to_swap = max(
+            nodes,
+            key=lambda v: nx.algorithms.cut_size(
+                G, _swap_node_partition(cut, v), weight=weight
+            ),
+            default=None,
+        )
+        potential_cut = _swap_node_partition(cut, best_node_to_swap)
+        potential_cut_size = nx.algorithms.cut_size(G, potential_cut, weight=weight)
+
+        if potential_cut_size > current_cut_size:
+            cut = potential_cut
+            current_cut_size = potential_cut_size
+        else:
+            break
+
+    partition = (cut, G.nodes - cut)
+    return current_cut_size, partition
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/ramsey.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/ramsey.py
new file mode 100644
index 00000000..0552e4a9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/ramsey.py
@@ -0,0 +1,53 @@
+"""
+Ramsey numbers.
+"""
+
+import networkx as nx
+from networkx.utils import not_implemented_for
+
+from ...utils import arbitrary_element
+
+__all__ = ["ramsey_R2"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def ramsey_R2(G):
+    r"""Compute the largest clique and largest independent set in `G`.
+
+    This can be used to estimate bounds for the 2-color
+    Ramsey number `R(2;s,t)` for `G`.
+
+    This is a recursive implementation which could run into trouble
+    for large recursions. Note that self-loop edges are ignored.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        Undirected graph
+
+    Returns
+    -------
+    max_pair : (set, set) tuple
+        Maximum clique, Maximum independent set.
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If the graph is directed or is a multigraph.
+    """
+    if not G:
+        return set(), set()
+
+    node = arbitrary_element(G)
+    nbrs = (nbr for nbr in nx.all_neighbors(G, node) if nbr != node)
+    nnbrs = nx.non_neighbors(G, node)
+    c_1, i_1 = ramsey_R2(G.subgraph(nbrs).copy())
+    c_2, i_2 = ramsey_R2(G.subgraph(nnbrs).copy())
+
+    c_1.add(node)
+    i_2.add(node)
+    # Choose the larger of the two cliques and the larger of the two
+    # independent sets, according to cardinality.
+    return max(c_1, c_2, key=len), max(i_1, i_2, key=len)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/steinertree.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/steinertree.py
new file mode 100644
index 00000000..f4840eff
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/steinertree.py
@@ -0,0 +1,231 @@
+from itertools import chain
+
+import networkx as nx
+from networkx.utils import not_implemented_for, pairwise
+
+__all__ = ["metric_closure", "steiner_tree"]
+
+
+@not_implemented_for("directed")
+@nx._dispatchable(edge_attrs="weight", returns_graph=True)
+def metric_closure(G, weight="weight"):
+    """Return the metric closure of a graph.
+
+    The metric closure of a graph *G* is the complete graph in which each edge
+    is weighted by the shortest path distance between the nodes in *G* .
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    Returns
+    -------
+    NetworkX graph
+        Metric closure of the graph `G`.
+
+    """
+    M = nx.Graph()
+
+    Gnodes = set(G)
+
+    # check for connected graph while processing first node
+    all_paths_iter = nx.all_pairs_dijkstra(G, weight=weight)
+    u, (distance, path) = next(all_paths_iter)
+    if Gnodes - set(distance):
+        msg = "G is not a connected graph. metric_closure is not defined."
+        raise nx.NetworkXError(msg)
+    Gnodes.remove(u)
+    for v in Gnodes:
+        M.add_edge(u, v, distance=distance[v], path=path[v])
+
+    # first node done -- now process the rest
+    for u, (distance, path) in all_paths_iter:
+        Gnodes.remove(u)
+        for v in Gnodes:
+            M.add_edge(u, v, distance=distance[v], path=path[v])
+
+    return M
+
+
+def _mehlhorn_steiner_tree(G, terminal_nodes, weight):
+    paths = nx.multi_source_dijkstra_path(G, terminal_nodes)
+
+    d_1 = {}
+    s = {}
+    for v in G.nodes():
+        s[v] = paths[v][0]
+        d_1[(v, s[v])] = len(paths[v]) - 1
+
+    # G1-G4 names match those from the Mehlhorn 1988 paper.
+    G_1_prime = nx.Graph()
+    for u, v, data in G.edges(data=True):
+        su, sv = s[u], s[v]
+        weight_here = d_1[(u, su)] + data.get(weight, 1) + d_1[(v, sv)]
+        if not G_1_prime.has_edge(su, sv):
+            G_1_prime.add_edge(su, sv, weight=weight_here)
+        else:
+            new_weight = min(weight_here, G_1_prime[su][sv]["weight"])
+            G_1_prime.add_edge(su, sv, weight=new_weight)
+
+    G_2 = nx.minimum_spanning_edges(G_1_prime, data=True)
+
+    G_3 = nx.Graph()
+    for u, v, d in G_2:
+        path = nx.shortest_path(G, u, v, weight)
+        for n1, n2 in pairwise(path):
+            G_3.add_edge(n1, n2)
+
+    G_3_mst = list(nx.minimum_spanning_edges(G_3, data=False))
+    if G.is_multigraph():
+        G_3_mst = (
+            (u, v, min(G[u][v], key=lambda k: G[u][v][k][weight])) for u, v in G_3_mst
+        )
+    G_4 = G.edge_subgraph(G_3_mst).copy()
+    _remove_nonterminal_leaves(G_4, terminal_nodes)
+    return G_4.edges()
+
+
+def _kou_steiner_tree(G, terminal_nodes, weight):
+    # H is the subgraph induced by terminal_nodes in the metric closure M of G.
+    M = metric_closure(G, weight=weight)
+    H = M.subgraph(terminal_nodes)
+
+    # Use the 'distance' attribute of each edge provided by M.
+    mst_edges = nx.minimum_spanning_edges(H, weight="distance", data=True)
+
+    # Create an iterator over each edge in each shortest path; repeats are okay
+    mst_all_edges = chain.from_iterable(pairwise(d["path"]) for u, v, d in mst_edges)
+    if G.is_multigraph():
+        mst_all_edges = (
+            (u, v, min(G[u][v], key=lambda k: G[u][v][k][weight]))
+            for u, v in mst_all_edges
+        )
+
+    # Find the MST again, over this new set of edges
+    G_S = G.edge_subgraph(mst_all_edges)
+    T_S = nx.minimum_spanning_edges(G_S, weight="weight", data=False)
+
+    # Leaf nodes that are not terminal might still remain; remove them here
+    T_H = G.edge_subgraph(T_S).copy()
+    _remove_nonterminal_leaves(T_H, terminal_nodes)
+
+    return T_H.edges()
+
+
+def _remove_nonterminal_leaves(G, terminals):
+    terminal_set = set(terminals)
+    leaves = {n for n in G if len(set(G[n]) - {n}) == 1}
+    nonterminal_leaves = leaves - terminal_set
+
+    while nonterminal_leaves:
+        # Removing a node may create new non-terminal leaves, so we limit
+        # search for candidate non-terminal nodes to neighbors of current
+        # non-terminal nodes
+        candidate_leaves = set.union(*(set(G[n]) for n in nonterminal_leaves))
+        candidate_leaves -= nonterminal_leaves | terminal_set
+        # Remove current set of non-terminal nodes
+        G.remove_nodes_from(nonterminal_leaves)
+        # Find any new non-terminal nodes from the set of candidates
+        leaves = {n for n in candidate_leaves if len(set(G[n]) - {n}) == 1}
+        nonterminal_leaves = leaves - terminal_set
+
+
+ALGORITHMS = {
+    "kou": _kou_steiner_tree,
+    "mehlhorn": _mehlhorn_steiner_tree,
+}
+
+
+@not_implemented_for("directed")
+@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
+def steiner_tree(G, terminal_nodes, weight="weight", method=None):
+    r"""Return an approximation to the minimum Steiner tree of a graph.
+
+    The minimum Steiner tree of `G` w.r.t a set of `terminal_nodes` (also *S*)
+    is a tree within `G` that spans those nodes and has minimum size (sum of
+    edge weights) among all such trees.
+
+    The approximation algorithm is specified with the `method` keyword
+    argument. All three available algorithms produce a tree whose weight is
+    within a ``(2 - (2 / l))`` factor of the weight of the optimal Steiner tree,
+    where ``l`` is the minimum number of leaf nodes across all possible Steiner
+    trees.
+
+    * ``"kou"`` [2]_ (runtime $O(|S| |V|^2)$) computes the minimum spanning tree of
+      the subgraph of the metric closure of *G* induced by the terminal nodes,
+      where the metric closure of *G* is the complete graph in which each edge is
+      weighted by the shortest path distance between the nodes in *G*.
+
+    * ``"mehlhorn"`` [3]_ (runtime $O(|E|+|V|\log|V|)$) modifies Kou et al.'s
+      algorithm, beginning by finding the closest terminal node for each
+      non-terminal. This data is used to create a complete graph containing only
+      the terminal nodes, in which edge is weighted with the shortest path
+      distance between them. The algorithm then proceeds in the same way as Kou
+      et al..
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    terminal_nodes : list
+         A list of terminal nodes for which minimum steiner tree is
+         to be found.
+
+    weight : string (default = 'weight')
+        Use the edge attribute specified by this string as the edge weight.
+        Any edge attribute not present defaults to 1.
+
+    method : string, optional (default = 'mehlhorn')
+        The algorithm to use to approximate the Steiner tree.
+        Supported options: 'kou', 'mehlhorn'.
+        Other inputs produce a ValueError.
+
+    Returns
+    -------
+    NetworkX graph
+        Approximation to the minimum steiner tree of `G` induced by
+        `terminal_nodes` .
+
+    Raises
+    ------
+    NetworkXNotImplemented
+        If `G` is directed.
+
+    ValueError
+        If the specified `method` is not supported.
+
+    Notes
+    -----
+    For multigraphs, the edge between two nodes with minimum weight is the
+    edge put into the Steiner tree.
+
+
+    References
+    ----------
+    .. [1] Steiner_tree_problem on Wikipedia.
+           https://en.wikipedia.org/wiki/Steiner_tree_problem
+    .. [2] Kou, L., G. Markowsky, and L. Berman. 1981.
+           ‘A Fast Algorithm for Steiner Trees’.
+           Acta Informatica 15 (2): 141–45.
+           https://doi.org/10.1007/BF00288961.
+    .. [3] Mehlhorn, Kurt. 1988.
+           ‘A Faster Approximation Algorithm for the Steiner Problem in Graphs’.
+           Information Processing Letters 27 (3): 125–28.
+           https://doi.org/10.1016/0020-0190(88)90066-X.
+    """
+    if method is None:
+        method = "mehlhorn"
+
+    try:
+        algo = ALGORITHMS[method]
+    except KeyError as e:
+        raise ValueError(f"{method} is not a valid choice for an algorithm.") from e
+
+    edges = algo(G, terminal_nodes, weight)
+    # For multigraph we should add the minimal weight edge keys
+    if G.is_multigraph():
+        edges = (
+            (u, v, min(G[u][v], key=lambda k: G[u][v][k][weight])) for u, v in edges
+        )
+    T = G.edge_subgraph(edges)
+    return T
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_approx_clust_coeff.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_approx_clust_coeff.py
new file mode 100644
index 00000000..5eab5c1e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_approx_clust_coeff.py
@@ -0,0 +1,41 @@
+import networkx as nx
+from networkx.algorithms.approximation import average_clustering
+
+# This approximation has to be exact in regular graphs
+# with no triangles or with all possible triangles.
+
+
+def test_petersen():
+    # Actual coefficient is 0
+    G = nx.petersen_graph()
+    assert average_clustering(G, trials=len(G) // 2) == nx.average_clustering(G)
+
+
+def test_petersen_seed():
+    # Actual coefficient is 0
+    G = nx.petersen_graph()
+    assert average_clustering(G, trials=len(G) // 2, seed=1) == nx.average_clustering(G)
+
+
+def test_tetrahedral():
+    # Actual coefficient is 1
+    G = nx.tetrahedral_graph()
+    assert average_clustering(G, trials=len(G) // 2) == nx.average_clustering(G)
+
+
+def test_dodecahedral():
+    # Actual coefficient is 0
+    G = nx.dodecahedral_graph()
+    assert average_clustering(G, trials=len(G) // 2) == nx.average_clustering(G)
+
+
+def test_empty():
+    G = nx.empty_graph(5)
+    assert average_clustering(G, trials=len(G) // 2) == 0
+
+
+def test_complete():
+    G = nx.complete_graph(5)
+    assert average_clustering(G, trials=len(G) // 2) == 1
+    G = nx.complete_graph(7)
+    assert average_clustering(G, trials=len(G) // 2) == 1
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_clique.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_clique.py
new file mode 100644
index 00000000..b40dcb90
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_clique.py
@@ -0,0 +1,112 @@
+"""Unit tests for the :mod:`networkx.algorithms.approximation.clique` module."""
+
+import networkx as nx
+from networkx.algorithms.approximation import (
+    clique_removal,
+    large_clique_size,
+    max_clique,
+    maximum_independent_set,
+)
+
+
+def is_independent_set(G, nodes):
+    """Returns True if and only if `nodes` is a clique in `G`.
+
+    `G` is a NetworkX graph. `nodes` is an iterable of nodes in
+    `G`.
+
+    """
+    return G.subgraph(nodes).number_of_edges() == 0
+
+
+def is_clique(G, nodes):
+    """Returns True if and only if `nodes` is an independent set
+    in `G`.
+
+    `G` is an undirected simple graph. `nodes` is an iterable of
+    nodes in `G`.
+
+    """
+    H = G.subgraph(nodes)
+    n = len(H)
+    return H.number_of_edges() == n * (n - 1) // 2
+
+
+class TestCliqueRemoval:
+    """Unit tests for the
+    :func:`~networkx.algorithms.approximation.clique_removal` function.
+
+    """
+
+    def test_trivial_graph(self):
+        G = nx.trivial_graph()
+        independent_set, cliques = clique_removal(G)
+        assert is_independent_set(G, independent_set)
+        assert all(is_clique(G, clique) for clique in cliques)
+        # In fact, we should only have 1-cliques, that is, singleton nodes.
+        assert all(len(clique) == 1 for clique in cliques)
+
+    def test_complete_graph(self):
+        G = nx.complete_graph(10)
+        independent_set, cliques = clique_removal(G)
+        assert is_independent_set(G, independent_set)
+        assert all(is_clique(G, clique) for clique in cliques)
+
+    def test_barbell_graph(self):
+        G = nx.barbell_graph(10, 5)
+        independent_set, cliques = clique_removal(G)
+        assert is_independent_set(G, independent_set)
+        assert all(is_clique(G, clique) for clique in cliques)
+
+
+class TestMaxClique:
+    """Unit tests for the :func:`networkx.algorithms.approximation.max_clique`
+    function.
+
+    """
+
+    def test_null_graph(self):
+        G = nx.null_graph()
+        assert len(max_clique(G)) == 0
+
+    def test_complete_graph(self):
+        graph = nx.complete_graph(30)
+        # this should return the entire graph
+        mc = max_clique(graph)
+        assert 30 == len(mc)
+
+    def test_maximal_by_cardinality(self):
+        """Tests that the maximal clique is computed according to maximum
+        cardinality of the sets.
+
+        For more information, see pull request #1531.
+
+        """
+        G = nx.complete_graph(5)
+        G.add_edge(4, 5)
+        clique = max_clique(G)
+        assert len(clique) > 1
+
+        G = nx.lollipop_graph(30, 2)
+        clique = max_clique(G)
+        assert len(clique) > 2
+
+
+def test_large_clique_size():
+    G = nx.complete_graph(9)
+    nx.add_cycle(G, [9, 10, 11])
+    G.add_edge(8, 9)
+    G.add_edge(1, 12)
+    G.add_node(13)
+
+    assert large_clique_size(G) == 9
+    G.remove_node(5)
+    assert large_clique_size(G) == 8
+    G.remove_edge(2, 3)
+    assert large_clique_size(G) == 7
+
+
+def test_independent_set():
+    # smoke test
+    G = nx.Graph()
+    assert len(maximum_independent_set(G)) == 0
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_connectivity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_connectivity.py
new file mode 100644
index 00000000..887db20b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_connectivity.py
@@ -0,0 +1,199 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms import approximation as approx
+
+
+def test_global_node_connectivity():
+    # Figure 1 chapter on Connectivity
+    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),
+        ]
+    )
+    assert 2 == approx.local_node_connectivity(G, 1, 11)
+    assert 2 == approx.node_connectivity(G)
+    assert 2 == approx.node_connectivity(G, 1, 11)
+
+
+def test_white_harary1():
+    # Figure 1b white and harary (2001)
+    # 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)
+    assert 1 == approx.node_connectivity(G)
+
+
+def test_complete_graphs():
+    for n in range(5, 25, 5):
+        G = nx.complete_graph(n)
+        assert n - 1 == approx.node_connectivity(G)
+        assert n - 1 == approx.node_connectivity(G, 0, 3)
+
+
+def test_empty_graphs():
+    for k in range(5, 25, 5):
+        G = nx.empty_graph(k)
+        assert 0 == approx.node_connectivity(G)
+        assert 0 == approx.node_connectivity(G, 0, 3)
+
+
+def test_petersen():
+    G = nx.petersen_graph()
+    assert 3 == approx.node_connectivity(G)
+    assert 3 == approx.node_connectivity(G, 0, 5)
+
+
+# Approximation fails with tutte graph
+# def test_tutte():
+#    G = nx.tutte_graph()
+#    assert_equal(3, approx.node_connectivity(G))
+
+
+def test_dodecahedral():
+    G = nx.dodecahedral_graph()
+    assert 3 == approx.node_connectivity(G)
+    assert 3 == approx.node_connectivity(G, 0, 5)
+
+
+def test_octahedral():
+    G = nx.octahedral_graph()
+    assert 4 == approx.node_connectivity(G)
+    assert 4 == approx.node_connectivity(G, 0, 5)
+
+
+# Approximation can fail with icosahedral graph depending
+# on iteration order.
+# def test_icosahedral():
+#    G=nx.icosahedral_graph()
+#    assert_equal(5, approx.node_connectivity(G))
+#    assert_equal(5, approx.node_connectivity(G, 0, 5))
+
+
+def test_only_source():
+    G = nx.complete_graph(5)
+    pytest.raises(nx.NetworkXError, approx.node_connectivity, G, s=0)
+
+
+def test_only_target():
+    G = nx.complete_graph(5)
+    pytest.raises(nx.NetworkXError, approx.node_connectivity, G, t=0)
+
+
+def test_missing_source():
+    G = nx.path_graph(4)
+    pytest.raises(nx.NetworkXError, approx.node_connectivity, G, 10, 1)
+
+
+def test_missing_target():
+    G = nx.path_graph(4)
+    pytest.raises(nx.NetworkXError, approx.node_connectivity, G, 1, 10)
+
+
+def test_source_equals_target():
+    G = nx.complete_graph(5)
+    pytest.raises(nx.NetworkXError, approx.local_node_connectivity, G, 0, 0)
+
+
+def test_directed_node_connectivity():
+    G = nx.cycle_graph(10, create_using=nx.DiGraph())  # only one direction
+    D = nx.cycle_graph(10).to_directed()  # 2 reciprocal edges
+    assert 1 == approx.node_connectivity(G)
+    assert 1 == approx.node_connectivity(G, 1, 4)
+    assert 2 == approx.node_connectivity(D)
+    assert 2 == approx.node_connectivity(D, 1, 4)
+
+
+class TestAllPairsNodeConnectivityApprox:
+    @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)
+        cls.directed_gnp = nx.gnp_random_graph(30, 0.1, directed=True)
+        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 = approx.all_pairs_node_connectivity(self.cycle)
+        for source in K_undir:
+            for target, k in K_undir[source].items():
+                assert k == 2
+        K_dir = approx.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 = approx.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 = approx.all_pairs_node_connectivity(self.path)
+        for source in K_undir:
+            for target, k in K_undir[source].items():
+                assert k == 1
+        K_dir = approx.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_cutoff(self):
+        for G in [self.K10, self.K5, self.K20]:
+            for mp in [2, 3, 4]:
+                paths = approx.all_pairs_node_connectivity(G, cutoff=mp)
+                for source in paths:
+                    for target, K in paths[source].items():
+                        assert K == mp
+
+    def test_all_pairs_connectivity_nbunch(self):
+        G = nx.complete_graph(5)
+        nbunch = [0, 2, 3]
+        C = approx.all_pairs_node_connectivity(G, nbunch=nbunch)
+        assert len(C) == len(nbunch)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_distance_measures.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_distance_measures.py
new file mode 100644
index 00000000..3809a8fc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_distance_measures.py
@@ -0,0 +1,59 @@
+"""Unit tests for the :mod:`networkx.algorithms.approximation.distance_measures` module."""
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.approximation import diameter
+
+
+class TestDiameter:
+    """Unit tests for the approximate diameter function
+    :func:`~networkx.algorithms.approximation.distance_measures.diameter`.
+    """
+
+    def test_null_graph(self):
+        """Test empty graph."""
+        G = nx.null_graph()
+        with pytest.raises(
+            nx.NetworkXError, match="Expected non-empty NetworkX graph!"
+        ):
+            diameter(G)
+
+    def test_undirected_non_connected(self):
+        """Test an undirected disconnected graph."""
+        graph = nx.path_graph(10)
+        graph.remove_edge(3, 4)
+        with pytest.raises(nx.NetworkXError, match="Graph not connected."):
+            diameter(graph)
+
+    def test_directed_non_strongly_connected(self):
+        """Test a directed non strongly connected graph."""
+        graph = nx.path_graph(10, create_using=nx.DiGraph())
+        with pytest.raises(nx.NetworkXError, match="DiGraph not strongly connected."):
+            diameter(graph)
+
+    def test_complete_undirected_graph(self):
+        """Test a complete undirected graph."""
+        graph = nx.complete_graph(10)
+        assert diameter(graph) == 1
+
+    def test_complete_directed_graph(self):
+        """Test a complete directed graph."""
+        graph = nx.complete_graph(10, create_using=nx.DiGraph())
+        assert diameter(graph) == 1
+
+    def test_undirected_path_graph(self):
+        """Test an undirected path graph with 10 nodes."""
+        graph = nx.path_graph(10)
+        assert diameter(graph) == 9
+
+    def test_directed_path_graph(self):
+        """Test a directed path graph with 10 nodes."""
+        graph = nx.path_graph(10).to_directed()
+        assert diameter(graph) == 9
+
+    def test_single_node(self):
+        """Test a graph which contains just a node."""
+        graph = nx.Graph()
+        graph.add_node(1)
+        assert diameter(graph) == 0
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_dominating_set.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_dominating_set.py
new file mode 100644
index 00000000..6b90d85e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_dominating_set.py
@@ -0,0 +1,78 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms.approximation import (
+    min_edge_dominating_set,
+    min_weighted_dominating_set,
+)
+
+
+class TestMinWeightDominatingSet:
+    def test_min_weighted_dominating_set(self):
+        graph = nx.Graph()
+        graph.add_edge(1, 2)
+        graph.add_edge(1, 5)
+        graph.add_edge(2, 3)
+        graph.add_edge(2, 5)
+        graph.add_edge(3, 4)
+        graph.add_edge(3, 6)
+        graph.add_edge(5, 6)
+
+        vertices = {1, 2, 3, 4, 5, 6}
+        # due to ties, this might be hard to test tight bounds
+        dom_set = min_weighted_dominating_set(graph)
+        for vertex in vertices - dom_set:
+            neighbors = set(graph.neighbors(vertex))
+            assert len(neighbors & dom_set) > 0, "Non dominating set found!"
+
+    def test_star_graph(self):
+        """Tests that an approximate dominating set for the star graph,
+        even when the center node does not have the smallest integer
+        label, gives just the center node.
+
+        For more information, see #1527.
+
+        """
+        # Create a star graph in which the center node has the highest
+        # label instead of the lowest.
+        G = nx.star_graph(10)
+        G = nx.relabel_nodes(G, {0: 9, 9: 0})
+        assert min_weighted_dominating_set(G) == {9}
+
+    def test_null_graph(self):
+        """Tests that the unique dominating set for the null graph is an empty set"""
+        G = nx.Graph()
+        assert min_weighted_dominating_set(G) == set()
+
+    def test_min_edge_dominating_set(self):
+        graph = nx.path_graph(5)
+        dom_set = min_edge_dominating_set(graph)
+
+        # this is a crappy way to test, but good enough for now.
+        for edge in graph.edges():
+            if edge in dom_set:
+                continue
+            else:
+                u, v = edge
+                found = False
+                for dom_edge in dom_set:
+                    found |= u == dom_edge[0] or u == dom_edge[1]
+                assert found, "Non adjacent edge found!"
+
+        graph = nx.complete_graph(10)
+        dom_set = min_edge_dominating_set(graph)
+
+        # this is a crappy way to test, but good enough for now.
+        for edge in graph.edges():
+            if edge in dom_set:
+                continue
+            else:
+                u, v = edge
+                found = False
+                for dom_edge in dom_set:
+                    found |= u == dom_edge[0] or u == dom_edge[1]
+                assert found, "Non adjacent edge found!"
+
+        graph = nx.Graph()  # empty Networkx graph
+        with pytest.raises(ValueError, match="Expected non-empty NetworkX graph!"):
+            min_edge_dominating_set(graph)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_kcomponents.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_kcomponents.py
new file mode 100644
index 00000000..65ba8021
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_kcomponents.py
@@ -0,0 +1,303 @@
+# Test for approximation to k-components algorithm
+import pytest
+
+import networkx as nx
+from networkx.algorithms.approximation import k_components
+from networkx.algorithms.approximation.kcomponents import _AntiGraph, _same
+
+
+def build_k_number_dict(k_components):
+    k_num = {}
+    for k, comps in sorted(k_components.items()):
+        for comp in comps:
+            for node in comp:
+                k_num[node] = k
+    return k_num
+
+
+##
+# 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_connectivity(G):
+    result = k_components(G)
+    for k, components in result.items():
+        if k < 3:
+            continue
+        for component in components:
+            C = G.subgraph(component)
+            K = nx.node_connectivity(C)
+            assert K >= k
+
+
+def test_torrents_and_ferraro_graph():
+    G = torrents_and_ferraro_graph()
+    _check_connectivity(G)
+
+
+def test_example_1():
+    G = graph_example_1()
+    _check_connectivity(G)
+
+
+def test_karate_0():
+    G = nx.karate_club_graph()
+    _check_connectivity(G)
+
+
+def test_karate_1():
+    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,
+    }
+    approx_karate_k_num = karate_k_num.copy()
+    approx_karate_k_num[24] = 2
+    approx_karate_k_num[25] = 2
+    G = nx.karate_club_graph()
+    k_comps = k_components(G)
+    k_num = build_k_number_dict(k_comps)
+    assert k_num in (karate_k_num, approx_karate_k_num)
+
+
+def test_example_1_detail_3_and_4():
+    G = graph_example_1()
+    result = k_components(G)
+    # 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])
+    # Finally check that the k-components detected have actually node
+    # connectivity >= k.
+    for k, components in result.items():
+        if k < 3:
+            continue
+        for component in components:
+            K = nx.node_connectivity(G.subgraph(component))
+            assert K >= k
+
+
+def test_directed():
+    with pytest.raises(nx.NetworkXNotImplemented):
+        G = nx.gnp_random_graph(10, 0.4, directed=True)
+        kc = k_components(G)
+
+
+def test_same():
+    equal = {"A": 2, "B": 2, "C": 2}
+    slightly_different = {"A": 2, "B": 1, "C": 2}
+    different = {"A": 2, "B": 8, "C": 18}
+    assert _same(equal)
+    assert not _same(slightly_different)
+    assert _same(slightly_different, tol=1)
+    assert not _same(different)
+    assert not _same(different, tol=4)
+
+
+class TestAntiGraph:
+    @classmethod
+    def setup_class(cls):
+        cls.Gnp = nx.gnp_random_graph(20, 0.8, seed=42)
+        cls.Anp = _AntiGraph(nx.complement(cls.Gnp))
+        cls.Gd = nx.davis_southern_women_graph()
+        cls.Ad = _AntiGraph(nx.complement(cls.Gd))
+        cls.Gk = nx.karate_club_graph()
+        cls.Ak = _AntiGraph(nx.complement(cls.Gk))
+        cls.GA = [(cls.Gnp, cls.Anp), (cls.Gd, cls.Ad), (cls.Gk, cls.Ak)]
+
+    def test_size(self):
+        for G, A in self.GA:
+            n = G.order()
+            s = len(list(G.edges())) + len(list(A.edges()))
+            assert s == (n * (n - 1)) / 2
+
+    def test_degree(self):
+        for G, A in self.GA:
+            assert sorted(G.degree()) == sorted(A.degree())
+
+    def test_core_number(self):
+        for G, A in self.GA:
+            assert nx.core_number(G) == nx.core_number(A)
+
+    def test_connected_components(self):
+        # ccs are same unless isolated nodes or any node has degree=len(G)-1
+        # graphs in self.GA avoid this problem
+        for G, A in self.GA:
+            gc = [set(c) for c in nx.connected_components(G)]
+            ac = [set(c) for c in nx.connected_components(A)]
+            for comp in ac:
+                assert comp in gc
+
+    def test_adj(self):
+        for G, A in self.GA:
+            for n, nbrs in G.adj.items():
+                a_adj = sorted((n, sorted(ad)) for n, ad in A.adj.items())
+                g_adj = sorted((n, sorted(ad)) for n, ad in G.adj.items())
+                assert a_adj == g_adj
+
+    def test_adjacency(self):
+        for G, A in self.GA:
+            a_adj = list(A.adjacency())
+            for n, nbrs in G.adjacency():
+                assert (n, set(nbrs)) in a_adj
+
+    def test_neighbors(self):
+        for G, A in self.GA:
+            node = list(G.nodes())[0]
+            assert set(G.neighbors(node)) == set(A.neighbors(node))
+
+    def test_node_not_in_graph(self):
+        for G, A in self.GA:
+            node = "non_existent_node"
+            pytest.raises(nx.NetworkXError, A.neighbors, node)
+            pytest.raises(nx.NetworkXError, G.neighbors, node)
+
+    def test_degree_thingraph(self):
+        for G, A in self.GA:
+            node = list(G.nodes())[0]
+            nodes = list(G.nodes())[1:4]
+            assert G.degree(node) == A.degree(node)
+            assert sum(d for n, d in G.degree()) == sum(d for n, d in A.degree())
+            # AntiGraph is a ThinGraph, so all the weights are 1
+            assert sum(d for n, d in A.degree()) == sum(
+                d for n, d in A.degree(weight="weight")
+            )
+            assert sum(d for n, d in G.degree(nodes)) == sum(
+                d for n, d in A.degree(nodes)
+            )
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_matching.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_matching.py
new file mode 100644
index 00000000..f50da3d2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_matching.py
@@ -0,0 +1,8 @@
+import networkx as nx
+import networkx.algorithms.approximation as a
+
+
+def test_min_maximal_matching():
+    # smoke test
+    G = nx.Graph()
+    assert len(a.min_maximal_matching(G)) == 0
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_maxcut.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_maxcut.py
new file mode 100644
index 00000000..ef042440
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_maxcut.py
@@ -0,0 +1,94 @@
+import random
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.approximation import maxcut
+
+
+@pytest.mark.parametrize(
+    "f", (nx.approximation.randomized_partitioning, nx.approximation.one_exchange)
+)
+@pytest.mark.parametrize("graph_constructor", (nx.DiGraph, nx.MultiGraph))
+def test_raises_on_directed_and_multigraphs(f, graph_constructor):
+    G = graph_constructor([(0, 1), (1, 2)])
+    with pytest.raises(nx.NetworkXNotImplemented):
+        f(G)
+
+
+def _is_valid_cut(G, set1, set2):
+    union = set1.union(set2)
+    assert union == set(G.nodes)
+    assert len(set1) + len(set2) == G.number_of_nodes()
+
+
+def _cut_is_locally_optimal(G, cut_size, set1):
+    # test if cut can be locally improved
+    for i, node in enumerate(set1):
+        cut_size_without_node = nx.algorithms.cut_size(
+            G, set1 - {node}, weight="weight"
+        )
+        assert cut_size_without_node <= cut_size
+
+
+def test_random_partitioning():
+    G = nx.complete_graph(5)
+    _, (set1, set2) = maxcut.randomized_partitioning(G, seed=5)
+    _is_valid_cut(G, set1, set2)
+
+
+def test_random_partitioning_all_to_one():
+    G = nx.complete_graph(5)
+    _, (set1, set2) = maxcut.randomized_partitioning(G, p=1)
+    _is_valid_cut(G, set1, set2)
+    assert len(set1) == G.number_of_nodes()
+    assert len(set2) == 0
+
+
+def test_one_exchange_basic():
+    G = nx.complete_graph(5)
+    random.seed(5)
+    for u, v, w in G.edges(data=True):
+        w["weight"] = random.randrange(-100, 100, 1) / 10
+
+    initial_cut = set(random.sample(sorted(G.nodes()), k=5))
+    cut_size, (set1, set2) = maxcut.one_exchange(
+        G, initial_cut, weight="weight", seed=5
+    )
+
+    _is_valid_cut(G, set1, set2)
+    _cut_is_locally_optimal(G, cut_size, set1)
+
+
+def test_one_exchange_optimal():
+    # Greedy one exchange should find the optimal solution for this graph (14)
+    G = nx.Graph()
+    G.add_edge(1, 2, weight=3)
+    G.add_edge(1, 3, weight=3)
+    G.add_edge(1, 4, weight=3)
+    G.add_edge(1, 5, weight=3)
+    G.add_edge(2, 3, weight=5)
+
+    cut_size, (set1, set2) = maxcut.one_exchange(G, weight="weight", seed=5)
+
+    _is_valid_cut(G, set1, set2)
+    _cut_is_locally_optimal(G, cut_size, set1)
+    # check global optimality
+    assert cut_size == 14
+
+
+def test_negative_weights():
+    G = nx.complete_graph(5)
+    random.seed(5)
+    for u, v, w in G.edges(data=True):
+        w["weight"] = -1 * random.random()
+
+    initial_cut = set(random.sample(sorted(G.nodes()), k=5))
+    cut_size, (set1, set2) = maxcut.one_exchange(G, initial_cut, weight="weight")
+
+    # make sure it is a valid cut
+    _is_valid_cut(G, set1, set2)
+    # check local optimality
+    _cut_is_locally_optimal(G, cut_size, set1)
+    # test that all nodes are in the same partition
+    assert len(set1) == len(G.nodes) or len(set2) == len(G.nodes)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_ramsey.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_ramsey.py
new file mode 100644
index 00000000..32fe1fb8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_ramsey.py
@@ -0,0 +1,31 @@
+import networkx as nx
+import networkx.algorithms.approximation as apxa
+
+
+def test_ramsey():
+    # this should only find the complete graph
+    graph = nx.complete_graph(10)
+    c, i = apxa.ramsey_R2(graph)
+    cdens = nx.density(graph.subgraph(c))
+    assert cdens == 1.0, "clique not correctly found by ramsey!"
+    idens = nx.density(graph.subgraph(i))
+    assert idens == 0.0, "i-set not correctly found by ramsey!"
+
+    # this trivial graph has no cliques. should just find i-sets
+    graph = nx.trivial_graph()
+    c, i = apxa.ramsey_R2(graph)
+    assert c == {0}, "clique not correctly found by ramsey!"
+    assert i == {0}, "i-set not correctly found by ramsey!"
+
+    graph = nx.barbell_graph(10, 5, nx.Graph())
+    c, i = apxa.ramsey_R2(graph)
+    cdens = nx.density(graph.subgraph(c))
+    assert cdens == 1.0, "clique not correctly found by ramsey!"
+    idens = nx.density(graph.subgraph(i))
+    assert idens == 0.0, "i-set not correctly found by ramsey!"
+
+    # add self-loops and test again
+    graph.add_edges_from([(n, n) for n in range(0, len(graph), 2)])
+    cc, ii = apxa.ramsey_R2(graph)
+    assert cc == c
+    assert ii == i
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_steinertree.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_steinertree.py
new file mode 100644
index 00000000..1b074757
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_steinertree.py
@@ -0,0 +1,265 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms.approximation.steinertree import (
+    _remove_nonterminal_leaves,
+    metric_closure,
+    steiner_tree,
+)
+from networkx.utils import edges_equal
+
+
+class TestSteinerTree:
+    @classmethod
+    def setup_class(cls):
+        G1 = nx.Graph()
+        G1.add_edge(1, 2, weight=10)
+        G1.add_edge(2, 3, weight=10)
+        G1.add_edge(3, 4, weight=10)
+        G1.add_edge(4, 5, weight=10)
+        G1.add_edge(5, 6, weight=10)
+        G1.add_edge(2, 7, weight=1)
+        G1.add_edge(7, 5, weight=1)
+
+        G2 = nx.Graph()
+        G2.add_edge(0, 5, weight=6)
+        G2.add_edge(1, 2, weight=2)
+        G2.add_edge(1, 5, weight=3)
+        G2.add_edge(2, 4, weight=4)
+        G2.add_edge(3, 5, weight=5)
+        G2.add_edge(4, 5, weight=1)
+
+        G3 = nx.Graph()
+        G3.add_edge(1, 2, weight=8)
+        G3.add_edge(1, 9, weight=3)
+        G3.add_edge(1, 8, weight=6)
+        G3.add_edge(1, 10, weight=2)
+        G3.add_edge(1, 14, weight=3)
+        G3.add_edge(2, 3, weight=6)
+        G3.add_edge(3, 4, weight=3)
+        G3.add_edge(3, 10, weight=2)
+        G3.add_edge(3, 11, weight=1)
+        G3.add_edge(4, 5, weight=1)
+        G3.add_edge(4, 11, weight=1)
+        G3.add_edge(5, 6, weight=4)
+        G3.add_edge(5, 11, weight=2)
+        G3.add_edge(5, 12, weight=1)
+        G3.add_edge(5, 13, weight=3)
+        G3.add_edge(6, 7, weight=2)
+        G3.add_edge(6, 12, weight=3)
+        G3.add_edge(6, 13, weight=1)
+        G3.add_edge(7, 8, weight=3)
+        G3.add_edge(7, 9, weight=3)
+        G3.add_edge(7, 11, weight=5)
+        G3.add_edge(7, 13, weight=2)
+        G3.add_edge(7, 14, weight=4)
+        G3.add_edge(8, 9, weight=2)
+        G3.add_edge(9, 14, weight=1)
+        G3.add_edge(10, 11, weight=2)
+        G3.add_edge(10, 14, weight=1)
+        G3.add_edge(11, 12, weight=1)
+        G3.add_edge(11, 14, weight=7)
+        G3.add_edge(12, 14, weight=3)
+        G3.add_edge(12, 15, weight=1)
+        G3.add_edge(13, 14, weight=4)
+        G3.add_edge(13, 15, weight=1)
+        G3.add_edge(14, 15, weight=2)
+
+        cls.G1 = G1
+        cls.G2 = G2
+        cls.G3 = G3
+        cls.G1_term_nodes = [1, 2, 3, 4, 5]
+        cls.G2_term_nodes = [0, 2, 3]
+        cls.G3_term_nodes = [1, 3, 5, 6, 8, 10, 11, 12, 13]
+
+        cls.methods = ["kou", "mehlhorn"]
+
+    def test_connected_metric_closure(self):
+        G = self.G1.copy()
+        G.add_node(100)
+        pytest.raises(nx.NetworkXError, metric_closure, G)
+
+    def test_metric_closure(self):
+        M = metric_closure(self.G1)
+        mc = [
+            (1, 2, {"distance": 10, "path": [1, 2]}),
+            (1, 3, {"distance": 20, "path": [1, 2, 3]}),
+            (1, 4, {"distance": 22, "path": [1, 2, 7, 5, 4]}),
+            (1, 5, {"distance": 12, "path": [1, 2, 7, 5]}),
+            (1, 6, {"distance": 22, "path": [1, 2, 7, 5, 6]}),
+            (1, 7, {"distance": 11, "path": [1, 2, 7]}),
+            (2, 3, {"distance": 10, "path": [2, 3]}),
+            (2, 4, {"distance": 12, "path": [2, 7, 5, 4]}),
+            (2, 5, {"distance": 2, "path": [2, 7, 5]}),
+            (2, 6, {"distance": 12, "path": [2, 7, 5, 6]}),
+            (2, 7, {"distance": 1, "path": [2, 7]}),
+            (3, 4, {"distance": 10, "path": [3, 4]}),
+            (3, 5, {"distance": 12, "path": [3, 2, 7, 5]}),
+            (3, 6, {"distance": 22, "path": [3, 2, 7, 5, 6]}),
+            (3, 7, {"distance": 11, "path": [3, 2, 7]}),
+            (4, 5, {"distance": 10, "path": [4, 5]}),
+            (4, 6, {"distance": 20, "path": [4, 5, 6]}),
+            (4, 7, {"distance": 11, "path": [4, 5, 7]}),
+            (5, 6, {"distance": 10, "path": [5, 6]}),
+            (5, 7, {"distance": 1, "path": [5, 7]}),
+            (6, 7, {"distance": 11, "path": [6, 5, 7]}),
+        ]
+        assert edges_equal(list(M.edges(data=True)), mc)
+
+    def test_steiner_tree(self):
+        valid_steiner_trees = [
+            [
+                [
+                    (1, 2, {"weight": 10}),
+                    (2, 3, {"weight": 10}),
+                    (2, 7, {"weight": 1}),
+                    (3, 4, {"weight": 10}),
+                    (5, 7, {"weight": 1}),
+                ],
+                [
+                    (1, 2, {"weight": 10}),
+                    (2, 7, {"weight": 1}),
+                    (3, 4, {"weight": 10}),
+                    (4, 5, {"weight": 10}),
+                    (5, 7, {"weight": 1}),
+                ],
+                [
+                    (1, 2, {"weight": 10}),
+                    (2, 3, {"weight": 10}),
+                    (2, 7, {"weight": 1}),
+                    (4, 5, {"weight": 10}),
+                    (5, 7, {"weight": 1}),
+                ],
+            ],
+            [
+                [
+                    (0, 5, {"weight": 6}),
+                    (1, 2, {"weight": 2}),
+                    (1, 5, {"weight": 3}),
+                    (3, 5, {"weight": 5}),
+                ],
+                [
+                    (0, 5, {"weight": 6}),
+                    (4, 2, {"weight": 4}),
+                    (4, 5, {"weight": 1}),
+                    (3, 5, {"weight": 5}),
+                ],
+            ],
+            [
+                [
+                    (1, 10, {"weight": 2}),
+                    (3, 10, {"weight": 2}),
+                    (3, 11, {"weight": 1}),
+                    (5, 12, {"weight": 1}),
+                    (6, 13, {"weight": 1}),
+                    (8, 9, {"weight": 2}),
+                    (9, 14, {"weight": 1}),
+                    (10, 14, {"weight": 1}),
+                    (11, 12, {"weight": 1}),
+                    (12, 15, {"weight": 1}),
+                    (13, 15, {"weight": 1}),
+                ]
+            ],
+        ]
+        for method in self.methods:
+            for G, term_nodes, valid_trees in zip(
+                [self.G1, self.G2, self.G3],
+                [self.G1_term_nodes, self.G2_term_nodes, self.G3_term_nodes],
+                valid_steiner_trees,
+            ):
+                S = steiner_tree(G, term_nodes, method=method)
+                assert any(
+                    edges_equal(list(S.edges(data=True)), valid_tree)
+                    for valid_tree in valid_trees
+                )
+
+    def test_multigraph_steiner_tree(self):
+        G = nx.MultiGraph()
+        G.add_edges_from(
+            [
+                (1, 2, 0, {"weight": 1}),
+                (2, 3, 0, {"weight": 999}),
+                (2, 3, 1, {"weight": 1}),
+                (3, 4, 0, {"weight": 1}),
+                (3, 5, 0, {"weight": 1}),
+            ]
+        )
+        terminal_nodes = [2, 4, 5]
+        expected_edges = [
+            (2, 3, 1, {"weight": 1}),  # edge with key 1 has lower weight
+            (3, 4, 0, {"weight": 1}),
+            (3, 5, 0, {"weight": 1}),
+        ]
+        for method in self.methods:
+            S = steiner_tree(G, terminal_nodes, method=method)
+            assert edges_equal(S.edges(data=True, keys=True), expected_edges)
+
+    def test_remove_nonterminal_leaves(self):
+        G = nx.path_graph(10)
+        _remove_nonterminal_leaves(G, [4, 5, 6])
+
+        assert list(G) == [4, 5, 6]  # only the terminal nodes are left
+
+
+@pytest.mark.parametrize("method", ("kou", "mehlhorn"))
+def test_steiner_tree_weight_attribute(method):
+    G = nx.star_graph(4)
+    # Add an edge attribute that is named something other than "weight"
+    nx.set_edge_attributes(G, {e: 10 for e in G.edges}, name="distance")
+    H = nx.approximation.steiner_tree(G, [1, 3], method=method, weight="distance")
+    assert nx.utils.edges_equal(H.edges, [(0, 1), (0, 3)])
+
+
+@pytest.mark.parametrize("method", ("kou", "mehlhorn"))
+def test_steiner_tree_multigraph_weight_attribute(method):
+    G = nx.cycle_graph(3, create_using=nx.MultiGraph)
+    nx.set_edge_attributes(G, {e: 10 for e in G.edges}, name="distance")
+    G.add_edge(2, 0, distance=5)
+    H = nx.approximation.steiner_tree(G, list(G), method=method, weight="distance")
+    assert len(H.edges) == 2 and H.has_edge(2, 0, key=1)
+    assert sum(dist for *_, dist in H.edges(data="distance")) == 15
+
+
+@pytest.mark.parametrize("method", (None, "mehlhorn", "kou"))
+def test_steiner_tree_methods(method):
+    G = nx.star_graph(4)
+    expected = nx.Graph([(0, 1), (0, 3)])
+    st = nx.approximation.steiner_tree(G, [1, 3], method=method)
+    assert nx.utils.edges_equal(st.edges, expected.edges)
+
+
+def test_steiner_tree_method_invalid():
+    G = nx.star_graph(4)
+    with pytest.raises(
+        ValueError, match="invalid_method is not a valid choice for an algorithm."
+    ):
+        nx.approximation.steiner_tree(G, terminal_nodes=[1, 3], method="invalid_method")
+
+
+def test_steiner_tree_remove_non_terminal_leaves_self_loop_edges():
+    # To verify that the last step of the steiner tree approximation
+    # behaves in the case where a non-terminal leaf has a self loop edge
+    G = nx.path_graph(10)
+
+    # Add self loops to the terminal nodes
+    G.add_edges_from([(2, 2), (3, 3), (4, 4), (7, 7), (8, 8)])
+
+    # Remove non-terminal leaves
+    _remove_nonterminal_leaves(G, [4, 5, 6, 7])
+
+    # The terminal nodes should be left
+    assert list(G) == [4, 5, 6, 7]  # only the terminal nodes are left
+
+
+def test_steiner_tree_non_terminal_leaves_multigraph_self_loop_edges():
+    # To verify that the last step of the steiner tree approximation
+    # behaves in the case where a non-terminal leaf has a self loop edge
+    G = nx.MultiGraph()
+    G.add_edges_from([(i, i + 1) for i in range(10)])
+    G.add_edges_from([(2, 2), (3, 3), (4, 4), (4, 4), (7, 7)])
+
+    # Remove non-terminal leaves
+    _remove_nonterminal_leaves(G, [4, 5, 6, 7])
+
+    # Only the terminal nodes should be left
+    assert list(G) == [4, 5, 6, 7]
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_traveling_salesman.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_traveling_salesman.py
new file mode 100644
index 00000000..2084c19a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_traveling_salesman.py
@@ -0,0 +1,977 @@
+"""Unit tests for the traveling_salesman module."""
+
+import random
+
+import pytest
+
+import networkx as nx
+import networkx.algorithms.approximation as nx_app
+
+pairwise = nx.utils.pairwise
+
+
+def test_christofides_hamiltonian():
+    random.seed(42)
+    G = nx.complete_graph(20)
+    for u, v in G.edges():
+        G[u][v]["weight"] = random.randint(0, 10)
+
+    H = nx.Graph()
+    H.add_edges_from(pairwise(nx_app.christofides(G)))
+    H.remove_edges_from(nx.find_cycle(H))
+    assert len(H.edges) == 0
+
+    tree = nx.minimum_spanning_tree(G, weight="weight")
+    H = nx.Graph()
+    H.add_edges_from(pairwise(nx_app.christofides(G, tree)))
+    H.remove_edges_from(nx.find_cycle(H))
+    assert len(H.edges) == 0
+
+
+def test_christofides_incomplete_graph():
+    G = nx.complete_graph(10)
+    G.remove_edge(0, 1)
+    pytest.raises(nx.NetworkXError, nx_app.christofides, G)
+
+
+def test_christofides_ignore_selfloops():
+    G = nx.complete_graph(5)
+    G.add_edge(3, 3)
+    cycle = nx_app.christofides(G)
+    assert len(cycle) - 1 == len(G) == len(set(cycle))
+
+
+# set up graphs for other tests
+class TestBase:
+    @classmethod
+    def setup_class(cls):
+        cls.DG = nx.DiGraph()
+        cls.DG.add_weighted_edges_from(
+            {
+                ("A", "B", 3),
+                ("A", "C", 17),
+                ("A", "D", 14),
+                ("B", "A", 3),
+                ("B", "C", 12),
+                ("B", "D", 16),
+                ("C", "A", 13),
+                ("C", "B", 12),
+                ("C", "D", 4),
+                ("D", "A", 14),
+                ("D", "B", 15),
+                ("D", "C", 2),
+            }
+        )
+        cls.DG_cycle = ["D", "C", "B", "A", "D"]
+        cls.DG_cost = 31.0
+
+        cls.DG2 = nx.DiGraph()
+        cls.DG2.add_weighted_edges_from(
+            {
+                ("A", "B", 3),
+                ("A", "C", 17),
+                ("A", "D", 14),
+                ("B", "A", 30),
+                ("B", "C", 2),
+                ("B", "D", 16),
+                ("C", "A", 33),
+                ("C", "B", 32),
+                ("C", "D", 34),
+                ("D", "A", 14),
+                ("D", "B", 15),
+                ("D", "C", 2),
+            }
+        )
+        cls.DG2_cycle = ["D", "A", "B", "C", "D"]
+        cls.DG2_cost = 53.0
+
+        cls.unweightedUG = nx.complete_graph(5, nx.Graph())
+        cls.unweightedDG = nx.complete_graph(5, nx.DiGraph())
+
+        cls.incompleteUG = nx.Graph()
+        cls.incompleteUG.add_weighted_edges_from({(0, 1, 1), (1, 2, 3)})
+        cls.incompleteDG = nx.DiGraph()
+        cls.incompleteDG.add_weighted_edges_from({(0, 1, 1), (1, 2, 3)})
+
+        cls.UG = nx.Graph()
+        cls.UG.add_weighted_edges_from(
+            {
+                ("A", "B", 3),
+                ("A", "C", 17),
+                ("A", "D", 14),
+                ("B", "C", 12),
+                ("B", "D", 16),
+                ("C", "D", 4),
+            }
+        )
+        cls.UG_cycle = ["D", "C", "B", "A", "D"]
+        cls.UG_cost = 33.0
+
+        cls.UG2 = nx.Graph()
+        cls.UG2.add_weighted_edges_from(
+            {
+                ("A", "B", 1),
+                ("A", "C", 15),
+                ("A", "D", 5),
+                ("B", "C", 16),
+                ("B", "D", 8),
+                ("C", "D", 3),
+            }
+        )
+        cls.UG2_cycle = ["D", "C", "B", "A", "D"]
+        cls.UG2_cost = 25.0
+
+
+def validate_solution(soln, cost, exp_soln, exp_cost):
+    assert soln == exp_soln
+    assert cost == exp_cost
+
+
+def validate_symmetric_solution(soln, cost, exp_soln, exp_cost):
+    assert soln == exp_soln or soln == exp_soln[::-1]
+    assert cost == exp_cost
+
+
+class TestGreedyTSP(TestBase):
+    def test_greedy(self):
+        cycle = nx_app.greedy_tsp(self.DG, source="D")
+        cost = sum(self.DG[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, ["D", "C", "B", "A", "D"], 31.0)
+
+        cycle = nx_app.greedy_tsp(self.DG2, source="D")
+        cost = sum(self.DG2[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, ["D", "C", "B", "A", "D"], 78.0)
+
+        cycle = nx_app.greedy_tsp(self.UG, source="D")
+        cost = sum(self.UG[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, ["D", "C", "B", "A", "D"], 33.0)
+
+        cycle = nx_app.greedy_tsp(self.UG2, source="D")
+        cost = sum(self.UG2[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, ["D", "C", "A", "B", "D"], 27.0)
+
+    def test_not_complete_graph(self):
+        pytest.raises(nx.NetworkXError, nx_app.greedy_tsp, self.incompleteUG)
+        pytest.raises(nx.NetworkXError, nx_app.greedy_tsp, self.incompleteDG)
+
+    def test_not_weighted_graph(self):
+        nx_app.greedy_tsp(self.unweightedUG)
+        nx_app.greedy_tsp(self.unweightedDG)
+
+    def test_two_nodes(self):
+        G = nx.Graph()
+        G.add_weighted_edges_from({(1, 2, 1)})
+        cycle = nx_app.greedy_tsp(G)
+        cost = sum(G[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, [1, 2, 1], 2)
+
+    def test_ignore_selfloops(self):
+        G = nx.complete_graph(5)
+        G.add_edge(3, 3)
+        cycle = nx_app.greedy_tsp(G)
+        assert len(cycle) - 1 == len(G) == len(set(cycle))
+
+
+class TestSimulatedAnnealingTSP(TestBase):
+    tsp = staticmethod(nx_app.simulated_annealing_tsp)
+
+    def test_simulated_annealing_directed(self):
+        cycle = self.tsp(self.DG, "greedy", source="D", seed=42)
+        cost = sum(self.DG[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, self.DG_cycle, self.DG_cost)
+
+        initial_sol = ["D", "B", "A", "C", "D"]
+        cycle = self.tsp(self.DG, initial_sol, source="D", seed=42)
+        cost = sum(self.DG[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, self.DG_cycle, self.DG_cost)
+
+        initial_sol = ["D", "A", "C", "B", "D"]
+        cycle = self.tsp(self.DG, initial_sol, move="1-0", source="D", seed=42)
+        cost = sum(self.DG[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, self.DG_cycle, self.DG_cost)
+
+        cycle = self.tsp(self.DG2, "greedy", source="D", seed=42)
+        cost = sum(self.DG2[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, self.DG2_cycle, self.DG2_cost)
+
+        cycle = self.tsp(self.DG2, "greedy", move="1-0", source="D", seed=42)
+        cost = sum(self.DG2[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, self.DG2_cycle, self.DG2_cost)
+
+    def test_simulated_annealing_undirected(self):
+        cycle = self.tsp(self.UG, "greedy", source="D", seed=42)
+        cost = sum(self.UG[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, self.UG_cycle, self.UG_cost)
+
+        cycle = self.tsp(self.UG2, "greedy", source="D", seed=42)
+        cost = sum(self.UG2[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_symmetric_solution(cycle, cost, self.UG2_cycle, self.UG2_cost)
+
+        cycle = self.tsp(self.UG2, "greedy", move="1-0", source="D", seed=42)
+        cost = sum(self.UG2[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_symmetric_solution(cycle, cost, self.UG2_cycle, self.UG2_cost)
+
+    def test_error_on_input_order_mistake(self):
+        # see issue #4846 https://github.com/networkx/networkx/issues/4846
+        pytest.raises(TypeError, self.tsp, self.UG, weight="weight")
+        pytest.raises(nx.NetworkXError, self.tsp, self.UG, "weight")
+
+    def test_not_complete_graph(self):
+        pytest.raises(nx.NetworkXError, self.tsp, self.incompleteUG, "greedy", source=0)
+        pytest.raises(nx.NetworkXError, self.tsp, self.incompleteDG, "greedy", source=0)
+
+    def test_ignore_selfloops(self):
+        G = nx.complete_graph(5)
+        G.add_edge(3, 3)
+        cycle = self.tsp(G, "greedy")
+        assert len(cycle) - 1 == len(G) == len(set(cycle))
+
+    def test_not_weighted_graph(self):
+        self.tsp(self.unweightedUG, "greedy")
+        self.tsp(self.unweightedDG, "greedy")
+
+    def test_two_nodes(self):
+        G = nx.Graph()
+        G.add_weighted_edges_from({(1, 2, 1)})
+
+        cycle = self.tsp(G, "greedy", source=1, seed=42)
+        cost = sum(G[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, [1, 2, 1], 2)
+
+        cycle = self.tsp(G, [1, 2, 1], source=1, seed=42)
+        cost = sum(G[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        validate_solution(cycle, cost, [1, 2, 1], 2)
+
+    def test_failure_of_costs_too_high_when_iterations_low(self):
+        # Simulated Annealing Version:
+        # set number of moves low and alpha high
+        cycle = self.tsp(
+            self.DG2, "greedy", source="D", move="1-0", alpha=1, N_inner=1, seed=42
+        )
+        cost = sum(self.DG2[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        print(cycle, cost)
+        assert cost > self.DG2_cost
+
+        # Try with an incorrect initial guess
+        initial_sol = ["D", "A", "B", "C", "D"]
+        cycle = self.tsp(
+            self.DG,
+            initial_sol,
+            source="D",
+            move="1-0",
+            alpha=0.1,
+            N_inner=1,
+            max_iterations=1,
+            seed=42,
+        )
+        cost = sum(self.DG[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        print(cycle, cost)
+        assert cost > self.DG_cost
+
+
+class TestThresholdAcceptingTSP(TestSimulatedAnnealingTSP):
+    tsp = staticmethod(nx_app.threshold_accepting_tsp)
+
+    def test_failure_of_costs_too_high_when_iterations_low(self):
+        # Threshold Version:
+        # set number of moves low and number of iterations low
+        cycle = self.tsp(
+            self.DG2,
+            "greedy",
+            source="D",
+            move="1-0",
+            N_inner=1,
+            max_iterations=1,
+            seed=4,
+        )
+        cost = sum(self.DG2[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        assert cost > self.DG2_cost
+
+        # set threshold too low
+        initial_sol = ["D", "A", "B", "C", "D"]
+        cycle = self.tsp(
+            self.DG, initial_sol, source="D", move="1-0", threshold=-3, seed=42
+        )
+        cost = sum(self.DG[n][nbr]["weight"] for n, nbr in pairwise(cycle))
+        assert cost > self.DG_cost
+
+
+# Tests for function traveling_salesman_problem
+def test_TSP_method():
+    G = nx.cycle_graph(9)
+    G[4][5]["weight"] = 10
+
+    # Test using the old currying method
+    sa_tsp = lambda G, weight: nx_app.simulated_annealing_tsp(
+        G, "greedy", weight, source=4, seed=1
+    )
+
+    path = nx_app.traveling_salesman_problem(
+        G,
+        method=sa_tsp,
+        cycle=False,
+    )
+    print(path)
+    assert path == [4, 3, 2, 1, 0, 8, 7, 6, 5]
+
+
+def test_TSP_unweighted():
+    G = nx.cycle_graph(9)
+    path = nx_app.traveling_salesman_problem(G, nodes=[3, 6], cycle=False)
+    assert path in ([3, 4, 5, 6], [6, 5, 4, 3])
+
+    cycle = nx_app.traveling_salesman_problem(G, nodes=[3, 6])
+    assert cycle in ([3, 4, 5, 6, 5, 4, 3], [6, 5, 4, 3, 4, 5, 6])
+
+
+def test_TSP_weighted():
+    G = nx.cycle_graph(9)
+    G[0][1]["weight"] = 2
+    G[1][2]["weight"] = 2
+    G[2][3]["weight"] = 2
+    G[3][4]["weight"] = 4
+    G[4][5]["weight"] = 5
+    G[5][6]["weight"] = 4
+    G[6][7]["weight"] = 2
+    G[7][8]["weight"] = 2
+    G[8][0]["weight"] = 2
+    tsp = nx_app.traveling_salesman_problem
+
+    # path between 3 and 6
+    expected_paths = ([3, 2, 1, 0, 8, 7, 6], [6, 7, 8, 0, 1, 2, 3])
+    # cycle between 3 and 6
+    expected_cycles = (
+        [3, 2, 1, 0, 8, 7, 6, 7, 8, 0, 1, 2, 3],
+        [6, 7, 8, 0, 1, 2, 3, 2, 1, 0, 8, 7, 6],
+    )
+    # path through all nodes
+    expected_tourpaths = ([5, 6, 7, 8, 0, 1, 2, 3, 4], [4, 3, 2, 1, 0, 8, 7, 6, 5])
+
+    # Check default method
+    cycle = tsp(G, nodes=[3, 6], weight="weight")
+    assert cycle in expected_cycles
+
+    path = tsp(G, nodes=[3, 6], weight="weight", cycle=False)
+    assert path in expected_paths
+
+    tourpath = tsp(G, weight="weight", cycle=False)
+    assert tourpath in expected_tourpaths
+
+    # Check all methods
+    methods = [
+        (nx_app.christofides, {}),
+        (nx_app.greedy_tsp, {}),
+        (
+            nx_app.simulated_annealing_tsp,
+            {"init_cycle": "greedy"},
+        ),
+        (
+            nx_app.threshold_accepting_tsp,
+            {"init_cycle": "greedy"},
+        ),
+    ]
+    for method, kwargs in methods:
+        cycle = tsp(G, nodes=[3, 6], weight="weight", method=method, **kwargs)
+        assert cycle in expected_cycles
+
+        path = tsp(
+            G, nodes=[3, 6], weight="weight", method=method, cycle=False, **kwargs
+        )
+        assert path in expected_paths
+
+        tourpath = tsp(G, weight="weight", method=method, cycle=False, **kwargs)
+        assert tourpath in expected_tourpaths
+
+
+def test_TSP_incomplete_graph_short_path():
+    G = nx.cycle_graph(9)
+    G.add_edges_from([(4, 9), (9, 10), (10, 11), (11, 0)])
+    G[4][5]["weight"] = 5
+
+    cycle = nx_app.traveling_salesman_problem(G)
+    print(cycle)
+    assert len(cycle) == 17 and len(set(cycle)) == 12
+
+    # make sure that cutting one edge out of complete graph formulation
+    # cuts out many edges out of the path of the TSP
+    path = nx_app.traveling_salesman_problem(G, cycle=False)
+    print(path)
+    assert len(path) == 13 and len(set(path)) == 12
+
+
+def test_held_karp_ascent():
+    """
+    Test the Held-Karp relaxation with the ascent method
+    """
+    import networkx.algorithms.approximation.traveling_salesman as tsp
+
+    np = pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+
+    # Adjacency matrix from page 1153 of the 1970 Held and Karp paper
+    # which have been edited to be directional, but also symmetric
+    G_array = np.array(
+        [
+            [0, 97, 60, 73, 17, 52],
+            [97, 0, 41, 52, 90, 30],
+            [60, 41, 0, 21, 35, 41],
+            [73, 52, 21, 0, 95, 46],
+            [17, 90, 35, 95, 0, 81],
+            [52, 30, 41, 46, 81, 0],
+        ]
+    )
+
+    solution_edges = [(1, 3), (2, 4), (3, 2), (4, 0), (5, 1), (0, 5)]
+
+    G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
+    opt_hk, z_star = tsp.held_karp_ascent(G)
+
+    # Check that the optimal weights are the same
+    assert round(opt_hk, 2) == 207.00
+    # Check that the z_stars are the same
+    solution = nx.DiGraph()
+    solution.add_edges_from(solution_edges)
+    assert nx.utils.edges_equal(z_star.edges, solution.edges)
+
+
+def test_ascent_fractional_solution():
+    """
+    Test the ascent method using a modified version of Figure 2 on page 1140
+    in 'The Traveling Salesman Problem and Minimum Spanning Trees' by Held and
+    Karp
+    """
+    import networkx.algorithms.approximation.traveling_salesman as tsp
+
+    np = pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+
+    # This version of Figure 2 has all of the edge weights multiplied by 100
+    # and is a complete directed graph with infinite edge weights for the
+    # edges not listed in the original graph
+    G_array = np.array(
+        [
+            [0, 100, 100, 100000, 100000, 1],
+            [100, 0, 100, 100000, 1, 100000],
+            [100, 100, 0, 1, 100000, 100000],
+            [100000, 100000, 1, 0, 100, 100],
+            [100000, 1, 100000, 100, 0, 100],
+            [1, 100000, 100000, 100, 100, 0],
+        ]
+    )
+
+    solution_z_star = {
+        (0, 1): 5 / 12,
+        (0, 2): 5 / 12,
+        (0, 5): 5 / 6,
+        (1, 0): 5 / 12,
+        (1, 2): 1 / 3,
+        (1, 4): 5 / 6,
+        (2, 0): 5 / 12,
+        (2, 1): 1 / 3,
+        (2, 3): 5 / 6,
+        (3, 2): 5 / 6,
+        (3, 4): 1 / 3,
+        (3, 5): 1 / 2,
+        (4, 1): 5 / 6,
+        (4, 3): 1 / 3,
+        (4, 5): 1 / 2,
+        (5, 0): 5 / 6,
+        (5, 3): 1 / 2,
+        (5, 4): 1 / 2,
+    }
+
+    G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
+    opt_hk, z_star = tsp.held_karp_ascent(G)
+
+    # Check that the optimal weights are the same
+    assert round(opt_hk, 2) == 303.00
+    # Check that the z_stars are the same
+    assert {key: round(z_star[key], 4) for key in z_star} == {
+        key: round(solution_z_star[key], 4) for key in solution_z_star
+    }
+
+
+def test_ascent_method_asymmetric():
+    """
+    Tests the ascent method using a truly asymmetric graph for which the
+    solution has been brute forced
+    """
+    import networkx.algorithms.approximation.traveling_salesman as tsp
+
+    np = pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+
+    G_array = np.array(
+        [
+            [0, 26, 63, 59, 69, 31, 41],
+            [62, 0, 91, 53, 75, 87, 47],
+            [47, 82, 0, 90, 15, 9, 18],
+            [68, 19, 5, 0, 58, 34, 93],
+            [11, 58, 53, 55, 0, 61, 79],
+            [88, 75, 13, 76, 98, 0, 40],
+            [41, 61, 55, 88, 46, 45, 0],
+        ]
+    )
+
+    solution_edges = [(0, 1), (1, 3), (3, 2), (2, 5), (5, 6), (4, 0), (6, 4)]
+
+    G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
+    opt_hk, z_star = tsp.held_karp_ascent(G)
+
+    # Check that the optimal weights are the same
+    assert round(opt_hk, 2) == 190.00
+    # Check that the z_stars match.
+    solution = nx.DiGraph()
+    solution.add_edges_from(solution_edges)
+    assert nx.utils.edges_equal(z_star.edges, solution.edges)
+
+
+def test_ascent_method_asymmetric_2():
+    """
+    Tests the ascent method using a truly asymmetric graph for which the
+    solution has been brute forced
+    """
+    import networkx.algorithms.approximation.traveling_salesman as tsp
+
+    np = pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+
+    G_array = np.array(
+        [
+            [0, 45, 39, 92, 29, 31],
+            [72, 0, 4, 12, 21, 60],
+            [81, 6, 0, 98, 70, 53],
+            [49, 71, 59, 0, 98, 94],
+            [74, 95, 24, 43, 0, 47],
+            [56, 43, 3, 65, 22, 0],
+        ]
+    )
+
+    solution_edges = [(0, 5), (5, 4), (1, 3), (3, 0), (2, 1), (4, 2)]
+
+    G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
+    opt_hk, z_star = tsp.held_karp_ascent(G)
+
+    # Check that the optimal weights are the same
+    assert round(opt_hk, 2) == 144.00
+    # Check that the z_stars match.
+    solution = nx.DiGraph()
+    solution.add_edges_from(solution_edges)
+    assert nx.utils.edges_equal(z_star.edges, solution.edges)
+
+
+def test_held_karp_ascent_asymmetric_3():
+    """
+    Tests the ascent method using a truly asymmetric graph with a fractional
+    solution for which the solution has been brute forced.
+
+    In this graph their are two different optimal, integral solutions (which
+    are also the overall atsp solutions) to the Held Karp relaxation. However,
+    this particular graph has two different tours of optimal value and the
+    possible solutions in the held_karp_ascent function are not stored in an
+    ordered data structure.
+    """
+    import networkx.algorithms.approximation.traveling_salesman as tsp
+
+    np = pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+
+    G_array = np.array(
+        [
+            [0, 1, 5, 2, 7, 4],
+            [7, 0, 7, 7, 1, 4],
+            [4, 7, 0, 9, 2, 1],
+            [7, 2, 7, 0, 4, 4],
+            [5, 5, 4, 4, 0, 3],
+            [3, 9, 1, 3, 4, 0],
+        ]
+    )
+
+    solution1_edges = [(0, 3), (1, 4), (2, 5), (3, 1), (4, 2), (5, 0)]
+
+    solution2_edges = [(0, 3), (3, 1), (1, 4), (4, 5), (2, 0), (5, 2)]
+
+    G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
+    opt_hk, z_star = tsp.held_karp_ascent(G)
+
+    assert round(opt_hk, 2) == 13.00
+    # Check that the z_stars are the same
+    solution1 = nx.DiGraph()
+    solution1.add_edges_from(solution1_edges)
+    solution2 = nx.DiGraph()
+    solution2.add_edges_from(solution2_edges)
+    assert nx.utils.edges_equal(z_star.edges, solution1.edges) or nx.utils.edges_equal(
+        z_star.edges, solution2.edges
+    )
+
+
+def test_held_karp_ascent_fractional_asymmetric():
+    """
+    Tests the ascent method using a truly asymmetric graph with a fractional
+    solution for which the solution has been brute forced
+    """
+    import networkx.algorithms.approximation.traveling_salesman as tsp
+
+    np = pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+
+    G_array = np.array(
+        [
+            [0, 100, 150, 100000, 100000, 1],
+            [150, 0, 100, 100000, 1, 100000],
+            [100, 150, 0, 1, 100000, 100000],
+            [100000, 100000, 1, 0, 150, 100],
+            [100000, 2, 100000, 100, 0, 150],
+            [2, 100000, 100000, 150, 100, 0],
+        ]
+    )
+
+    solution_z_star = {
+        (0, 1): 5 / 12,
+        (0, 2): 5 / 12,
+        (0, 5): 5 / 6,
+        (1, 0): 5 / 12,
+        (1, 2): 5 / 12,
+        (1, 4): 5 / 6,
+        (2, 0): 5 / 12,
+        (2, 1): 5 / 12,
+        (2, 3): 5 / 6,
+        (3, 2): 5 / 6,
+        (3, 4): 5 / 12,
+        (3, 5): 5 / 12,
+        (4, 1): 5 / 6,
+        (4, 3): 5 / 12,
+        (4, 5): 5 / 12,
+        (5, 0): 5 / 6,
+        (5, 3): 5 / 12,
+        (5, 4): 5 / 12,
+    }
+
+    G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
+    opt_hk, z_star = tsp.held_karp_ascent(G)
+
+    # Check that the optimal weights are the same
+    assert round(opt_hk, 2) == 304.00
+    # Check that the z_stars are the same
+    assert {key: round(z_star[key], 4) for key in z_star} == {
+        key: round(solution_z_star[key], 4) for key in solution_z_star
+    }
+
+
+def test_spanning_tree_distribution():
+    """
+    Test that we can create an exponential distribution of spanning trees such
+    that the probability of each tree is proportional to the product of edge
+    weights.
+
+    Results of this test have been confirmed with hypothesis testing from the
+    created distribution.
+
+    This test uses the symmetric, fractional Held Karp solution.
+    """
+    import networkx.algorithms.approximation.traveling_salesman as tsp
+
+    pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+
+    z_star = {
+        (0, 1): 5 / 12,
+        (0, 2): 5 / 12,
+        (0, 5): 5 / 6,
+        (1, 0): 5 / 12,
+        (1, 2): 1 / 3,
+        (1, 4): 5 / 6,
+        (2, 0): 5 / 12,
+        (2, 1): 1 / 3,
+        (2, 3): 5 / 6,
+        (3, 2): 5 / 6,
+        (3, 4): 1 / 3,
+        (3, 5): 1 / 2,
+        (4, 1): 5 / 6,
+        (4, 3): 1 / 3,
+        (4, 5): 1 / 2,
+        (5, 0): 5 / 6,
+        (5, 3): 1 / 2,
+        (5, 4): 1 / 2,
+    }
+
+    solution_gamma = {
+        (0, 1): -0.6383,
+        (0, 2): -0.6827,
+        (0, 5): 0,
+        (1, 2): -1.0781,
+        (1, 4): 0,
+        (2, 3): 0,
+        (5, 3): -0.2820,
+        (5, 4): -0.3327,
+        (4, 3): -0.9927,
+    }
+
+    # The undirected support of z_star
+    G = nx.MultiGraph()
+    for u, v in z_star:
+        if (u, v) in G.edges or (v, u) in G.edges:
+            continue
+        G.add_edge(u, v)
+
+    gamma = tsp.spanning_tree_distribution(G, z_star)
+
+    assert {key: round(gamma[key], 4) for key in gamma} == solution_gamma
+
+
+def test_asadpour_tsp():
+    """
+    Test the complete asadpour tsp algorithm with the fractional, symmetric
+    Held Karp solution. This test also uses an incomplete graph as input.
+    """
+    # This version of Figure 2 has all of the edge weights multiplied by 100
+    # and the 0 weight edges have a weight of 1.
+    pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+
+    edge_list = [
+        (0, 1, 100),
+        (0, 2, 100),
+        (0, 5, 1),
+        (1, 2, 100),
+        (1, 4, 1),
+        (2, 3, 1),
+        (3, 4, 100),
+        (3, 5, 100),
+        (4, 5, 100),
+        (1, 0, 100),
+        (2, 0, 100),
+        (5, 0, 1),
+        (2, 1, 100),
+        (4, 1, 1),
+        (3, 2, 1),
+        (4, 3, 100),
+        (5, 3, 100),
+        (5, 4, 100),
+    ]
+
+    G = nx.DiGraph()
+    G.add_weighted_edges_from(edge_list)
+
+    tour = nx_app.traveling_salesman_problem(
+        G, weight="weight", method=nx_app.asadpour_atsp, seed=19
+    )
+
+    # Check that the returned list is a valid tour. Because this is an
+    # incomplete graph, the conditions are not as strict. We need the tour to
+    #
+    #   Start and end at the same node
+    #   Pass through every vertex at least once
+    #   Have a total cost at most ln(6) / ln(ln(6)) = 3.0723 times the optimal
+    #
+    # For the second condition it is possible to have the tour pass through the
+    # same vertex more then. Imagine that the tour on the complete version takes
+    # an edge not in the original graph. In the output this is substituted with
+    # the shortest path between those vertices, allowing vertices to appear more
+    # than once.
+    #
+    # Even though we are using a fixed seed, multiple tours have been known to
+    # be returned. The first two are from the original development of this test,
+    # and the third one from issue #5913 on GitHub. If other tours are returned,
+    # add it on the list of expected tours.
+    expected_tours = [
+        [1, 4, 5, 0, 2, 3, 2, 1],
+        [3, 2, 0, 1, 4, 5, 3],
+        [3, 2, 1, 0, 5, 4, 3],
+    ]
+
+    assert tour in expected_tours
+
+
+def test_asadpour_real_world():
+    """
+    This test uses airline prices between the six largest cities in the US.
+
+        * New York City -> JFK
+        * Los Angeles -> LAX
+        * Chicago -> ORD
+        * Houston -> IAH
+        * Phoenix -> PHX
+        * Philadelphia -> PHL
+
+    Flight prices from August 2021 using Delta or American airlines to get
+    nonstop flight. The brute force solution found the optimal tour to cost $872
+
+    This test also uses the `source` keyword argument to ensure that the tour
+    always starts at city 0.
+    """
+    np = pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+
+    G_array = np.array(
+        [
+            # JFK  LAX  ORD  IAH  PHX  PHL
+            [0, 243, 199, 208, 169, 183],  # JFK
+            [277, 0, 217, 123, 127, 252],  # LAX
+            [297, 197, 0, 197, 123, 177],  # ORD
+            [303, 169, 197, 0, 117, 117],  # IAH
+            [257, 127, 160, 117, 0, 319],  # PHX
+            [183, 332, 217, 117, 319, 0],  # PHL
+        ]
+    )
+
+    node_list = ["JFK", "LAX", "ORD", "IAH", "PHX", "PHL"]
+
+    expected_tours = [
+        ["JFK", "LAX", "PHX", "ORD", "IAH", "PHL", "JFK"],
+        ["JFK", "ORD", "PHX", "LAX", "IAH", "PHL", "JFK"],
+    ]
+
+    G = nx.from_numpy_array(G_array, nodelist=node_list, create_using=nx.DiGraph)
+
+    tour = nx_app.traveling_salesman_problem(
+        G, weight="weight", method=nx_app.asadpour_atsp, seed=37, source="JFK"
+    )
+
+    assert tour in expected_tours
+
+
+def test_asadpour_real_world_path():
+    """
+    This test uses airline prices between the six largest cities in the US. This
+    time using a path, not a cycle.
+
+        * New York City -> JFK
+        * Los Angeles -> LAX
+        * Chicago -> ORD
+        * Houston -> IAH
+        * Phoenix -> PHX
+        * Philadelphia -> PHL
+
+    Flight prices from August 2021 using Delta or American airlines to get
+    nonstop flight. The brute force solution found the optimal tour to cost $872
+    """
+    np = pytest.importorskip("numpy")
+    pytest.importorskip("scipy")
+
+    G_array = np.array(
+        [
+            # JFK  LAX  ORD  IAH  PHX  PHL
+            [0, 243, 199, 208, 169, 183],  # JFK
+            [277, 0, 217, 123, 127, 252],  # LAX
+            [297, 197, 0, 197, 123, 177],  # ORD
+            [303, 169, 197, 0, 117, 117],  # IAH
+            [257, 127, 160, 117, 0, 319],  # PHX
+            [183, 332, 217, 117, 319, 0],  # PHL
+        ]
+    )
+
+    node_list = ["JFK", "LAX", "ORD", "IAH", "PHX", "PHL"]
+
+    expected_paths = [
+        ["ORD", "PHX", "LAX", "IAH", "PHL", "JFK"],
+        ["JFK", "PHL", "IAH", "ORD", "PHX", "LAX"],
+    ]
+
+    G = nx.from_numpy_array(G_array, nodelist=node_list, create_using=nx.DiGraph)
+
+    path = nx_app.traveling_salesman_problem(
+        G, weight="weight", cycle=False, method=nx_app.asadpour_atsp, seed=56
+    )
+
+    assert path in expected_paths
+
+
+def test_asadpour_disconnected_graph():
+    """
+    Test that the proper exception is raised when asadpour_atsp is given an
+    disconnected graph.
+    """
+
+    G = nx.complete_graph(4, create_using=nx.DiGraph)
+    # have to set edge weights so that if the exception is not raised, the
+    # function will complete and we will fail the test
+    nx.set_edge_attributes(G, 1, "weight")
+    G.add_node(5)
+
+    pytest.raises(nx.NetworkXError, nx_app.asadpour_atsp, G)
+
+
+def test_asadpour_incomplete_graph():
+    """
+    Test that the proper exception is raised when asadpour_atsp is given an
+    incomplete graph
+    """
+
+    G = nx.complete_graph(4, create_using=nx.DiGraph)
+    # have to set edge weights so that if the exception is not raised, the
+    # function will complete and we will fail the test
+    nx.set_edge_attributes(G, 1, "weight")
+    G.remove_edge(0, 1)
+
+    pytest.raises(nx.NetworkXError, nx_app.asadpour_atsp, G)
+
+
+def test_asadpour_empty_graph():
+    """
+    Test the asadpour_atsp function with an empty graph
+    """
+    G = nx.DiGraph()
+
+    pytest.raises(nx.NetworkXError, nx_app.asadpour_atsp, G)
+
+
+@pytest.mark.slow
+def test_asadpour_integral_held_karp():
+    """
+    This test uses an integral held karp solution and the held karp function
+    will return a graph rather than a dict, bypassing most of the asadpour
+    algorithm.
+
+    At first glance, this test probably doesn't look like it ensures that we
+    skip the rest of the asadpour algorithm, but it does. We are not fixing a
+    see for the random number generator, so if we sample any spanning trees
+    the approximation would be different basically every time this test is
+    executed but it is not since held karp is deterministic and we do not
+    reach the portion of the code with the dependence on random numbers.
+    """
+    np = pytest.importorskip("numpy")
+
+    G_array = np.array(
+        [
+            [0, 26, 63, 59, 69, 31, 41],
+            [62, 0, 91, 53, 75, 87, 47],
+            [47, 82, 0, 90, 15, 9, 18],
+            [68, 19, 5, 0, 58, 34, 93],
+            [11, 58, 53, 55, 0, 61, 79],
+            [88, 75, 13, 76, 98, 0, 40],
+            [41, 61, 55, 88, 46, 45, 0],
+        ]
+    )
+
+    G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
+
+    for _ in range(2):
+        tour = nx_app.traveling_salesman_problem(G, method=nx_app.asadpour_atsp)
+
+        assert [1, 3, 2, 5, 2, 6, 4, 0, 1] == tour
+
+
+def test_directed_tsp_impossible():
+    """
+    Test the asadpour algorithm with a graph without a hamiltonian circuit
+    """
+    pytest.importorskip("numpy")
+
+    # In this graph, once we leave node 0 we cannot return
+    edges = [
+        (0, 1, 10),
+        (0, 2, 11),
+        (0, 3, 12),
+        (1, 2, 4),
+        (1, 3, 6),
+        (2, 1, 3),
+        (2, 3, 2),
+        (3, 1, 5),
+        (3, 2, 1),
+    ]
+
+    G = nx.DiGraph()
+    G.add_weighted_edges_from(edges)
+
+    pytest.raises(nx.NetworkXError, nx_app.traveling_salesman_problem, G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_treewidth.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_treewidth.py
new file mode 100644
index 00000000..461b0f2e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_treewidth.py
@@ -0,0 +1,280 @@
+import itertools
+
+import networkx as nx
+from networkx.algorithms.approximation import (
+    treewidth_min_degree,
+    treewidth_min_fill_in,
+)
+from networkx.algorithms.approximation.treewidth import (
+    MinDegreeHeuristic,
+    min_fill_in_heuristic,
+)
+
+
+def is_tree_decomp(graph, decomp):
+    """Check if the given tree decomposition is valid."""
+    for x in graph.nodes():
+        appear_once = False
+        for bag in decomp.nodes():
+            if x in bag:
+                appear_once = True
+                break
+        assert appear_once
+
+    # Check if each connected pair of nodes are at least once together in a bag
+    for x, y in graph.edges():
+        appear_together = False
+        for bag in decomp.nodes():
+            if x in bag and y in bag:
+                appear_together = True
+                break
+        assert appear_together
+
+    # Check if the nodes associated with vertex v form a connected subset of T
+    for v in graph.nodes():
+        subset = []
+        for bag in decomp.nodes():
+            if v in bag:
+                subset.append(bag)
+        sub_graph = decomp.subgraph(subset)
+        assert nx.is_connected(sub_graph)
+
+
+class TestTreewidthMinDegree:
+    """Unit tests for the min_degree function"""
+
+    @classmethod
+    def setup_class(cls):
+        """Setup for different kinds of trees"""
+        cls.complete = nx.Graph()
+        cls.complete.add_edge(1, 2)
+        cls.complete.add_edge(2, 3)
+        cls.complete.add_edge(1, 3)
+
+        cls.small_tree = nx.Graph()
+        cls.small_tree.add_edge(1, 3)
+        cls.small_tree.add_edge(4, 3)
+        cls.small_tree.add_edge(2, 3)
+        cls.small_tree.add_edge(3, 5)
+        cls.small_tree.add_edge(5, 6)
+        cls.small_tree.add_edge(5, 7)
+        cls.small_tree.add_edge(6, 7)
+
+        cls.deterministic_graph = nx.Graph()
+        cls.deterministic_graph.add_edge(0, 1)  # deg(0) = 1
+
+        cls.deterministic_graph.add_edge(1, 2)  # deg(1) = 2
+
+        cls.deterministic_graph.add_edge(2, 3)
+        cls.deterministic_graph.add_edge(2, 4)  # deg(2) = 3
+
+        cls.deterministic_graph.add_edge(3, 4)
+        cls.deterministic_graph.add_edge(3, 5)
+        cls.deterministic_graph.add_edge(3, 6)  # deg(3) = 4
+
+        cls.deterministic_graph.add_edge(4, 5)
+        cls.deterministic_graph.add_edge(4, 6)
+        cls.deterministic_graph.add_edge(4, 7)  # deg(4) = 5
+
+        cls.deterministic_graph.add_edge(5, 6)
+        cls.deterministic_graph.add_edge(5, 7)
+        cls.deterministic_graph.add_edge(5, 8)
+        cls.deterministic_graph.add_edge(5, 9)  # deg(5) = 6
+
+        cls.deterministic_graph.add_edge(6, 7)
+        cls.deterministic_graph.add_edge(6, 8)
+        cls.deterministic_graph.add_edge(6, 9)  # deg(6) = 6
+
+        cls.deterministic_graph.add_edge(7, 8)
+        cls.deterministic_graph.add_edge(7, 9)  # deg(7) = 5
+
+        cls.deterministic_graph.add_edge(8, 9)  # deg(8) = 4
+
+    def test_petersen_graph(self):
+        """Test Petersen graph tree decomposition result"""
+        G = nx.petersen_graph()
+        _, decomp = treewidth_min_degree(G)
+        is_tree_decomp(G, decomp)
+
+    def test_small_tree_treewidth(self):
+        """Test small tree
+
+        Test if the computed treewidth of the known self.small_tree is 2.
+        As we know which value we can expect from our heuristic, values other
+        than two are regressions
+        """
+        G = self.small_tree
+        # the order of removal should be [1,2,4]3[5,6,7]
+        # (with [] denoting any order of the containing nodes)
+        # resulting in treewidth 2 for the heuristic
+        treewidth, _ = treewidth_min_fill_in(G)
+        assert treewidth == 2
+
+    def test_heuristic_abort(self):
+        """Test heuristic abort condition for fully connected graph"""
+        graph = {}
+        for u in self.complete:
+            graph[u] = set()
+            for v in self.complete[u]:
+                if u != v:  # ignore self-loop
+                    graph[u].add(v)
+
+        deg_heuristic = MinDegreeHeuristic(graph)
+        node = deg_heuristic.best_node(graph)
+        if node is None:
+            pass
+        else:
+            assert False
+
+    def test_empty_graph(self):
+        """Test empty graph"""
+        G = nx.Graph()
+        _, _ = treewidth_min_degree(G)
+
+    def test_two_component_graph(self):
+        G = nx.Graph()
+        G.add_node(1)
+        G.add_node(2)
+        treewidth, _ = treewidth_min_degree(G)
+        assert treewidth == 0
+
+    def test_not_sortable_nodes(self):
+        G = nx.Graph([(0, "a")])
+        treewidth_min_degree(G)
+
+    def test_heuristic_first_steps(self):
+        """Test first steps of min_degree heuristic"""
+        graph = {
+            n: set(self.deterministic_graph[n]) - {n} for n in self.deterministic_graph
+        }
+        deg_heuristic = MinDegreeHeuristic(graph)
+        elim_node = deg_heuristic.best_node(graph)
+        print(f"Graph {graph}:")
+        steps = []
+
+        while elim_node is not None:
+            print(f"Removing {elim_node}:")
+            steps.append(elim_node)
+            nbrs = graph[elim_node]
+
+            for u, v in itertools.permutations(nbrs, 2):
+                if v not in graph[u]:
+                    graph[u].add(v)
+
+            for u in graph:
+                if elim_node in graph[u]:
+                    graph[u].remove(elim_node)
+
+            del graph[elim_node]
+            print(f"Graph {graph}:")
+            elim_node = deg_heuristic.best_node(graph)
+
+        # check only the first 5 elements for equality
+        assert steps[:5] == [0, 1, 2, 3, 4]
+
+
+class TestTreewidthMinFillIn:
+    """Unit tests for the treewidth_min_fill_in function."""
+
+    @classmethod
+    def setup_class(cls):
+        """Setup for different kinds of trees"""
+        cls.complete = nx.Graph()
+        cls.complete.add_edge(1, 2)
+        cls.complete.add_edge(2, 3)
+        cls.complete.add_edge(1, 3)
+
+        cls.small_tree = nx.Graph()
+        cls.small_tree.add_edge(1, 2)
+        cls.small_tree.add_edge(2, 3)
+        cls.small_tree.add_edge(3, 4)
+        cls.small_tree.add_edge(1, 4)
+        cls.small_tree.add_edge(2, 4)
+        cls.small_tree.add_edge(4, 5)
+        cls.small_tree.add_edge(5, 6)
+        cls.small_tree.add_edge(5, 7)
+        cls.small_tree.add_edge(6, 7)
+
+        cls.deterministic_graph = nx.Graph()
+        cls.deterministic_graph.add_edge(1, 2)
+        cls.deterministic_graph.add_edge(1, 3)
+        cls.deterministic_graph.add_edge(3, 4)
+        cls.deterministic_graph.add_edge(2, 4)
+        cls.deterministic_graph.add_edge(3, 5)
+        cls.deterministic_graph.add_edge(4, 5)
+        cls.deterministic_graph.add_edge(3, 6)
+        cls.deterministic_graph.add_edge(5, 6)
+
+    def test_petersen_graph(self):
+        """Test Petersen graph tree decomposition result"""
+        G = nx.petersen_graph()
+        _, decomp = treewidth_min_fill_in(G)
+        is_tree_decomp(G, decomp)
+
+    def test_small_tree_treewidth(self):
+        """Test if the computed treewidth of the known self.small_tree is 2"""
+        G = self.small_tree
+        # the order of removal should be [1,2,4]3[5,6,7]
+        # (with [] denoting any order of the containing nodes)
+        # resulting in treewidth 2 for the heuristic
+        treewidth, _ = treewidth_min_fill_in(G)
+        assert treewidth == 2
+
+    def test_heuristic_abort(self):
+        """Test if min_fill_in returns None for fully connected graph"""
+        graph = {}
+        for u in self.complete:
+            graph[u] = set()
+            for v in self.complete[u]:
+                if u != v:  # ignore self-loop
+                    graph[u].add(v)
+        next_node = min_fill_in_heuristic(graph)
+        if next_node is None:
+            pass
+        else:
+            assert False
+
+    def test_empty_graph(self):
+        """Test empty graph"""
+        G = nx.Graph()
+        _, _ = treewidth_min_fill_in(G)
+
+    def test_two_component_graph(self):
+        G = nx.Graph()
+        G.add_node(1)
+        G.add_node(2)
+        treewidth, _ = treewidth_min_fill_in(G)
+        assert treewidth == 0
+
+    def test_not_sortable_nodes(self):
+        G = nx.Graph([(0, "a")])
+        treewidth_min_fill_in(G)
+
+    def test_heuristic_first_steps(self):
+        """Test first steps of min_fill_in heuristic"""
+        graph = {
+            n: set(self.deterministic_graph[n]) - {n} for n in self.deterministic_graph
+        }
+        print(f"Graph {graph}:")
+        elim_node = min_fill_in_heuristic(graph)
+        steps = []
+
+        while elim_node is not None:
+            print(f"Removing {elim_node}:")
+            steps.append(elim_node)
+            nbrs = graph[elim_node]
+
+            for u, v in itertools.permutations(nbrs, 2):
+                if v not in graph[u]:
+                    graph[u].add(v)
+
+            for u in graph:
+                if elim_node in graph[u]:
+                    graph[u].remove(elim_node)
+
+            del graph[elim_node]
+            print(f"Graph {graph}:")
+            elim_node = min_fill_in_heuristic(graph)
+
+        # check only the first 2 elements for equality
+        assert steps[:2] == [6, 5]
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_vertex_cover.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_vertex_cover.py
new file mode 100644
index 00000000..5cc5a38d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests/test_vertex_cover.py
@@ -0,0 +1,68 @@
+import networkx as nx
+from networkx.algorithms.approximation import min_weighted_vertex_cover
+
+
+def is_cover(G, node_cover):
+    return all({u, v} & node_cover for u, v in G.edges())
+
+
+class TestMWVC:
+    """Unit tests for the approximate minimum weighted vertex cover
+    function,
+    :func:`~networkx.algorithms.approximation.vertex_cover.min_weighted_vertex_cover`.
+
+    """
+
+    def test_unweighted_directed(self):
+        # Create a star graph in which half the nodes are directed in
+        # and half are directed out.
+        G = nx.DiGraph()
+        G.add_edges_from((0, v) for v in range(1, 26))
+        G.add_edges_from((v, 0) for v in range(26, 51))
+        cover = min_weighted_vertex_cover(G)
+        assert 1 == len(cover)
+        assert is_cover(G, cover)
+
+    def test_unweighted_undirected(self):
+        # create a simple star graph
+        size = 50
+        sg = nx.star_graph(size)
+        cover = min_weighted_vertex_cover(sg)
+        assert 1 == len(cover)
+        assert is_cover(sg, cover)
+
+    def test_weighted(self):
+        wg = nx.Graph()
+        wg.add_node(0, weight=10)
+        wg.add_node(1, weight=1)
+        wg.add_node(2, weight=1)
+        wg.add_node(3, weight=1)
+        wg.add_node(4, weight=1)
+
+        wg.add_edge(0, 1)
+        wg.add_edge(0, 2)
+        wg.add_edge(0, 3)
+        wg.add_edge(0, 4)
+
+        wg.add_edge(1, 2)
+        wg.add_edge(2, 3)
+        wg.add_edge(3, 4)
+        wg.add_edge(4, 1)
+
+        cover = min_weighted_vertex_cover(wg, weight="weight")
+        csum = sum(wg.nodes[node]["weight"] for node in cover)
+        assert 4 == csum
+        assert is_cover(wg, cover)
+
+    def test_unweighted_self_loop(self):
+        slg = nx.Graph()
+        slg.add_node(0)
+        slg.add_node(1)
+        slg.add_node(2)
+
+        slg.add_edge(0, 1)
+        slg.add_edge(2, 2)
+
+        cover = min_weighted_vertex_cover(slg)
+        assert 2 == len(cover)
+        assert is_cover(slg, cover)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py
new file mode 100644
index 00000000..2080c99a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py
@@ -0,0 +1,1501 @@
+"""
+=================================
+Travelling Salesman Problem (TSP)
+=================================
+
+Implementation of approximate algorithms
+for solving and approximating the TSP problem.
+
+Categories of algorithms which are implemented:
+
+- Christofides (provides a 3/2-approximation of TSP)
+- Greedy
+- Simulated Annealing (SA)
+- Threshold Accepting (TA)
+- Asadpour Asymmetric Traveling Salesman Algorithm
+
+The Travelling Salesman Problem tries to find, given the weight
+(distance) between all points where a salesman has to visit, the
+route so that:
+
+- The total distance (cost) which the salesman travels is minimized.
+- The salesman returns to the starting point.
+- Note that for a complete graph, the salesman visits each point once.
+
+The function `travelling_salesman_problem` allows for incomplete
+graphs by finding all-pairs shortest paths, effectively converting
+the problem to a complete graph problem. It calls one of the
+approximate methods on that problem and then converts the result
+back to the original graph using the previously found shortest paths.
+
+TSP is an NP-hard problem in combinatorial optimization,
+important in operations research and theoretical computer science.
+
+http://en.wikipedia.org/wiki/Travelling_salesman_problem
+"""
+
+import math
+
+import networkx as nx
+from networkx.algorithms.tree.mst import random_spanning_tree
+from networkx.utils import not_implemented_for, pairwise, py_random_state
+
+__all__ = [
+    "traveling_salesman_problem",
+    "christofides",
+    "asadpour_atsp",
+    "greedy_tsp",
+    "simulated_annealing_tsp",
+    "threshold_accepting_tsp",
+]
+
+
+def swap_two_nodes(soln, seed):
+    """Swap two nodes in `soln` to give a neighbor solution.
+
+    Parameters
+    ----------
+    soln : list of nodes
+        Current cycle of nodes
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    Returns
+    -------
+    list
+        The solution after move is applied. (A neighbor solution.)
+
+    Notes
+    -----
+        This function assumes that the incoming list `soln` is a cycle
+        (that the first and last element are the same) and also that
+        we don't want any move to change the first node in the list
+        (and thus not the last node either).
+
+        The input list is changed as well as returned. Make a copy if needed.
+
+    See Also
+    --------
+        move_one_node
+    """
+    a, b = seed.sample(range(1, len(soln) - 1), k=2)
+    soln[a], soln[b] = soln[b], soln[a]
+    return soln
+
+
+def move_one_node(soln, seed):
+    """Move one node to another position to give a neighbor solution.
+
+    The node to move and the position to move to are chosen randomly.
+    The first and last nodes are left untouched as soln must be a cycle
+    starting at that node.
+
+    Parameters
+    ----------
+    soln : list of nodes
+        Current cycle of nodes
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    Returns
+    -------
+    list
+        The solution after move is applied. (A neighbor solution.)
+
+    Notes
+    -----
+        This function assumes that the incoming list `soln` is a cycle
+        (that the first and last element are the same) and also that
+        we don't want any move to change the first node in the list
+        (and thus not the last node either).
+
+        The input list is changed as well as returned. Make a copy if needed.
+
+    See Also
+    --------
+        swap_two_nodes
+    """
+    a, b = seed.sample(range(1, len(soln) - 1), k=2)
+    soln.insert(b, soln.pop(a))
+    return soln
+
+
+@not_implemented_for("directed")
+@nx._dispatchable(edge_attrs="weight")
+def christofides(G, weight="weight", tree=None):
+    """Approximate a solution of the traveling salesman problem
+
+    Compute a 3/2-approximation of the traveling salesman problem
+    in a complete undirected graph using Christofides [1]_ algorithm.
+
+    Parameters
+    ----------
+    G : Graph
+        `G` should be a complete weighted undirected graph.
+        The distance between all pairs of nodes should be included.
+
+    weight : string, optional (default="weight")
+        Edge data key corresponding to the edge weight.
+        If any edge does not have this attribute the weight is set to 1.
+
+    tree : NetworkX graph or None (default: None)
+        A minimum spanning tree of G. Or, if None, the minimum spanning
+        tree is computed using :func:`networkx.minimum_spanning_tree`
+
+    Returns
+    -------
+    list
+        List of nodes in `G` along a cycle with a 3/2-approximation of
+        the minimal Hamiltonian cycle.
+
+    References
+    ----------
+    .. [1] Christofides, Nicos. "Worst-case analysis of a new heuristic for
+       the travelling salesman problem." No. RR-388. Carnegie-Mellon Univ
+       Pittsburgh Pa Management Sciences Research Group, 1976.
+    """
+    # Remove selfloops if necessary
+    loop_nodes = nx.nodes_with_selfloops(G)
+    try:
+        node = next(loop_nodes)
+    except StopIteration:
+        pass
+    else:
+        G = G.copy()
+        G.remove_edge(node, node)
+        G.remove_edges_from((n, n) for n in loop_nodes)
+    # Check that G is a complete graph
+    N = len(G) - 1
+    # This check ignores selfloops which is what we want here.
+    if any(len(nbrdict) != N for n, nbrdict in G.adj.items()):
+        raise nx.NetworkXError("G must be a complete graph.")
+
+    if tree is None:
+        tree = nx.minimum_spanning_tree(G, weight=weight)
+    L = G.copy()
+    L.remove_nodes_from([v for v, degree in tree.degree if not (degree % 2)])
+    MG = nx.MultiGraph()
+    MG.add_edges_from(tree.edges)
+    edges = nx.min_weight_matching(L, weight=weight)
+    MG.add_edges_from(edges)
+    return _shortcutting(nx.eulerian_circuit(MG))
+
+
+def _shortcutting(circuit):
+    """Remove duplicate nodes in the path"""
+    nodes = []
+    for u, v in circuit:
+        if v in nodes:
+            continue
+        if not nodes:
+            nodes.append(u)
+        nodes.append(v)
+    nodes.append(nodes[0])
+    return nodes
+
+
+@nx._dispatchable(edge_attrs="weight")
+def traveling_salesman_problem(
+    G, weight="weight", nodes=None, cycle=True, method=None, **kwargs
+):
+    """Find the shortest path in `G` connecting specified nodes
+
+    This function allows approximate solution to the traveling salesman
+    problem on networks that are not complete graphs and/or where the
+    salesman does not need to visit all nodes.
+
+    This function proceeds in two steps. First, it creates a complete
+    graph using the all-pairs shortest_paths between nodes in `nodes`.
+    Edge weights in the new graph are the lengths of the paths
+    between each pair of nodes in the original graph.
+    Second, an algorithm (default: `christofides` for undirected and
+    `asadpour_atsp` for directed) is used to approximate the minimal Hamiltonian
+    cycle on this new graph. The available algorithms are:
+
+     - christofides
+     - greedy_tsp
+     - simulated_annealing_tsp
+     - threshold_accepting_tsp
+     - asadpour_atsp
+
+    Once the Hamiltonian Cycle is found, this function post-processes to
+    accommodate the structure of the original graph. If `cycle` is ``False``,
+    the biggest weight edge is removed to make a Hamiltonian path.
+    Then each edge on the new complete graph used for that analysis is
+    replaced by the shortest_path between those nodes on the original graph.
+    If the input graph `G` includes edges with weights that do not adhere to
+    the triangle inequality, such as when `G` is not a complete graph (i.e
+    length of non-existent edges is infinity), then the returned path may
+    contain some repeating nodes (other than the starting node).
+
+    Parameters
+    ----------
+    G : NetworkX graph
+        A possibly weighted graph
+
+    nodes : collection of nodes (default=G.nodes)
+        collection (list, set, etc.) of nodes to visit
+
+    weight : string, optional (default="weight")
+        Edge data key corresponding to the edge weight.
+        If any edge does not have this attribute the weight is set to 1.
+
+    cycle : bool (default: True)
+        Indicates whether a cycle should be returned, or a path.
+        Note: the cycle is the approximate minimal cycle.
+        The path simply removes the biggest edge in that cycle.
+
+    method : function (default: None)
+        A function that returns a cycle on all nodes and approximates
+        the solution to the traveling salesman problem on a complete
+        graph. The returned cycle is then used to find a corresponding
+        solution on `G`. `method` should be callable; take inputs
+        `G`, and `weight`; and return a list of nodes along the cycle.
+
+        Provided options include :func:`christofides`, :func:`greedy_tsp`,
+        :func:`simulated_annealing_tsp` and :func:`threshold_accepting_tsp`.
+
+        If `method is None`: use :func:`christofides` for undirected `G` and
+        :func:`asadpour_atsp` for directed `G`.
+
+    **kwargs : dict
+        Other keyword arguments to be passed to the `method` function passed in.
+
+    Returns
+    -------
+    list
+        List of nodes in `G` along a path with an approximation of the minimal
+        path through `nodes`.
+
+    Raises
+    ------
+    NetworkXError
+        If `G` is a directed graph it has to be strongly connected or the
+        complete version cannot be generated.
+
+    Examples
+    --------
+    >>> tsp = nx.approximation.traveling_salesman_problem
+    >>> G = nx.cycle_graph(9)
+    >>> G[4][5]["weight"] = 5  # all other weights are 1
+    >>> tsp(G, nodes=[3, 6])
+    [3, 2, 1, 0, 8, 7, 6, 7, 8, 0, 1, 2, 3]
+    >>> path = tsp(G, cycle=False)
+    >>> path in ([4, 3, 2, 1, 0, 8, 7, 6, 5], [5, 6, 7, 8, 0, 1, 2, 3, 4])
+    True
+
+    While no longer required, you can still build (curry) your own function
+    to provide parameter values to the methods.
+
+    >>> SA_tsp = nx.approximation.simulated_annealing_tsp
+    >>> method = lambda G, weight: SA_tsp(G, "greedy", weight=weight, temp=500)
+    >>> path = tsp(G, cycle=False, method=method)
+    >>> path in ([4, 3, 2, 1, 0, 8, 7, 6, 5], [5, 6, 7, 8, 0, 1, 2, 3, 4])
+    True
+
+    Otherwise, pass other keyword arguments directly into the tsp function.
+
+    >>> path = tsp(
+    ...     G,
+    ...     cycle=False,
+    ...     method=nx.approximation.simulated_annealing_tsp,
+    ...     init_cycle="greedy",
+    ...     temp=500,
+    ... )
+    >>> path in ([4, 3, 2, 1, 0, 8, 7, 6, 5], [5, 6, 7, 8, 0, 1, 2, 3, 4])
+    True
+    """
+    if method is None:
+        if G.is_directed():
+            method = asadpour_atsp
+        else:
+            method = christofides
+    if nodes is None:
+        nodes = list(G.nodes)
+
+    dist = {}
+    path = {}
+    for n, (d, p) in nx.all_pairs_dijkstra(G, weight=weight):
+        dist[n] = d
+        path[n] = p
+
+    if G.is_directed():
+        # If the graph is not strongly connected, raise an exception
+        if not nx.is_strongly_connected(G):
+            raise nx.NetworkXError("G is not strongly connected")
+        GG = nx.DiGraph()
+    else:
+        GG = nx.Graph()
+    for u in nodes:
+        for v in nodes:
+            if u == v:
+                continue
+            GG.add_edge(u, v, weight=dist[u][v])
+
+    best_GG = method(GG, weight=weight, **kwargs)
+
+    if not cycle:
+        # find and remove the biggest edge
+        (u, v) = max(pairwise(best_GG), key=lambda x: dist[x[0]][x[1]])
+        pos = best_GG.index(u) + 1
+        while best_GG[pos] != v:
+            pos = best_GG[pos:].index(u) + 1
+        best_GG = best_GG[pos:-1] + best_GG[:pos]
+
+    best_path = []
+    for u, v in pairwise(best_GG):
+        best_path.extend(path[u][v][:-1])
+    best_path.append(v)
+    return best_path
+
+
+@not_implemented_for("undirected")
+@py_random_state(2)
+@nx._dispatchable(edge_attrs="weight", mutates_input=True)
+def asadpour_atsp(G, weight="weight", seed=None, source=None):
+    """
+    Returns an approximate solution to the traveling salesman problem.
+
+    This approximate solution is one of the best known approximations for the
+    asymmetric traveling salesman problem developed by Asadpour et al,
+    [1]_. The algorithm first solves the Held-Karp relaxation to find a lower
+    bound for the weight of the cycle. Next, it constructs an exponential
+    distribution of undirected spanning trees where the probability of an
+    edge being in the tree corresponds to the weight of that edge using a
+    maximum entropy rounding scheme. Next we sample that distribution
+    $2 \\lceil \\ln n \\rceil$ times and save the minimum sampled tree once the
+    direction of the arcs is added back to the edges. Finally, we augment
+    then short circuit that graph to find the approximate tour for the
+    salesman.
+
+    Parameters
+    ----------
+    G : nx.DiGraph
+        The graph should be a complete weighted directed graph. The
+        distance between all paris of nodes should be included and the triangle
+        inequality should hold. That is, the direct edge between any two nodes
+        should be the path of least cost.
+
+    weight : string, optional (default="weight")
+        Edge data key corresponding to the edge weight.
+        If any edge does not have this attribute the weight is set to 1.
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    source : node label (default=`None`)
+        If given, return the cycle starting and ending at the given node.
+
+    Returns
+    -------
+    cycle : list of nodes
+        Returns the cycle (list of nodes) that a salesman can follow to minimize
+        the total weight of the trip.
+
+    Raises
+    ------
+    NetworkXError
+        If `G` is not complete or has less than two nodes, the algorithm raises
+        an exception.
+
+    NetworkXError
+        If `source` is not `None` and is not a node in `G`, the algorithm raises
+        an exception.
+
+    NetworkXNotImplemented
+        If `G` is an undirected graph.
+
+    References
+    ----------
+    .. [1] A. Asadpour, M. X. Goemans, A. Madry, S. O. Gharan, and A. Saberi,
+       An o(log n/log log n)-approximation algorithm for the asymmetric
+       traveling salesman problem, Operations research, 65 (2017),
+       pp. 1043–1061
+
+    Examples
+    --------
+    >>> import networkx as nx
+    >>> import networkx.algorithms.approximation as approx
+    >>> G = nx.complete_graph(3, create_using=nx.DiGraph)
+    >>> nx.set_edge_attributes(
+    ...     G,
+    ...     {(0, 1): 2, (1, 2): 2, (2, 0): 2, (0, 2): 1, (2, 1): 1, (1, 0): 1},
+    ...     "weight",
+    ... )
+    >>> tour = approx.asadpour_atsp(G, source=0)
+    >>> tour
+    [0, 2, 1, 0]
+    """
+    from math import ceil, exp
+    from math import log as ln
+
+    # Check that G is a complete graph
+    N = len(G) - 1
+    if N < 2:
+        raise nx.NetworkXError("G must have at least two nodes")
+    # This check ignores selfloops which is what we want here.
+    if any(len(nbrdict) - (n in nbrdict) != N for n, nbrdict in G.adj.items()):
+        raise nx.NetworkXError("G is not a complete DiGraph")
+    # Check that the source vertex, if given, is in the graph
+    if source is not None and source not in G.nodes:
+        raise nx.NetworkXError("Given source node not in G.")
+
+    opt_hk, z_star = held_karp_ascent(G, weight)
+
+    # Test to see if the ascent method found an integer solution or a fractional
+    # solution. If it is integral then z_star is a nx.Graph, otherwise it is
+    # a dict
+    if not isinstance(z_star, dict):
+        # Here we are using the shortcutting method to go from the list of edges
+        # returned from eulerian_circuit to a list of nodes
+        return _shortcutting(nx.eulerian_circuit(z_star, source=source))
+
+    # Create the undirected support of z_star
+    z_support = nx.MultiGraph()
+    for u, v in z_star:
+        if (u, v) not in z_support.edges:
+            edge_weight = min(G[u][v][weight], G[v][u][weight])
+            z_support.add_edge(u, v, **{weight: edge_weight})
+
+    # Create the exponential distribution of spanning trees
+    gamma = spanning_tree_distribution(z_support, z_star)
+
+    # Write the lambda values to the edges of z_support
+    z_support = nx.Graph(z_support)
+    lambda_dict = {(u, v): exp(gamma[(u, v)]) for u, v in z_support.edges()}
+    nx.set_edge_attributes(z_support, lambda_dict, "weight")
+    del gamma, lambda_dict
+
+    # Sample 2 * ceil( ln(n) ) spanning trees and record the minimum one
+    minimum_sampled_tree = None
+    minimum_sampled_tree_weight = math.inf
+    for _ in range(2 * ceil(ln(G.number_of_nodes()))):
+        sampled_tree = random_spanning_tree(z_support, "weight", seed=seed)
+        sampled_tree_weight = sampled_tree.size(weight)
+        if sampled_tree_weight < minimum_sampled_tree_weight:
+            minimum_sampled_tree = sampled_tree.copy()
+            minimum_sampled_tree_weight = sampled_tree_weight
+
+    # Orient the edges in that tree to keep the cost of the tree the same.
+    t_star = nx.MultiDiGraph()
+    for u, v, d in minimum_sampled_tree.edges(data=weight):
+        if d == G[u][v][weight]:
+            t_star.add_edge(u, v, **{weight: d})
+        else:
+            t_star.add_edge(v, u, **{weight: d})
+
+    # Find the node demands needed to neutralize the flow of t_star in G
+    node_demands = {n: t_star.out_degree(n) - t_star.in_degree(n) for n in t_star}
+    nx.set_node_attributes(G, node_demands, "demand")
+
+    # Find the min_cost_flow
+    flow_dict = nx.min_cost_flow(G, "demand")
+
+    # Build the flow into t_star
+    for source, values in flow_dict.items():
+        for target in values:
+            if (source, target) not in t_star.edges and values[target] > 0:
+                # IF values[target] > 0 we have to add that many edges
+                for _ in range(values[target]):
+                    t_star.add_edge(source, target)
+
+    # Return the shortcut eulerian circuit
+    circuit = nx.eulerian_circuit(t_star, source=source)
+    return _shortcutting(circuit)
+
+
+@nx._dispatchable(edge_attrs="weight", mutates_input=True, returns_graph=True)
+def held_karp_ascent(G, weight="weight"):
+    """
+    Minimizes the Held-Karp relaxation of the TSP for `G`
+
+    Solves the Held-Karp relaxation of the input complete digraph and scales
+    the output solution for use in the Asadpour [1]_ ASTP algorithm.
+
+    The Held-Karp relaxation defines the lower bound for solutions to the
+    ATSP, although it does return a fractional solution. This is used in the
+    Asadpour algorithm as an initial solution which is later rounded to a
+    integral tree within the spanning tree polytopes. This function solves
+    the relaxation with the branch and bound method in [2]_.
+
+    Parameters
+    ----------
+    G : nx.DiGraph
+        The graph should be a complete weighted directed graph.
+        The distance between all paris of nodes should be included.
+
+    weight : string, optional (default="weight")
+        Edge data key corresponding to the edge weight.
+        If any edge does not have this attribute the weight is set to 1.
+
+    Returns
+    -------
+    OPT : float
+        The cost for the optimal solution to the Held-Karp relaxation
+    z : dict or nx.Graph
+        A symmetrized and scaled version of the optimal solution to the
+        Held-Karp relaxation for use in the Asadpour algorithm.
+
+        If an integral solution is found, then that is an optimal solution for
+        the ATSP problem and that is returned instead.
+
+    References
+    ----------
+    .. [1] A. Asadpour, M. X. Goemans, A. Madry, S. O. Gharan, and A. Saberi,
+       An o(log n/log log n)-approximation algorithm for the asymmetric
+       traveling salesman problem, Operations research, 65 (2017),
+       pp. 1043–1061
+
+    .. [2] M. Held, R. M. Karp, The traveling-salesman problem and minimum
+           spanning trees, Operations Research, 1970-11-01, Vol. 18 (6),
+           pp.1138-1162
+    """
+    import numpy as np
+    from scipy import optimize
+
+    def k_pi():
+        """
+        Find the set of minimum 1-Arborescences for G at point pi.
+
+        Returns
+        -------
+        Set
+            The set of minimum 1-Arborescences
+        """
+        # Create a copy of G without vertex 1.
+        G_1 = G.copy()
+        minimum_1_arborescences = set()
+        minimum_1_arborescence_weight = math.inf
+
+        # node is node '1' in the Held and Karp paper
+        n = next(G.__iter__())
+        G_1.remove_node(n)
+
+        # Iterate over the spanning arborescences of the graph until we know
+        # that we have found the minimum 1-arborescences. My proposed strategy
+        # is to find the most extensive root to connect to from 'node 1' and
+        # the least expensive one. We then iterate over arborescences until
+        # the cost of the basic arborescence is the cost of the minimum one
+        # plus the difference between the most and least expensive roots,
+        # that way the cost of connecting 'node 1' will by definition not by
+        # minimum
+        min_root = {"node": None, weight: math.inf}
+        max_root = {"node": None, weight: -math.inf}
+        for u, v, d in G.edges(n, data=True):
+            if d[weight] < min_root[weight]:
+                min_root = {"node": v, weight: d[weight]}
+            if d[weight] > max_root[weight]:
+                max_root = {"node": v, weight: d[weight]}
+
+        min_in_edge = min(G.in_edges(n, data=True), key=lambda x: x[2][weight])
+        min_root[weight] = min_root[weight] + min_in_edge[2][weight]
+        max_root[weight] = max_root[weight] + min_in_edge[2][weight]
+
+        min_arb_weight = math.inf
+        for arb in nx.ArborescenceIterator(G_1):
+            arb_weight = arb.size(weight)
+            if min_arb_weight == math.inf:
+                min_arb_weight = arb_weight
+            elif arb_weight > min_arb_weight + max_root[weight] - min_root[weight]:
+                break
+            # We have to pick the root node of the arborescence for the out
+            # edge of the first vertex as that is the only node without an
+            # edge directed into it.
+            for N, deg in arb.in_degree:
+                if deg == 0:
+                    # root found
+                    arb.add_edge(n, N, **{weight: G[n][N][weight]})
+                    arb_weight += G[n][N][weight]
+                    break
+
+            # We can pick the minimum weight in-edge for the vertex with
+            # a cycle. If there are multiple edges with the same, minimum
+            # weight, We need to add all of them.
+            #
+            # Delete the edge (N, v) so that we cannot pick it.
+            edge_data = G[N][n]
+            G.remove_edge(N, n)
+            min_weight = min(G.in_edges(n, data=weight), key=lambda x: x[2])[2]
+            min_edges = [
+                (u, v, d) for u, v, d in G.in_edges(n, data=weight) if d == min_weight
+            ]
+            for u, v, d in min_edges:
+                new_arb = arb.copy()
+                new_arb.add_edge(u, v, **{weight: d})
+                new_arb_weight = arb_weight + d
+                # Check to see the weight of the arborescence, if it is a
+                # new minimum, clear all of the old potential minimum
+                # 1-arborescences and add this is the only one. If its
+                # weight is above the known minimum, do not add it.
+                if new_arb_weight < minimum_1_arborescence_weight:
+                    minimum_1_arborescences.clear()
+                    minimum_1_arborescence_weight = new_arb_weight
+                # We have a 1-arborescence, add it to the set
+                if new_arb_weight == minimum_1_arborescence_weight:
+                    minimum_1_arborescences.add(new_arb)
+            G.add_edge(N, n, **edge_data)
+
+        return minimum_1_arborescences
+
+    def direction_of_ascent():
+        """
+        Find the direction of ascent at point pi.
+
+        See [1]_ for more information.
+
+        Returns
+        -------
+        dict
+            A mapping from the nodes of the graph which represents the direction
+            of ascent.
+
+        References
+        ----------
+        .. [1] M. Held, R. M. Karp, The traveling-salesman problem and minimum
+           spanning trees, Operations Research, 1970-11-01, Vol. 18 (6),
+           pp.1138-1162
+        """
+        # 1. Set d equal to the zero n-vector.
+        d = {}
+        for n in G:
+            d[n] = 0
+        del n
+        # 2. Find a 1-Arborescence T^k such that k is in K(pi, d).
+        minimum_1_arborescences = k_pi()
+        while True:
+            # Reduce K(pi) to K(pi, d)
+            # Find the arborescence in K(pi) which increases the lest in
+            # direction d
+            min_k_d_weight = math.inf
+            min_k_d = None
+            for arborescence in minimum_1_arborescences:
+                weighted_cost = 0
+                for n, deg in arborescence.degree:
+                    weighted_cost += d[n] * (deg - 2)
+                if weighted_cost < min_k_d_weight:
+                    min_k_d_weight = weighted_cost
+                    min_k_d = arborescence
+
+            # 3. If sum of d_i * v_{i, k} is greater than zero, terminate
+            if min_k_d_weight > 0:
+                return d, min_k_d
+            # 4. d_i = d_i + v_{i, k}
+            for n, deg in min_k_d.degree:
+                d[n] += deg - 2
+            # Check that we do not need to terminate because the direction
+            # of ascent does not exist. This is done with linear
+            # programming.
+            c = np.full(len(minimum_1_arborescences), -1, dtype=int)
+            a_eq = np.empty((len(G) + 1, len(minimum_1_arborescences)), dtype=int)
+            b_eq = np.zeros(len(G) + 1, dtype=int)
+            b_eq[len(G)] = 1
+            for arb_count, arborescence in enumerate(minimum_1_arborescences):
+                n_count = len(G) - 1
+                for n, deg in arborescence.degree:
+                    a_eq[n_count][arb_count] = deg - 2
+                    n_count -= 1
+                a_eq[len(G)][arb_count] = 1
+            program_result = optimize.linprog(
+                c, A_eq=a_eq, b_eq=b_eq, method="highs-ipm"
+            )
+            # If the constants exist, then the direction of ascent doesn't
+            if program_result.success:
+                # There is no direction of ascent
+                return None, minimum_1_arborescences
+
+            # 5. GO TO 2
+
+    def find_epsilon(k, d):
+        """
+        Given the direction of ascent at pi, find the maximum distance we can go
+        in that direction.
+
+        Parameters
+        ----------
+        k_xy : set
+            The set of 1-arborescences which have the minimum rate of increase
+            in the direction of ascent
+
+        d : dict
+            The direction of ascent
+
+        Returns
+        -------
+        float
+            The distance we can travel in direction `d`
+        """
+        min_epsilon = math.inf
+        for e_u, e_v, e_w in G.edges(data=weight):
+            if (e_u, e_v) in k.edges:
+                continue
+            # Now, I have found a condition which MUST be true for the edges to
+            # be a valid substitute. The edge in the graph which is the
+            # substitute is the one with the same terminated end. This can be
+            # checked rather simply.
+            #
+            # Find the edge within k which is the substitute. Because k is a
+            # 1-arborescence, we know that they is only one such edges
+            # leading into every vertex.
+            if len(k.in_edges(e_v, data=weight)) > 1:
+                raise Exception
+            sub_u, sub_v, sub_w = next(k.in_edges(e_v, data=weight).__iter__())
+            k.add_edge(e_u, e_v, **{weight: e_w})
+            k.remove_edge(sub_u, sub_v)
+            if (
+                max(d for n, d in k.in_degree()) <= 1
+                and len(G) == k.number_of_edges()
+                and nx.is_weakly_connected(k)
+            ):
+                # Ascent method calculation
+                if d[sub_u] == d[e_u] or sub_w == e_w:
+                    # Revert to the original graph
+                    k.remove_edge(e_u, e_v)
+                    k.add_edge(sub_u, sub_v, **{weight: sub_w})
+                    continue
+                epsilon = (sub_w - e_w) / (d[e_u] - d[sub_u])
+                if 0 < epsilon < min_epsilon:
+                    min_epsilon = epsilon
+            # Revert to the original graph
+            k.remove_edge(e_u, e_v)
+            k.add_edge(sub_u, sub_v, **{weight: sub_w})
+
+        return min_epsilon
+
+    # I have to know that the elements in pi correspond to the correct elements
+    # in the direction of ascent, even if the node labels are not integers.
+    # Thus, I will use dictionaries to made that mapping.
+    pi_dict = {}
+    for n in G:
+        pi_dict[n] = 0
+    del n
+    original_edge_weights = {}
+    for u, v, d in G.edges(data=True):
+        original_edge_weights[(u, v)] = d[weight]
+    dir_ascent, k_d = direction_of_ascent()
+    while dir_ascent is not None:
+        max_distance = find_epsilon(k_d, dir_ascent)
+        for n, v in dir_ascent.items():
+            pi_dict[n] += max_distance * v
+        for u, v, d in G.edges(data=True):
+            d[weight] = original_edge_weights[(u, v)] + pi_dict[u]
+        dir_ascent, k_d = direction_of_ascent()
+    nx._clear_cache(G)
+    # k_d is no longer an individual 1-arborescence but rather a set of
+    # minimal 1-arborescences at the maximum point of the polytope and should
+    # be reflected as such
+    k_max = k_d
+
+    # Search for a cycle within k_max. If a cycle exists, return it as the
+    # solution
+    for k in k_max:
+        if len([n for n in k if k.degree(n) == 2]) == G.order():
+            # Tour found
+            # TODO: this branch does not restore original_edge_weights of G!
+            return k.size(weight), k
+
+    # Write the original edge weights back to G and every member of k_max at
+    # the maximum point. Also average the number of times that edge appears in
+    # the set of minimal 1-arborescences.
+    x_star = {}
+    size_k_max = len(k_max)
+    for u, v, d in G.edges(data=True):
+        edge_count = 0
+        d[weight] = original_edge_weights[(u, v)]
+        for k in k_max:
+            if (u, v) in k.edges():
+                edge_count += 1
+                k[u][v][weight] = original_edge_weights[(u, v)]
+        x_star[(u, v)] = edge_count / size_k_max
+    # Now symmetrize the edges in x_star and scale them according to (5) in
+    # reference [1]
+    z_star = {}
+    scale_factor = (G.order() - 1) / G.order()
+    for u, v in x_star:
+        frequency = x_star[(u, v)] + x_star[(v, u)]
+        if frequency > 0:
+            z_star[(u, v)] = scale_factor * frequency
+    del x_star
+    # Return the optimal weight and the z dict
+    return next(k_max.__iter__()).size(weight), z_star
+
+
+@nx._dispatchable
+def spanning_tree_distribution(G, z):
+    """
+    Find the asadpour exponential distribution of spanning trees.
+
+    Solves the Maximum Entropy Convex Program in the Asadpour algorithm [1]_
+    using the approach in section 7 to build an exponential distribution of
+    undirected spanning trees.
+
+    This algorithm ensures that the probability of any edge in a spanning
+    tree is proportional to the sum of the probabilities of the tress
+    containing that edge over the sum of the probabilities of all spanning
+    trees of the graph.
+
+    Parameters
+    ----------
+    G : nx.MultiGraph
+        The undirected support graph for the Held Karp relaxation
+
+    z : dict
+        The output of `held_karp_ascent()`, a scaled version of the Held-Karp
+        solution.
+
+    Returns
+    -------
+    gamma : dict
+        The probability distribution which approximately preserves the marginal
+        probabilities of `z`.
+    """
+    from math import exp
+    from math import log as ln
+
+    def q(e):
+        """
+        The value of q(e) is described in the Asadpour paper is "the
+        probability that edge e will be included in a spanning tree T that is
+        chosen with probability proportional to exp(gamma(T))" which
+        basically means that it is the total probability of the edge appearing
+        across the whole distribution.
+
+        Parameters
+        ----------
+        e : tuple
+            The `(u, v)` tuple describing the edge we are interested in
+
+        Returns
+        -------
+        float
+            The probability that a spanning tree chosen according to the
+            current values of gamma will include edge `e`.
+        """
+        # Create the laplacian matrices
+        for u, v, d in G.edges(data=True):
+            d[lambda_key] = exp(gamma[(u, v)])
+        G_Kirchhoff = nx.total_spanning_tree_weight(G, lambda_key)
+        G_e = nx.contracted_edge(G, e, self_loops=False)
+        G_e_Kirchhoff = nx.total_spanning_tree_weight(G_e, lambda_key)
+
+        # Multiply by the weight of the contracted edge since it is not included
+        # in the total weight of the contracted graph.
+        return exp(gamma[(e[0], e[1])]) * G_e_Kirchhoff / G_Kirchhoff
+
+    # initialize gamma to the zero dict
+    gamma = {}
+    for u, v, _ in G.edges:
+        gamma[(u, v)] = 0
+
+    # set epsilon
+    EPSILON = 0.2
+
+    # pick an edge attribute name that is unlikely to be in the graph
+    lambda_key = "spanning_tree_distribution's secret attribute name for lambda"
+
+    while True:
+        # We need to know that know that no values of q_e are greater than
+        # (1 + epsilon) * z_e, however changing one gamma value can increase the
+        # value of a different q_e, so we have to complete the for loop without
+        # changing anything for the condition to be meet
+        in_range_count = 0
+        # Search for an edge with q_e > (1 + epsilon) * z_e
+        for u, v in gamma:
+            e = (u, v)
+            q_e = q(e)
+            z_e = z[e]
+            if q_e > (1 + EPSILON) * z_e:
+                delta = ln(
+                    (q_e * (1 - (1 + EPSILON / 2) * z_e))
+                    / ((1 - q_e) * (1 + EPSILON / 2) * z_e)
+                )
+                gamma[e] -= delta
+                # Check that delta had the desired effect
+                new_q_e = q(e)
+                desired_q_e = (1 + EPSILON / 2) * z_e
+                if round(new_q_e, 8) != round(desired_q_e, 8):
+                    raise nx.NetworkXError(
+                        f"Unable to modify probability for edge ({u}, {v})"
+                    )
+            else:
+                in_range_count += 1
+        # Check if the for loop terminated without changing any gamma
+        if in_range_count == len(gamma):
+            break
+
+    # Remove the new edge attributes
+    for _, _, d in G.edges(data=True):
+        if lambda_key in d:
+            del d[lambda_key]
+
+    return gamma
+
+
+@nx._dispatchable(edge_attrs="weight")
+def greedy_tsp(G, weight="weight", source=None):
+    """Return a low cost cycle starting at `source` and its cost.
+
+    This approximates a solution to the traveling salesman problem.
+    It finds a cycle of all the nodes that a salesman can visit in order
+    to visit many nodes while minimizing total distance.
+    It uses a simple greedy algorithm.
+    In essence, this function returns a large cycle given a source point
+    for which the total cost of the cycle is minimized.
+
+    Parameters
+    ----------
+    G : Graph
+        The Graph should be a complete weighted undirected graph.
+        The distance between all pairs of nodes should be included.
+
+    weight : string, optional (default="weight")
+        Edge data key corresponding to the edge weight.
+        If any edge does not have this attribute the weight is set to 1.
+
+    source : node, optional (default: first node in list(G))
+        Starting node.  If None, defaults to ``next(iter(G))``
+
+    Returns
+    -------
+    cycle : list of nodes
+        Returns the cycle (list of nodes) that a salesman
+        can follow to minimize total weight of the trip.
+
+    Raises
+    ------
+    NetworkXError
+        If `G` is not complete, the algorithm raises an exception.
+
+    Examples
+    --------
+    >>> from networkx.algorithms import approximation as approx
+    >>> G = nx.DiGraph()
+    >>> G.add_weighted_edges_from(
+    ...     {
+    ...         ("A", "B", 3),
+    ...         ("A", "C", 17),
+    ...         ("A", "D", 14),
+    ...         ("B", "A", 3),
+    ...         ("B", "C", 12),
+    ...         ("B", "D", 16),
+    ...         ("C", "A", 13),
+    ...         ("C", "B", 12),
+    ...         ("C", "D", 4),
+    ...         ("D", "A", 14),
+    ...         ("D", "B", 15),
+    ...         ("D", "C", 2),
+    ...     }
+    ... )
+    >>> cycle = approx.greedy_tsp(G, source="D")
+    >>> cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle))
+    >>> cycle
+    ['D', 'C', 'B', 'A', 'D']
+    >>> cost
+    31
+
+    Notes
+    -----
+    This implementation of a greedy algorithm is based on the following:
+
+    - The algorithm adds a node to the solution at every iteration.
+    - The algorithm selects a node not already in the cycle whose connection
+      to the previous node adds the least cost to the cycle.
+
+    A greedy algorithm does not always give the best solution.
+    However, it can construct a first feasible solution which can
+    be passed as a parameter to an iterative improvement algorithm such
+    as Simulated Annealing, or Threshold Accepting.
+
+    Time complexity: It has a running time $O(|V|^2)$
+    """
+    # Check that G is a complete graph
+    N = len(G) - 1
+    # This check ignores selfloops which is what we want here.
+    if any(len(nbrdict) - (n in nbrdict) != N for n, nbrdict in G.adj.items()):
+        raise nx.NetworkXError("G must be a complete graph.")
+
+    if source is None:
+        source = nx.utils.arbitrary_element(G)
+
+    if G.number_of_nodes() == 2:
+        neighbor = next(G.neighbors(source))
+        return [source, neighbor, source]
+
+    nodeset = set(G)
+    nodeset.remove(source)
+    cycle = [source]
+    next_node = source
+    while nodeset:
+        nbrdict = G[next_node]
+        next_node = min(nodeset, key=lambda n: nbrdict[n].get(weight, 1))
+        cycle.append(next_node)
+        nodeset.remove(next_node)
+    cycle.append(cycle[0])
+    return cycle
+
+
+@py_random_state(9)
+@nx._dispatchable(edge_attrs="weight")
+def simulated_annealing_tsp(
+    G,
+    init_cycle,
+    weight="weight",
+    source=None,
+    temp=100,
+    move="1-1",
+    max_iterations=10,
+    N_inner=100,
+    alpha=0.01,
+    seed=None,
+):
+    """Returns an approximate solution to the traveling salesman problem.
+
+    This function uses simulated annealing to approximate the minimal cost
+    cycle through the nodes. Starting from a suboptimal solution, simulated
+    annealing perturbs that solution, occasionally accepting changes that make
+    the solution worse to escape from a locally optimal solution. The chance
+    of accepting such changes decreases over the iterations to encourage
+    an optimal result.  In summary, the function returns a cycle starting
+    at `source` for which the total cost is minimized. It also returns the cost.
+
+    The chance of accepting a proposed change is related to a parameter called
+    the temperature (annealing has a physical analogue of steel hardening
+    as it cools). As the temperature is reduced, the chance of moves that
+    increase cost goes down.
+
+    Parameters
+    ----------
+    G : Graph
+        `G` should be a complete weighted graph.
+        The distance between all pairs of nodes should be included.
+
+    init_cycle : list of all nodes or "greedy"
+        The initial solution (a cycle through all nodes returning to the start).
+        This argument has no default to make you think about it.
+        If "greedy", use `greedy_tsp(G, weight)`.
+        Other common starting cycles are `list(G) + [next(iter(G))]` or the final
+        result of `simulated_annealing_tsp` when doing `threshold_accepting_tsp`.
+
+    weight : string, optional (default="weight")
+        Edge data key corresponding to the edge weight.
+        If any edge does not have this attribute the weight is set to 1.
+
+    source : node, optional (default: first node in list(G))
+        Starting node.  If None, defaults to ``next(iter(G))``
+
+    temp : int, optional (default=100)
+        The algorithm's temperature parameter. It represents the initial
+        value of temperature
+
+    move : "1-1" or "1-0" or function, optional (default="1-1")
+        Indicator of what move to use when finding new trial solutions.
+        Strings indicate two special built-in moves:
+
+        - "1-1": 1-1 exchange which transposes the position
+          of two elements of the current solution.
+          The function called is :func:`swap_two_nodes`.
+          For example if we apply 1-1 exchange in the solution
+          ``A = [3, 2, 1, 4, 3]``
+          we can get the following by the transposition of 1 and 4 elements:
+          ``A' = [3, 2, 4, 1, 3]``
+        - "1-0": 1-0 exchange which moves an node in the solution
+          to a new position.
+          The function called is :func:`move_one_node`.
+          For example if we apply 1-0 exchange in the solution
+          ``A = [3, 2, 1, 4, 3]``
+          we can transfer the fourth element to the second position:
+          ``A' = [3, 4, 2, 1, 3]``
+
+        You may provide your own functions to enact a move from
+        one solution to a neighbor solution. The function must take
+        the solution as input along with a `seed` input to control
+        random number generation (see the `seed` input here).
+        Your function should maintain the solution as a cycle with
+        equal first and last node and all others appearing once.
+        Your function should return the new solution.
+
+    max_iterations : int, optional (default=10)
+        Declared done when this number of consecutive iterations of
+        the outer loop occurs without any change in the best cost solution.
+
+    N_inner : int, optional (default=100)
+        The number of iterations of the inner loop.
+
+    alpha : float between (0, 1), optional (default=0.01)
+        Percentage of temperature decrease in each iteration
+        of outer loop
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    Returns
+    -------
+    cycle : list of nodes
+        Returns the cycle (list of nodes) that a salesman
+        can follow to minimize total weight of the trip.
+
+    Raises
+    ------
+    NetworkXError
+        If `G` is not complete the algorithm raises an exception.
+
+    Examples
+    --------
+    >>> from networkx.algorithms import approximation as approx
+    >>> G = nx.DiGraph()
+    >>> G.add_weighted_edges_from(
+    ...     {
+    ...         ("A", "B", 3),
+    ...         ("A", "C", 17),
+    ...         ("A", "D", 14),
+    ...         ("B", "A", 3),
+    ...         ("B", "C", 12),
+    ...         ("B", "D", 16),
+    ...         ("C", "A", 13),
+    ...         ("C", "B", 12),
+    ...         ("C", "D", 4),
+    ...         ("D", "A", 14),
+    ...         ("D", "B", 15),
+    ...         ("D", "C", 2),
+    ...     }
+    ... )
+    >>> cycle = approx.simulated_annealing_tsp(G, "greedy", source="D")
+    >>> cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle))
+    >>> cycle
+    ['D', 'C', 'B', 'A', 'D']
+    >>> cost
+    31
+    >>> incycle = ["D", "B", "A", "C", "D"]
+    >>> cycle = approx.simulated_annealing_tsp(G, incycle, source="D")
+    >>> cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle))
+    >>> cycle
+    ['D', 'C', 'B', 'A', 'D']
+    >>> cost
+    31
+
+    Notes
+    -----
+    Simulated Annealing is a metaheuristic local search algorithm.
+    The main characteristic of this algorithm is that it accepts
+    even solutions which lead to the increase of the cost in order
+    to escape from low quality local optimal solutions.
+
+    This algorithm needs an initial solution. If not provided, it is
+    constructed by a simple greedy algorithm. At every iteration, the
+    algorithm selects thoughtfully a neighbor solution.
+    Consider $c(x)$ cost of current solution and $c(x')$ cost of a
+    neighbor solution.
+    If $c(x') - c(x) <= 0$ then the neighbor solution becomes the current
+    solution for the next iteration. Otherwise, the algorithm accepts
+    the neighbor solution with probability $p = exp - ([c(x') - c(x)] / temp)$.
+    Otherwise the current solution is retained.
+
+    `temp` is a parameter of the algorithm and represents temperature.
+
+    Time complexity:
+    For $N_i$ iterations of the inner loop and $N_o$ iterations of the
+    outer loop, this algorithm has running time $O(N_i * N_o * |V|)$.
+
+    For more information and how the algorithm is inspired see:
+    http://en.wikipedia.org/wiki/Simulated_annealing
+    """
+    if move == "1-1":
+        move = swap_two_nodes
+    elif move == "1-0":
+        move = move_one_node
+    if init_cycle == "greedy":
+        # Construct an initial solution using a greedy algorithm.
+        cycle = greedy_tsp(G, weight=weight, source=source)
+        if G.number_of_nodes() == 2:
+            return cycle
+
+    else:
+        cycle = list(init_cycle)
+        if source is None:
+            source = cycle[0]
+        elif source != cycle[0]:
+            raise nx.NetworkXError("source must be first node in init_cycle")
+        if cycle[0] != cycle[-1]:
+            raise nx.NetworkXError("init_cycle must be a cycle. (return to start)")
+
+        if len(cycle) - 1 != len(G) or len(set(G.nbunch_iter(cycle))) != len(G):
+            raise nx.NetworkXError("init_cycle should be a cycle over all nodes in G.")
+
+        # Check that G is a complete graph
+        N = len(G) - 1
+        # This check ignores selfloops which is what we want here.
+        if any(len(nbrdict) - (n in nbrdict) != N for n, nbrdict in G.adj.items()):
+            raise nx.NetworkXError("G must be a complete graph.")
+
+        if G.number_of_nodes() == 2:
+            neighbor = next(G.neighbors(source))
+            return [source, neighbor, source]
+
+    # Find the cost of initial solution
+    cost = sum(G[u][v].get(weight, 1) for u, v in pairwise(cycle))
+
+    count = 0
+    best_cycle = cycle.copy()
+    best_cost = cost
+    while count <= max_iterations and temp > 0:
+        count += 1
+        for i in range(N_inner):
+            adj_sol = move(cycle, seed)
+            adj_cost = sum(G[u][v].get(weight, 1) for u, v in pairwise(adj_sol))
+            delta = adj_cost - cost
+            if delta <= 0:
+                # Set current solution the adjacent solution.
+                cycle = adj_sol
+                cost = adj_cost
+
+                if cost < best_cost:
+                    count = 0
+                    best_cycle = cycle.copy()
+                    best_cost = cost
+            else:
+                # Accept even a worse solution with probability p.
+                p = math.exp(-delta / temp)
+                if p >= seed.random():
+                    cycle = adj_sol
+                    cost = adj_cost
+        temp -= temp * alpha
+
+    return best_cycle
+
+
+@py_random_state(9)
+@nx._dispatchable(edge_attrs="weight")
+def threshold_accepting_tsp(
+    G,
+    init_cycle,
+    weight="weight",
+    source=None,
+    threshold=1,
+    move="1-1",
+    max_iterations=10,
+    N_inner=100,
+    alpha=0.1,
+    seed=None,
+):
+    """Returns an approximate solution to the traveling salesman problem.
+
+    This function uses threshold accepting methods to approximate the minimal cost
+    cycle through the nodes. Starting from a suboptimal solution, threshold
+    accepting methods perturb that solution, accepting any changes that make
+    the solution no worse than increasing by a threshold amount. Improvements
+    in cost are accepted, but so are changes leading to small increases in cost.
+    This allows the solution to leave suboptimal local minima in solution space.
+    The threshold is decreased slowly as iterations proceed helping to ensure
+    an optimum. In summary, the function returns a cycle starting at `source`
+    for which the total cost is minimized.
+
+    Parameters
+    ----------
+    G : Graph
+        `G` should be a complete weighted graph.
+        The distance between all pairs of nodes should be included.
+
+    init_cycle : list or "greedy"
+        The initial solution (a cycle through all nodes returning to the start).
+        This argument has no default to make you think about it.
+        If "greedy", use `greedy_tsp(G, weight)`.
+        Other common starting cycles are `list(G) + [next(iter(G))]` or the final
+        result of `simulated_annealing_tsp` when doing `threshold_accepting_tsp`.
+
+    weight : string, optional (default="weight")
+        Edge data key corresponding to the edge weight.
+        If any edge does not have this attribute the weight is set to 1.
+
+    source : node, optional (default: first node in list(G))
+        Starting node.  If None, defaults to ``next(iter(G))``
+
+    threshold : int, optional (default=1)
+        The algorithm's threshold parameter. It represents the initial
+        threshold's value
+
+    move : "1-1" or "1-0" or function, optional (default="1-1")
+        Indicator of what move to use when finding new trial solutions.
+        Strings indicate two special built-in moves:
+
+        - "1-1": 1-1 exchange which transposes the position
+          of two elements of the current solution.
+          The function called is :func:`swap_two_nodes`.
+          For example if we apply 1-1 exchange in the solution
+          ``A = [3, 2, 1, 4, 3]``
+          we can get the following by the transposition of 1 and 4 elements:
+          ``A' = [3, 2, 4, 1, 3]``
+        - "1-0": 1-0 exchange which moves an node in the solution
+          to a new position.
+          The function called is :func:`move_one_node`.
+          For example if we apply 1-0 exchange in the solution
+          ``A = [3, 2, 1, 4, 3]``
+          we can transfer the fourth element to the second position:
+          ``A' = [3, 4, 2, 1, 3]``
+
+        You may provide your own functions to enact a move from
+        one solution to a neighbor solution. The function must take
+        the solution as input along with a `seed` input to control
+        random number generation (see the `seed` input here).
+        Your function should maintain the solution as a cycle with
+        equal first and last node and all others appearing once.
+        Your function should return the new solution.
+
+    max_iterations : int, optional (default=10)
+        Declared done when this number of consecutive iterations of
+        the outer loop occurs without any change in the best cost solution.
+
+    N_inner : int, optional (default=100)
+        The number of iterations of the inner loop.
+
+    alpha : float between (0, 1), optional (default=0.1)
+        Percentage of threshold decrease when there is at
+        least one acceptance of a neighbor solution.
+        If no inner loop moves are accepted the threshold remains unchanged.
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    Returns
+    -------
+    cycle : list of nodes
+        Returns the cycle (list of nodes) that a salesman
+        can follow to minimize total weight of the trip.
+
+    Raises
+    ------
+    NetworkXError
+        If `G` is not complete the algorithm raises an exception.
+
+    Examples
+    --------
+    >>> from networkx.algorithms import approximation as approx
+    >>> G = nx.DiGraph()
+    >>> G.add_weighted_edges_from(
+    ...     {
+    ...         ("A", "B", 3),
+    ...         ("A", "C", 17),
+    ...         ("A", "D", 14),
+    ...         ("B", "A", 3),
+    ...         ("B", "C", 12),
+    ...         ("B", "D", 16),
+    ...         ("C", "A", 13),
+    ...         ("C", "B", 12),
+    ...         ("C", "D", 4),
+    ...         ("D", "A", 14),
+    ...         ("D", "B", 15),
+    ...         ("D", "C", 2),
+    ...     }
+    ... )
+    >>> cycle = approx.threshold_accepting_tsp(G, "greedy", source="D")
+    >>> cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle))
+    >>> cycle
+    ['D', 'C', 'B', 'A', 'D']
+    >>> cost
+    31
+    >>> incycle = ["D", "B", "A", "C", "D"]
+    >>> cycle = approx.threshold_accepting_tsp(G, incycle, source="D")
+    >>> cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle))
+    >>> cycle
+    ['D', 'C', 'B', 'A', 'D']
+    >>> cost
+    31
+
+    Notes
+    -----
+    Threshold Accepting is a metaheuristic local search algorithm.
+    The main characteristic of this algorithm is that it accepts
+    even solutions which lead to the increase of the cost in order
+    to escape from low quality local optimal solutions.
+
+    This algorithm needs an initial solution. This solution can be
+    constructed by a simple greedy algorithm. At every iteration, it
+    selects thoughtfully a neighbor solution.
+    Consider $c(x)$ cost of current solution and $c(x')$ cost of
+    neighbor solution.
+    If $c(x') - c(x) <= threshold$ then the neighbor solution becomes the current
+    solution for the next iteration, where the threshold is named threshold.
+
+    In comparison to the Simulated Annealing algorithm, the Threshold
+    Accepting algorithm does not accept very low quality solutions
+    (due to the presence of the threshold value). In the case of
+    Simulated Annealing, even a very low quality solution can
+    be accepted with probability $p$.
+
+    Time complexity:
+    It has a running time $O(m * n * |V|)$ where $m$ and $n$ are the number
+    of times the outer and inner loop run respectively.
+
+    For more information and how algorithm is inspired see:
+    https://doi.org/10.1016/0021-9991(90)90201-B
+
+    See Also
+    --------
+    simulated_annealing_tsp
+
+    """
+    if move == "1-1":
+        move = swap_two_nodes
+    elif move == "1-0":
+        move = move_one_node
+    if init_cycle == "greedy":
+        # Construct an initial solution using a greedy algorithm.
+        cycle = greedy_tsp(G, weight=weight, source=source)
+        if G.number_of_nodes() == 2:
+            return cycle
+
+    else:
+        cycle = list(init_cycle)
+        if source is None:
+            source = cycle[0]
+        elif source != cycle[0]:
+            raise nx.NetworkXError("source must be first node in init_cycle")
+        if cycle[0] != cycle[-1]:
+            raise nx.NetworkXError("init_cycle must be a cycle. (return to start)")
+
+        if len(cycle) - 1 != len(G) or len(set(G.nbunch_iter(cycle))) != len(G):
+            raise nx.NetworkXError("init_cycle is not all and only nodes.")
+
+        # Check that G is a complete graph
+        N = len(G) - 1
+        # This check ignores selfloops which is what we want here.
+        if any(len(nbrdict) - (n in nbrdict) != N for n, nbrdict in G.adj.items()):
+            raise nx.NetworkXError("G must be a complete graph.")
+
+        if G.number_of_nodes() == 2:
+            neighbor = list(G.neighbors(source))[0]
+            return [source, neighbor, source]
+
+    # Find the cost of initial solution
+    cost = sum(G[u][v].get(weight, 1) for u, v in pairwise(cycle))
+
+    count = 0
+    best_cycle = cycle.copy()
+    best_cost = cost
+    while count <= max_iterations:
+        count += 1
+        accepted = False
+        for i in range(N_inner):
+            adj_sol = move(cycle, seed)
+            adj_cost = sum(G[u][v].get(weight, 1) for u, v in pairwise(adj_sol))
+            delta = adj_cost - cost
+            if delta <= threshold:
+                accepted = True
+
+                # Set current solution the adjacent solution.
+                cycle = adj_sol
+                cost = adj_cost
+
+                if cost < best_cost:
+                    count = 0
+                    best_cycle = cycle.copy()
+                    best_cost = cost
+        if accepted:
+            threshold -= threshold * alpha
+
+    return best_cycle
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/treewidth.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/treewidth.py
new file mode 100644
index 00000000..31d73f63
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/treewidth.py
@@ -0,0 +1,252 @@
+"""Functions for computing treewidth decomposition.
+
+Treewidth of an undirected graph is a number associated with the graph.
+It can be defined as the size of the largest vertex set (bag) in a tree
+decomposition of the graph minus one.
+
+`Wikipedia: Treewidth <https://en.wikipedia.org/wiki/Treewidth>`_
+
+The notions of treewidth and tree decomposition have gained their
+attractiveness partly because many graph and network problems that are
+intractable (e.g., NP-hard) on arbitrary graphs become efficiently
+solvable (e.g., with a linear time algorithm) when the treewidth of the
+input graphs is bounded by a constant [1]_ [2]_.
+
+There are two different functions for computing a tree decomposition:
+:func:`treewidth_min_degree` and :func:`treewidth_min_fill_in`.
+
+.. [1] Hans L. Bodlaender and Arie M. C. A. Koster. 2010. "Treewidth
+      computations I.Upper bounds". Inf. Comput. 208, 3 (March 2010),259-275.
+      http://dx.doi.org/10.1016/j.ic.2009.03.008
+
+.. [2] Hans L. Bodlaender. "Discovering Treewidth". Institute of Information
+      and Computing Sciences, Utrecht University.
+      Technical Report UU-CS-2005-018.
+      http://www.cs.uu.nl
+
+.. [3] K. Wang, Z. Lu, and J. Hicks *Treewidth*.
+      https://web.archive.org/web/20210507025929/http://web.eecs.utk.edu/~cphill25/cs594_spring2015_projects/treewidth.pdf
+
+"""
+
+import itertools
+import sys
+from heapq import heapify, heappop, heappush
+
+import networkx as nx
+from networkx.utils import not_implemented_for
+
+__all__ = ["treewidth_min_degree", "treewidth_min_fill_in"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable(returns_graph=True)
+def treewidth_min_degree(G):
+    """Returns a treewidth decomposition using the Minimum Degree heuristic.
+
+    The heuristic chooses the nodes according to their degree, i.e., first
+    the node with the lowest degree is chosen, then the graph is updated
+    and the corresponding node is removed. Next, a new node with the lowest
+    degree is chosen, and so on.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    Returns
+    -------
+    Treewidth decomposition : (int, Graph) tuple
+          2-tuple with treewidth and the corresponding decomposed tree.
+    """
+    deg_heuristic = MinDegreeHeuristic(G)
+    return treewidth_decomp(G, lambda graph: deg_heuristic.best_node(graph))
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable(returns_graph=True)
+def treewidth_min_fill_in(G):
+    """Returns a treewidth decomposition using the Minimum Fill-in heuristic.
+
+    The heuristic chooses a node from the graph, where the number of edges
+    added turning the neighborhood of the chosen node into clique is as
+    small as possible.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    Returns
+    -------
+    Treewidth decomposition : (int, Graph) tuple
+        2-tuple with treewidth and the corresponding decomposed tree.
+    """
+    return treewidth_decomp(G, min_fill_in_heuristic)
+
+
+class MinDegreeHeuristic:
+    """Implements the Minimum Degree heuristic.
+
+    The heuristic chooses the nodes according to their degree
+    (number of neighbors), i.e., first the node with the lowest degree is
+    chosen, then the graph is updated and the corresponding node is
+    removed. Next, a new node with the lowest degree is chosen, and so on.
+    """
+
+    def __init__(self, graph):
+        self._graph = graph
+
+        # nodes that have to be updated in the heap before each iteration
+        self._update_nodes = []
+
+        self._degreeq = []  # a heapq with 3-tuples (degree,unique_id,node)
+        self.count = itertools.count()
+
+        # build heap with initial degrees
+        for n in graph:
+            self._degreeq.append((len(graph[n]), next(self.count), n))
+        heapify(self._degreeq)
+
+    def best_node(self, graph):
+        # update nodes in self._update_nodes
+        for n in self._update_nodes:
+            # insert changed degrees into degreeq
+            heappush(self._degreeq, (len(graph[n]), next(self.count), n))
+
+        # get the next valid (minimum degree) node
+        while self._degreeq:
+            (min_degree, _, elim_node) = heappop(self._degreeq)
+            if elim_node not in graph or len(graph[elim_node]) != min_degree:
+                # outdated entry in degreeq
+                continue
+            elif min_degree == len(graph) - 1:
+                # fully connected: abort condition
+                return None
+
+            # remember to update nodes in the heap before getting the next node
+            self._update_nodes = graph[elim_node]
+            return elim_node
+
+        # the heap is empty: abort
+        return None
+
+
+def min_fill_in_heuristic(graph):
+    """Implements the Minimum Degree heuristic.
+
+    Returns the node from the graph, where the number of edges added when
+    turning the neighborhood of the chosen node into clique is as small as
+    possible. This algorithm chooses the nodes using the Minimum Fill-In
+    heuristic. The running time of the algorithm is :math:`O(V^3)` and it uses
+    additional constant memory."""
+
+    if len(graph) == 0:
+        return None
+
+    min_fill_in_node = None
+
+    min_fill_in = sys.maxsize
+
+    # sort nodes by degree
+    nodes_by_degree = sorted(graph, key=lambda x: len(graph[x]))
+    min_degree = len(graph[nodes_by_degree[0]])
+
+    # abort condition (handle complete graph)
+    if min_degree == len(graph) - 1:
+        return None
+
+    for node in nodes_by_degree:
+        num_fill_in = 0
+        nbrs = graph[node]
+        for nbr in nbrs:
+            # count how many nodes in nbrs current nbr is not connected to
+            # subtract 1 for the node itself
+            num_fill_in += len(nbrs - graph[nbr]) - 1
+            if num_fill_in >= 2 * min_fill_in:
+                break
+
+        num_fill_in /= 2  # divide by 2 because of double counting
+
+        if num_fill_in < min_fill_in:  # update min-fill-in node
+            if num_fill_in == 0:
+                return node
+            min_fill_in = num_fill_in
+            min_fill_in_node = node
+
+    return min_fill_in_node
+
+
+@nx._dispatchable(returns_graph=True)
+def treewidth_decomp(G, heuristic=min_fill_in_heuristic):
+    """Returns a treewidth decomposition using the passed heuristic.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+    heuristic : heuristic function
+
+    Returns
+    -------
+    Treewidth decomposition : (int, Graph) tuple
+        2-tuple with treewidth and the corresponding decomposed tree.
+    """
+
+    # make dict-of-sets structure
+    graph = {n: set(G[n]) - {n} for n in G}
+
+    # stack containing nodes and neighbors in the order from the heuristic
+    node_stack = []
+
+    # get first node from heuristic
+    elim_node = heuristic(graph)
+    while elim_node is not None:
+        # connect all neighbors with each other
+        nbrs = graph[elim_node]
+        for u, v in itertools.permutations(nbrs, 2):
+            if v not in graph[u]:
+                graph[u].add(v)
+
+        # push node and its current neighbors on stack
+        node_stack.append((elim_node, nbrs))
+
+        # remove node from graph
+        for u in graph[elim_node]:
+            graph[u].remove(elim_node)
+
+        del graph[elim_node]
+        elim_node = heuristic(graph)
+
+    # the abort condition is met; put all remaining nodes into one bag
+    decomp = nx.Graph()
+    first_bag = frozenset(graph.keys())
+    decomp.add_node(first_bag)
+
+    treewidth = len(first_bag) - 1
+
+    while node_stack:
+        # get node and its neighbors from the stack
+        (curr_node, nbrs) = node_stack.pop()
+
+        # find a bag all neighbors are in
+        old_bag = None
+        for bag in decomp.nodes:
+            if nbrs <= bag:
+                old_bag = bag
+                break
+
+        if old_bag is None:
+            # no old_bag was found: just connect to the first_bag
+            old_bag = first_bag
+
+        # create new node for decomposition
+        nbrs.add(curr_node)
+        new_bag = frozenset(nbrs)
+
+        # update treewidth
+        treewidth = max(treewidth, len(new_bag) - 1)
+
+        # add edge to decomposition (implicitly also adds the new node)
+        decomp.add_edge(old_bag, new_bag)
+
+    return treewidth, decomp
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/vertex_cover.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/vertex_cover.py
new file mode 100644
index 00000000..13d7167c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/vertex_cover.py
@@ -0,0 +1,83 @@
+"""Functions for computing an approximate minimum weight vertex cover.
+
+A |vertex cover|_ is a subset of nodes such that each edge in the graph
+is incident to at least one node in the subset.
+
+.. _vertex cover: https://en.wikipedia.org/wiki/Vertex_cover
+.. |vertex cover| replace:: *vertex cover*
+
+"""
+
+import networkx as nx
+
+__all__ = ["min_weighted_vertex_cover"]
+
+
+@nx._dispatchable(node_attrs="weight")
+def min_weighted_vertex_cover(G, weight=None):
+    r"""Returns an approximate minimum weighted vertex cover.
+
+    The set of nodes returned by this function is guaranteed to be a
+    vertex cover, and the total weight of the set is guaranteed to be at
+    most twice the total weight of the minimum weight vertex cover. In
+    other words,
+
+    .. math::
+
+       w(S) \leq 2 * w(S^*),
+
+    where $S$ is the vertex cover returned by this function,
+    $S^*$ is the vertex cover of minimum weight out of all vertex
+    covers of the graph, and $w$ is the function that computes the
+    sum of the weights of each node in that given set.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    weight : string, optional (default = None)
+        If None, every node has weight 1. If a string, use this node
+        attribute as the node weight. A node without this attribute is
+        assumed to have weight 1.
+
+    Returns
+    -------
+    min_weighted_cover : set
+        Returns a set of nodes whose weight sum is no more than twice
+        the weight sum of the minimum weight vertex cover.
+
+    Notes
+    -----
+    For a directed graph, a vertex cover has the same definition: a set
+    of nodes such that each edge in the graph is incident to at least
+    one node in the set. Whether the node is the head or tail of the
+    directed edge is ignored.
+
+    This is the local-ratio algorithm for computing an approximate
+    vertex cover. The algorithm greedily reduces the costs over edges,
+    iteratively building a cover. The worst-case runtime of this
+    implementation is $O(m \log n)$, where $n$ is the number
+    of nodes and $m$ the number of edges in the graph.
+
+    References
+    ----------
+    .. [1] Bar-Yehuda, R., and Even, S. (1985). "A local-ratio theorem for
+       approximating the weighted vertex cover problem."
+       *Annals of Discrete Mathematics*, 25, 27–46
+       <http://www.cs.technion.ac.il/~reuven/PDF/vc_lr.pdf>
+
+    """
+    cost = dict(G.nodes(data=weight, default=1))
+    # While there are uncovered edges, choose an uncovered and update
+    # the cost of the remaining edges.
+    cover = set()
+    for u, v in G.edges():
+        if u in cover or v in cover:
+            continue
+        if cost[u] <= cost[v]:
+            cover.add(u)
+            cost[v] -= cost[u]
+        else:
+            cover.add(v)
+            cost[u] -= cost[v]
+    return cover