Search code examples
pythonpython-3.xor-toolsvehicle-routing

How do I specify an end location for routes in Google OR-Tools?


I have a working Vehicle Routing Problem solution implemented using Google's OR-Tools in Python. I have a time matrix of 16 locations, time windows for each location, and penalties for dropping each location. All values are in units of seconds. I am intentionally solving this with just one vehicle (essentially solving a Traveling Salesperson Problem). I am allowing the vehicle to wait at any location for as long as is needed. I have set the drop penalty extremely high for some locations because I do not want them to ever be dropped.

Each location that is represented in the Time Matrix will have a time window, which indicates the time since the beginning of the day (28800 is the equivalent of 8:00am, and 64800 is the equivalent of 6:00pm, etc.) I set the upper bound maximum value as 64800 because there I want the vehicle to be done by 6:00pm.

I have designated the the first location in the matrix as the start location.
Now, I want the second location in the matrix to be the end location.

I have referenced the OR-Tools documentation for this, but have not found success. I have also found a similar question posted on Stack Overflow, but did not find any particularly helpful responses.

Below is my source code - it runs successfully, but does use the second location in the matrix as the end location in the solution it creates.

Source

from ortools.constraint_solver import pywrapcp
from ortools.constraint_solver import routing_enums_pb2

Matrix = [
  [0, 0, 550, 743, 790, 606, 810, 684, 1500, 101, 1001, 722, 533, 574, 1126, 713],
  [0, 0, 550, 743, 790, 606, 810, 684, 1500, 101, 1001, 722, 533, 574, 1126, 713],
  [530, 530, 0, 609, 953, 620, 676, 550, 1544, 598, 875, 443, 489, 737, 1114, 914],
  [622, 622, 475, 0, 962, 570, 453, 144, 1695, 585, 1073, 641, 738, 745, 966, 922],
  [760, 760, 1014, 1147, 0, 1018, 1208, 1088, 1029, 780, 1375, 1180, 976, 609, 1293, 450],
  [629, 629, 621, 716, 989, 0, 715, 706, 1654, 516, 1219, 787, 734, 728, 1247, 650],
  [867, 867, 720, 567, 1109, 653, 0, 392, 1330, 830, 1318, 886, 983, 990, 731, 1092],
  [671, 671, 524, 213, 1011, 619, 395, 0, 1724, 634, 1122, 690, 788, 795, 867, 971],
  [1372, 1372, 1549, 1758, 1030, 1593, 1358, 1731, 0, 1392, 1987, 1714, 1588, 1221, 1443, 1101],
  [132, 132, 615, 764, 688, 545, 830, 704, 1398, 0, 990, 787, 547, 472, 1126, 612],
  [930, 930, 960, 1198, 1318, 1209, 1265, 1138, 2028, 1031, 0, 526, 866, 1078, 1703, 1272],
  [759, 759, 549, 786, 1130, 797, 853, 727, 1721, 718, 556, 0, 579, 914, 1291, 1091],
  [437, 437, 541, 836, 887, 764, 902, 776, 1597, 538, 877, 576, 0, 671, 1340, 811],
  [621, 621, 880, 1013, 486, 926, 1079, 953, 1196, 640, 1005, 1046, 837, 0, 1259, 441],
  [1608, 1608, 1517, 1654, 1547, 1562, 1088, 1480, 1610, 1484, 2115, 1683, 1794, 1556, 0, 1436],
  [515, 515, 769, 902, 373, 773, 969, 842, 1078, 534, 1130, 935, 731, 364, 1140, 0]
]

Windows = [[28800, 28800], [64800, 64800], [43200, 43200], [50400, 50400], [21600, 79200], [21600, 79200], [21600, 79200], [21600, 79200], [21600, 79200], [21600, 79200], [21600, 79200], [21600, 79200], [21600, 79200], [21600, 79200], [21600, 79200], [21600, 79200]]

Durations = [0, 0, 1800, 3600, 3600, 7200, 1800, 1800, 1800, 1800, 1800, 1800, 1800, 1800, 1800, 1800]

Penalties = [100000, 100000, 576460752303423487, 576460752303423487, 100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000, 100000]

# Create the routing index manager.
manager = pywrapcp.RoutingIndexManager(len(Matrix), 1, 0)

# Create Routing Model.
routing = pywrapcp.RoutingModel(manager)

# Create and register a transit callback.
def transit_callback(from_index, to_index):
  # Returns the travel time between the two nodes.
  # Convert from routing variable Index to time matrix NodeIndex.
  from_node = manager.IndexToNode(from_index)
  to_node = manager.IndexToNode(to_index)
  return Matrix[from_node][to_node] + Durations[from_node]

