Package tsp

Top-level package for tsp.

Expand source code
# -*- coding: utf-8 -*-

"""Top-level package for tsp."""
__author__ = """Andreas Klose"""
__email__ = 'aklose@math.au.dk'
__version__ = '3.0'
__date__ = '14/04/2023'

from . import tsp_plot
from . import tsp_heu
from . import tsp_match
from . import tsp_trees
from . import tsp_hk
from . import tsp_cpx
from . import concorde

from .tsp_heu import nearestNeighbour
from .tsp_heu import doInsertion
from .tsp_heu import minSpanTree
from .tsp_heu import christofides

from .tsp_plot import displayPoints
from .tsp_plot import displayRoute
from .tsp_plot import plotEdgeList
from .tsp_plot import set_tk_window

from .tsp_match import assignLB
from .tsp_match import twoMatch

from .tsp_trees import oneTree
from .tsp_trees import fast1Tree
from .tsp_hk import LR_1tree

from .concorde import solveTSP

from .tsp_cpx import CPXtspSolve

from itertools import combinations as combinat
import matplotlib.pyplot as plt 

import tsplib95 as tl
import platform

#-----------------------------------------------------------------------

def default_wait():
    """
    Default wait function used for waiting on user input/action to
    proceed with computations after plotting if Matplot.pyplot runs
    in interactive mode. The action is to click into the drawing window.
    """
    plt.waitforbuttonpress(timeout=-1)
    
#-----------------------------------------------------------------------

class TSP:
    
    def __init__(self, fname=None, problem = None, interactive=True, 
                 anim_route=False, no_wait=False, fontsize=6 ):
        """
        Initialize instance of class TSP. 
        
        Parameters:
          
            **fname** : str, optional    
                None  or the name of the input data file readable by module
                tsplib95. If fname is not None, the problem data is
                read from file fname using tsplib95.load(). Note that
                then "problem" is ignored even if it is not None.
                
            **problem** : class, optional  
                None or a tsp problem instance (cf. module tsplib95).  
            
            **interactive** : bool, optional  
                If True and Matplotlib.pyplot is in non-interactive mode,
                pyplot is put into interactive mode. This then usually
                requires that after plotting something, a function
                is called to wait for user input/action. This is in
                this case, a click inside the window as long as the
                option no_wait is not True, otherwise there is no
                waiting after plotting.
                Default=True.  
            
            **anim_route** : bool, optional  
                If True, Hamiltonian cycles are drawn in animated way, slowly
                edge by edge, if the solution is drawn/displayed. Otherwise,
                the cycle is just drawn if displayed.
            
            **fontsize** : int, optional    
                Size of font in drawings. Default = 6. 
        """        
        self.anim_route = anim_route
        """ Draw route in animated way or not"""
        
        self.fontsize = fontsize
        """Fontsize used in drawings"""
            
        self.one = 0.99999
        """Approximation for 1"""
        
        self.problem = None
        """The problem instance (cf. tsplib95 for documentation)"""
           
        self.route = None
        """The sequence nodes are visited in current solution."""
        
        self.routeLen = 0
        """The length of the current route"""
        
        self.broute = None
        """Sequence of nodes as visited in best solution so far"""
        
        self.brouteLen = 0
        """Length of best route found so far"""
        
        self.sol_method = ""
        """Name of the solution method that created the solution"""
        
        self.lobnd = None
        """A lower bound on the shortest route"""
        
        self.elist = None
        """A list of edges, e.g., in a minimum weight 1-tree"""             
 
        self.xvals = None
        """Solution values to edge variables"""
        
        self.errMsg = ""

        self.logFile = 'nul' if 'Windows' in platform.system() else '/dev/null'
        
        self.wait = None
        
        if interactive and not plt.isinteractive():
            if not no_wait: self.wait = default_wait
            plt.ion()
            
        if not fname is None:
            try:
                p = tl.load(fname)
                self.problem = p
            except:
                self.problem = None
                self.errMsg = "Unable to read data from file "+fname
        else:
            self.problem = problem
        
        p = self.problem
        if p is not None:
            # Set the weight function
            try:
                _ = p.wfunc # tsplib95 version before 0.7.1
            except:
                self.problem.wfunc = p._wfunc # tsplib95, version 0.7.1
            # Check if symmetric TSP
            if not p.is_symmetric():
                if p.is_special() or self.really_asymmetric():
                    self.errMsg = "Cannot treat the asymmetric TSP."
                    self.problem = None
            else:
                # Distance matrix is full matrix type. Change to lower row,
                # as otherwise stupid tsplib95 thinks it is asymmetric
                p.edge_weight_format = 'LOWER_ROW'
                
        if interactive and not plt.isinteractive(): plt.ion()
        if not interactive and plt.isinteractive(): plt.ioff()        
               
    def really_asymmetric(self):
        """
        Checks if distances in the TSP problem are asymmetric
        """
        p = self.problem 
        return max(abs(p.get_weight(e[0],e[1])-p.get_weight(e[1],e[0])) \
                for e in combinat(p.get_nodes(),2)) > 0           
    
    def get_length(self, route):
        """
        Compute length of route "route"
        """
        if route is None or self.problem is None: return 0
        p = self.problem
        L = sum(p.wfunc(route[i-1],route[i]) for i in range(1,p.dimension))
        if route[0] != route[-1]: L += p.wfunc(route[-1],route[0])
        return L
        
    def displayPoints(self):
        """
        Plot the nodes/points appearing in the problem instance
        """
        if self.problem.is_depictable(): 
            displayPoints(self.problem, wait_func=self.wait)
        
    def showSol(self):
        """
        Plot the route.
        """
        if self.problem.is_depictable():
            displayRoute(self.problem,self.route,self.routeLen,animate=self.anim_route,\
                         wait_func=self.wait, fontsize=self.fontsize,\
                         title=self.sol_method)
        else:
            print("Solution obtained by",self.sol_method)
            print("    Route length:",self.routeLen)
            print("    Route       :",self.route)
    
    def showBestSol(self):
        """
        Plot the best route found so far
        """
        if self.problem.is_depictable():
            displayRoute(self.problem,self.broute,self.brouteLen,animate=self.anim_route,\
                         wait_func=self.wait, fontsize=self.fontsize,\
                         title="Best solution found so far")
        else:
            print("Best solution found so far:")
            print("    Route length:",self.routeLen)
            print("    Route       :",self.route)
    
    def showEdgeList( self ):
        """
        Plot the edges selected in a relaxation that was solved to
        obtain a lower bound on the shortest route.
        
        Parameters:
            **xvals** : None or list of float  
                If not None the (fractional) values of the edge
                variables in the solution to the relaxation.
        """            
        if self.problem.is_depictable():
            info = self.sol_method + ". Lower bound: "+str(self.lobnd)
            elst = self.elist
            efract = None 
            if not self.xvals is None: 
                efract = [elst[i] for i in range(len(elst)) if self.xvals[i] < self.one]           
            plotEdgeList( self.problem, elst, specialEdges=efract, title = info,\
                          fontsize=self.fontsize, wait_func=self.wait )
        else:
            print("Lower bound   :",self.lobnd)
            print("Edges selected:",self.elist)
    
    def solve( self, method, anim=False, wait_func=None, nodeLim=None, log_output=True ):   
        """
        Solve the problem instance using method "method!. The solution
        is stored in a list self.route that gives the sequence in which
        the nodes are visited and in a float (or int) called routeLen,
        the length of the route.
        
        Parameters:
            **method** : str  
                Indicates the solution method to be applied and must 
                be one of the following:   
                - NN      for nearest neighbour heuristic  
                - NI      for nearest insertion  
                - FI      for farthest insertion  
                - MST     for min. spanning tree heuristic  
                - CHRISTO for Christofides' heuristic  
                - LK      for Concorde's Lin-Kernighan heuristic  
                - CC      for Concorde's exact branch-and-cut method  
                - CPX     for Cplex MIP solver  
                - 2CFF    for Cplex applied on the 2-commodity flow formulation  
                - 2CFFSCE for 2-commodity flow + subcyle elimination cuts
    
            **anim** : bool    
                If True, the solution method is run with animation active
                (as far as it has animation).
                
            **nodeLim** : int  
                Limits the number of nodes that Cplex is going to enumerate
                If anim=True, it is strongly recommended to set the NodeLim
                to 1 so that just at the root node animated graphics are
                shown.  
                  
            **log_output** : bool    
                If True, the default, and Cplex is used to solve the problem,
                Cplex sends messages on the solution progress to the screen.                
          
          Remark:  
             The methods that use Cplex (CPX, 2CFF, 2CFFSCE) makes use of the
             attributes "route" to set an initial start solution, provided
             of course route is not None.
        """     
        if anim and not self.problem.is_depictable(): anim=False
        method.replace(" ","")       
        method = method.upper()        
        
        logFile = None if log_output else self.logFile
        
        if anim and plt.isinteractive() and wait_func is None:
            wait_func = self.wait 
        
        s, r, L = None, None, None
        if method == 'NN':
            s = 'Nearest neighbour route '
            L, r = nearestNeighbour( self.problem )
        elif method == 'NI':
            s = 'Nearest insertion route '
            L, r = doInsertion( self.problem )
        elif method == 'FI':
            s = 'Farthest insertion route '
            L, r = doInsertion( self.problem, nearest=False )
        elif method == 'MST':
            s = 'MST heuristic solution '
            L, r = minSpanTree( self.problem, display=anim, animate=self.anim_route,\
                                wait_func=wait_func )
        elif method == 'CHRISTO':
            s = 'Christofides solution '
            L, r = christofides( self.problem, display=anim, animate=self.anim_route,\
                                 wait_func=wait_func )
        elif method == 'LK':
            s = 'Lin-Kernighan (Concorde) solution '
            L, r = solveTSP( self.problem, exact=False, logFile=logFile )
        elif method == 'CC':
            s = 'Exact solution using Concorde '
            L, r = solveTSP( self.problem, exact=True, logFile=self.logFile)
        elif ('CPX' in method) or ('2CFF' in method):
            # Apply Cplex to obtain an (optimal) solution
            model = 'SCE' if 'CPX' in method else '2CF'
            if model == '2CF' and 'SCE' in method: model='2CF-SCE'
            lb, L, r = CPXtspSolve(self.problem, startRoute=self.broute, \
                                   formulation=model, nodeLim=nodeLim, \
                                   anim=anim, wait_func=wait_func,\
                                   log_output=log_output )
            s = "No feasible solution found. Lower bound="+str(lb) if r is None \
                else "Cplex solution "
            if r is None: self.lobnd = lb     
           
        if not s is None: self.sol_method = s
        if not r is None: 
            self.route = r 
            self.routeLen = self.get_length(r) if L is None else L
            if self.broute is None or self.brouteLen > L: 
                self.broute = self.route
                self.brouteLen = L
                 

    def get_lobnd(self, method, log_output=False ):
        """
        Compute lower bound on shortest route.
        
        Parameters:
        
            **method** : str    
                Indicates the method to compute the lower bound:      
                - OTREE  for one tree lower bound  
                - FOTREE for the fast one tree lower bound  
                - LRTREE for 1-tree Lagrangian lower bound  
                - ASS    for the assignment lower bound  
                - 2M     for the 2-matching lower bound  
                - F2M    for the fractional 2-matching lower bound
                  
            **log_output** : bool  
                If True, the Lagrangian method as well as methods
                2M and F2M that use Cplex send messages to the screen.
        """
        method.replace(" ","")
        method = method.upper()
        
        w = 0
        elst = None
        xvals = None
        
        if method=='OTREE':
            # Obtain 1-tree lower bound
            w, elst = oneTree( self.problem )
            s = "1-tree bound"
        elif method=='FOTREE':
            # Obtain fast 1-tree lower bound
            w, elst = fast1Tree( self.problem )
            s = "Reinelt's fast 1-tree bound"
        elif method=='LRTREE':
            # Obtain Lagrangian 1-tree lower bound
            w, elst, _  = LR_1tree( self.problem, silent=not log_output )
            s = "Lagrangian 1-tree bound"
        elif method=='ASS':
            # Obtain assignment lower bound
            w, elst = assignLB( self.problem )
            s = "Assignment lower bound"
        elif method=='2M':
            # Obtain 2-matching lower bound
            w, elst = twoMatch( self.problem, log_output=log_output )
            s = "2-matching lower bound"
        elif method=='F2M':
            # Obtain fractional 2-matching lower bound
            w, elst, xvals = twoMatch( self.problem, fractional=True,\
                                       log_output=log_output )
            s = "Fractional 2-matching LB"
        
        if not s is None: self.sol_method = s
        if not w is None: self.lobnd = w
        if not elst is None: self.elist = elst 
        self.xvals = xvals 
         
