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