Module tsp.tsp_heu

TSP - Some simple heuristics for the symmetric TSP.

Expand source code
"""
    TSP - Some simple heuristics for the symmetric TSP.
"""
import numpy as np
import networkx as nx
from tsp.tsp_plot import displayRoute
from tsp.tsp_plot import plotEdgeList
    
#---------------------------------------------------------------------

def __chk_wfunc( problem ):
    """
    Check which version of tsplib95 applies and set the (edge) weight
    function accordingly
    """
    try:
        _ = problem.wfunc # tsplib95 version before 0.7.1
    except:
        problem.wfunc = problem._wfunc # tsplib95, version 0.7.1
            
#---------------------------------------------------------------------

def nearestNeighbour( problem ):
    """
    Computes solution using nearest neighbour with random start point.

    Parameters:
        **problem** : class  
            TSP problem object has returned by function load_problem
            of the package tsplib95.

    Returns:
        **routeLen** : int  
            Length of the route.  
        **route** : list of int   
            List of points on the route where route[-1] is the last node visited.
    """
    __chk_wfunc( problem )
    unRouted = [ i for i in problem.get_nodes()]
    routeLen = 0
    # Choose random start point
    node = np.random.choice(unRouted)
    route = [ node ]
    unRouted.remove(node)
    while len(unRouted) > 0:
        nextNode = min( unRouted, key = lambda j : problem.wfunc(node,j) )
        unRouted.remove(nextNode)
        route.append(nextNode)
        routeLen += problem.wfunc(node,nextNode)
        node = nextNode
    routeLen += problem.wfunc(node,route[0])
    return routeLen, route

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

def doInsertion( problem, nearest=True ):
    """
    Computes solution using nearest or farthest insertion with random 
    start point.

    Parameters:
        **problem** : class  
            TSP problem object has returned by function load_problem
            of the package tsplib95.  
        **nearest** : bool, optional  
            If false, farthest insertion is applied. Otherwise
            nearest insertion is applied (default)

    Returns:
        **routeLen** : int  
            Length of the route.  
        **route** : list of int   
            List of points on the route where route[-1] is the last node visited.
    """
    def insCost( node, i, route ):
        """
        Return cost to insert node at position between i and i+1 in 
        the route.
        """
        j1 = route[i]
        j2 = route[(i+1) % len(route)]
        delta = problem.wfunc(node,j1)+problem.wfunc(node,j2)-problem.wfunc(j1,j2)
        return delta
    
    __chk_wfunc(problem)
    
    # Initialize the the route with some randomly selected edge
    unRouted = [ node for node in problem.get_nodes()]
    n1 = np.random.choice(unRouted)
    unRouted.remove(n1)
    n2 = np.random.choice(unRouted)
    unRouted.remove(n2)
    route = [n1,n2,n1]
    routeLen = problem.wfunc(n1,n2)*2
    sign = 1
    if not nearest : sign = -1
    while len(unRouted) > 0:
        # Determine unrouted node closest (or farthest) to a routed node
        nextNode = None
        minDist  = np.Inf
        for j in unRouted:
            dist = min( [sign*problem.wfunc(j,node) for node in route] )
            if dist < minDist:
                dist = minDist
                nextNode = j
        # Insert nextNode at cheapest insertion place
        ins_at = min([i for i in range(len(route))],\
                      key = lambda i : insCost( nextNode, i, route ) )
        routeLen += insCost(nextNode,ins_at,route)
        route.insert(ins_at+1,nextNode)
        unRouted.remove( nextNode )

    return routeLen, route[:-1]

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