#----------------------------------------------------------------------

def tsp_gui():
    """
    Graphical user interface to methods from the class TSP.
    """   
    import tkinter as tk
    from tkinter import messagebox
    from tkinter import filedialog
    from tkinter.simpledialog import askinteger
    import sys, os
    
    wmTitle = 'PyTSP - Methods for the TSP'
    wmVersion ="  Version "+__version__+"       "

    nodeLim = 1       # Node limit for Cplex solver
    animCtl = None    # Control variable animation on/off
    logCtl  = None    # Control variable log output on/off
    
    tsp = None        # TSP instance
    root = None       # Main window
    myText = None     # Text widget for log output
    menuBar = None    # Main menu
    plotMenu = None   # Menu for plotting commands

    # Class to redirect output
    class RedirectText(object):
        def __init__(self, text_ctrl):
            self.output = text_ctrl
 
        def write(self, string):
            self.output.insert(tk.END,string)
  
        def flush(self):
            return

    # Waiting function
    def wait_tk():
        plt.waitforbuttonpress(timeout=-1)

    # On closing of the main window
    def ask_closing():
        if tk.messagebox.askokcancel("Quit", "Do you want to quit?"):
            root.destroy()
     
    # Command that does nothing
    nothing = lambda : None
                   
    # Command for file menu      
    def openFile():
        nonlocal tsp, menuBar, plotMenu
        ftypes = [("TSP files","*.tsp")]
        fname = filedialog.askopenfilename( parent=root,
                   title="Select data file", filetypes = ftypes )
        if not fname is None and len(fname) > 0 and os.path.isfile(fname):
            tsp = TSP(fname,interactive=False,no_wait=True)
            if not tsp is None: 
                tsp.displayPoints()
                menuBar.entryconfig("Solve",state="normal")
                menuBar.entryconfig("Bound",state="normal")
                menuBar.entryconfig("Plot",state="normal")
                plotMenu.entryconfig("Plot solution", state="disabled")
                plotMenu.entryconfig("Plot best solution", state="disabled")
                plotMenu.entryconfig("Plot lowbound", state="disabled")
                
    # Command for menu item "about"        
    about = lambda : tk.messagebox.showinfo(wmTitle, wmVersion )

    def fix_nodeLim():
        nonlocal nodeLim
        prompt = "Current node limit is "+str(nodeLim)+". Enter new value:"
        nLim = askinteger("Cplex node limit",prompt,minvalue=1)
        if nLim is not None: nodeLim = nLim
                    
    def solve( method ):
        if not tsp is None:
            if not myText is None: myText.delete( "1.0", tk.END )
            tsp.solve(method, anim=animCtl.get()>0, wait_func=wait_tk, nodeLim=nodeLim,\
                      log_output=logCtl.get()>0)
            if not tsp.route is None: 
                plotMenu.entryconfig("Plot solution", state="normal")
                plotMenu.entryconfig("Plot best solution", state="normal")
                tsp.showSol()
            
    def lobnd( method ):
        if not tsp is None:
            if not myText is None: myText.delete( "1.0", tk.END )
            tsp.get_lobnd( method, log_output = logCtl.get()>0 )
            if not tsp.elist is None: 
                plotMenu.entryconfig("Plot lowbound", state="normal")
                tsp.showEdgeList()
                                    
    # Command: Solve -> Nearest neighbor
    solveNN = lambda : solve('NN')    
    
    # Command: Solve -> Nearest insertion
    solveNI = lambda : solve('NI')
        
    # Command: Solve -> Fartest insertion
    solveFI = lambda : solve('FI')
        
    # Command: Solve -> Min. span tree    
    solveMST = lambda : solve('MST')
      
    # Command: Solve -> Christofides  
    solveCHRISTO = lambda : solve('CHRISTO')
    
    # Command: Solve -> Lin-Kernighan            
    solveLK = lambda : solve('LK')
     
    # Command: Solve -> Concorde  
    solveCC = lambda : solve('CC')
    
    # Command: Solve -> Cplex    
    solveCPX = lambda : solve('CPX')   

    # Command: Solve -> Cplex 2CF formulation    
    solve2CFF = lambda : solve('2CFF')
    
    # Command: Solve -> Cplex 2CF formulation with SCE cuts
    solve2CFFSCE = lambda : solve('2CFFSCE')
    
    # Command: Bound -> 1-tree    
    bnd1T = lambda : lobnd('OTREE')
  
    # Command: Bound -> Fast 1-tree
    bndF1T = lambda : lobnd('FOTREE')

    # Command: Bound -> Lagrangian 1-tree
    bndLR1T = lambda : lobnd('LRTREE' )

    # Command: Bound -> Assignment
    bndAss = lambda : lobnd('ASS')
 
    # Command: Bound -> 2-matching
    bnd2M = lambda : lobnd('2M')
    
    # Commmand: Bound -> fractional 2-matching
    bndF2M = lambda : lobnd('F2M' )
    
    # Command: Plot points
    plotP = lambda : None if tsp is None else tsp.displayPoints()
  
    # Command: Plot solution  
    plotS = lambda : None if tsp is None or tsp.route is None else tsp.showSol()
    
    # Command: Plot best solution  
    plotB = lambda : None if tsp is None or tsp.broute is None else tsp.showBestSol()
        
    # Command: Plot lowbound (selected edges)
    plotE = lambda : None if tsp is None or tsp.elist is None else tsp.showEdgeList()
                               
    # Initialize main window              
    root = tk.Tk()
    root.protocol("WM_DELETE_WINDOW", ask_closing)
    root.title(wmTitle)
    root.minsize(400,300)
    frame = tk.Frame(root)
    frame.pack()
    set_tk_window(root)
    
    # If not run interatively, create a window for log output
    if sys.stdin and sys.stdin.isatty():
        logWin = None
    else:
        logWin = tk.Tk()
        logWin.protocol("WM_DELETE_WINDOW", nothing )
        logWin.title("PyTSP - log output")
        logWin.geometry("400x300+300+300")
        # Make a text element for output
        myScroll = tk.Scrollbar(logWin)
        myText = tk.Text(logWin, height=300, width=500)
        myScroll.pack(side=tk.RIGHT, fill=tk.Y)  
        myText.pack(side=tk.LEFT, fill=tk.Y)
        myScroll.config(command=myText.yview)
        myText.config(yscrollcommand=myScroll.set)
        # Redirect stdout to the text window   
        old_out = sys.stdout
        sys.stdout = RedirectText(myText)
 
    # Main menu
    menuBar = tk.Menu(root,disabledforeground='grey')
   
    # File menu
    fileMenu = tk.Menu(menuBar, tearoff=0)
    fileMenu.add_command(label="Open", underline=0, accelerator="O", command=openFile)
    fileMenu.add_command(label="Exit", underline=1, accelerator="X", command=root.quit)
    menuBar.add_cascade(label="File", underline=0, accelerator="Alt+F", menu=fileMenu)

    # Menu item - Solve
    solveMenu = tk.Menu(menuBar, tearoff=0 )
    solveMenu.add_command(label="Nearest neighbour", underline=0, accelerator="N", command=solveNN)
    solveMenu.add_command(label="Nearest insertion", underline=8, accelerator="I", command=solveNI)
    solveMenu.add_command(label="Farthest insertion", underline=0, accelerator="F", command=solveFI)
    solveMenu.add_command(label="Min. spanning tree", underline=0, accelerator="M", command=solveMST)
    solveMenu.add_command(label="Christofides", underline=0, accelerator="C", command=solveCHRISTO)
    solveMenu.add_command(label="Lin-Kernighan", underline=0, accelerator="L", command=solveLK)
    solveMenu.add_command(label="Concorde B&C", underline=1, accelerator="O", command=solveCC)
    solveMenu.add_command(label="CPLEX", underline=1, accelerator="P", command=solveCPX)
    solveMenu.add_command(label="CPLEX-2CFF", underline=6, accelerator="2", command=solve2CFF)
    solveMenu.add_command(label="2CFF with SCE cuts", underline=10, accelerator="S", command=solve2CFFSCE)
    menuBar.add_cascade(label="Solve", underline=0, accelerator="Alt+S", menu=solveMenu)
    menuBar.entryconfig("Solve",state='disabled')
 
    # Menu item - Bound
    bndMenu = tk.Menu(menuBar, tearoff=0 )
    bndMenu.add_command(label='1-tree', underline=0,accelerator="1",command=bnd1T)
    bndMenu.add_command(label='Fast 1-tree', underline=0,accelerator="F",command=bndF1T)
    bndMenu.add_command(label='Lagrangian 1-tree', underline=0,accelerator="L",command=bndLR1T)
    bndMenu.add_command(label='Assignment', underline=0,accelerator="A",command=bndAss)
    bndMenu.add_command(label='2-matching', underline=0,accelerator="2",command=bnd2M)
    bndMenu.add_command(label='2-matching (fract)', underline=2,accelerator="M",command=bndF2M)
    menuBar.add_cascade(label="Bound", underline=0, accelerator="Alt+B", menu=bndMenu)
    menuBar.entryconfig("Bound",state='disabled')
 
    # Menu item - Plot
    plotMenu = tk.Menu(menuBar, tearoff=0 )
    plotMenu.add_command(label="Plot points", underline=0, accelerator="P",command=plotP)
    plotMenu.add_command(label="Plot solution", underline=5, accelerator="S", command=plotS)
    plotMenu.add_command(label="Plot best solution", underline=5, accelerator="B", command=plotB)
    plotMenu.add_command(label='Plot lowbound', underline=5,accelerator="L",command=plotE)
    menuBar.add_cascade(label="Plot",underline=0, accelerator="Alt+P", menu=plotMenu);
    menuBar.entryconfig("Plot",state='disabled')
 
    # Menu item - Options
    optMenu = tk.Menu(menuBar, tearoff=0 )
    animCtl = tk.IntVar()
    animCtl.set(1)
    optMenu.add_checkbutton(label='Animate',onvalue=1,offvalue=0,variable=animCtl)
    logCtl = tk.IntVar()
    logCtl.set(1)
    optMenu.add_checkbutton(label='Log output',onvalue=1,offvalue=0,variable=logCtl)
    optMenu.add_command(label="Node limit", underline=0, accelerator="N", command=fix_nodeLim)
    menuBar.add_cascade(label="Options", underline=0, accelerator="Alt+O", menu=optMenu)

    menuBar.add_command(label="About", underline=0, accelerator="Alt+A", command=about);

    root.config(menu=menuBar)
    root.mainloop()

    if not logWin is None: sys.stdout = old_out
    

