Module tsp.tsp_cpx

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

Expand source code
"""
    TSP - Solving the symmetric TSP by means of CPLEX's MIP solver and docplex
"""
import networkx as nx
from tsp.tsp_plot import plotEdgeList

from docplex.mp.model import Model
from docplex.mp.callbacks.cb_mixin import ConstraintCallbackMixin
from docplex.mp.solution import SolveSolution
from cplex.callbacks import UserCutCallback
from cplex.callbacks import LazyConstraintCallback

from itertools import combinations as combinat

cpx_wait = None

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

class __FindSCEcuts( ConstraintCallbackMixin, UserCutCallback ):
    """
    Finds sub-cycle elimination (SCE) constraints violated by a 
    fractional solution to the current node LP. Limit maybe to first or 
    first three nodes in search tree!!!
    """
    
    def __init__(self, env):
        UserCutCallback.__init__(self, env)
        ConstraintCallbackMixin.__init__(self)
        self.eps = 1e-5
        self.nb_cuts = 0
    
    def __call__(self):
        
        x = self.x         # docplex edge decision variables 
        G = self.G         # graph obtained from problem.get_graph()
        n = len(G)         # number of nodes in the problem/graph
        model = self.model # handle to the model
        
        # Get list of edges with positive value in the current LP solution
        elist = list(e for e in x.keys() if self.get_values(x[e]._index) > self.eps)
        
        # If with animation, show the (fractional) solution
        if self.anim:
            objv = self.get_objective_value()
            efrac = [ e for e in elist if self.get_values(x[e]._index) < 0.99 ]
            plotEdgeList(self.problem,elist,specialEdges=efrac,wait_func=cpx_wait,\
                         title="Fractional LP solution of objval="+str(objv) )
                       
        # Build the solution's support graph, i.e. the sub-graph induced by
        # the edges in the list "elist" found above.
        supG = nx.edge_subgraph(G, elist)
        
        # If the support graph G is not connected, build the SCE constraint
        # based on the graph's smallest component. Note that this cannot
        # happen if the 2-commodity flow formulation is used
        S = None
        if self.modtype != '2cf':
            S = min( nx.connected_components(supG), key=len )
            weight = 1.0
        
        # If the graph is connected, obtain the minimum-weight cut set
        # using the algorithm of Stoer and Wagner
        if (S is None) or (len(S)==n):
            # Set the edge weights equal to the variable's solution values
            for e in elist:
                supG[e[0]][e[1]]['weight'] = self.get_values( x[e]._index )
            weight, part = nx.stoer_wagner(supG)    
            S = part[0] if len(part[0]) < len(part[1]) else part[1]            
        
        # If the cutset constraint is violated, include the cut in 
        # form of the equivalent subcycle elimination constraint
        if ( len(S) > 2 ) and ( weight < 1.98):
            E = list(combinat(sorted(S),2))
            sce = self.linear_ct_to_cplex( model.sum(x[e] for e in E) <= len(S)-1 )
            self.add( *sce )      
            self.nb_cuts += 1
            
#---------------------------------------------------------------------

class __FindLazySCEs( ConstraintCallbackMixin, LazyConstraintCallback ):
    """
    Finds sub-cycle elimination (SCE) constraints violated by an
    integer solution to the current node LP. Note that this can only
    be the case if the 2-commodity flow formulation is not used
    and instead the fractional 2-matching problem is the initial
    relaxation.
    """
    
    def __init__(self, env):
        LazyConstraintCallback.__init__(self, env)
        ConstraintCallbackMixin.__init__(self)
        self.one = 0.99999
        self.nb_lazy = 0
        
    def __call__(self):     
        """ 
        Solution is integer. Check for sub-cycles and if there is one
        store the smallest and include the corresponding subcycle
        elimination constraint.
        """
        x = self.x         # docplex edge decision variables 
        G = self.G         # graph obtained from problem.get_graph()
        n = len(G)         # number of nodes in the problem/graph
        model = self.model # handle to the docplex model
        
        # Get list of edges selected in the integer solution
        elist = list(e for e in x.keys() if self.get_values(x[e]._index) > self.one)
        
        # If with animation, show the (fractional) solution
        if self.anim:
            objv = self.get_objective_value()
            __title = "Checking lazy constraints. Integer solution objval="+str(objv)
            plotEdgeList(self.problem, elist, title=__title, wait_func=cpx_wait )
        
        # Find smallest component of the support graph
        S = min( nx.connected_components(nx.edge_subgraph(G,elist)), key=len )
        
        # Add subcycle elimination constraint if not connected
        if len(S) < n:
            E = list(combinat(sorted(S),2))
            sce = self.linear_ct_to_cplex( model.sum(x[e] for e in E) <= len(S)-1 )
            self.add( *sce )      
            self.nb_lazy += 1
        