def minSpanTree( problem, display=True, animate=True, wait_func=None,
                 fontsize=6):
    """
    Apply the minimum spanning tree heuristic.

    Parameters:
        **problem** : class  
            TSP problem object has returned by function load_problem
            of the package tsplib95.    
        **display** : bool, optional  
            I true (default) a figure of the minimum spanning tree, 
            the Eulerian cycle and the resulting route is displayed.  
        **animate** : bool, optional  
            If true (default) the Eulerian cycle and the route are 
            drawn in an animated way.  
        **wait_func** : function, optional     
            Waiting function called after drawing (intermediate) solutions
            when display=True. Will be required when Matplotlib.pyplot
            is in interactive mode, as then the window has not explicitly
            to be destroyed to proceed with the computations. The function
            itself does not take any arguments, and should ask for some
            user action to continue.  
        **fontsize** : int, optional  
            Size of the font used to display node numbers (only
            relevant if display=True)
              
    Returns:
        **routeLen** : int  
            Length of the route.  
        **route** : list of int      
            List of points on the route where route[-1] is the last node visited.
    """
    __chk_wfunc(problem)
    # Represent problem data using a networkx graph object
    G = problem.get_graph()

    # Obtain the minimum spanning tree and the Eulerian graph by doubling the tree
    T = nx.minimum_spanning_tree(G)
    E = nx.MultiGraph( T )
    E.add_edges_from(T.edges)
    tLen = sum( T.get_edge_data(*e)['weight'] for e in T.edges )

    # Display the minimum spanning tree 
    if display: 
        plotEdgeList( problem, E.edges, title='Minimum spanning '+\
                              'tree/Eulerian graph. Length= '+str(tLen),\
                              fontsize=fontsize, wait_func=wait_func)
    circuit = [u for u,_ in nx.eulerian_circuit(E)]
    circuit.append(circuit[0])
    if display:
        displayRoute( problem, circuit, 2*tLen, animate=animate,\
                     title='Eulerian circuit',fontsize=fontsize,wait_func=wait_func)

    # Build the TSP route by taking shortcuts in the Eulerian cycle
    route = list(dict.fromkeys(circuit)) # This removes duplicates from the circuit
    routeLen = sum( problem.wfunc(route[i-1],route[i]) for i in range(1,problem.dimension) )
    routeLen += problem.wfunc(route[-1],route[0])
    if display:
        displayRoute( problem, route, routeLen, animate=animate,\
                     title='Found tour',fontsize=fontsize,wait_func=wait_func)
            
    return routeLen, route    
        
#---------------------------------------------------------------------

def christofides( problem, display=True, animate=True, wait_func=None,
                  fontsize=6 ):
    """
    Apply the Christofides' heuristic.

    Parameters:
        **problem** : class  
            TSP problem object has returned by function load_problem
            of the package tsplib95.  
        **display** : bool, optional  
            I true (default) a figure of the minimum spanning tree, 
            the Eulerian cycle and the resulting route is displayed.  
        **animate** : bool, optional  
            If true (default) the Eulerian cycle and the route are 
            drawn in an animated way.  
        **wait_func** : function, optional     
            Waiting function called after drawing (intermediate) solutions
            when display=True. Will be required when Matplotlib.pyplot
            is in interactive mode, as then the window has not explicitly
            to be destroyed to proceed with the computations. The function
            itself does not take any arguments, and should ask for some
            user action to continue.  
        **fontsize** : int, optional  
            Size of the font used to display node numbers (only
            relevant if display=True)
                            
    Returns:
        **routeLen** : int  
            Length of the route.  
        **route** : list of int    
            List of points on the route where route[-1] is the last node visited.
    """
    __chk_wfunc(problem)
        
    # Represent problem data using a networkx graph object
    G = problem.get_graph()

    # The graph may contain edges connecting a node to itself
    # We first remove all these simple loops
    G.remove_edges_from(nx.selfloop_edges(G))

    # Initialize the graph (that later will be Eulerian)
    # as a multigraph that first contains the edges from
    # a minimim spanning tree of G
    E = nx.MultiGraph( nx.minimum_spanning_tree(G) )
    tLen = sum( E.get_edge_data(*e)['weight'] for e in E.edges )
    
    # Find the nodes of odd degree in the minimum spanning tree
    odd = [ node for node in E.nodes if E.degree(node) % 2 > 0 ]

    # Display the minimum spanning tree and mark the odd nodes
    if display: 
        plotEdgeList( problem, E.edges, specialNodes=odd, title=\
                      'Minimum spanning tree (odd nodes are red)',\
                      fontsize=fontsize, wait_func=wait_func)

    # Obtain the subgraph of G induced by the "odd" nodes
    oddG = G.subgraph( odd )

    # Compute a perfect minimum-cost matching in the graph O of 
    # odd nodes. We do that by transforming the problem into the 
    # problem of finding a maximum weight matching of maximum 
    # cardinality. Note that this changes the weights also in G
    # and not not only in oddG.
    wmax = max( oddG[e[0]][e[1]]['weight'] for e in oddG.edges )
    for e in oddG.edges:
        oddG[e[0]][e[1]]['weight'] = wmax - oddG[e[0]][e[1]]['weight']
    M = nx.max_weight_matching( oddG, maxcardinality=True )
    
    # Add the edges from the matching to the spanning tree to obtain Eulerian graph
    tLen += sum( problem.wfunc(e[0],e[1]) for e in M )
    E.add_edges_from( M )
    if display:
        plotEdgeList( problem, E.edges, specialEdges=M, specialNodes=odd,\
                      title="Spanning tree + matching of odd nodes (red edges)",\
                      fontsize=fontsize, wait_func=wait_func )

    # Obtain an Eulerian circuit in the graph oddG+M
    circuit = [u for u,_ in nx.eulerian_circuit(E)]
    circuit.append(circuit[0])
    if display:
        displayRoute( problem, circuit, 2*tLen, title='Eulerian circuit', \
                      animate=animate,fontsize=fontsize, wait_func=wait_func)

    # Build the TSP route by taking shortcuts in the Eulerian cycle
    route = list(dict.fromkeys(circuit)) # This removes duplicates from the circuit
    routeLen = sum( problem.wfunc(route[i-1],route[i]) for i in range(1,problem.dimension) )
    routeLen += problem.wfunc(route[-1],route[0])
    if display:
        displayRoute( problem, route, routeLen, title='Found tour',\
                      animate=animate,fontsize=fontsize,wait_func=wait_func)

    return routeLen, route

