or-tools: SlackVar for multi-depot example does not work as expected
The example below is a very simple vrp with capacity constraint, requiring one return journey to the depot. The truck should be able to travel to nodes 2 and 3 and accumulate a load of 20/25. It should then drop this at the depot, which has a demand of -25 and a SlackVar range of 0 25 before returning to node 4 and then back to the actual depot.
Example of a possible solution:
0 Load(0) -> 2 Load(10) -> 3 Load(20) -> 1 Load(0) -> 4 Load(20) -> 0 Load(20).
However the solver does not find this (or any) solution. The SlackVar at the dummy depot should allow the above route but it is not working expected.
Also note, if you set the capacity to 30 then it will solve the problem as it can now return to the dummy depot with the exact load required.
from __future__ import print_function
import math
from ortools.constraint_solver import pywrapcp
from ortools.constraint_solver import routing_enums_pb2
def euclid_distance(x1, y1, x2, y2):
# Euclidean distance between points.
dist = math.sqrt((x1 - x2)**2 + (y1 - y2)**2)
return dist
def create_distance_matrix(locations):
# Create the distance matrix.
size = len(locations)
dist_matrix = {}
for from_node in range(size):
dist_matrix[from_node] = {}
for to_node in range(size):
x1 = locations[from_node][0]
y1 = locations[from_node][1]
x2 = locations[to_node][0]
y2 = locations[to_node][1]
dist_matrix[from_node][to_node] = euclid_distance(x1, y1, x2, y2)
return dist_matrix
def create_data_model():
"""Creates the data for the example."""
data = {}
# Array of distances between locations.
_locations = [
(0, 0),
(0, 0),
(-50, 50),
(0, 100),
(50, 50)]
capacities = 25
_distances = create_distance_matrix(_locations)
demands = [
0,
-capacities,
10,
10,
20]
data["distances"] = _distances
data["num_locations"] = len(_distances)
data["num_vehicles"] = 1
data["depot"] = 0
data["demands"] = demands
data["vehicle_capacities"] = capacities
return data
def create_distance_callback(data):
"""Creates callback to return distance between points."""
distances = data["distances"]
def distance_callback(from_node, to_node):
"""Returns the manhattan distance between the two nodes"""
return distances[from_node][to_node]
return distance_callback
def create_demand_callback(data):
"""Creates callback to get demands at each location."""
def demand_callback(from_node, to_node):
del to_node
return data["demands"][from_node]
return demand_callback
def add_capacity_constraints(routing, data, demand_callback):
"""Adds capacity constraint"""
capacity = "Capacity"
routing.AddDimension(
demand_callback,
0, # null capacity slack
data["vehicle_capacities"], # vehicle maximum capacities
True, # start cumul to zero
capacity)
# Add Slack for reseting to zero unload depot nodes.
# e.g. vehicle with load 10/15 arrives at node 1 (depot unload)
# so we have CumulVar = 10(current load) + -15(unload) + 5(slack) = 0.
capacity_dimension = routing.GetDimensionOrDie(capacity)
for node_index in [1]:
index = routing.NodeToIndex(node_index)
capacity_dimension.SlackVar(index).SetRange(0, data["vehicle_capacities"])
def print_solution(data, routing, assignment):
"""Print routes on console."""
total_dist = 0
for vehicle_id in range(data["num_vehicles"]):
index = routing.Start(vehicle_id)
plan_output = 'Route for vehicle {0}:\n'.format(vehicle_id)
route_dist = 0
route_load = 0
while not routing.IsEnd(index):
node_index = routing.IndexToNode(index)
next_node_index = routing.IndexToNode(assignment.Value(routing.NextVar(index)))
route_dist += routing.GetArcCostForVehicle(node_index, next_node_index, vehicle_id)
route_load += data["demands"][node_index]
plan_output += ' {0} Load({1}) -> '.format(node_index, route_load)
index = assignment.Value(routing.NextVar(index))
node_index = routing.IndexToNode(index)
total_dist += route_dist
plan_output += ' {0} Load({1})\n'.format(node_index, route_load)
plan_output += 'Distance of the route: {0}m\n'.format(route_dist)
plan_output += 'Load of the route: {0}\n'.format(route_load)
print(plan_output)
print('Total Distance of all routes: {0}m'.format(total_dist))
def main():
"""Entry point of the program"""
# Instantiate the data problem.
data = create_data_model()
# Create Routing Model
routing = pywrapcp.RoutingModel(
data["num_locations"],
data["num_vehicles"],
data["depot"])
# Define weight of each edge
distance_callback = create_distance_callback(data)
routing.SetArcCostEvaluatorOfAllVehicles(distance_callback)
# Add Capacity constraint
demand_callback = create_demand_callback(data)
add_capacity_constraints(routing, data, demand_callback)
# Setting first solution heuristic (cheapest addition).
search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
# Solve the problem.
assignment = routing.SolveWithParameters(search_parameters)
if assignment:
print_solution(data, routing, assignment)
else:
print('No solution has been found')
if __name__ == '__main__':
main()
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Comments: 18 (4 by maintainers)
Commits related to this issue
- Update cvrp_reload.py — committed to google/or-tools by Mizux 4 years ago
Above slackVar is set to the range of 0 and capacity (25). The vrp_reload example has the same issue. A truck will only return the the dummy depots if it’s capacity is exactly full. If you use the above locations, demands and capacity in the vrp_reload example it will also fail to find a route.
Guys, any updates on this?
Updating my previous comment, the link should now be https://github.com/google/or-tools/blob/master/ortools/constraint_solver/routing.cc#L5797
This just came up again in the response to issue #1891 and on the follow-up on the mailing list.
I think maybe the code should fail rather than succeeding when trying to call SetRange(a,b) on an IntConst.
At https://github.com/google/or-tools/blob/811ab90009d4886e6cd7c75730ccf250c190e4e1/ortools/constraint_solver/expressions.cc#L2571-L2575
, maybe the code should be
Why? Because if you call set range on an IntConst, the caller probably thinks that the full range is available, but instead it will just be the single value.
Or promote the IntConst to IntVar
But silently succeeding is leading to mystery bugs
Note that you can’t just say
because MakeIntVar right away has the optimization that any range of 0 is immediately an IntConst value:
Which again means you can’t go back in after the fact and change the min and max of this constant value.
Poking around, perhaps this line is the problem: https://github.com/google/or-tools/blob/811ab90009d4886e6cd7c75730ccf250c190e4e1/ortools/constraint_solver/routing.cc#L5797-L5802
So in the original way, slack_max is zero in the definition of the dimension. So there are two issues. First, the slack is never taken into account in the transit_expr. And second, according to expressions.cc, line 2576 or so, calling “SetRange” on a IntConst variable doesn’t quite do what its name might imply:
In the rearranging way suggested by @270ajay the slack_max is not zero, so the slacks_[i] array are all IntVar’s with a range from 0 to max, which ends up being a
DomainIntVar
, notIntConst
, and the transit expression actually cares about the value of the slack variable. The implementation of SetRange for DomainIntVar actually does do what you think it does.So I think this is a bug. Not sure how to fix it though, other than to change the example to be like @270ajay suggestion?
@quinlivanb
there is another way to model that: instead of setting 0 slack in the dimension and providing a range of slack for dummy node, we can set max slack in the dimension, and set 0 slack for non-dummy nodes.
if you update the
add_capacity_constraints
to this, it works:you may also need to update
print_solution
function Output:Downside of this way of modeling: Sometimes you may see vehicle visiting dummy node/reloading when it may not be required. However, it is not very frequent.
@Mizux Hey, do you have any more input on this? We are still blocked by this issue. I’ve tested on every available example using capacity slack and they all have the same issue. The truck will only return to dummy depot when the capacity of the node matches the load in the truck exactly.
in the cvrp_reload.py example, removing the following line has no effect. And If you increase the capacity to 17 it will start dropping nodes.
capacity_dimension.SlackVar(index).SetRange(0, vehicle_capacity)
I hope you can help with this, thanks!