Sub-modules

tsp.concorde

TSP - Python interface to Concorde - A C-library for solving TSPs …

tsp.tsp_cpx

TSP - Solving the symmetric TSP by means of CPLEX's MIP solver and docplex

tsp.tsp_heu

TSP - Some simple heuristics for the symmetric TSP.

tsp.tsp_hk

TSP - Held and Karp (Lagrangian) lower bound for the symmetric TSP based on 1-trees.

tsp.tsp_match

TSP - 2-matching and assignment lower bound for the symmetric TSP.

tsp.tsp_plot

TSP - plotting routines.

tsp.tsp_trees

TSP - Computation of 1-trees as relaxation of the symmetric TSP.

Functions

def default_wait()

Default wait function used for waiting on user input/action to proceed with computations after plotting if Matplot.pyplot runs in interactive mode. The action is to click into the drawing window.

Expand source code
def default_wait():
    """
    Default wait function used for waiting on user input/action to
    proceed with computations after plotting if Matplot.pyplot runs
    in interactive mode. The action is to click into the drawing window.
    """
    plt.waitforbuttonpress(timeout=-1)
def tsp_gui()

Graphical user interface to methods from the class TSP.

Expand source code
def tsp_gui():
    """
    Graphical user interface to methods from the class TSP.
    """   
    import tkinter as tk
    from tkinter import messagebox
    from tkinter import filedialog
    from tkinter.simpledialog import askinteger
    import sys, os
    
    wmTitle = 'PyTSP - Methods for the TSP'
    wmVersion ="  Version "+__version__+"       "

    nodeLim = 1       # Node limit for Cplex solver
    animCtl = None    # Control variable animation on/off
    logCtl  = None    # Control variable log output on/off
    
    tsp = None        # TSP instance
    root = None       # Main window
    myText = None     # Text widget for log output
    menuBar = None    # Main menu
    plotMenu = None   # Menu for plotting commands

    # Class to redirect output
    class RedirectText(object):
        def __init__(self, text_ctrl):
            self.output = text_ctrl
 
        def write(self, string):
            self.output.insert(tk.END,string)
  
        def flush(self):
            return

    # Waiting function
    def wait_tk():
        plt.waitforbuttonpress(timeout=-1)

    # On closing of the main window
    def ask_closing():
        if tk.messagebox.askokcancel("Quit", "Do you want to quit?"):
            root.destroy()
     
    # Command that does nothing
    nothing = lambda : None
                   
    # Command for file menu      
    def openFile():
        nonlocal tsp, menuBar, plotMenu
        ftypes = [("TSP files","*.tsp")]
        fname = filedialog.askopenfilename( parent=root,
                   title="Select data file", filetypes = ftypes )
        if not fname is None and len(fname) > 0 and os.path.isfile(fname):
            tsp = TSP(fname,interactive=False,no_wait=True)
            if not tsp is None: 
                tsp.displayPoints()
                menuBar.entryconfig("Solve",state="normal")
                menuBar.entryconfig("Bound",state="normal")
                menuBar.entryconfig("Plot",state="normal")
                plotMenu.entryconfig("Plot solution", state="disabled")
                plotMenu.entryconfig("Plot best solution", state="disabled")
                plotMenu.entryconfig("Plot lowbound", state="disabled")
                
    # Command for menu item "about"        
    about = lambda : tk.messagebox.showinfo(wmTitle, wmVersion )

    def fix_nodeLim():
        nonlocal nodeLim
        prompt = "Current node limit is "+str(nodeLim)+". Enter new value:"
        nLim = askinteger("Cplex node limit",prompt,minvalue=1)
        if nLim is not None: nodeLim = nLim
                    
    def solve( method ):
        if not tsp is None:
            if not myText is None: myText.delete( "1.0", tk.END )
            tsp.solve(method, anim=animCtl.get()>0, wait_func=wait_tk, nodeLim=nodeLim,\
                      log_output=logCtl.get()>0)
            if not tsp.route is None: 
                plotMenu.entryconfig("Plot solution", state="normal")
                plotMenu.entryconfig("Plot best solution", state="normal")
                tsp.showSol()
            
    def lobnd( method ):
        if not tsp is None:
            if not myText is None: myText.delete( "1.0", tk.END )
            tsp.get_lobnd( method, log_output = logCtl.get()>0 )
            if not tsp.elist is None: 
                plotMenu.entryconfig("Plot lowbound", state="normal")
                tsp.showEdgeList()
                                    
    # Command: Solve -> Nearest neighbor
    solveNN = lambda : solve('NN')    
    
    # Command: Solve -> Nearest insertion
    solveNI = lambda : solve('NI')
        
    # Command: Solve -> Fartest insertion
    solveFI = lambda : solve('FI')
        
    # Command: Solve -> Min. span tree    
    solveMST = lambda : solve('MST')
      
    # Command: Solve -> Christofides  
    solveCHRISTO = lambda : solve('CHRISTO')
    
    # Command: Solve -> Lin-Kernighan            
    solveLK = lambda : solve('LK')
     
    # Command: Solve -> Concorde  
    solveCC = lambda : solve('CC')
    
    # Command: Solve -> Cplex    
    solveCPX = lambda : solve('CPX')   

    # Command: Solve -> Cplex 2CF formulation    
    solve2CFF = lambda : solve('2CFF')
    
    # Command: Solve -> Cplex 2CF formulation with SCE cuts
    solve2CFFSCE = lambda : solve('2CFFSCE')
    
    # Command: Bound -> 1-tree    
    bnd1T = lambda : lobnd('OTREE')
  
    # Command: Bound -> Fast 1-tree
    bndF1T = lambda : lobnd('FOTREE')

    # Command: Bound -> Lagrangian 1-tree
    bndLR1T = lambda : lobnd('LRTREE' )

    # Command: Bound -> Assignment
    bndAss = lambda : lobnd('ASS')
 
    # Command: Bound -> 2-matching
    bnd2M = lambda : lobnd('2M')
    
    # Commmand: Bound -> fractional 2-matching
    bndF2M = lambda : lobnd('F2M' )
    
    # Command: Plot points
    plotP = lambda : None if tsp is None else tsp.displayPoints()
  
    # Command: Plot solution  
    plotS = lambda : None if tsp is None or tsp.route is None else tsp.showSol()
    
    # Command: Plot best solution  
    plotB = lambda : None if tsp is None or tsp.broute is None else tsp.showBestSol()
        
    # Command: Plot lowbound (selected edges)
    plotE = lambda : None if tsp is None or tsp.elist is None else tsp.showEdgeList()
                               
    # Initialize main window              
    root = tk.Tk()
    root.protocol("WM_DELETE_WINDOW", ask_closing)
    root.title(wmTitle)
    root.minsize(400,300)
    frame = tk.Frame(root)
    frame.pack()
    set_tk_window(root)
    
    # If not run interatively, create a window for log output
    if sys.stdin and sys.stdin.isatty():
        logWin = None
    else:
        logWin = tk.Tk()
        logWin.protocol("WM_DELETE_WINDOW", nothing )
        logWin.title("PyTSP - log output")
        logWin.geometry("400x300+300+300")
        # Make a text element for output
        myScroll = tk.Scrollbar(logWin)
        myText = tk.Text(logWin, height=300, width=500)
        myScroll.pack(side=tk.RIGHT, fill=tk.Y)  
        myText.pack(side=tk.LEFT, fill=tk.Y)
        myScroll.config(command=myText.yview)
        myText.config(yscrollcommand=myScroll.set)
        # Redirect stdout to the text window   
        old_out = sys.stdout
        sys.stdout = RedirectText(myText)
 
    # Main menu
    menuBar = tk.Menu(root,disabledforeground='grey')
   
    # File menu
    fileMenu = tk.Menu(menuBar, tearoff=0)
    fileMenu.add_command(label="Open", underline=0, accelerator="O", command=openFile)
    fileMenu.add_command(label="Exit", underline=1, accelerator="X", command=root.quit)
    menuBar.add_cascade(label="File", underline=0, accelerator="Alt+F", menu=fileMenu)

    # Menu item - Solve
    solveMenu = tk.Menu(menuBar, tearoff=0 )
    solveMenu.add_command(label="Nearest neighbour", underline=0, accelerator="N", command=solveNN)
    solveMenu.add_command(label="Nearest insertion", underline=8, accelerator="I", command=solveNI)
    solveMenu.add_command(label="Farthest insertion", underline=0, accelerator="F", command=solveFI)
    solveMenu.add_command(label="Min. spanning tree", underline=0, accelerator="M", command=solveMST)
    solveMenu.add_command(label="Christofides", underline=0, accelerator="C", command=solveCHRISTO)
    solveMenu.add_command(label="Lin-Kernighan", underline=0, accelerator="L", command=solveLK)
    solveMenu.add_command(label="Concorde B&C", underline=1, accelerator="O", command=solveCC)
    solveMenu.add_command(label="CPLEX", underline=1, accelerator="P", command=solveCPX)
    solveMenu.add_command(label="CPLEX-2CFF", underline=6, accelerator="2", command=solve2CFF)
    solveMenu.add_command(label="2CFF with SCE cuts", underline=10, accelerator="S", command=solve2CFFSCE)
    menuBar.add_cascade(label="Solve", underline=0, accelerator="Alt+S", menu=solveMenu)
    menuBar.entryconfig("Solve",state='disabled')
 
    # Menu item - Bound
    bndMenu = tk.Menu(menuBar, tearoff=0 )
    bndMenu.add_command(label='1-tree', underline=0,accelerator="1",command=bnd1T)
    bndMenu.add_command(label='Fast 1-tree', underline=0,accelerator="F",command=bndF1T)
    bndMenu.add_command(label='Lagrangian 1-tree', underline=0,accelerator="L",command=bndLR1T)
    bndMenu.add_command(label='Assignment', underline=0,accelerator="A",command=bndAss)
    bndMenu.add_command(label='2-matching', underline=0,accelerator="2",command=bnd2M)
    bndMenu.add_command(label='2-matching (fract)', underline=2,accelerator="M",command=bndF2M)
    menuBar.add_cascade(label="Bound", underline=0, accelerator="Alt+B", menu=bndMenu)
    menuBar.entryconfig("Bound",state='disabled')
 
    # Menu item - Plot
    plotMenu = tk.Menu(menuBar, tearoff=0 )
    plotMenu.add_command(label="Plot points", underline=0, accelerator="P",command=plotP)
    plotMenu.add_command(label="Plot solution", underline=5, accelerator="S", command=plotS)
    plotMenu.add_command(label="Plot best solution", underline=5, accelerator="B", command=plotB)
    plotMenu.add_command(label='Plot lowbound', underline=5,accelerator="L",command=plotE)
    menuBar.add_cascade(label="Plot",underline=0, accelerator="Alt+P", menu=plotMenu);
    menuBar.entryconfig("Plot",state='disabled')
 
    # Menu item - Options
    optMenu = tk.Menu(menuBar, tearoff=0 )
    animCtl = tk.IntVar()
    animCtl.set(1)
    optMenu.add_checkbutton(label='Animate',onvalue=1,offvalue=0,variable=animCtl)
    logCtl = tk.IntVar()
    logCtl.set(1)
    optMenu.add_checkbutton(label='Log output',onvalue=1,offvalue=0,variable=logCtl)
    optMenu.add_command(label="Node limit", underline=0, accelerator="N", command=fix_nodeLim)
    menuBar.add_cascade(label="Options", underline=0, accelerator="Alt+O", menu=optMenu)

    menuBar.add_command(label="About", underline=0, accelerator="Alt+A", command=about);

    root.config(menu=menuBar)
    root.mainloop()

    if not logWin is None: sys.stdout = old_out