Functions

def christofides(problem, display=True, animate=True, wait_func=None, fontsize=6)

Apply the Christofides' heuristic.

Parameters

problem : class
TSP problem object has returned by function load_problem of the package tsplib95.
display : bool, optional
I true (default) a figure of the minimum spanning tree, the Eulerian cycle and the resulting route is displayed.
animate : bool, optional
If true (default) the Eulerian cycle and the route are drawn in an animated way.
wait_func : function, optional
Waiting function called after drawing (intermediate) solutions when display=True. Will be required when Matplotlib.pyplot is in interactive mode, as then the window has not explicitly to be destroyed to proceed with the computations. The function itself does not take any arguments, and should ask for some user action to continue.
fontsize : int, optional
Size of the font used to display node numbers (only relevant if display=True)

Returns

routeLen : int
Length of the route.
route : list of int
List of points on the route where route[-1] is the last node visited.

Expand source code
def christofides( problem, display=True, animate=True, wait_func=None,
                  fontsize=6 ):
    """
    Apply the Christofides' heuristic.

    Parameters:
        **problem** : class  
            TSP problem object has returned by function load_problem
            of the package tsplib95.  
        **display** : bool, optional  
            I true (default) a figure of the minimum spanning tree, 
            the Eulerian cycle and the resulting route is displayed.  
        **animate** : bool, optional  
            If true (default) the Eulerian cycle and the route are 
            drawn in an animated way.  
        **wait_func** : function, optional     
            Waiting function called after drawing (intermediate) solutions
            when display=True. Will be required when Matplotlib.pyplot
            is in interactive mode, as then the window has not explicitly
            to be destroyed to proceed with the computations. The function
            itself does not take any arguments, and should ask for some
            user action to continue.  
        **fontsize** : int, optional  
            Size of the font used to display node numbers (only
            relevant if display=True)
                            
    Returns:
        **routeLen** : int  
            Length of the route.  
        **route** : list of int    
            List of points on the route where route[-1] is the last node visited.
    """
    __chk_wfunc(problem)
        
    # Represent problem data using a networkx graph object
    G = problem.get_graph()

    # The graph may contain edges connecting a node to itself
    # We first remove all these simple loops
    G.remove_edges_from(nx.selfloop_edges(G))

    # Initialize the graph (that later will be Eulerian)
    # as a multigraph that first contains the edges from
    # a minimim spanning tree of G
    E = nx.MultiGraph( nx.minimum_spanning_tree(G) )
    tLen = sum( E.get_edge_data(*e)['weight'] for e in E.edges )
    
    # Find the nodes of odd degree in the minimum spanning tree
    odd = [ node for node in E.nodes if E.degree(node) % 2 > 0 ]

    # Display the minimum spanning tree and mark the odd nodes
    if display: 
        plotEdgeList( problem, E.edges, specialNodes=odd, title=\
                      'Minimum spanning tree (odd nodes are red)',\
                      fontsize=fontsize, wait_func=wait_func)

    # Obtain the subgraph of G induced by the "odd" nodes
    oddG = G.subgraph( odd )

    # Compute a perfect minimum-cost matching in the graph O of 
    # odd nodes. We do that by transforming the problem into the 
    # problem of finding a maximum weight matching of maximum 
    # cardinality. Note that this changes the weights also in G
    # and not not only in oddG.
    wmax = max( oddG[e[0]][e[1]]['weight'] for e in oddG.edges )
    for e in oddG.edges:
        oddG[e[0]][e[1]]['weight'] = wmax - oddG[e[0]][e[1]]['weight']
    M = nx.max_weight_matching( oddG, maxcardinality=True )
    
    # Add the edges from the matching to the spanning tree to obtain Eulerian graph
    tLen += sum( problem.wfunc(e[0],e[1]) for e in M )
    E.add_edges_from( M )
    if display:
        plotEdgeList( problem, E.edges, specialEdges=M, specialNodes=odd,\
                      title="Spanning tree + matching of odd nodes (red edges)",\
                      fontsize=fontsize, wait_func=wait_func )

    # Obtain an Eulerian circuit in the graph oddG+M
    circuit = [u for u,_ in nx.eulerian_circuit(E)]
    circuit.append(circuit[0])
    if display:
        displayRoute( problem, circuit, 2*tLen, title='Eulerian circuit', \
                      animate=animate,fontsize=fontsize, wait_func=wait_func)

    # Build the TSP route by taking shortcuts in the Eulerian cycle
    route = list(dict.fromkeys(circuit)) # This removes duplicates from the circuit
    routeLen = sum( problem.wfunc(route[i-1],route[i]) for i in range(1,problem.dimension) )
    routeLen += problem.wfunc(route[-1],route[0])
    if display:
        displayRoute( problem, route, routeLen, title='Found tour',\
                      animate=animate,fontsize=fontsize,wait_func=wait_func)

    return routeLen, route
