Reputation: 592
I am modeling a Time-constrained CVRP. The problem is to minimize the total travel time (not including the package dropping time) subject to vehicle (delivery) capacity and total time spent (per vehicle) constraints. The package dropping time refers to an additional time to be spent at each node, and the total time spent equals to the travel time plus this additional time. I have the below model that works for a single vehicle-type case. I would like to introduce two-vehicle type concept in there, meaning that I have a set of V1
type vehicles and another set of V2
type vehicles. The only difference of the vehicle-types is the per time cost of travel. Let x
denote the per time unit cost of travel by V1
, and y
denote the per time unit travel cost of V2
. How can I design the model so that it incorporates this additional aspect?
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
def create_data_model(n_vehicles):
"""Stores the data for the problem."""
data = {}
data['time_matrix'] = TT #travel time
data['num_vehicles'] = n_vehicles
data['depot'] = 0
data['demands'] = demands
data['vehicle_capacities'] = vehicle_capacities
data['service_time'] = service_time
return data
def print_solution(data, manager, routing, solution):
"""Prints solution on console."""
print(f'Objective: {solution.ObjectiveValue()}')
max_route_time = 0; tour = {i:[] for i in range(data['num_vehicles'])}
for vehicle_id in range(data['num_vehicles']):
index = routing.Start(vehicle_id)
plan_output = 'Route for vehicle {}:\n'.format(vehicle_id)
route_time = 0
while not routing.IsEnd(index):
plan_output += ' {} -> '.format(manager.IndexToNode(index))
tour[vehicle_id].append(manager.IndexToNode(index))
previous_index = index
index = solution.Value(routing.NextVar(index))
if previous_index != 0 and previous_index <= len(data['service_time'])-1:
service_time = data['service_time'][previous_index]
else:
service_time = 0
route_time += (routing.GetArcCostForVehicle(
previous_index, index, vehicle_id) + service_time)
plan_output += '{}\n'.format(manager.IndexToNode(index))
tour[vehicle_id].append(manager.IndexToNode(index))
plan_output += 'Travel time of the route: {} sec\n'.format(route_time)
print(plan_output)
max_route_time = max(route_time, max_route_time)
print('Maximum of the route time: {} sec'.format(max_route_time))
return(tour)
def main(n_vehicles):
number_of_veh = [n_vehicles][0]
solution_found = False
while solution_found == False:
"""Entry point of the program."""
# Instantiate the data problem.
data = create_data_model(number_of_veh)
# 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 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)
# Create and register a transit callback.
def time_callback2(from_index, to_index):
"""Returns the 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)
if from_node != 0:
return data['time_matrix'][from_node][to_node] + data['service_time'][from_node]
else:
return data['time_matrix'][from_node][to_node]
transit_callback_index2 = routing.RegisterTransitCallback(time_callback2)
# Add Time constraint.
dimension_name = 'Time'
routing.AddDimension(
transit_callback_index2,
0, # no slack
Operational_hours*3600, # vehicle maximum travel time
True, # start cumul to zero
dimension_name)
time_dimension = routing.GetDimensionOrDie(dimension_name)
def demand_callback(from_index):
"""Returns the demand of the node."""
# Convert from routing variable Index to demands NodeIndex.
from_node = manager.IndexToNode(from_index)
return data['demands'][from_node]
demand_callback_index = routing.RegisterUnaryTransitCallback(
demand_callback)
routing.AddDimensionWithVehicleCapacity(
demand_callback_index,
0, # null capacity slack
data['vehicle_capacities'], # vehicle maximum capacities
True, # start cumul to zero
'Capacity')
# 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.FromSeconds(VRP_time_limit)
# Solve the problem.
solution = routing.SolveWithParameters(search_parameters)
# Print solution on console.
if solution:
tour = print_solution(data, manager, routing, solution)
solution_found = True
else:
print('No solution found! Increasing the vehicle numbers by one and resolving.\n')
solution_found = False
number_of_veh += 1
return(tour, number_of_veh)
EDIT
Based on @Mizux' answer, I have written the following, which produced the below error.
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
def create_data_model(n_vehicles):
"""Stores the data for the problem."""
data = {}
TT = np.array(df.values)
data['time_matrix'] = TT
data['num_vehicles'] = n_vehicles
data['depot'] = 0
data['demands'] = demands
if len(vehicle_capacities) < n_vehicles:
data['vehicle_capacities'] = [vehicle_capacities[0]]*n_vehicles
else:
data['vehicle_capacities'] = vehicle_capacities
data['service_time'] = service_time
return data
def print_solution(data, manager, routing, solution):
"""Prints solution on console."""
print(f'Objective: {solution.ObjectiveValue()}')
max_route_time = 0; tour = {i:[] for i in range(data['num_vehicles'])}
for vehicle_id in range(data['num_vehicles']):
index = routing.Start(vehicle_id)
plan_output = 'Route for vehicle {}:\n'.format(vehicle_id)
route_time = 0
while not routing.IsEnd(index):
plan_output += ' {} -> '.format(manager.IndexToNode(index))
tour[vehicle_id].append(manager.IndexToNode(index))
previous_index = index
index = solution.Value(routing.NextVar(index))
if previous_index != 0 and previous_index <= len(data['service_time'])-1:
service_time = data['service_time'][previous_index]
else:
service_time = 0
route_time += (routing.GetArcCostForVehicle(
previous_index, index, vehicle_id) + service_time)
plan_output += '{}\n'.format(manager.IndexToNode(index))
tour[vehicle_id].append(manager.IndexToNode(index))
plan_output += 'Travel time of the route: {} sec\n'.format(route_time)
print(plan_output)
max_route_time = max(route_time, max_route_time)
print('Maximum of the route time: {} sec'.format(max_route_time))
return(tour)
def main(n_vehicles, cost1, cost2):
number_of_veh = [n_vehicles][0]
solution_found = False
while solution_found == False:
Num_of_Class6 = int(n_vehicles*Percent_of_Class6)
Num_of_Hybrid = n_vehicles - Num_of_Class6
V = list(range(n_vehicles))
V2 = list(set(np.random.choice(V, size=Num_of_Class6, replace=False)))
V1 = list(set(V)-set(V2))
"""Entry point of the program."""
# Instantiate the data problem.
data = create_data_model(number_of_veh)
# Create the routing index manager.
manager = pywrapcp.RoutingIndexManager(len(data['time_matrix']),
data['num_vehicles'], data['depot'])
# Create Routing Model.
routing = pywrapcp.RoutingModel(manager)
'''Major Diff Starts Here'''
# Create and register a transit callback.
def time_callback(from_index, to_index, cost):
"""Returns the 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]*cost
range_extender_callback = partial(time_callback, cost=cost1)
class6_callback = partial(time_callback, cost=cost2)
transit_callback_index_V1 = routing.RegisterTransitCallback(range_extender_callback)
transit_callback_index_V2 = routing.RegisterTransitCallback(class6_callback)
'''Major Diff Ends Here'''
# Define cost of each arc.
for v in V1:
routing.SetArcCostEvaluatorOfVehicle(transit_callback_index_V1, v)
for v in V2:
routing.SetArcCostEvaluatorOfVehicle(transit_callback_index_V2, v)
# Create and register a transit callback.
def time_callback2(from_index, to_index):
"""Returns the 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)
if from_node != 0:
return data['time_matrix'][from_node][to_node] + data['service_time'][from_node]
else:
return data['time_matrix'][from_node][to_node]
transit_callback_index2 = routing.RegisterTransitCallback(time_callback2)
# Add Time constraint.
dimension_name = 'Time'
routing.AddDimension(
transit_callback_index2,
0, # no slack
Operational_hours*3600, # vehicle maximum travel time
True, # start cumul to zero
dimension_name)
time_dimension = routing.GetDimensionOrDie(dimension_name)
def demand_callback(from_index):
"""Returns the demand of the node."""
# Convert from routing variable Index to demands NodeIndex.
from_node = manager.IndexToNode(from_index)
return data['demands'][from_node]
demand_callback_index = routing.RegisterUnaryTransitCallback(
demand_callback)
routing.AddDimensionWithVehicleCapacity(
demand_callback_index,
0, # null capacity slack
data['vehicle_capacities'], # vehicle maximum capacities
True, # start cumul to zero
'Capacity')
# 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.FromSeconds(VRP_time_limit)
# Solve the problem.
solution = routing.SolveWithParameters(search_parameters)
# Print solution on console.
if solution:
tour = print_solution(data, manager, routing, solution)
solution_found = True
else:
print('No solution found! Increasing the vehicle numbers by one and resolving.\n')
solution_found = False
number_of_veh += 1
return(tour, number_of_veh, V1, V2)
main(n_vehicles, cost1, cost2)
The output is:
Beginning the Googe OR-tools to solve the problem.
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-15-0447402a4e3d> in <module>
166 return(tour, number_of_veh, V1, V2)
167
--> 168 final_tour,number_of_veh, V1, V2 = main(n_vehicles, cost1, cost2)
<ipython-input-15-0447402a4e3d> in main(n_vehicles, cost1, cost2)
104 routing.SetArcCostEvaluatorOfVehicle(transit_callback_index_V1, v)
105 for v in V2:
--> 106 routing.SetArcCostEvaluatorOfVehicle(transit_callback_index_V2, v)
107
108 # Create and register a transit callback.
~/.local/lib/python3.7/site-packages/ortools/constraint_solver/pywrapcp.py in SetArcCostEvaluatorOfVehicle(self, evaluator_index, vehicle)
5224 def SetArcCostEvaluatorOfVehicle(self, evaluator_index: "int", vehicle: "int") -> "void":
5225 r""" Sets the cost function for a given vehicle route."""
-> 5226 return _pywrapcp.RoutingModel_SetArcCostEvaluatorOfVehicle(self, evaluator_index, vehicle)
5227
5228 def SetFixedCostOfAllVehicles(self, cost: "int64_t") -> "void":
TypeError: in method 'RoutingModel_SetArcCostEvaluatorOfVehicle', argument 3 of type 'int'
Upvotes: 2
Views: 1008
Reputation: 592
To solve the problem, I followed Mizux' answer. I have the below MWE for future consideration. I hope it helps to someone!
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
import numpy as np
from functools import partial
n_vehicles = 4 #Number of vehicles
max_vehicle_tt = 3600 #Maximum travel time for each vehicle (excludes service times)
data = {}
data['time_matrix'] = np.array([[ 0, 1187, 1200, 1110, 1134, 892, 1526, 903, 1482, 1544],
[1232, 0, 13, 90, 67, 426, 537, 419, 493, 555],
[1218, 57, 0, 82, 73, 412, 523, 405, 479, 541],
[1177, 90, 90, 0, 23, 370, 481, 364, 438, 500],
[1187, 80, 67, 23, 0, 380, 491, 374, 448, 509],
[ 870, 390, 403, 314, 337, 0, 729, 17, 686, 747],
[1539, 557, 543, 485, 495, 733, 0, 726, 53, 68],
[ 882, 384, 397, 307, 331, 17, 723, 0, 679, 741],
[1496, 514, 500, 442, 451, 689, 53, 683, 0, 122],
[1584, 602, 588, 530, 539, 777, 68, 771, 122, 0]])
data['num_vehicles'] = n_vehicles
data['depot'] = 0
data['demands'] = [0, 4, 4, 3, 1, 4, 12, 1, 24, 20]
data['vehicle_capacities'] = [30]*data['num_vehicles']
data['service_time'] = [0, 18, 18, 27, 25, 11, 92, 6, 239, 143]
def print_solution(data, manager, routing, solution):
"""Prints solution on console."""
print(f'Objective: {solution.ObjectiveValue()}')
max_route_time = 0; tour = {i:[] for i in range(data['num_vehicles'])}
for vehicle_id in range(data['num_vehicles']):
index = routing.Start(vehicle_id)
plan_output = 'Route for vehicle {}:\n'.format(vehicle_id)
route_time = 0
while not routing.IsEnd(index):
plan_output += ' {} -> '.format(manager.IndexToNode(index))
tour[vehicle_id].append(manager.IndexToNode(index))
previous_index = index
index = solution.Value(routing.NextVar(index))
if previous_index != 0 and previous_index <= len(data['service_time'])-1:
service_time = data['service_time'][previous_index]
else:
service_time = 0
route_time += (routing.GetArcCostForVehicle(
previous_index, index, vehicle_id) + service_time)
plan_output += '{}\n'.format(manager.IndexToNode(index))
tour[vehicle_id].append(manager.IndexToNode(index))
plan_output += 'Travel time of the route: {} sec\n'.format(route_time)
print(plan_output)
max_route_time = max(route_time, max_route_time)
print('Maximum of the route time: {} sec'.format(max_route_time))
return(tour)
def main(n_vehicles, cost1, cost2):
np.random.seed(0)
Num_of_Class6 = int(n_vehicles*0.6)
Num_of_Hybrid = n_vehicles - Num_of_Class6
V = list(range(n_vehicles))
V2 = list(set(np.random.choice(V, size=Num_of_Class6, replace=False)))
V1 = list(set(V)-set(V2))
print('V1:%s'%V1); print('V2:%s'%V2)
# 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, cost):
"""Returns the 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]*cost
range_extender_callback = partial(time_callback, cost=cost1)
class6_callback = partial(time_callback, cost=cost2)
transit_callback_index_V1 = routing.RegisterTransitCallback(range_extender_callback)
transit_callback_index_V2 = routing.RegisterTransitCallback(class6_callback)
# Define cost of each arc.
for v in V1:
routing.SetArcCostEvaluatorOfVehicle(transit_callback_index_V1, int(v))
for v in V2:
routing.SetArcCostEvaluatorOfVehicle(transit_callback_index_V2, int(v))
# Create and register a transit callback to limit the total travel+service time
def time_callback2(from_index, to_index):
"""Returns the 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)
if from_node != 0:
return data['time_matrix'][from_node][to_node] + data['service_time'][from_node]
else:
return data['time_matrix'][from_node][to_node]
transit_callback_index2 = routing.RegisterTransitCallback(time_callback2)
# Add Time constraint.
dimension_name = 'Time'
routing.AddDimensionWithVehicleCapacity(
transit_callback_index2,
0, # no slack
[max_vehicle_tt]*data['num_vehicles'], # vehicle maximum travel time
True, # start cumul to zero
dimension_name)
time_dimension = routing.GetDimensionOrDie(dimension_name)
def demand_callback(from_index):
"""Returns the demand of the node."""
# Convert from routing variable Index to demands NodeIndex.
from_node = manager.IndexToNode(from_index)
return data['demands'][from_node]
demand_callback_index = routing.RegisterUnaryTransitCallback(
demand_callback)
routing.AddDimensionWithVehicleCapacity(
demand_callback_index,
0, # null capacity slack
data['vehicle_capacities'], # vehicle maximum capacities
True, # start cumul to zero
'Capacity')
# 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.FromSeconds(1)
# Solve the problem.
solution = routing.SolveWithParameters(search_parameters)
# Print solution on console.
if solution:
tour = print_solution(data, manager, routing, solution)
return(tour, V1, V2)
else:
print('No solution found!\n')
tour,V1,V2 = main(n_vehicles,0.5,0.7)
Bonus: Use the below function to check critical solution metrics.
pairs = {}; serv_time = {}; tt ={}; cont_to_obj = {}
for i,j in tour.items():
if len(j) > 2:
serv_time[i] = sum([data['service_time'][k] for k in j])
print('Service time for vehicle %s: %s.'%(i,serv_time[i]))
num_deliveries = sum([data['demands'][k] for k in j])
print('Number of deliveries for vehicle %s: %s.'%(i,num_deliveries))
pairs[i] = list(zip(j,j[1:]))
tt[i] = sum([data['time_matrix'][k] for k in pairs[i]])
print('Travel time for vehicle %s: %s.'%(i,tt))
print('Total time for vehicle %s: %s'%(i,serv_time[i]+tt[i]))
if i in V1:
cont_to_obj[i] = sum([int(data['time_matrix'][k]*0.002244) for k in pairs[i]])
else:
cont_to_obj[i] = sum([int(data['time_matrix'][k]*0.0080517) for k in pairs[i]])
print('Contribution to the obj. fun. for vehicle %s: %s\n'%(i,cont_to_obj[i]))
Upvotes: 2
Reputation: 9281
Simply register two transits callbacks (i.e. one per vehicle type)
Then use the overload of AddDimension() to pass an array of registered transit callback index.
e.G. Mizux/vrp_multiple_transit.py
Upvotes: 1