Classes

class TSP (fname=None, problem=None, interactive=True, anim_route=False, no_wait=False, fontsize=6)

Initialize instance of class TSP.

Parameters

fname : str, optional
None or the name of the input data file readable by module tsplib95. If fname is not None, the problem data is read from file fname using tsplib95.load(). Note that then "problem" is ignored even if it is not None.

problem : class, optional
None or a tsp problem instance (cf. module tsplib95).

interactive : bool, optional
If True and Matplotlib.pyplot is in non-interactive mode, pyplot is put into interactive mode. This then usually requires that after plotting something, a function is called to wait for user input/action. This is in this case, a click inside the window as long as the option no_wait is not True, otherwise there is no waiting after plotting. Default=True.

anim_route : bool, optional
If True, Hamiltonian cycles are drawn in animated way, slowly edge by edge, if the solution is drawn/displayed. Otherwise, the cycle is just drawn if displayed.

fontsize : int, optional
Size of font in drawings. Default = 6.

Expand source code
class TSP:
    
    def __init__(self, fname=None, problem = None, interactive=True, 
                 anim_route=False, no_wait=False, fontsize=6 ):
        """
        Initialize instance of class TSP. 
        
        Parameters:
          
            **fname** : str, optional    
                None  or the name of the input data file readable by module
                tsplib95. If fname is not None, the problem data is
                read from file fname using tsplib95.load(). Note that
                then "problem" is ignored even if it is not None.
                
            **problem** : class, optional  
                None or a tsp problem instance (cf. module tsplib95).  
            
            **interactive** : bool, optional  
                If True and Matplotlib.pyplot is in non-interactive mode,
                pyplot is put into interactive mode. This then usually
                requires that after plotting something, a function
                is called to wait for user input/action. This is in
                this case, a click inside the window as long as the
                option no_wait is not True, otherwise there is no
                waiting after plotting.
                Default=True.  
            
            **anim_route** : bool, optional  
                If True, Hamiltonian cycles are drawn in animated way, slowly
                edge by edge, if the solution is drawn/displayed. Otherwise,
                the cycle is just drawn if displayed.
            
            **fontsize** : int, optional    
                Size of font in drawings. Default = 6. 
        """        
        self.anim_route = anim_route
        """ Draw route in animated way or not"""
        
        self.fontsize = fontsize
        """Fontsize used in drawings"""
            
        self.one = 0.99999
        """Approximation for 1"""
        
        self.problem = None
        """The problem instance (cf. tsplib95 for documentation)"""
           
        self.route = None
        """The sequence nodes are visited in current solution."""
        
        self.routeLen = 0
        """The length of the current route"""
        
        self.broute = None
        """Sequence of nodes as visited in best solution so far"""
        
        self.brouteLen = 0
        """Length of best route found so far"""
        
        self.sol_method = ""
        """Name of the solution method that created the solution"""
        
        self.lobnd = None
        """A lower bound on the shortest route"""
        
        self.elist = None
        """A list of edges, e.g., in a minimum weight 1-tree"""             
 
        self.xvals = None
        """Solution values to edge variables"""
        
        self.errMsg = ""

        self.logFile = 'nul' if 'Windows' in platform.system() else '/dev/null'
        
        self.wait = None
        
        if interactive and not plt.isinteractive():
            if not no_wait: self.wait = default_wait
            plt.ion()
            
        if not fname is None:
            try:
                p = tl.load(fname)
                self.problem = p
            except:
                self.problem = None
                self.errMsg = "Unable to read data from file "+fname
        else:
            self.problem = problem
        
        p = self.problem
        if p is not None:
            # Set the weight function
            try:
                _ = p.wfunc # tsplib95 version before 0.7.1
            except:
                self.problem.wfunc = p._wfunc # tsplib95, version 0.7.1
            # Check if symmetric TSP
            if not p.is_symmetric():
                if p.is_special() or self.really_asymmetric():
                    self.errMsg = "Cannot treat the asymmetric TSP."
                    self.problem = None
            else:
                # Distance matrix is full matrix type. Change to lower row,
                # as otherwise stupid tsplib95 thinks it is asymmetric
                p.edge_weight_format = 'LOWER_ROW'
                
        if interactive and not plt.isinteractive(): plt.ion()
        if not interactive and plt.isinteractive(): plt.ioff()        
               
    def really_asymmetric(self):
        """
        Checks if distances in the TSP problem are asymmetric
        """
        p = self.problem 
        return max(abs(p.get_weight(e[0],e[1])-p.get_weight(e[1],e[0])) \
                for e in combinat(p.get_nodes(),2)) > 0           
    
    def get_length(self, route):
        """
        Compute length of route "route"
        """
        if route is None or self.problem is None: return 0
        p = self.problem
        L = sum(p.wfunc(route[i-1],route[i]) for i in range(1,p.dimension))
        if route[0] != route[-1]: L += p.wfunc(route[-1],route[0])
        return L
        
    def displayPoints(self):
        """
        Plot the nodes/points appearing in the problem instance
        """
        if self.problem.is_depictable(): 
            displayPoints(self.problem, wait_func=self.wait)
        
    def showSol(self):
        """
        Plot the route.
        """
        if self.problem.is_depictable():
            displayRoute(self.problem,self.route,self.routeLen,animate=self.anim_route,\
                         wait_func=self.wait, fontsize=self.fontsize,\
                         title=self.sol_method)
        else:
            print("Solution obtained by",self.sol_method)
            print("    Route length:",self.routeLen)
            print("    Route       :",self.route)
    
    def showBestSol(self):
        """
        Plot the best route found so far
        """
        if self.problem.is_depictable():
            displayRoute(self.problem,self.broute,self.brouteLen,animate=self.anim_route,\
                         wait_func=self.wait, fontsize=self.fontsize,\
                         title="Best solution found so far")
        else:
            print("Best solution found so far:")
            print("    Route length:",self.routeLen)
            print("    Route       :",self.route)
    
    def showEdgeList( self ):
        """
        Plot the edges selected in a relaxation that was solved to
        obtain a lower bound on the shortest route.
        
        Parameters:
            **xvals** : None or list of float  
                If not None the (fractional) values of the edge
                variables in the solution to the relaxation.
        """            
        if self.problem.is_depictable():
            info = self.sol_method + ". Lower bound: "+str(self.lobnd)
            elst = self.elist
            efract = None 
            if not self.xvals is None: 
                efract = [elst[i] for i in range(len(elst)) if self.xvals[i] < self.one]           
            plotEdgeList( self.problem, elst, specialEdges=efract, title = info,\
                          fontsize=self.fontsize, wait_func=self.wait )
        else:
            print("Lower bound   :",self.lobnd)
            print("Edges selected:",self.elist)
    
    def solve( self, method, anim=False, wait_func=None, nodeLim=None, log_output=True ):   
        """
        Solve the problem instance using method "method!. The solution
        is stored in a list self.route that gives the sequence in which
        the nodes are visited and in a float (or int) called routeLen,
        the length of the route.
        
        Parameters:
            **method** : str  
                Indicates the solution method to be applied and must 
                be one of the following:   
                - NN      for nearest neighbour heuristic  
                - NI      for nearest insertion  
                - FI      for farthest insertion  
                - MST     for min. spanning tree heuristic  
                - CHRISTO for Christofides' heuristic  
                - LK      for Concorde's Lin-Kernighan heuristic  
                - CC      for Concorde's exact branch-and-cut method  
                - CPX     for Cplex MIP solver  
                - 2CFF    for Cplex applied on the 2-commodity flow formulation  
                - 2CFFSCE for 2-commodity flow + subcyle elimination cuts
    
            **anim** : bool    
                If True, the solution method is run with animation active
                (as far as it has animation).
                
            **nodeLim** : int  
                Limits the number of nodes that Cplex is going to enumerate
                If anim=True, it is strongly recommended to set the NodeLim
                to 1 so that just at the root node animated graphics are
                shown.  
                  
            **log_output** : bool    
                If True, the default, and Cplex is used to solve the problem,
                Cplex sends messages on the solution progress to the screen.                
          
          Remark:  
             The methods that use Cplex (CPX, 2CFF, 2CFFSCE) makes use of the
             attributes "route" to set an initial start solution, provided
             of course route is not None.
        """     
        if anim and not self.problem.is_depictable(): anim=False
        method.replace(" ","")       
        method = method.upper()        
        
        logFile = None if log_output else self.logFile
        
        if anim and plt.isinteractive() and wait_func is None:
            wait_func = self.wait 
        
        s, r, L = None, None, None
        if method == 'NN':
            s = 'Nearest neighbour route '
            L, r = nearestNeighbour( self.problem )
        elif method == 'NI':
            s = 'Nearest insertion route '
            L, r = doInsertion( self.problem )
        elif method == 'FI':
            s = 'Farthest insertion route '
            L, r = doInsertion( self.problem, nearest=False )
        elif method == 'MST':
            s = 'MST heuristic solution '
            L, r = minSpanTree( self.problem, display=anim, animate=self.anim_route,\
                                wait_func=wait_func )
        elif method == 'CHRISTO':
            s = 'Christofides solution '
            L, r = christofides( self.problem, display=anim, animate=self.anim_route,\
                                 wait_func=wait_func )
        elif method == 'LK':
            s = 'Lin-Kernighan (Concorde) solution '
            L, r = solveTSP( self.problem, exact=False, logFile=logFile )
        elif method == 'CC':
            s = 'Exact solution using Concorde '
            L, r = solveTSP( self.problem, exact=True, logFile=self.logFile)
        elif ('CPX' in method) or ('2CFF' in method):
            # Apply Cplex to obtain an (optimal) solution
            model = 'SCE' if 'CPX' in method else '2CF'
            if model == '2CF' and 'SCE' in method: model='2CF-SCE'
            lb, L, r = CPXtspSolve(self.problem, startRoute=self.broute, \
                                   formulation=model, nodeLim=nodeLim, \
                                   anim=anim, wait_func=wait_func,\
                                   log_output=log_output )
            s = "No feasible solution found. Lower bound="+str(lb) if r is None \
                else "Cplex solution "
            if r is None: self.lobnd = lb     
           
        if not s is None: self.sol_method = s
        if not r is None: 
            self.route = r 
            self.routeLen = self.get_length(r) if L is None else L
            if self.broute is None or self.brouteLen > L: 
                self.broute = self.route
                self.brouteLen = L
                 

    def get_lobnd(self, method, log_output=False ):
        """
        Compute lower bound on shortest route.
        
        Parameters:
        
            **method** : str    
                Indicates the method to compute the lower bound:      
                - OTREE  for one tree lower bound  
                - FOTREE for the fast one tree lower bound  
                - LRTREE for 1-tree Lagrangian lower bound  
                - ASS    for the assignment lower bound  
                - 2M     for the 2-matching lower bound  
                - F2M    for the fractional 2-matching lower bound
                  
            **log_output** : bool  
                If True, the Lagrangian method as well as methods
                2M and F2M that use Cplex send messages to the screen.
        """
        method.replace(" ","")
        method = method.upper()
        
        w = 0
        elst = None
        xvals = None
        
        if method=='OTREE':
            # Obtain 1-tree lower bound
            w, elst = oneTree( self.problem )
            s = "1-tree bound"
        elif method=='FOTREE':
            # Obtain fast 1-tree lower bound
            w, elst = fast1Tree( self.problem )
            s = "Reinelt's fast 1-tree bound"
        elif method=='LRTREE':
            # Obtain Lagrangian 1-tree lower bound
            w, elst, _  = LR_1tree( self.problem, silent=not log_output )
            s = "Lagrangian 1-tree bound"
        elif method=='ASS':
            # Obtain assignment lower bound
            w, elst = assignLB( self.problem )
            s = "Assignment lower bound"
        elif method=='2M':
            # Obtain 2-matching lower bound
            w, elst = twoMatch( self.problem, log_output=log_output )
            s = "2-matching lower bound"
        elif method=='F2M':
            # Obtain fractional 2-matching lower bound
            w, elst, xvals = twoMatch( self.problem, fractional=True,\
                                       log_output=log_output )
            s = "Fractional 2-matching LB"
        
        if not s is None: self.sol_method = s
        if not w is None: self.lobnd = w
        if not elst is None: self.elist = elst 
        self.xvals = xvals 

