Initializing help system before first use

The travelling salesman problem


Type: Programming
Rating: 2 (easy-medium)
Description: The file tsp.py retrieves a problem instance from https://www.math.uwaterloo.ca/tsp/world/countries.html and creates a corresponding TSP instance, then solves it using the Xpress Optimizer library with the appropriate callback. In tsp_numpy.py, a randomly generated TSP problem is modeled using NumPy vectors and matrices and solved using the Optimizer's libraries and callback functions.
File(s): tsp.py, tsp_numpy.py


tsp.py
# Solve an instance of the TSP with Xpress using callbacks.

# Usage: Retrieve an example from
# https://www.math.uwaterloo.ca/tsp/world/countries.html
# and load the TSP instance, then solve it using the Xpress Optimizer
# library with the appropriate callback. Once the optimization is over
# (i.e. the time limit is reached, or we find an optimal solution) the
# optimal tour is displayed using matplotlib.
#
# (C) 1983-2025 Fair Isaac Corporation

import networkx as nx
import xpress as xp
import re
import math

from matplotlib import pyplot as plt
from urllib.request import urlretrieve

# Download instance from TSPLib
#
# Replace with any of the following for a different instance:
#
# ar9152.tsp   (9125 nodes)
# bm33708.tsp (33708 nodes)
# ch71009.tsp (71009 nodes)
# dj38.tsp       (38 nodes)
# eg7146.tsp   (7146 nodes)
# fi10639.tsp (10639 nodes)
# gr9882.tsp   (9882 nodes)
# ho14473.tsp (14473 nodes)
# ei8246.tsp   (8246 nodes)
# ja9847.tsp   (9847 nodes)
# kz9976.tsp   (9976 nodes)
# lu980.tsp     (980 nodes)
# mo14185.tsp (14185 nodes)
# nu3496.tsp   (3496 nodes)
# mu1979.tsp   (1979 nodes)
# pm8079.tsp   (8079 nodes)
# qa194.tsp     (194 nodes)
# rw1621.tsp   (1621 nodes)
# sw24978.tsp (24978 nodes)
# tz6117.tsp   (6117 nodes)
# uy734.tsp     (734 nodes)
# vm22775.tsp (22775 nodes)
# wi29.tsp       (29 nodes)
# ym7663.tsp   (7663 nodes)
# zi929.tsp     (929 nodes)
# ca4663.tsp   (4663 nodes)
# it16862.tsp (16862 nodes)

filename = 'wi29.tsp'

urlretrieve('https://www.math.uwaterloo.ca/tsp/world/' + filename, filename)

# Read file consisting of lines of the form "k: x y" where k is the
# point's index while x and y are the coordinates of the point. The
# distances are assumed to be Euclidean.

instance = open(filename, 'r')
coord_section = False
points = {}

G = nx.Graph()

#
# Coordinates of the points in the graph.
#

for line in instance.readlines():

    if re.match('NODE_COORD_SECTION.*', line):
        coord_section = True
        continue
    elif re.match('EOF.*', line):
        break

    if coord_section:
        coord = line.split(' ')
        index = int(coord[0])
        cx = float(coord[1])
        cy = float(coord[2])
        points[index] = (cx, cy)
        G.add_node(index, pos=(cx, cy))

instance.close()

print("Downloaded instance, created graph.")

# Callback for checking if the solution forms a tour.
#
# Returns a tuple (a,b) with:
# a: True if the solution is to be rejected, False otherwise
# b: real cutoff value
def cbpreintsol(prob, G, soltype, cutoff):
    """
    Use this function to refuse a solution unless it forms a tour.
    """

    # Obtain solution, then start at node 1 to see if the solutions at
    # one form a tour. The vector s is binary as this is a preintsol()
    # callback.

    s = prob.getCallbackSolution()

    reject = False
    nextnode = 1
    tour = []

    while nextnode != 1 or len(tour) == 0:
        # Find the edge leaving nextnode.
        edge = None
        for j in V:
            if j != nextnode and s[x[nextnode, j].index] > 0.5:
                edge = x[nextnode, j]
                nextnode = j
                break
        if edge is None:
            break
        tour.append(edge)

    # If there are n arcs in the loop, the solution is feasible.
    if len(tour) < n:
        # The tour given by the current solution does not pass through
        # all the nodes and is thus infeasible.
        # If soltype is non-zero then we reject by setting reject=True.
        # If instead soltype is zero then the solution came from an
        # integral node. In this case we can reject by adding a cut
        # that cuts off that solution. Note that we must NOT set
        # reject=True in that case because that would result in just
        # dropping the node, no matter whether we add cuts or not.
        if soltype != 0:
            reject = True
        else:
            # The solution is infeasible and it was obtained from an integral
            # node. In this case we can generate and inject a cut that cuts
            # off this solution so that we don't find it again.

            # Presolve cut in order to add it to the presolved problem.
            colind, rowcoef, drhsp, status = prob.presolveRow(rowtype='L',
                                             origcolind=tour,
                                             origrowcoef=[1] * len(tour),
                                             origrhs=len(tour) - 1)
            # Since mipdualreductions=0, presolving the cut must succeed, and
            # the cut should never be relaxed as this would imply that it did
            # not cut off a subtour.
            assert status == 0

            prob.addCuts(cuttype=[1],
                         rowtype=['L'],
                         rhs=[drhsp],
                         start=[0, len(colind)],
                         colind=colind,
                         cutcoef=rowcoef)
    # To accept the cutoff, return second element of tuple as None.
    return (reject, None)

