aboutsummaryrefslogtreecommitdiff
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-master.tar.gz
two version of R2R are hereHEADmaster
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)