Instance variables

var anim_route

Draw route in animated way or not

var broute

Sequence of nodes as visited in best solution so far

var brouteLen

Length of best route found so far

var elist

A list of edges, e.g., in a minimum weight 1-tree

var fontsize

Fontsize used in drawings

var lobnd

A lower bound on the shortest route

var one

Approximation for 1

var problem

The problem instance (cf. tsplib95 for documentation)

var route

The sequence nodes are visited in current solution.

var routeLen

The length of the current route

var sol_method

Name of the solution method that created the solution

var xvals

Solution values to edge variables

Methods

def displayPoints(self)

Plot the nodes/points appearing in the problem instance

Expand source code
def displayPoints(self):
    """
    Plot the nodes/points appearing in the problem instance
    """
    if self.problem.is_depictable(): 
        displayPoints(self.problem, wait_func=self.wait)
def get_length(self, route)

Compute length of route "route"

Expand source code
def get_length(self, route):
    """
    Compute length of route "route"
    """
    if route is None or self.problem is None: return 0
    p = self.problem
    L = sum(p.wfunc(route[i-1],route[i]) for i in range(1,p.dimension))
    if route[0] != route[-1]: L += p.wfunc(route[-1],route[0])
    return L
def get_lobnd(self, method, log_output=False)

Compute lower bound on shortest route.