# Formulate problem, set callback function and solve.

n = len(points)    # Number of nodes.
V = range(1, n+1)  # Set of nodes.

# Set of arcs (i.e. all pairs since it is a complete graph).
A = [(i, j) for i in V for j in V if i != j]

p = xp.problem()

x = {(i, j): p.addVariable(name='x_{0}_{1}'.format(i, j),
                           vartype=xp.binary) for (i, j) in A}

conservation_in = [xp.Sum(x[i, j] for j in V if j != i) == 1 for i in V]
conservation_out = [xp.Sum(x[j, i] for j in V if j != i) == 1 for i in V]

p.addConstraint(conservation_in, conservation_out)

# Objective function: total distance travelled.
p.setObjective(xp.Sum(math.sqrt((points[i][0] - points[j][0])**2 +
                                (points[i][1] - points[j][1])**2) * x[i, j]
                      for (i, j) in A))

# Should find a reasonable solution within 20 seconds.
p.controls.timelimit = 20

p.addPreIntsolCallback(cbpreintsol, G, 1)

# Disable dual reductions (in order not to cut optimal solutions)
# and nonlinear reductions, in order to be able to presolve the
# cuts.
p.controls.mipdualreductions = 0

p.optimize()

if p.attributes.solstatus not in [xp.SolStatus.OPTIMAL, xp.SolStatus.FEASIBLE]:
    print("Solve status:", p.attributes.solvestatus.name)
    print("Solution status:", p.attributes.solstatus.name)
else:
    # Read solution and store it in the graph.
    sol = p.getSolution()
    try:
        for (i, j) in A:
            if sol[x[i, j].index] > 0.5:
                G.add_edge(i, j)

        # Display best tour found.
        pos = nx.get_node_attributes(G, 'pos')

        nx.draw(G, points)  # Create a graph with the tour.
        plt.show()          # Display it interactively.
    except:
        print('Could not draw solution')

tsp_numpy.py
# TSP example using numpy functions (for efficiency).
#
# (C) 1983-2025 Fair Isaac Corporation

import xpress as xp
import numpy as np

def cb_preintsol(prob, data, soltype, cutoff):
    '''Callback for checking if solution is acceptable.'''

    n = data
    xsol = prob.getCallbackSolution()
    xsolf = np.array(xsol)      # flattened
    xsol  = xsolf.reshape(n, n)  # matrix-shaped
    nextc = np.argmax(xsol, axis=1)

    i = 0
    ncities = 1

    # Scan cities in order until we get back to 0 or the solution is
    # wrong and we're diverging.
    while nextc[i] != 0 and ncities < n:
        ncities += 1
        i = nextc[i]

    reject = False
    if ncities < n:
        # The tour given by the current solution does not pass through
        # all the nodes and is thus infeasible.
        # If soltype is non-zero then we reject by setting reject=True.
        # If instead soltype is zero then the solution came from an
        # integral node. In this case we can reject by adding a cut
        # that cuts off that solution. Note that we must NOT set
        # reject=True in that case because that would result in just
        # dropping the node, no matter whether we add cuts or not.
        if soltype != 0:
            reject = True
        else:
            # Obtain an order by checking the maximum of the variable matrix
            # for each row.
            unchecked = np.zeros(n)
            ngroup = 0

            # Initialize the vectors to be passed to addcuts.
            cut_mstart = [0]
            cut_ind = []
            cut_coe = []
            cut_rhs = []

            nnz = 0
            ncuts = 0

            while np.min(unchecked) == 0 and ngroup <= n:
                '''Seek a tour
                '''

                ngroup += 1
                firstcity = np.argmin(unchecked)
                assert (unchecked[firstcity] == 0)
                i = firstcity
                ncities = 0
                # Scan cities in order.
                while True:
                    unchecked[i] = ngroup  # Mark city i with its new group, to be used in addcut.
                    ncities += 1
                    i = nextc[i]

                    if i == firstcity or ncities > n + 1:
                        break

                assert ncities < n # We know solution is infeasible.

                # Unchecked[unchecked == ngroup] marks nodes to be made part of
                # subtour elimination inequality.

                # Find indices of current subtour. S is the set of nodes
                # traversed by the subtour, compS is its complement.
                S     = np.where(unchecked == ngroup)[0].tolist()
                compS = np.where(unchecked != ngroup)[0].tolist()

                indices = [i*n+j for i in S for j in compS]

                # Check if solution violates the cut, and if so add the cut to
                # the list.
                if sum(xsolf[i] for i in indices) < 1 - 1e-3:

                    # Presolve cut in order to add it to the presolved problem.
                    mcolsp, dvalp, drhsp, status = prob.presolveRow(rowtype='G',
                                                     origcolind=indices,
                                                     origrowcoef=np.ones(len(indices)),
                                                     origrhs=1)
                    # Since mipdualreductions=0, presolving the cut must succeed, and the cut should
                    # never be relaxed as this would imply that it did not cut off a subtour.
                    assert status == 0

                    nnz += len(mcolsp)
                    ncuts += 1

                    cut_ind.extend(mcolsp)
                    cut_coe.extend(dvalp)
                    cut_rhs.append(drhsp)
                    cut_mstart.append(nnz)

                if ncuts > 0:
                    assert (len(cut_mstart) == ncuts + 1)
                    assert (len(cut_ind) == nnz)

                    prob.addCuts(cuttype=[0] * ncuts,
                                 rowtype=['G'] * ncuts,
                                 rhs=cut_rhs,
                                 start=cut_mstart,
                                 colind=cut_ind,
                                 cutcoef=cut_coe)

    return (reject, None)