#---------------------------------------------------------------------

def __makeRoute( edges ):
    """
    Transforms solution given as edge list to a solution in permutation 
    form.
    
    Parameters:
        edges : list of int
            The list of edges selected in the solution.
            
    Returns:
        route : list of int
            The route as a list of nodes.             
    """
    e = edges.pop(0)
    route = [ e[0], e[1] ]
    while len(edges) > 0:
        last = route[-1]
        edge = next( e for e in edges if last in e )
        succ = edge[1] if last==edge[0] else edge[0]
        route.append(succ)
        edges.remove(edge)        
    return( route )

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

def __build_model( problem, use2CNF ):
    """
    Set up the mathematical program of the TSP using either just the
    2-matching problem or the 2-commodity network flow formulation.
    
    Parameters :
    
        problem : class
            TSP problem object has returned by function load_problem
            of package tsplib95
        use2CNF : boolean
            If true, the 2-commodity flow formulations is used.
            Otherwise 2-matching+subcycle elimination
    
    Returns :
        model : class
            handle to the model
        x     : list of binary variables
    """
    
    # number of nodes 
    n = problem.dimension 
    
    # Create Cplex mathematical programming model
    if use2CNF:
        model = Model("2-commodity flow TSP formulation")
    else:
        model = Model("2-matching + subcycle elimination")    
    
    # List of nodes and edges
    nodes = list(problem.get_nodes())
    edges = list( combinat( nodes, 2 ) )
    
    # Function to access edges incident to a node
    get_edges = lambda node : list( (i,node) for i in nodes if i < node ) \
                             +list( (node,i) for i in nodes if i > node )

    # list of binary edge variables
    x = model.binary_var_dict( edges, name = 'x', key_format = '_%s') 
      
    # Add the objective function to the model
    model.minimize( model.dotf( x, lambda e: problem.wfunc(e[0],e[1]) ) )
    
    # Add degree constraint for each node
    model.add_constraints_( (model.sum(x[e] for e in get_edges(i)) == 2) for i in nodes )
    
    if use2CNF:
        # Matrix of flow variables f(i,j)
        f = model.continuous_var_matrix(nodes, nodes, name = 'f', key_format='_%s')
  
        # Add flow constraints for each node except the first
        model.add_constraints_( (model.sum((f[i,j]-f[j,i]) for i in nodes if i!=j) == 2)\
                                 for j in nodes[1:] )
        
        # Add constraints fij + fji = n*x(ij) for each edge (ij)
        model.add_constraints_( (f[e[0],e[1]]+f[e[1],e[0]] - n*x[e] == 0) for e in edges )
         
    return model, x

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

def __setStartSol ( G, model, x, route ):
    """
    Pass an initial solution given by "route" to the cplex solver.
    
    Parameters:
        G     : networkx graph
            The graph representing the problem instance
        model : class
            The model as returned by docplex.mp.model.Model()
        x     : dict of variables
            The edge variables
        route : list of int
            The initial solution give as sequence of nodes
    """
    routeLen = sum([G[route[i-1]][route[i]]['weight'] for i in range(1,len(route))]) 
    routeLen += G[route[0]][route[-1]]['weight']
    
    var_value = dict( zip(x.values(),[0]*G.number_of_edges()) )
    
    edge = lambda u,v: (u,v) if u < v else (v,u)
    for u,v in zip( route[:-1],route[1:] ):
        var_value[x[edge(u,v)]] = 1
    var_value[x[edge(route[-1],route[0])]] = 1
                      
    sol = SolveSolution(model, var_value_map=var_value )
    model.add_mip_start(sol)
    
    return routeLen
    
#---------------------------------------------------------------------