Parameters

method : str
Indicates the method to compute the lower bound:
- OTREE for one tree lower bound
- FOTREE for the fast one tree lower bound
- LRTREE for 1-tree Lagrangian lower bound
- ASS for the assignment lower bound
- 2M for the 2-matching lower bound
- F2M for the fractional 2-matching lower bound

log_output : bool
If True, the Lagrangian method as well as methods 2M and F2M that use Cplex send messages to the screen.

Expand source code
def get_lobnd(self, method, log_output=False ):
    """
    Compute lower bound on shortest route.
    
    Parameters:
    
        **method** : str    
            Indicates the method to compute the lower bound:      
            - OTREE  for one tree lower bound  
            - FOTREE for the fast one tree lower bound  
            - LRTREE for 1-tree Lagrangian lower bound  
            - ASS    for the assignment lower bound  
            - 2M     for the 2-matching lower bound  
            - F2M    for the fractional 2-matching lower bound
              
        **log_output** : bool  
            If True, the Lagrangian method as well as methods
            2M and F2M that use Cplex send messages to the screen.
    """
    method.replace(" ","")
    method = method.upper()
    
    w = 0
    elst = None
    xvals = None
    
    if method=='OTREE':
        # Obtain 1-tree lower bound
        w, elst = oneTree( self.problem )
        s = "1-tree bound"
    elif method=='FOTREE':
        # Obtain fast 1-tree lower bound
        w, elst = fast1Tree( self.problem )
        s = "Reinelt's fast 1-tree bound"
    elif method=='LRTREE':
        # Obtain Lagrangian 1-tree lower bound
        w, elst, _  = LR_1tree( self.problem, silent=not log_output )
        s = "Lagrangian 1-tree bound"
    elif method=='ASS':
        # Obtain assignment lower bound
        w, elst = assignLB( self.problem )
        s = "Assignment lower bound"
    elif method=='2M':
        # Obtain 2-matching lower bound
        w, elst = twoMatch( self.problem, log_output=log_output )
        s = "2-matching lower bound"
    elif method=='F2M':
        # Obtain fractional 2-matching lower bound
        w, elst, xvals = twoMatch( self.problem, fractional=True,\
                                   log_output=log_output )
        s = "Fractional 2-matching LB"
    
    if not s is None: self.sol_method = s
    if not w is None: self.lobnd = w
    if not elst is None: self.elist = elst 
    self.xvals = xvals 
