Skip to main content

Modifying tsp.py for generic ordering of cities/nodes.

Answered

Comments

6 comments

  • Official comment
    Simranjit Kaur
    • Gurobi Staff
    This post is more than three years old. Some information may not be up to date. For current information, please check the Gurobi Documentation or Knowledge Base. If you need more help, please create a new post in the community forum, or try Gurobot, our chatbot interface offering instant, expert-level support.
  • Jaromił Najman
    • Gurobi Staff

    I am not sure if I really understand the issue here. Can't you just rename your cities and order them as is expected by the tsp.py example?

    N = [1,0,4,8]       # The cities
    n = len(N)         # number of cities
    namemap = {}
    for i in range(n):
        namemap[i] = N[i]
    dist = {(0, 1): 79.42921376924235,
            (0, 2): 63.34824385884742,
            (0, 3): 26.1725046566048,
            (1, 2): 119.20570456148481,
            (1, 3): 85.47514258543241,
            (2, 3): 39.395431207184416}
    m = gp.Model()
    # Create variables
    x = m.addVars(dist.keys(), obj=dist, vtype=GRB.BINARY, name='x')
    for i,j in x.keys():
        x[j, i] = x[i, j]  # edge in opposite direction
    # Add degree-2 constraint
    m.addConstrs(x.sum(i, '*') == 2 for i in range(n))
    # Optimize model
    m._vars = x
    m.Params.LazyConstraints = 1
    m.optimize(subtourelim)
    vals = m.getAttr('x', x)
    tour = subtour(vals)
    #assert len(tour) == n
    print('')
    for i in range(len(tour)):
      tour[i] = namemap[i]
    print('Optimal tour: %s' % str(tour))
    print('Optimal cost: %g' % m.ObjVal)
    print('')

    Does the optimum result of running tsp.py also take into consideration the return to depot?

    Yes, the final tour is a cycle. Just the output does not make it clear.

    I also don't really understand why a depot matters for tsp? You are searching for a cycle visiting all cities, so any city can be a depot no?

    Best regards, 
    Jaromił

    0
  • Lirim Kadriu
    • Gurobi-versary
    • First Comment
    • First Question

    My understanding of the TSP is that given a set of cities N = [1,2,...,n], it returns the order in which the cities should be visited such as to minimize the travel distance. In the example above my cities are, unordered, N = [1,0,4,8]. I want to know, starting from 0, how can I visit 1,4, 8 and return to zero while minimizing the distance.

    Running the code you provided just returns [1,0,4,8] which doesn't tell me anything about the order. I assume, ,since this is a cycle, I can rearrange the output to be [0,4,8,1,0] but I get the same regardless of N.

    Obviously the output [1,0,4,8] could be the optimal route but I get the same regardless of problem size, even If I try n = 50, the tsp.py never changes the ordering but just returns N.

    0
  • Jaromił Najman
    • Gurobi Staff

    Running the code you provided just returns [1,0,4,8] which doesn't tell me anything about the order. I assume, ,since this is a cycle, I can rearrange the output to be [0,4,8,1,0] but I get the same regardless of N.

    Correct, it looks like [1,0,4,8,1] is just the optimal solution in your case.

    If I try n = 50, the tsp.py never changes the ordering but just returns N.

    Could you post an example where the final solution is actually not optimal? When I run the tsp.py example without your changes, the optimal route is very different from just [0,1,2,3,...].

    0
  • Lirim Kadriu
    • Gurobi-versary
    • First Comment
    • First Question

    Here is a completely reproducible example. Note that instead of hardcoding dist, I just calculated all distances between 0,...,max(N) in dist and then create a new dictionary dists that is passed to the model, determined by the contents of N. In the code below, no matter what N is, that particular order is always printed as output from tsp.py. The heuristic can't be that good that it accidentally constructs optimal N each time.

    Just do describe my full problem: I'm using a construction heuristic to determine feasible routes for a cVRP. Once each vehicle k has been assigned all it's customers such that their combined demand does not exceed the maximum load capacity of the vehicle, I decide the optimal order for each vehicle using the TSP. This means for example that if I have ALL the customers A = [1,2,3,4,5,6,7,8,9,10,11,12] (0 = depot) and the heuristic assigns

    vehicle_1 = [12,8,9,4]

    vehicle_1 = [2,7,3,11]

    vehicle_1 = [10,1,6,5]

    I'll myself just insert the depot 0 in each of the above using vehicle_k.insert(0,0). Now, I want this TSP program to output the optimal route for each vehicle as vehicle_1_opt =[0,...,0] and the optimal order should be in between. In the example below, my N represents only one vehicle. Once I fix the case with one vehicle then it's trivial to just loop/iteratively solve the TSP for each vehicle and sum their costs. I hope my explanation makes sense.

    from itertools import combinations, permutations
    import gurobipy as gp
    from gurobipy import GRB

    # Callback - use lazy constraints to eliminate sub-tours
    def subtourelim(model, where):
        if where == GRB.Callback.MIPSOL:
            vals = model.cbGetSolution(model._vars)
            # find the shortest cycle in the selected edge list
            tour = subtour(vals)
            if len(tour) < n:
                # add subtour elimination constr. for every pair of cities in tour
                model.cbLazy(gp.quicksum(model._vars[i, j]
                                        for i, j in combinations(tour, 2))
                            <= len(tour)-1)

    # Given a tuplelist of edges, find the shortest subtour

    def subtour(vals):
        # make a list of edges selected in the solution
        edges = gp.tuplelist((i, j) for i, j in vals.keys()
                            if vals[i, j] > 0.5)
        unvisited = list(range(n))
        cycle = range(n+1)  # initial length has 1 more city
        while unvisited:  # true if list is non-empty
            thiscycle = []
            neighbors = unvisited
            while neighbors:
                current = neighbors[0]
                thiscycle.append(current)
                unvisited.remove(current)
                neighbors = [j for i, j in edges.select(current, '*')
                            if j in unvisited]
            if len(cycle) > len(thiscycle):
                cycle = thiscycle
        return cycle

    random.seed(1)

    N = [12,8,9,4]          # the cities for one vehicle to visit
    n = len(N)              # number of cities

    # Create n random points
    points = [(0, 0)]
    points += [(random.randint(0, 100), random.randint(0, 100)) for i in range(max(N))]

    # Dictionary of ALL distances
    dist = {(i, j):
            math.sqrt(sum((points[i][k]-points[j][k])**2 for k in range(2)))
            for i in range(max(N)+1) for j in range(max(N)+1)}

    namemap = {}

    for i in range(n):
        namemap[i] = N[i]

    # Dictionary of distances to be passed to model
    dists = {}

    for i in list(combinations(namemap.keys(),2)):
        dists[i] = dist[(namemap[i[0]], namemap[i[1]])]

    m = gp.Model()

    # Create variables
    x = m.addVars(dists.keys(), obj=dists, vtype=GRB.BINARY, name='x')

    for i,j in x.keys():
        x[j, i] = x[i, j]  # edge in opposite direction

    # Add degree-2 constraint
    m.addConstrs(x.sum(i, '*') == 2 for i in range(n))

    # Optimize model
    m._vars = x
    m.Params.LazyConstraints = 1
    m.optimize(subtourelim)

    vals = m.getAttr('x', x)
    tour = subtour(vals)

    assert len(tour) == n

    print('')

    for i in range(len(tour)):
      tour[i] = namemap[i]

    print('Original tour: %s' % str(N))
    print('Optimal tour: %s' % str(tour))
    print('Optimal cost: %g' % m.ObjVal)
    print('')
    0
  • Jaromił Najman
    • Gurobi Staff

    It looks like the mapping part is not fully correct. It should read

    for i in range(len(tour)):
    tour[i] = namemap[tour[i]]

    Best regards, 
    Jaromił

    1

Post is closed for comments.