def CPXtspSolve( problem, G = None, startRoute=None, formulation='SCE',\
                 nodeLim=None, anim=False, wait_func=None, log_output=False ):
    """
    Uses CPLEX's MIP solver via docplex for solving the symmetric TSP. 
    Subcycle elimination constraints are included via cut-callback functions.
      
    Parameters:
        **problem** : class  
            TSP problem object has returned by function load_problem
            of package tsplib95.  
        **G** : object  
            If not None, the networkx graph of the problem.
            Default = None.  
        **startRoute** : list of int, optional  
            If not None it specifies a feasible route given as the 
            sequence in which the nodes are visited. Default=None  
        **formulation** : string, optional  
            If equal to 'SCE', the initial problem formulation
            is the 2-matching problem. Subcycle elimination constraints
            are then generated on the fly as user cuts and lazy constraints.
            If equal to '2CF', the 2-commodity flow formulation is applied
            and no subcycle elimination constraints are generated.
            If equal to '2CF-SCE', again the 2-commodity flow formulation
            is used but subcycle elimination constraints are generated as cuts.  
        **nodeLim** : int, optional  
            Limit on the nodes to be enumerated. Default=None                 
        **anim** : bool, optional  
            If true, the (fractional) LP solution obtained before 
            calling a cut callback is displayed (attention: can be 
            time consuming). Default=False.        
        **wait_func** : function, optional  
            Function that is called to wait for any user action if run
            in animation mode (anim=True), so that execution first continues
            on return from the wait_func. Default=None      
        **log_output** : bool, optional  
            If true, the Cplex solver sends information about the solution progress
            to the screen. Otherwise, it is just silent.                  

    Returns:
        **lowbnd** : float  
            A lower bound or the optimal solution value.  
        **routeLen** : int  
            Length of the computed route.  
        **route** : list of int  
            The route computed.
    """
    
    # Waiting function if used in animated way
    global cpx_wait
    cpx_wait = wait_func
    
    # Get ther right edge weight function from tsplib95
    try:
        _ = problem.wfunc # tsplib95 version before 0.7.1
    except:
        problem.wfunc = problem._wfunc # tsplib95, version 0.7.1

    # Creat the problem graph if not passed as argument
    G_local = ( G is None )
    if G_local:
        G = problem.get_graph()
        G.remove_edges_from(nx.selfloop_edges(G))
                        
    # Build the model
    use2CF = '2CF' in formulation.upper()
    addSCE = (not use2CF) or ('SCE' in formulation.upper())
    model, x  = __build_model( problem, use2CF )
    
    #Initial route provided?
    routeLen = None
    route = startRoute
    if (not route is None): routeLen = __setStartSol(G, model, x, route)
   
    # Set the node limit if given
    if not nodeLim is None: model.parameters.mip.limits.nodes.set(nodeLim)

    # Register the lazy constraint callback
    if not use2CF:
        model.register_callback( __FindLazySCEs )
        __FindLazySCEs.G = G
        __FindLazySCEs.x = x
        __FindLazySCEs.anim = anim
        if anim: __FindLazySCEs.problem = problem
    
    # Register the user cut callback
    if addSCE:
        model.register_callback( __FindSCEcuts )
        __FindSCEcuts.G = G
        __FindSCEcuts.x = x 
        __FindSCEcuts.anim = anim
        __FindSCEcuts.modtype = '2cf' if use2CF else 'SCE'
        if anim: __FindSCEcuts.problem = problem
    # Solve problem with lazy constraints
    sol = model.solve(log_output=log_output)
    
    # Obtain status of the solution
    best_bnd = model.solve_details.best_bound
    if not sol is None:
        objval = model.objective_value
        if (route is None) or (objval < routeLen ):
            edges = list ( (sol.get_value_dict(x,keep_zeros=False)).keys() )
            route = __makeRoute( edges )
            routeLen = objval

    model.end()
    cpx_wait = None

    # If graph is not a local object, remove the edge attribute "var" again. 
    # Restore also the original weights
    if not G_local:
        for e in G.edges: G[e[0]][e[1]]['weight'] = problem.wfunc(e[0],e[1])

    return best_bnd, routeLen, route

Functions

def CPXtspSolve(problem, G=None, startRoute=None, formulation='SCE', nodeLim=None, anim=False, wait_func=None, log_output=False)

Uses CPLEX's MIP solver via docplex for solving the symmetric TSP. Subcycle elimination constraints are included via cut-callback functions.

Parameters

problem : class
TSP problem object has returned by function load_problem of package tsplib95.
G : object
If not None, the networkx graph of the problem. Default = None.
startRoute : list of int, optional
If not None it specifies a feasible route given as the sequence in which the nodes are visited. Default=None
formulation : string, optional
If equal to 'SCE', the initial problem formulation is the 2-matching problem. Subcycle elimination constraints are then generated on the fly as user cuts and lazy constraints. If equal to '2CF', the 2-commodity flow formulation is applied and no subcycle elimination constraints are generated. If equal to '2CF-SCE', again the 2-commodity flow formulation is used but subcycle elimination constraints are generated as cuts.
nodeLim : int, optional
Limit on the nodes to be enumerated. Default=None
anim : bool, optional
If true, the (fractional) LP solution obtained before calling a cut callback is displayed (attention: can be time consuming). Default=False.
wait_func : function, optional
Function that is called to wait for any user action if run in animation mode (anim=True), so that execution first continues on return from the wait_func. Default=None
log_output : bool, optional
If true, the Cplex solver sends information about the solution progress to the screen. Otherwise, it is just silent.

