I find reading all values of an assignment, obtained from
assignment = routing.SolveWithParameters(search_params)
of routing problems with time windows quiet tricky. First of all, there are nodes and indices. I obtain the indices of a vehicle (route) via
index = routing.Start(vehicle)
indices = [index]
while not routing.IsEnd(index):
index = assignment.Value(routing.NextVar(index))
indices.append(index)
and the corresponding nodes are obtained by
nodes = [routing.IndexToNode(x) for x in indices]
For a particular routing problem with 5 stops and depot=0
the solver finds an assignment with the following indices and nodes:
vehicle: 0
indices: [0, 1, 6]
nodes: [0, 1, 0]
vehicle: 1
indices: [5, 4, 3, 2, 7]
nodes: [0, 4, 3, 2, 0]
So there are three more indices than nodes, because each vehicle starts and ends in the depot. I have defined a cost of 1 for every transit and reading the cost values via
cost_dimension = routing.GetDimensionOrDie("cost")
cost_vars = [cost_dimension.CumulVar(x) for x in indices]
costs = [assignment.Value(x) for x in cost_vars]
seems to work:
vehicle: 0
indices: [0, 1, 6]
nodes: [0, 1, 0]
costs: [0, 1, 2]
vehicle: 1
indices: [5, 4, 3, 2, 7]
nodes: [0, 4, 3, 2, 0]
costs: [0, 1, 2, 3, 4]
But when I add time constraints I run into problems. Let's first look at the code that defines the problem. The time unit is minutes.
def time_function(x,y):
return 30
evaluator = time_function
slack_max = 40
capacity = 24*60
fix_start_cumul_to_zero = False
name = "time"
routing.AddDimension(evaluator, slack_max, capacity, fix_start_cumul_to_zero, name)
time_dimension = routing.GetDimensionOrDie("time")
time_windows = [(7*60, 8*60),(9*60, 10*60),(9*60, 10*60),
(9*60, 10*60),(9*60, 10*60)]
for node, time_window in enumerate(time_windows):
time_dimension.CumulVar(node).SetRange(time_window[0], time_window[1])
routing.AddToAssignment(time_dimension.SlackVar(node))
So each trip takes 30 minutes, vehicles may be idle at a stop for 40 minutes (slack_max=40
) and each stop should be serviced between 9am and 10am. The range constraints that are enforced via time_windows[0]
are intended to define the starting times of each trip in the morning. But since the depot is the first and last stop of each route they could also be interpreted as arriving times in the evening.
So here is my first difficulty with time windows: the depot appears twice on each route but the range constraint is defined on nodes. I am assuming that the routing model is not designed to take two windows for the depot?
Let me continue to get to the second part of my question. I set fix_start_cumul_to_zero = False
so that routes may start at any time. Also note that routing.AddToAssignment(time_dimension.SlackVar(node))
is supposed to give me access to the slack variables later. Now, when I inspect the time values per index, via
time_vars = [time_dimension.CumulVar(x) for x in indices]
times.append([assignment.Value(x) for x in time_vars])
formated with datetime, I get reasonable results:
vehicle: 0
indices: [0, 1, 6]
nodes: [0, 1, 0]
costs: [0, 1, 2]
times: ['7:50:00', '9:00:00', '9:30:00']
vehicle: 1
indices: [5, 4, 3, 2, 7]
nodes: [0, 4, 3, 2, 0]
costs: [0, 1, 2, 3, 4]
times: ['7:50:00', '9:00:00', '9:30:00', '10:00:00', '10:30:00']
The solver apparently favors early departure times. Given the max slack of 40min, each vehicle might also start a bit later, e.g. at 8am.
The trouble begins when I try to read the slack variables via
slack_vars = [time_dimension.SlackVar(x) for x in indices]
slacks = [assignment.Value(x) for x in slack_vars]
The program crashes with the message:
SystemError: returned NULL without setting an error
which suggests that time_dimension
does not have a slack variable for every index. Is that right? Why not?
Thanks for reading this excessive post. Here are the two questions:
- Is it possible to define arrival and departure time windows for the depot?
- How to properly read the slacks for time windows for all nodes, including the depo?