I have run into a problem trying to create this.
The problem is that when I add a transit hub to a route, I essentially split the route into two pickup and deliveries, where I need to ensure that the first delivery is completed before the second one can be picked up.
I first tried to do this using :
routing.AddPickupAndDelivery(delivery1_index,pickup2_index),
but this didn't work.
Instead I tried to do it using a cumulated time dimension:
routing.solver().Add(time_dimension.CumulVar(delivery1_index) <= time_dimension.CumulVar(pickup2_index))
This sucessfully enforced that the delivery of the first package was completed before pickup of the second, and it also still allowed the two packages to be handled by different vehicles. Unfortunately, it seems that it somehow prevents the earlier disjunction from being active, meaning that when I add this constraint it no longer has the option of either using the transit hub or delivering the package directly, instead it now has to use the transit hub, which is not what I want.
constraintActive = routing.ActiveVar(delivery1_index) * routing.ActiveVar(pickup2_index)
routing.solver().Add(constraintActive * time_dimension.CumulVar(delivery1_index) <= constraintActive * time_dimension.CumulVar(pickup2_index))
however this didn't seem to have any effect either.
So how can I enforce that one delivery is completed before another is picked up, without making those two deliveries obligatory (removing the disjunction).
Full code for reproduceability:
from functools import partial
from time import perf_counter
import numpy as np
from ortools.constraint_solver import pywrapcp, routing_enums_pb2
def vrp(data):
matrix_len = len(data['demands'])
n_v = data['num_vehicles']
v_start = [len(data['demands'])-1]*n_v
v_end = [len(data['demands'])-1]*n_v
M = 10*np.ones((matrix_len,matrix_len),dtype=np.int64)
M[:,-1] = 0
# M[0,1] = 10
# M[1,0] = 10
M[-1,:] = 0
M = M.tolist()
manager = pywrapcp.RoutingIndexManager(matrix_len, n_v, v_start, v_end)
# Create Routing Model.
routing = pywrapcp.RoutingModel(manager)
routing.AddDisjunction([manager.NodeToIndex(0), manager.NodeToIndex(2)])
routing.AddDisjunction([manager.NodeToIndex(0), manager.NodeToIndex(3)])
routing.AddDisjunction([manager.NodeToIndex(1), manager.NodeToIndex(4)])
routing.AddDisjunction([manager.NodeToIndex(1), manager.NodeToIndex(5)])
pickup_index = manager.NodeToIndex(0)
delivery_index = manager.NodeToIndex(1)
routing.AddPickupAndDelivery(pickup_index, delivery_index)
routing.solver().Add(routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index))
#
pickup_index = manager.NodeToIndex(2)
delivery_index = manager.NodeToIndex(3)
routing.AddPickupAndDelivery(pickup_index, delivery_index)
routing.solver().Add(routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index))
pickup_index = manager.NodeToIndex(4)
delivery_index = manager.NodeToIndex(5)
routing.AddPickupAndDelivery(pickup_index, delivery_index)
routing.solver().Add(routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index))
# Add time dimension.
def time_callback(from_index, to_index):
"""Returns the distance between the two nodes."""
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return M[from_node][to_node]
transit_callback_index = routing.RegisterTransitCallback(time_callback)
dimension_name = "Time"
routing.AddDimension(
transit_callback_index,
100, # no slack
3000, # vehicle maximum travel distance
True, # start cumul to zero
dimension_name,
)
time_dimension = routing.GetDimensionOrDie(dimension_name)
pickup_index = manager.NodeToIndex(3)
delivery_index = manager.NodeToIndex(4)
constraintActive = routing.ActiveVar(pickup_index) * routing.ActiveVar(delivery_index)
routing.solver().Add(constraintActive * time_dimension.CumulVar(pickup_index) <= constraintActive * time_dimension.CumulVar(delivery_index))
# routing.solver().Add(routing.VehicleVar(pickup_index) != routing.VehicleVar(delivery_index)) # Used to check whether the 2 pickup and deliveries can be forced to use 2 different vehicles (which we want to make sure is possible)
# Define the metric which we want to minimize
def cost_callback(from_index, to_index):
"""The cost function which we want to minimize."""
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return M[from_node][to_node]
cost_callback_index = routing.RegisterTransitCallback(cost_callback)
routing.SetArcCostEvaluatorOfAllVehicles(cost_callback_index)
model_parameters = pywrapcp.DefaultRoutingModelParameters()
# 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)
search_parameters.solution_limit = 100
# Solve the problem.
t3 = perf_counter()
print("Starting solver...")
solution = routing.SolveWithParameters(search_parameters)
t4 = perf_counter()
if solution:
print(f"Found solution in {t4 - t3:2.2f}s")
else:
print(f"Failed to find solution in {t4 - t3:2.2f}s")
return manager, routing, solution
def print_solution(data, manager, routing, assignment):
"""Prints assignment on console."""
print(f"Objective: {assignment.ObjectiveValue()}")
# Display dropped nodes.
dropped_nodes = "Dropped nodes:"
for node in range(routing.Size()):
if routing.IsStart(node) or routing.IsEnd(node):
continue
if assignment.Value(routing.NextVar(node)) == node:
dropped_nodes += f" {manager.IndexToNode(node)}"
print(dropped_nodes)
# Display routes
total_distance = 0
total_load = 0
for vehicle_id in range(data["num_vehicles"]):
index = routing.Start(vehicle_id)
plan_output = f"Route for vehicle {vehicle_id}:\n"
route_distance = 0
route_load = 0
while not routing.IsEnd(index):
node_index = manager.IndexToNode(index)
route_load += data["demands"][node_index]
plan_output += f" {node_index} Reward({route_load}) -> "
previous_index = index
index = assignment.Value(routing.NextVar(index))
route_distance += routing.GetArcCostForVehicle(
previous_index, index, vehicle_id
)
plan_output += f" {manager.IndexToNode(index)} Reward({route_load})\n"
plan_output += f"Distance of the route: {route_distance}m\n"
plan_output += f"Reward of the route: {route_load}\n"
print(plan_output)
total_distance += route_distance
total_load += route_load
print(f"Total Distance of all routes: {total_distance}m")
print(f"Total Reward of all routes: {total_load}")
if __name__ == "__main__":
data = {}
data['num_vehicles'] = 2
data['demands'] = [1,2,3,4,5,6,0]
manager, routing, solution = vrp(data)
print_solution(data, manager, routing, solution)
print('done')