Now that my proposal was accepted by NetworkX for the 2021 Google Summer of Code (GSoC), I can get more into the technical details of how I plan to implement the Asadpour algorithm within NetworkX.

In this post I am going to outline my thought process for the control scheme of my implementation and create function stubs according to my GSoC proposal. Most of the work for this project will happen in netowrkx.algorithms.approximation.traveling_salesman.py, where I will finish the last algorithm for the Traveling Salesman Problem so it can be merged into the project. The main function in traveling_salesman.py is

def traveling_salesman_problem(G, weight="weight", nodes=None, cycle=True, method=None):
"""
...

Parameters
----------
G : NetworkX graph
Undirected 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:threshold_accepting_tsp for directed G.

To specify parameters for these provided functions, construct lambda
functions that state the specific value. method must have 2 inputs.
(See examples).

...
"""

All user calls to find an approximation to the traveling salesman problem will go through this function. My implementation of the Asadpour algorithm will also need to be compatible with this function. traveling_salesman_problem will handle creating a new, complete graph using the weight of the shortest path between nodes $$u$$ and $$v$$ as the weight of that arc, so we know that by the time the graph is passed to the Asadpour algorithm it is a complete digraph which satisfies the triangle inequality. The main function also handles the nodes and cycles parameters by only copying the necessary nodes into the complete digraph before calling the requested method and afterwards searching for and removing the largest arc within the returned cycle. Thus, the parent function for the Asadpour algorithm only needs to deal with the graph itself and the weights or costs of the arcs in the graph.

My controlling function will have the following signature and I have included a draft of the docstring as well.

def asadpour_tsp(G, weight="weight"):
"""
Returns an O( log n / log log n ) 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 \\\\\\log n$ times and saves the minimum sampled tree once
the direction of the arcs is added back to the edges. Finally,
we argument 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 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.

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, the algorithm raises an exception.

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
"""
pass

Following my GSoC proposal, the next function is held_karp, which will solve the Held-Karp relaxation on the complete digraph using the ellipsoid method (See my last two posts here and here for my thoughts on why and how to accomplish this). Solving the Held-Karp relaxation is the first step in the algorithm.

Recall that the Held-Karp relaxation is defined as the following linear program:

$\begin{array}{c l l} \text{min} & \sum_{a} c(a)x_a \\\ \text{s.t.} & x(\delta^+(U)) \geqslant 1 & \forall\ U \subset V \text{ and } U \not= \emptyset \\\ & x(\delta^+(v)) = x(\delta^-(v)) = 1 & \forall\ v \in V \\\ & x_a \geqslant 0 & \forall\ a \end{array}$

and that it is a semi-infinite program so it is too large to be solved in conventional forms. The algorithm uses the solution to the Held-Karp relaxation to create a vector $$z^*$$ which is a symmetrized and slightly scaled down version of the true Held-Karp solution $$x^*$$. $$z^*$$ is defined as

$z^*_{{u, v}} = \frac{n - 1}{n} \left(x^*_{uv} + x^*_{vu}\right)$

and since this is what the algorithm using to build the rest of the approximation, this should be one of the return values from held_karp. I will also return the value of the cost of $$x^*$$, which is denoted as $$c(x^*)$$ or $$OPT_{HK}$$ in the Asadpour paper [1].

Additionally, the separation oracle will be defined as an inner function within held_karp. At the present moment I am not sure what the exact parameters for the separation oracle, sep_oracle, but it should be the the point the algorithm wishes to test and will need to access the graph the algorithm is relaxing. In particular, I’m not sure yet how I will represent the hyperplane which is returned by the separation oracle.

def _held_karp(G, weight="weight"):
"""
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 ellipsoid method for linear programs.

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_star : numpy array
A symmetrized and scaled version of the optimal solution to the
Held-Karp relaxation for use in the Asadpour algorithm

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
"""

def sep_oracle(point):
"""
The separation oracle used in the ellipsoid algorithm to solve the
Held-Karp relaxation.

This 'black-box' takes a point and check to see if it violates any
of the Held-Karp constraints, which are defined as

- The out-degree of all non-empty subsets of $V$ is at lest one.
- The in-degree and out-degree of each vertex in $V$ is equal to
one. Note that if a vertex has more than one incoming or
outgoing arcs the values of each could be less than one so long
as they sum to one.
- The current value for each arc is greater
than zero.

Parameters
----------
point : numpy array
The point in n dimensional space we will to test to see if it
violations any of the Held-Karp constraints.

Returns
-------
numpy array
The hyperplane which was the most violated by point, i.e the
hyperplane defining the polytope of spanning trees which point
was farthest from, None if no constraints are violated.
"""
pass

