Module tsp.tsp_match

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

Expand source code
"""
    TSP - 2-matching and assignment lower bound for the symmetric TSP.
"""
import networkx as nx
from docplex.mp.model import Model
from itertools import combinations as combinat
  
#---------------------------------------------------------------------

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 assignLB ( problem ):
    """
    Uses networkx min_cost_flow algorithm for computes the assignment
    lower bound. Alternatively, also networkx network simplex algorithm
    could be used instead.

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

    Returns:
        **cost** : int  
            The lower bound (objective value of the assignment problem).  
        **elist** : list of int  
            The list of edges in the assignment.
    """
    # Get the right edge weight function from tsplib95
    __chk_wfunc(problem)
    
    # Solve assignment problem as a minimum-cost network flow problem
    n = problem.dimension
    G = nx.DiGraph()
    for i in problem.get_nodes(): G.add_node(i, demand = -1 )
    for j in problem.get_nodes(): G.add_node(j+n, demand = 1 )
    for i in problem.get_nodes():
        for j in problem.get_nodes():
            if i != j:
                G.add_edge(i,j+n, weight=problem.wfunc(i,j), capacity = n+1 )
    flow = nx.min_cost_flow( G )
    cost = nx.cost_of_flow(G,flow)

    # Retrieve list of edges in the assignment
    elist = [ (e[0],e[1]-n) for e in G.edges if flow[e[0]][e[1]] > 0 ]
    return cost, elist    

#---------------------------------------------------------------------        
        
def twoMatch( problem, fractional=False, log_output=True ):
    """
    Computes the integer or fractional 2-matching lower bound. The 
    (integer) linear program of the 2-matching problem is solved using 
    the Cplex optimizer via docplex

    Parameters:
        **problem** : class  
            The TSP problem object has returned by function load_problem
            of package tsplib95.  
        **fractional** : bool, optional  
            If true, only the linear relaxation of the 2-matching 
            problem is solved. Default=False.  
        **log_output** : bool, optional    
            If true, Cplex sends messages to screen when solving the
            2-matching problem. Default=True.

    Returns:
        **weight** : int or float  
            The (fractional) 2-matching lower bound.  
         **elist** : list of pairs of int  
             The list of edges in the 2-matching and a corresponding.  
         **xvals** : list of floats       
            List of (fractional) solution values. xvals is only
            returned if fractional is true.
    """
    __chk_wfunc( problem )
    model = Model("2-matching")
    
    # 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 (or fractional) edge variables
    if fractional:
        x = model.continuous_var_dict(edges, ub=1.0, name = 'x', key_format = '_%s')
    else:
        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 )
    
    # Solve the model
    sol = model.solve(log_output=log_output)
    
    # Retrieve objective value and edges with positive solution value
    weight = model.objective_value
    xdict = sol.get_value_dict(x,keep_zeros=False)
    edges = list( xdict.keys() )
    
    model.end()
    if fractional: 
        xvals = list( xdict.values() )
        return weight, edges, xvals
    else:
        return weight, edges

Functions

def assignLB(problem)

Uses networkx min_cost_flow algorithm for computes the assignment lower bound. Alternatively, also networkx network simplex algorithm could be used instead.

Parameters

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

Returns

cost : int
The lower bound (objective value of the assignment problem).
elist : list of int
The list of edges in the assignment.

Expand source code
def assignLB ( problem ):
    """
    Uses networkx min_cost_flow algorithm for computes the assignment
    lower bound. Alternatively, also networkx network simplex algorithm
    could be used instead.

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

    Returns:
        **cost** : int  
            The lower bound (objective value of the assignment problem).  
        **elist** : list of int  
            The list of edges in the assignment.
    """
    # Get the right edge weight function from tsplib95
    __chk_wfunc(problem)
    
    # Solve assignment problem as a minimum-cost network flow problem
    n = problem.dimension
    G = nx.DiGraph()
    for i in problem.get_nodes(): G.add_node(i, demand = -1 )
    for j in problem.get_nodes(): G.add_node(j+n, demand = 1 )
    for i in problem.get_nodes():
        for j in problem.get_nodes():
            if i != j:
                G.add_edge(i,j+n, weight=problem.wfunc(i,j), capacity = n+1 )
    flow = nx.min_cost_flow( G )
    cost = nx.cost_of_flow(G,flow)

    # Retrieve list of edges in the assignment
    elist = [ (e[0],e[1]-n) for e in G.edges if flow[e[0]][e[1]] > 0 ]
    return cost, elist    
def twoMatch(problem, fractional=False, log_output=True)

Computes the integer or fractional 2-matching lower bound. The (integer) linear program of the 2-matching problem is solved using the Cplex optimizer via docplex

Parameters

problem : class
The TSP problem object has returned by function load_problem of package tsplib95.
fractional : bool, optional
If true, only the linear relaxation of the 2-matching problem is solved. Default=False.
log_output : bool, optional
If true, Cplex sends messages to screen when solving the 2-matching problem. Default=True.

Returns

weight : int or float
The (fractional) 2-matching lower bound.
elist : list of pairs of int
The list of edges in the 2-matching and a corresponding.
xvals : list of floats
List of (fractional) solution values. xvals is only returned if fractional is true.

Expand source code
def twoMatch( problem, fractional=False, log_output=True ):
    """
    Computes the integer or fractional 2-matching lower bound. The 
    (integer) linear program of the 2-matching problem is solved using 
    the Cplex optimizer via docplex

    Parameters:
        **problem** : class  
            The TSP problem object has returned by function load_problem
            of package tsplib95.  
        **fractional** : bool, optional  
            If true, only the linear relaxation of the 2-matching 
            problem is solved. Default=False.  
        **log_output** : bool, optional    
            If true, Cplex sends messages to screen when solving the
            2-matching problem. Default=True.

    Returns:
        **weight** : int or float  
            The (fractional) 2-matching lower bound.  
         **elist** : list of pairs of int  
             The list of edges in the 2-matching and a corresponding.  
         **xvals** : list of floats       
            List of (fractional) solution values. xvals is only
            returned if fractional is true.
    """
    __chk_wfunc( problem )
    model = Model("2-matching")
    
    # 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 (or fractional) edge variables
    if fractional:
        x = model.continuous_var_dict(edges, ub=1.0, name = 'x', key_format = '_%s')
    else:
        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 )
    
    # Solve the model
    sol = model.solve(log_output=log_output)
    
    # Retrieve objective value and edges with positive solution value
    weight = model.objective_value
    xdict = sol.get_value_dict(x,keep_zeros=False)
    edges = list( xdict.keys() )
    
    model.end()
    if fractional: 
        xvals = list( xdict.values() )
        return weight, edges, xvals
    else:
        return weight, edges