def print_sol(p, n):
    '''Print the solution: order of nodes and cost.'''

    xsol = np.array(p.getSolution()).reshape(n, n)
    nextc = np.argmax(xsol, axis=1)

    i = 0

    # Scan cities in order.
    tour = []
    while i != 0 or len(tour) == 0:
        tour.append(str(i))
        i = nextc[i]
    print('->'.join(tour), '->0; cost: ', p.attributes.objval, sep='')


def create_initial_tour(n):
    '''Returns a permuted trivial solution 0->1->2->...->(n-1)->0.'''

    sol = np.zeros((n, n))
    p = np.random.permutation(n)
    for i in range(n):
        sol[p[i], p[(i + 1) % n]] = 1
    return sol.flatten()


def solve_opttour():
    '''Create a random TSP problem.'''

    n = 50
    CITIES = range(n)  # Set of cities: 0..n-1.

    np.random.seed(3)

    X = 100 * np.random.rand(n)
    Y = 100 * np.random.rand(n)

    # Compute distance matrix.
    dist = np.ceil(np.sqrt ((X.reshape(n,1) - X.reshape(1,n))**2 +
                            (Y.reshape(n,1) - Y.reshape(1,n))**2))

    p = xp.problem()

    # Create variables as a square matrix of binary variables.
    fly = xp.array([p.addVariable(vartype=xp.binary, name='x_{0}_{1}'.format(i,j))
                    for i in CITIES for j in CITIES]).reshape(n,n)


    # Degree constraints.
    p.addConstraint(xp.Sum(fly[i,:]) - fly[i,i] == 1  for i in CITIES)
    p.addConstraint(xp.Sum(fly[:,i]) - fly[i,i] == 1  for i in CITIES)

    # Fix diagonals (i.e. city X -> city X) to zero.
    p.addConstraint(fly[i,i] == 0 for i in CITIES)

    # Objective function.
    p.setObjective (xp.Sum((dist * fly).flatten()))

    # Add callbacks.
    p.addPreIntsolCallback(cb_preintsol, n)

    # Disable dual reductions (in order not to cut optimal solutions)
    # and nonlinear reductions, in order to be able to presolve the
    # cuts.
    p.controls.mipdualreductions = 0

    # Create 10 trivial solutions: simple tour 0->1->2...->n->0
    # randomly permuted.
    for k in range(10):
        InitTour = create_initial_tour(n)
        p.addMipSol(solval=InitTour, name="InitTour_{}".format(k))

    p.optimize()

    if p.attributes.solstatus not in [xp.SolStatus.OPTIMAL, xp.SolStatus.FEASIBLE]:
        print("Solve status:", p.attributes.solvestatus.name)
        print("Solution status:", p.attributes.solstatus.name)
    else:
        print_sol(p,n)  # Print solution and cost.

if __name__ == '__main__':
    solve_opttour()

© 2001-2025 Fair Isaac Corporation. All rights reserved. This documentation is the property of Fair Isaac Corporation (“FICO”). Receipt or possession of this documentation does not convey rights to disclose, reproduce, make derivative works, use, or allow others to use it except solely for internal evaluation purposes to determine whether to purchase a license to the software described in this documentation, or as otherwise set forth in a written software license agreement between you and FICO (or a FICO affiliate). Use of this documentation and the software described in it must conform strictly to the foregoing permitted uses, and no other use is permitted.