I am trying to solve a vehicle routing problem where I wish to minimize the number of late deliveries. Through the use of SetCumulVarSoftUpperBound
, I was able to set soft time windows to allow for late deliveries. However, since the penalty given by SetCumulVarSoftUpperBound
is proportional to how much the bound has been exceeded, the solver will produce a route where multiple deliveries will be slightly late.
For example, given the following time matrix and windows:
data['time_matrix'] = [
[0, 2, 2, 2, 2], # depot
[2, 0, 2, 2, 2], # 1
[2, 2, 0, 2, 2], # 2
[2, 2, 2, 0, 2], # 3
[2, 2, 2, 2, 0], # 4
]
data['time_windows'] = [
(0, 0), # depot
(0, 2), # 1
(0, 3), # 2
(0, 4), # 3
(0, 6), # 4
]
The solver will return the solution of:
0 -> 1 (On Time) -> 2 (Late by 1) -> 3 (Late by 2) -> 4 (Late by 2)
. What I am looking for is a route that is closer to 0 -> 1 (On Time) -> 3 (On Time) -> 4 (On Time) -> 2 (Late by 5)
, where the number of late deliveries are kept to a minimum.
Any help would be much appreciated.
EDIT: Example program to illustrate the issue
import sys
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
def create_data_model(return_to_depot = False):
"""Stores the data for the problem."""
data = {}
data['time_matrix'] = [
[0, 2, 2, 2, 2], # depot
[2, 0, 2, 2, 2],
[2, 2, 0, 2, 2],
[2, 2, 2, 0, 2],
[2, 2, 2, 2, 0],
]
data['time_windows'] = [
(0, 0), # depot
(0, 2),
(0, 3),
(0, 4),
(0, 6),
]
data['num_vehicles'] = 1
data['depot'] = 0
return data
def print_solution(data, manager, routing, solution):
"""Prints solution on console."""
time_dimension = routing.GetDimensionOrDie('Time')
total_time = 0
for vehicle_id in range(data['num_vehicles']):
index = routing.Start(vehicle_id)
plan_output = 'Route for vehicle {}:\n'.format(vehicle_id)
while not routing.IsEnd(index):
time_var = time_dimension.CumulVar(index)
plan_output += '{0} (A:{1}, D:{2}, L:{3}) -> '.format(
manager.IndexToNode(index),
solution.Min(time_var),
solution.Max(time_var),
solution.Min(time_var) - data['time_windows'][manager.IndexToNode(index)][1])
index = solution.Value(routing.NextVar(index))
time_var = time_dimension.CumulVar(index)
plan_output += '\nTime of the route: {}min\n'.format(
solution.Min(time_var))
print(plan_output)
total_time += solution.Min(time_var)
def main():
"""Solve the VRP with time windows."""
# Instantiate the data problem.
data = create_data_model()
# Initialize the penalty value
penalty = sum([sum(i) for i in data['time_matrix']]) + 1
# Create the routing index manager.
manager = pywrapcp.RoutingIndexManager(len(data['time_matrix']),
data['num_vehicles'], data['depot'])
# Create Routing Model.
routing = pywrapcp.RoutingModel(manager)
# Create and register a transit callback.
def time_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 data['time_matrix'][from_node][to_node]
transit_callback_index = routing.RegisterTransitCallback(time_callback)
# Define cost of each arc.
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
# Add Time Windows constraint.
time = 'Time'
routing.AddDimension(
transit_callback_index,
sys.maxsize, # maximum waiting time
sys.maxsize, # maximum travel time per vehicle
False, # Don't force start cumul to zero.
time)
time_dimension = routing.GetDimensionOrDie(time)
# Add time window constraints for each location except depot.
for location_idx, time_window in enumerate(data['time_windows']):
if location_idx == data['depot']:
continue
index = manager.NodeToIndex(location_idx)
time_dimension.CumulVar(index).SetMin(time_window[0])
time_dimension.SetCumulVarSoftUpperBound(index, time_window[1], penalty)
# Add time window constraints for each vehicle start node.
depot_idx = data['depot']
for vehicle_id in range(data['num_vehicles']):
index = routing.Start(vehicle_id)
time_dimension.CumulVar(index).SetRange(
data['time_windows'][depot_idx][0],
data['time_windows'][depot_idx][1])
# Instantiate route start and end times to produce feasible times.
for i in range(data['num_vehicles']):
routing.AddVariableMinimizedByFinalizer(
time_dimension.CumulVar(routing.Start(i)))
routing.AddVariableMinimizedByFinalizer(
time_dimension.CumulVar(routing.End(i)))
# Setting first solution heuristic.
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
search_parameters.local_search_metaheuristic = (
routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
search_parameters.time_limit.seconds = 1
# Solve the problem.
solution = routing.SolveWithParameters(search_parameters)
# Print solution on console.
if solution:
print_solution(data, manager, routing, solution)
else:
print("\nNo solutions founds")
if __name__ == '__main__':
main()
I have managed to solve this issue by implementing a fixed penalty in addition to the proportional cost for violating time windows. By imposing a high fixed cost and a low proportional cost, the solver will then be more incentivized to delay an already late job than to be late for another job.
Implementation details can be found in this discussion thread over on the or-tools Github page. The brief rundown of the implementation is as follows:
setRange
) and a 'late' node with a soft time window (i.e. SetCumulVarSoftUpperBound
)