about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/tests
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/tests
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/tests')
-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
14 files changed, 2515 insertions, 0 deletions
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)