def doInsertion(problem, nearest=True)

Computes solution using nearest or farthest insertion with random start point.

Parameters

problem : class
TSP problem object has returned by function load_problem of the package tsplib95.
nearest : bool, optional
If false, farthest insertion is applied. Otherwise nearest insertion is applied (default)

Returns

routeLen : int
Length of the route.
route : list of int
List of points on the route where route[-1] is the last node visited.

Expand source code
def doInsertion( problem, nearest=True ):
    """
    Computes solution using nearest or farthest insertion with random 
    start point.

    Parameters:
        **problem** : class  
            TSP problem object has returned by function load_problem
            of the package tsplib95.  
        **nearest** : bool, optional  
            If false, farthest insertion is applied. Otherwise
            nearest insertion is applied (default)

    Returns:
        **routeLen** : int  
            Length of the route.  
        **route** : list of int   
            List of points on the route where route[-1] is the last node visited.
    """
    def insCost( node, i, route ):
        """
        Return cost to insert node at position between i and i+1 in 
        the route.
        """
        j1 = route[i]
        j2 = route[(i+1) % len(route)]
        delta = problem.wfunc(node,j1)+problem.wfunc(node,j2)-problem.wfunc(j1,j2)
        return delta
    
    __chk_wfunc(problem)
    
    # Initialize the the route with some randomly selected edge
    unRouted = [ node for node in problem.get_nodes()]
    n1 = np.random.choice(unRouted)
    unRouted.remove(n1)
    n2 = np.random.choice(unRouted)
    unRouted.remove(n2)
    route = [n1,n2,n1]
    routeLen = problem.wfunc(n1,n2)*2
    sign = 1
    if not nearest : sign = -1
    while len(unRouted) > 0:
        # Determine unrouted node closest (or farthest) to a routed node
        nextNode = None
        minDist  = np.Inf
        for j in unRouted:
            dist = min( [sign*problem.wfunc(j,node) for node in route] )
            if dist < minDist:
                dist = minDist
                nextNode = j
        # Insert nextNode at cheapest insertion place
        ins_at = min([i for i in range(len(route))],\
                      key = lambda i : insCost( nextNode, i, route ) )
        routeLen += insCost(nextNode,ins_at,route)
        route.insert(ins_at+1,nextNode)
        unRouted.remove( nextNode )

    return routeLen, route[:-1]
def minSpanTree(problem, display=True, animate=True, wait_func=None, fontsize=6)

Apply the minimum spanning tree heuristic.

Parameters