transit_callback_index = routing.RegisterTransitCallback(transit_callback)

# Define cost of each arc.
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)

# Add Time Windows constraint.
routing.AddDimension(
    transit_callback_index,
    64800,  # An upper bound for slack (the wait times at the locations).
    64800,  # An upper bound for the total time over each vehicle's route.
    False,  # Determine whether the cumulative variable is set to zero at the start of the vehicle's route.
    'Time')
time_dimension = routing.GetDimensionOrDie('Time')

# Allow to drop nodes.
for node in range(1, len(Matrix)):
  routing.AddDisjunction([manager.NodeToIndex(node)], Penalties[node])

# Add time window constraints for each location except depot.
for location_idx, time_window in enumerate(Windows):
  if location_idx == 0:
    continue
  index = manager.NodeToIndex(location_idx)
  time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1])

# Add time window constraints for each vehicle start node.
index = routing.Start(0)
time_dimension.CumulVar(index).SetRange(Windows[0][0],Windows[0][1])

# Instantiate route start and end times to produce feasible times.
routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.Start(0)))
routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.End(0)))

# Setting first solution heuristic. 
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)

# Setting local search metaheuristics:
search_parameters.local_search_metaheuristic = (routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
search_parameters.time_limit.seconds = 5
search_parameters.log_search = False

# Solve the problem.
solution = routing.SolveWithParameters(search_parameters)

result = {
  'Dropped': None,
  'Schedule': None
}

# Display dropped nodes.
dropped = []
for node in range(routing.Size()):
  if routing.IsStart(node) or routing.IsEnd(node):
    continue
  if solution.Value(routing.NextVar(node)) == node:
    dropped.append(manager.IndexToNode(node))
result['Dropped'] = dropped

# Return the solution the problem.
schedule = []
time = 0
index = routing.Start(0)
while not routing.IsEnd(index):
  time = time_dimension.CumulVar(index)
  schedule.append([manager.IndexToNode(index), solution.Min(time),solution.Max(time)])
  index = solution.Value(routing.NextVar(index))
time = time_dimension.CumulVar(index)
schedule.append([manager.IndexToNode(index), solution.Min(time),solution.Max(time)])
result['Schedule'] = schedule
print(result)

Output

{'Dropped': [4, 5], 'Schedule': [[0, 28800, 28800], [9, 28901, 29249], [13, 31173, 31521], [15, 33414, 33762], [8, 36292, 36640], [14, 39535, 39883], [2, 43200, 43200], [6, 45676, 46195], [7, 47868, 48387], [3, 50400, 50400], [11, 54641, 57541], [10, 56997, 59897], [12, 59663, 62563], [1, 64800, 64800], [0, 64800, 64800]]}

My understanding is that the main changes need to be made with the RoutingIndexManager.
Like the Google OR-Tools documentation seemed to indicate, I have tried the follow change:

manager = pywrapcp.RoutingIndexManager(len(Matrix),1,0)

to

manager = pywrapcp.RoutingIndexManager(len(Matrix),1,[0],[1])

but this causes an error:

WARNING: Logging before InitGoogleLogging() is written to STDERR
F0820 15:13:16.748222 62401984 routing.cc:1433] Check failed: kUnassigned != indices[i] (-1 vs. -1) 
*** Check failure stack trace: ***

Are there any blatant errors in my usage of OR Tools? I'm happy to answer any questions. Any help will be very appreciate! Thank you!


Solution

  • I received an answer for this question in the OR-Tools Google Groups forums.

    First, properly change the manager constructor code.

        # Create the routing index manager.
    [-] manager = pywrapcp.RoutingIndexManager(len(Matrix), 1, 0)
    [+] manager = pywrapcp.RoutingIndexManager(len(Matrix), 1, [0], [1])
    

    Second, you cannot specify a start or an end node in a disjunction:

        # Allow to drop nodes.
    [-] for node in range(1, len(Matrix)):
    [+] for node in range(2, len(Matrix)):
          routing.AddDisjunction([manager.NodeToIndex(node)], Penalties[node])
    

    Third, you cannot use NodeToIndex(node_index) on start or end nodes, since it is not always a bijection.

        # Add time window constraints for each location except start and end location.
        for location_idx, time_window in enumerate(Windows):
    [-]   if location_idx == 0:
    [+]   if location_idx == 0 or location_idx == 1:
            continue
          index = manager.NodeToIndex(location_idx)
          time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1])
    

    Finally, be sure to set a time window on the end location:

        index = routing.Start(0)
        time_dimension.CumulVar(index).SetRange(Windows[0][0],Windows[0][1])
    [+] index = routing.End(0)
    [+] time_dimension.CumulVar(index).SetRange(Windows[1][0],Windows[1][1])