def really_asymmetric(self)

Checks if distances in the TSP problem are asymmetric

Expand source code
def really_asymmetric(self):
    """
    Checks if distances in the TSP problem are asymmetric
    """
    p = self.problem 
    return max(abs(p.get_weight(e[0],e[1])-p.get_weight(e[1],e[0])) \
            for e in combinat(p.get_nodes(),2)) > 0           
def showBestSol(self)

Plot the best route found so far

Expand source code
def showBestSol(self):
    """
    Plot the best route found so far
    """
    if self.problem.is_depictable():
        displayRoute(self.problem,self.broute,self.brouteLen,animate=self.anim_route,\
                     wait_func=self.wait, fontsize=self.fontsize,\
                     title="Best solution found so far")
    else:
        print("Best solution found so far:")
        print("    Route length:",self.routeLen)
        print("    Route       :",self.route)
def showEdgeList(self)

Plot the edges selected in a relaxation that was solved to obtain a lower bound on the shortest route.

Parameters

xvals : None or list of float
If not None the (fractional) values of the edge variables in the solution to the relaxation.

Expand source code
def showEdgeList( self ):
    """
    Plot the edges selected in a relaxation that was solved to
    obtain a lower bound on the shortest route.
    
    Parameters:
        **xvals** : None or list of float  
            If not None the (fractional) values of the edge
            variables in the solution to the relaxation.
    """            
    if self.problem.is_depictable():
        info = self.sol_method + ". Lower bound: "+str(self.lobnd)
        elst = self.elist
        efract = None 
        if not self.xvals is None: 
            efract = [elst[i] for i in range(len(elst)) if self.xvals[i] < self.one]           
        plotEdgeList( self.problem, elst, specialEdges=efract, title = info,\
                      fontsize=self.fontsize, wait_func=self.wait )
    else:
        print("Lower bound   :",self.lobnd)
        print("Edges selected:",self.elist)