problem : class
TSP problem object has returned by function load_problem of the package tsplib95.
display : bool, optional
I true (default) a figure of the minimum spanning tree, the Eulerian cycle and the resulting route is displayed.
animate : bool, optional
If true (default) the Eulerian cycle and the route are drawn in an animated way.
wait_func : function, optional
Waiting function called after drawing (intermediate) solutions when display=True. Will be required when Matplotlib.pyplot is in interactive mode, as then the window has not explicitly to be destroyed to proceed with the computations. The function itself does not take any arguments, and should ask for some user action to continue.
fontsize : int, optional
Size of the font used to display node numbers (only relevant if display=True)

Returns

routeLen : int
Length of the route.
route : list of int
List of points on the route where route[-1] is the last node visited.

Expand source code
def minSpanTree( problem, display=True, animate=True, wait_func=None,
                 fontsize=6):
    """
    Apply the minimum spanning tree heuristic.

    Parameters:
        **problem** : class  
            TSP problem object has returned by function load_problem
            of the package tsplib95.    
        **display** : bool, optional  
            I true (default) a figure of the minimum spanning tree, 
            the Eulerian cycle and the resulting route is displayed.  
        **animate** : bool, optional  
            If true (default) the Eulerian cycle and the route are 
            drawn in an animated way.  
        **wait_func** : function, optional     
            Waiting function called after drawing (intermediate) solutions
            when display=True. Will be required when Matplotlib.pyplot
            is in interactive mode, as then the window has not explicitly
            to be destroyed to proceed with the computations. The function
            itself does not take any arguments, and should ask for some
            user action to continue.  
        **fontsize** : int, optional  
            Size of the font used to display node numbers (only
            relevant if display=True)
              
    Returns:
        **routeLen** : int  
            Length of the route.  
        **route** : list of int      
            List of points on the route where route[-1] is the last node visited.
    """
    __chk_wfunc(problem)
    # Represent problem data using a networkx graph object
    G = problem.get_graph()

    # Obtain the minimum spanning tree and the Eulerian graph by doubling the tree
    T = nx.minimum_spanning_tree(G)
    E = nx.MultiGraph( T )
    E.add_edges_from(T.edges)
    tLen = sum( T.get_edge_data(*e)['weight'] for e in T.edges )

    # Display the minimum spanning tree 
    if display: 
        plotEdgeList( problem, E.edges, title='Minimum spanning '+\
                              'tree/Eulerian graph. Length= '+str(tLen),\
                              fontsize=fontsize, wait_func=wait_func)
    circuit = [u for u,_ in nx.eulerian_circuit(E)]
    circuit.append(circuit[0])
    if display:
        displayRoute( problem, circuit, 2*tLen, animate=animate,\
                     title='Eulerian circuit',fontsize=fontsize,wait_func=wait_func)

    # Build the TSP route by taking shortcuts in the Eulerian cycle
    route = list(dict.fromkeys(circuit)) # This removes duplicates from the circuit
    routeLen = sum( problem.wfunc(route[i-1],route[i]) for i in range(1,problem.dimension) )
    routeLen += problem.wfunc(route[-1],route[0])
    if display:
        displayRoute( problem, route, routeLen, animate=animate,\
                     title='Found tour',fontsize=fontsize,wait_func=wait_func)
            
    return routeLen, route    
def nearestNeighbour(problem)

Computes solution using nearest neighbour with random start point.

Parameters

problem : class
TSP problem object has returned by function load_problem of the package tsplib95.

Returns

routeLen : int
Length of the route.
route : list of int
List of points on the route where route[-1] is the last node visited.

Expand source code
def nearestNeighbour( problem ):
    """
    Computes solution using nearest neighbour with random start point.

    Parameters:
        **problem** : class  
            TSP problem object has returned by function load_problem
            of the package tsplib95.

    Returns:
        **routeLen** : int  
            Length of the route.  
        **route** : list of int   
            List of points on the route where route[-1] is the last node visited.
    """
    __chk_wfunc( problem )
    unRouted = [ i for i in problem.get_nodes()]
    routeLen = 0
    # Choose random start point
    node = np.random.choice(unRouted)
    route = [ node ]
    unRouted.remove(node)
    while len(unRouted) > 0:
        nextNode = min( unRouted, key = lambda j : problem.wfunc(node,j) )
        unRouted.remove(nextNode)
        route.append(nextNode)
        routeLen += problem.wfunc(node,nextNode)
        node = nextNode
    routeLen += problem.wfunc(node,route[0])
    return routeLen, route