pass

Next the algorithm uses the symmetrized and scaled version of the Held-Karp solution to construct an exponential distribution of undirected spanning trees which preserves the marginal probabilities.

def _spanning_tree_distribution(z_star):
"""
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 trees
containing that edge over the sum of the probabilities of all spanning
trees of the graph.

Parameters
----------
z_star : numpy array
The output of _held_karp(), a scaled version of the Held-Karp
solution.

Returns
-------
gamma : numpy array
The probability distribution which approximately preserves the marginal
probabilities of z_star.
"""
pass

Now that the algorithm has the distribution of spanning trees, we need to sample them. Each sampled tree is a $$\lambda$$-random tree and can be sampled using algorithm A8 in [2].

def _sample_spanning_tree(G, gamma):
"""
Sample one spanning tree from the distribution defined by gamma,
roughly using algorithm A8 in [1]_ .

We 'shuffle' the edges in the graph, and then probabilistically
determine whether to add the edge conditioned on all of the previous
edges which were added to the tree. Probabilities are calculated using
Kirchhoff's Matrix Tree Theorem and a weighted Laplacian matrix.

Parameters
----------
G : nx.Graph
An undirected version of the original graph.

gamma : numpy array
The probabilities associated with each of the edges in the undirected
graph G.

Returns
-------
nx.Graph
A spanning tree using the distribution defined by gamma.

References
----------
.. [1] V. Kulkarni, Generating random combinatorial objects, Journal of
algorithms, 11 (1990), pp. 185–207
"""
pass

At this point there is only one function left to discuss, laplacian_matrix. This function already exists within NetworkX at networkx.linalg.laplacianmatrix.laplacian_matrix, and even though this is relatively simple to implement, I’d rather use an existing version than create duplicate code within the project. A deeper look at the function signature reveals

@not_implemented_for("directed")
def laplacian_matrix(G, nodelist=None, weight="weight"):
"""Returns the Laplacian matrix of G.

The graph Laplacian is the matrix L = D - A, where
A is the adjacency matrix and D is the diagonal matrix of node degrees.

Parameters
----------
G : graph
A NetworkX graph

nodelist : list, optional
The rows and columns are ordered according to the nodes in nodelist.
If nodelist is None, then the ordering is produced by G.nodes().

weight : string or None, optional (default='weight')
The edge data key used to compute each value in the matrix.
If None, then each edge has weight 1.

Returns
-------
L : SciPy sparse matrix
The Laplacian matrix of G.

Notes
-----
For MultiGraph/MultiDiGraph, the edges weights are summed.

--------
to_numpy_array
normalized_laplacian_matrix
laplacian_spectrum
"""

Which is exactly what I need, except the decorator states that it does not support directed graphs and this algorithm deals with those types of graphs. Fortunately, our distribution of spanning trees is for trees in a directed graph once the direction is disregarded, so we can actually use the existing function. The definition given in the Asadpour paper [1], is

$L_{i,j} = \left\{ \begin{array}{l l} -\lambda_e & e = (i, j) \in E \\\ \sum_{e \in \delta({i})} \lambda_e & i = j \\\ 0 & \text{otherwise} \end{array} \right.$

Where $$E$$ is defined as “Let $$E$$ be the support of graph of $$z^*$$ when the direction of the arcs are disregarded” on page 5 of the Asadpour paper. Thus, I can use the existing method without having to create a new one, which will save time and effort on this GSoC project.

In addition to being discussed here, these function stubs have been added to my fork of NetworkX on the bothTSP branch. The commit, Added function stubs and draft docstrings for the Asadpour algorithm is visible on my GitHub using that link.

## References#

[1] A. Asadpour, M. X. Goemans, A. Mardry, S. O. Ghran, 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, https://homes.cs.washington.edu/~shayan/atsp.pdf.

[2] V. Kulkarni, Generating random combinatorial objects, Journal of algorithms, 11 (1990), pp. 185–207