def showSol(self)

Plot the route.

Expand source code
def showSol(self):
    """
    Plot the route.
    """
    if self.problem.is_depictable():
        displayRoute(self.problem,self.route,self.routeLen,animate=self.anim_route,\
                     wait_func=self.wait, fontsize=self.fontsize,\
                     title=self.sol_method)
    else:
        print("Solution obtained by",self.sol_method)
        print("    Route length:",self.routeLen)
        print("    Route       :",self.route)
def solve(self, method, anim=False, wait_func=None, nodeLim=None, log_output=True)

Solve the problem instance using method "method!. The solution is stored in a list self.route that gives the sequence in which the nodes are visited and in a float (or int) called routeLen, the length of the route.

Parameters

method : str
Indicates the solution method to be applied and must be one of the following:
- NN for nearest neighbour heuristic
- NI for nearest insertion
- FI for farthest insertion
- MST for min. spanning tree heuristic
- CHRISTO for Christofides' heuristic
- LK for Concorde's Lin-Kernighan heuristic
- CC for Concorde's exact branch-and-cut method
- CPX for Cplex MIP solver
- 2CFF for Cplex applied on the 2-commodity flow formulation
- 2CFFSCE for 2-commodity flow + subcyle elimination cuts

anim : bool
If True, the solution method is run with animation active (as far as it has animation).

nodeLim : int
Limits the number of nodes that Cplex is going to enumerate If anim=True, it is strongly recommended to set the NodeLim to 1 so that just at the root node animated graphics are shown.

log_output : bool
If True, the default, and Cplex is used to solve the problem, Cplex sends messages on the solution progress to the screen.

Remark:
The methods that use Cplex (CPX, 2CFF, 2CFFSCE) makes use of the attributes "route" to set an initial start solution, provided of course route is not None.

Expand source code
def solve( self, method, anim=False, wait_func=None, nodeLim=None, log_output=True ):   
    """
    Solve the problem instance using method "method!. The solution
    is stored in a list self.route that gives the sequence in which
    the nodes are visited and in a float (or int) called routeLen,
    the length of the route.
    
    Parameters:
        **method** : str  
            Indicates the solution method to be applied and must 
            be one of the following:   
            - NN      for nearest neighbour heuristic  
            - NI      for nearest insertion  
            - FI      for farthest insertion  
            - MST     for min. spanning tree heuristic  
            - CHRISTO for Christofides' heuristic  
            - LK      for Concorde's Lin-Kernighan heuristic  
            - CC      for Concorde's exact branch-and-cut method  
            - CPX     for Cplex MIP solver  
            - 2CFF    for Cplex applied on the 2-commodity flow formulation  
            - 2CFFSCE for 2-commodity flow + subcyle elimination cuts

        **anim** : bool    
            If True, the solution method is run with animation active
            (as far as it has animation).
            
        **nodeLim** : int  
            Limits the number of nodes that Cplex is going to enumerate
            If anim=True, it is strongly recommended to set the NodeLim
            to 1 so that just at the root node animated graphics are
            shown.  
              
        **log_output** : bool    
            If True, the default, and Cplex is used to solve the problem,
            Cplex sends messages on the solution progress to the screen.                
      
      Remark:  
         The methods that use Cplex (CPX, 2CFF, 2CFFSCE) makes use of the
         attributes "route" to set an initial start solution, provided
         of course route is not None.
    """     
    if anim and not self.problem.is_depictable(): anim=False
    method.replace(" ","")       
    method = method.upper()        
    
    logFile = None if log_output else self.logFile
    
    if anim and plt.isinteractive() and wait_func is None:
        wait_func = self.wait 
    
    s, r, L = None, None, None
    if method == 'NN':
        s = 'Nearest neighbour route '
        L, r = nearestNeighbour( self.problem )
    elif method == 'NI':
        s = 'Nearest insertion route '
        L, r = doInsertion( self.problem )
    elif method == 'FI':
        s = 'Farthest insertion route '
        L, r = doInsertion( self.problem, nearest=False )
    elif method == 'MST':
        s = 'MST heuristic solution '
        L, r = minSpanTree( self.problem, display=anim, animate=self.anim_route,\
                            wait_func=wait_func )
    elif method == 'CHRISTO':
        s = 'Christofides solution '
        L, r = christofides( self.problem, display=anim, animate=self.anim_route,\
                             wait_func=wait_func )
    elif method == 'LK':
        s = 'Lin-Kernighan (Concorde) solution '
        L, r = solveTSP( self.problem, exact=False, logFile=logFile )
    elif method == 'CC':
        s = 'Exact solution using Concorde '
        L, r = solveTSP( self.problem, exact=True, logFile=self.logFile)
    elif ('CPX' in method) or ('2CFF' in method):
        # Apply Cplex to obtain an (optimal) solution
        model = 'SCE' if 'CPX' in method else '2CF'
        if model == '2CF' and 'SCE' in method: model='2CF-SCE'
        lb, L, r = CPXtspSolve(self.problem, startRoute=self.broute, \
                               formulation=model, nodeLim=nodeLim, \
                               anim=anim, wait_func=wait_func,\
                               log_output=log_output )
        s = "No feasible solution found. Lower bound="+str(lb) if r is None \
            else "Cplex solution "
        if r is None: self.lobnd = lb     
       
    if not s is None: self.sol_method = s
    if not r is None: 
        self.route = r 
        self.routeLen = self.get_length(r) if L is None else L
        if self.broute is None or self.brouteLen > L: 
            self.broute = self.route
            self.brouteLen = L