Returns

lowbnd : float
A lower bound or the optimal solution value.
routeLen : int
Length of the computed route.
route : list of int
The route computed.

Expand source code
def CPXtspSolve( problem, G = None, startRoute=None, formulation='SCE',\
                 nodeLim=None, anim=False, wait_func=None, log_output=False ):
    """
    Uses CPLEX's MIP solver via docplex for solving the symmetric TSP. 
    Subcycle elimination constraints are included via cut-callback functions.
      
    Parameters:
        **problem** : class  
            TSP problem object has returned by function load_problem
            of package tsplib95.  
        **G** : object  
            If not None, the networkx graph of the problem.
            Default = None.  
        **startRoute** : list of int, optional  
            If not None it specifies a feasible route given as the 
            sequence in which the nodes are visited. Default=None  
        **formulation** : string, optional  
            If equal to 'SCE', the initial problem formulation
            is the 2-matching problem. Subcycle elimination constraints
            are then generated on the fly as user cuts and lazy constraints.
            If equal to '2CF', the 2-commodity flow formulation is applied
            and no subcycle elimination constraints are generated.
            If equal to '2CF-SCE', again the 2-commodity flow formulation
            is used but subcycle elimination constraints are generated as cuts.  
        **nodeLim** : int, optional  
            Limit on the nodes to be enumerated. Default=None                 
        **anim** : bool, optional  
            If true, the (fractional) LP solution obtained before 
            calling a cut callback is displayed (attention: can be 
            time consuming). Default=False.        
        **wait_func** : function, optional  
            Function that is called to wait for any user action if run
            in animation mode (anim=True), so that execution first continues
            on return from the wait_func. Default=None      
        **log_output** : bool, optional  
            If true, the Cplex solver sends information about the solution progress
            to the screen. Otherwise, it is just silent.                  

    Returns:
        **lowbnd** : float  
            A lower bound or the optimal solution value.  
        **routeLen** : int  
            Length of the computed route.  
        **route** : list of int  
            The route computed.
    """
    
    # Waiting function if used in animated way
    global cpx_wait
    cpx_wait = wait_func
    
    # Get ther right edge weight function from tsplib95
    try:
        _ = problem.wfunc # tsplib95 version before 0.7.1
    except:
        problem.wfunc = problem._wfunc # tsplib95, version 0.7.1

    # Creat the problem graph if not passed as argument
    G_local = ( G is None )
    if G_local:
        G = problem.get_graph()
        G.remove_edges_from(nx.selfloop_edges(G))
                        
    # Build the model
    use2CF = '2CF' in formulation.upper()
    addSCE = (not use2CF) or ('SCE' in formulation.upper())
    model, x  = __build_model( problem, use2CF )
    
    #Initial route provided?
    routeLen = None
    route = startRoute
    if (not route is None): routeLen = __setStartSol(G, model, x, route)
   
    # Set the node limit if given
    if not nodeLim is None: model.parameters.mip.limits.nodes.set(nodeLim)

    # Register the lazy constraint callback
    if not use2CF:
        model.register_callback( __FindLazySCEs )
        __FindLazySCEs.G = G
        __FindLazySCEs.x = x
        __FindLazySCEs.anim = anim
        if anim: __FindLazySCEs.problem = problem
    
    # Register the user cut callback
    if addSCE:
        model.register_callback( __FindSCEcuts )
        __FindSCEcuts.G = G
        __FindSCEcuts.x = x 
        __FindSCEcuts.anim = anim
        __FindSCEcuts.modtype = '2cf' if use2CF else 'SCE'
        if anim: __FindSCEcuts.problem = problem
    # Solve problem with lazy constraints
    sol = model.solve(log_output=log_output)
    
    # Obtain status of the solution
    best_bnd = model.solve_details.best_bound
    if not sol is None:
        objval = model.objective_value
        if (route is None) or (objval < routeLen ):
            edges = list ( (sol.get_value_dict(x,keep_zeros=False)).keys() )
            route = __makeRoute( edges )
            routeLen = objval

    model.end()
    cpx_wait = None

    # If graph is not a local object, remove the edge attribute "var" again. 
    # Restore also the original weights
    if not G_local:
        for e in G.edges: G[e[0]][e[1]]['weight'] = problem.wfunc(e[0],e[1])

    return best_bnd, routeLen, route