I am using Google OR-Tools in python to solve a capacitated VRP with pickup/delivery. In many cases the solver works well and returns reasonable solutions, but we have found that for some data sets the solver will always return one job per truck regardless of the time involved for the route.
I have the model set up as follows:
My initial vehicle count is equal to the number of jobs in the data-set, and we allow OR-Tools to automatically minimize the truck count.
Each job's pickup location has a demand of 1 and each job's dropoff location has a demand of -1, to enforce delivery immediately after pickup.
We set the maximum drive time per vehicle to 8 hours.
Then, each job has an associated quantity attached for pickup, and we separate this job into multiple deliveries based on a truck's capacity. For instance, if a job requires 60 tons delivered, we represent that as three jobs at 20 tons each (the maximum a vehicle is allowed to carry on an interstate in the U.S)
Now, we have a simple data set with a pickup location at: 698 Longtown Rd, Columbia, SC and a dropoff location at: 121 Chappell Creek Rd Hopkins, SC. This is a drive time of 32 minutes, or a total trip time of 64 minutes. This job has an associated quantity of 60 tons, which will require 3 truck loads.
The results we receive from or-tools shows one load per truck, and this result does not change regardless of how long we allow the solver to run. The optimal solution would allow one truck to complete all loads, as this is still drastically under the 8 hour drive time limit.
Here is my code:
import json
import math
import traceback
import urllib
import redis
import requests
import boto3
from signal import signal, SIGINT, SIGTERM
from ortools.constraint_solver import pywrapcp, routing_enums_pb2
url = 'https://test-api.truckit.com/api/2/signin'
api_data = {"password": "", "username": ""}
response = requests.post(url, json=api_data)
api_data = response.json()
def build_auth_header(token):
header = {'Authorization': f'Token {token}'}
return header
class SignalHandler:
def __init__(self):
self.received_signal = False
signal(SIGINT, self._signal_handler)
signal(SIGTERM, self._signal_handler)
def _signal_handler(self, signal, frame):
print(f"handling signal {signal}, exiting gracefully")
self.received_signal = True
sqs = boto3.resource("sqs")
queue = sqs.get_queue_by_name(QueueName="")
redisClient = redis.Redis(host='', port=6379,
password='')
def create_distance_matrix(data):
addresses = data["addresses"]
API_key = data["API_key"]
origin_addresses = []
dest_addresses = addresses
distance_matrix = []
responses = {}
responses['destination_addresses'] = []
responses['origin_addresses'] = []
responses['rows'] = []
# Send q requests, returning max_rows rows per request.
for i in range(0, len(addresses)):
origin_addresses.clear()
origin_addresses.append(addresses[i])
for j in range(0, len(addresses), 25):
dest_addresses_request = addresses[j:j + 25]
response = send_request(origin_addresses, dest_addresses_request, API_key)
responses['origin_addresses'] = response['origin_addresses']
for destination_address in response['destination_addresses']:
responses['destination_addresses'].append(destination_address)
for row in response['rows']:
if len(responses['rows']) == 0:
responses['rows'].append(row)
else:
for element in row['elements']:
responses['rows'][0]['elements'].append(element)
distance_matrix += build_distance_matrix(responses)
responses['origin_addresses'].clear()
responses['destination_addresses'].clear()
responses['rows'].clear()
return distance_matrix
def send_request(origin_addresses, dest_addresses, API_key):
""" Build and send request for the given origin and destination addresses."""
def build_address_str(addresses):
# Build a pipe-separated string of addresses
address_str = ''
for i in range(len(addresses) - 1):
address_str += addresses[i] + '|'
address_str += addresses[-1]
return address_str
request = 'https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial'
origin_address_str = build_address_str(origin_addresses)
dest_address_str = build_address_str(dest_addresses)
request = request + '&origins=' + origin_address_str + '&destinations=' + \
dest_address_str + '&key=' + API_key
jsonResult = urllib.request.urlopen(request).read()
response = json.loads(jsonResult)
return response
def build_distance_matrix(response):
distance_matrix = []
for row in response['rows']:
row_list = [row['elements'][j]['duration']['value'] for j in range(len(row['elements']))]
distance_matrix.append(row_list)
return distance_matrix
def process_message(message_body):
print(f"processing message: {message_body}")
data = json.loads(message_body)
data_matrix = {}
data_matrix['problem_id'] = data['problemId']
data_matrix["addresses"] = []
data_matrix["pickups_deliveries"] = []
data_matrix["demands"] = []
data_matrix["jobOrderIDs"] = []
depot_address = str(data["depot"]["latitude"]) + "," + str(data["depot"]["longitude"])
data_matrix["jobOrderIDs"].append(0)
data_matrix["addresses"].append(depot_address)
hash_key = data["hashKey"]
for location in data["locationList"]:
pick_lat = location["PickupLatitude"]
pick_long = location["PickupLongitude"]
drop_lat = location["DropoffLatitude"]
drop_long = location["DropoffLongitude"]
jobOrderId = location["jobOrderID"]
demand = math.ceil(float(int(location["totalQuantity"]) / 20))
for i in range(0, demand):
data_matrix["addresses"].append(str(pick_lat) + ',' + str(pick_long))
data_matrix["addresses"].append(str(drop_lat) + ',' + str(drop_long))
data_matrix["jobOrderIDs"].append(str(jobOrderId))
data_matrix["jobOrderIDs"].append(str(jobOrderId))
data_matrix["demands"].append(0)
for i in range(1, len(data_matrix["addresses"]) - 1, 2):
data_matrix["pickups_deliveries"].append([i, i + 1])
data_matrix["demands"].append(1)
data_matrix["demands"].append(-1)
data_matrix["num_vehicles"] = int(len(data_matrix["addresses"]) / 2)
data_matrix["vehicle_capacities"] = []
for i in range(0, data_matrix["num_vehicles"]):
data_matrix["vehicle_capacities"].append(1)
data_matrix["depot"] = 0
data_matrix["API_key"] = ''
data_matrix["distance_matrix"] = create_distance_matrix(data_matrix)
# Create the routing index manager.
manager = pywrapcp.RoutingIndexManager(len(data_matrix['distance_matrix']),
data_matrix['num_vehicles'], data_matrix['depot'])
# Create Routing Model.
routing = pywrapcp.RoutingModel(manager)
# Define cost of each arc.
def distance_callback(from_index, to_index):
"""Returns the manhattan distance between the two nodes."""
# Convert from routing variable Index to distance matrix NodeIndex.
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return data_matrix['distance_matrix'][from_node][to_node]*1000
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
# Add Distance constraint.
dimension_name = 'Duration'
routing.AddDimension(
transit_callback_index,
0, # no slack
28800*1000, # vehicle maximum travel hours
True, # start cumul to zero
dimension_name)
distance_dimension = routing.GetDimensionOrDie(dimension_name)
distance_dimension.SetGlobalSpanCostCoefficient(100)
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_matrix['demands'][from_node]
demand_callback_index = routing.RegisterUnaryTransitCallback(
demand_callback)
routing.AddDimensionWithVehicleCapacity(
demand_callback_index,
0, # null capacity slack
data_matrix['vehicle_capacities'], # vehicle maximum capacities
True, # start cumul to zero
'Capacity')
# Define Transportation Requests.
for request in data_matrix['pickups_deliveries']:
pickup_index = manager.NodeToIndex(request[0])
delivery_index = manager.NodeToIndex(request[1])
routing.AddPickupAndDelivery(pickup_index, delivery_index)
routing.solver().Add(
routing.VehicleVar(pickup_index) == routing.VehicleVar(
delivery_index))
routing.solver().Add(
distance_dimension.CumulVar(pickup_index) <=
distance_dimension.CumulVar(delivery_index))
# Setting first solution heuristic.
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.local_search_metaheuristic = (
routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
search_parameters.time_limit.seconds = 1200
search_parameters.log_search = True
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.AUTOMATIC)
search_parameters.use_full_propagation = True
# Solve the problem.
solution = routing.SolveWithParameters(search_parameters)
if solution:
solution_dict = {}
for vehicle_id in range(data_matrix['num_vehicles']):
index = routing.Start(vehicle_id)
plan_output = ''
route_distance = 0
route_load = 0
while not routing.IsEnd(index):
node_index = manager.IndexToNode(index)
plan_output += '{0},'.format(data_matrix['jobOrderIDs'][node_index])
previous_index = index
index = solution.Value(routing.NextVar(index))
plan_output += '{0},'.format(data_matrix['jobOrderIDs'][manager.IndexToNode(index)])
plan_output = plan_output[:-1]
plan_words = plan_output.split(",")
plan_output = ''
for i in range(len(plan_words)):
if (i % 2 == 0):
plan_output += plan_words[i] + ","
plan_output = plan_output[:-1]
plan_output += ",0"
if plan_output != 0 and plan_output != str(0) and plan_output != str('0,0'):
print(plan_output)
solution_dict[vehicle_id] = plan_output
# trucks_url = 'https://test-api.truckit.com/api/2/trucks'
trucks_url = 'https://test-api.truckit.com/api/2/job-orders/smart-dispatch/' + str(data_matrix['problem_id'])
head = build_auth_header(api_data["authToken"])
status = {}
ride_list = []
dummy_location_dict = {}
dummy_id_dict = {}
dummy_id_dict["id"] = 0
dummy_id_dict["name"] = ""
dummy_location_dict["location"] = dummy_id_dict
dummy_location_dict["timestamp"] = 0
ride_list.append(dummy_location_dict)
redisClient.hset(hash_key, "solution", json.dumps(solution_dict))
redisClient.hset(hash_key, "ride_list", json.dumps(ride_list))
json_data = {"status": "completed"}
api_response = requests.post(trucks_url, headers=head, json=json_data)
print_solution(data_matrix, manager, routing, solution)
def print_solution(data, manager, routing, solution):
"""Prints solution on console."""
print(f'Objective: {solution.ObjectiveValue()}')
total_distance = 0
total_load = 0
for vehicle_id in range(data['num_vehicles']):
index = routing.Start(vehicle_id)
plan_output = 'Route for vehicle {}:\n'.format(vehicle_id)
route_distance = 0
route_load = 0
while not routing.IsEnd(index):
node_index = manager.IndexToNode(index)
plan_output += ' {0} -> '.format(node_index)
previous_index = index
index = solution.Value(routing.NextVar(index))
try:
distance = data['distance_matrix'][previous_index][index]
route_distance += distance
except:
distance = distance
plan_output += ' {0}\n'.format(manager.IndexToNode(index))
plan_output += 'Time of the route: {} hours\n'.format(str(float(route_distance / (60 * 60))))
print(plan_output)
total_distance += route_distance
print('Total distance of all routes: {}m'.format(total_distance))
if __name__ == "__main__":
signal_handler = SignalHandler()
while not signal_handler.received_signal:
messages = queue.receive_messages(
MaxNumberOfMessages=1,
WaitTimeSeconds=1
)
for message in messages:
try:
process_message(message.body)
message.delete()
except Exception as e:
print(f"exception while processing message: {repr(e)}")
traceback.print_exc()
continue
message.delete()
IF anyone has any suggestions as to what the problem may be, your help is greatly appreciated.
I want to generate a sequence of coordinates in order of distance from the origin. The sequence will obviously be infinite so just generating them all and sorting by distance will not work for me.
For those points that are the same distance I don't care about the order.
For example, here's some points, with their distance from the origin up to two steps away.
# d² = 0
(0,0,0)
# d² = 1
(0,0,-1)
(0,-1,0)
(-1,0,0)
(1,0,0)
(0,1,0)
(0,0,1)
# d² = 2
(0,-1,-1)
(-1,0,-1)
(1,0,-1)
(0,1,-1)
(-1,-1,0)
(1,-1,0)
(-1,1,0)
(1,1,0)
(0,-1,1)
(-1,0,1)
(1,0,1)
(0,1,1)
# d² = 3
(-1,-1,-1)
(1,-1,-1)
(-1,1,-1)
(1,1,-1)
(-1,-1,1)
(1,-1,1)
(-1,1,1)
(1,1,1)
# d² = 4
(0,0,-2)
(0,-2,0)
(-2,0,0)
(2,0,0)
(0,2,0)
(0,0,2)
# d² = 5
(0,-1,-2)
(-1,0,-2)
(1,0,-2)
(0,1,-2)
(0,-2,-1)
(-2,0,-1)
(2,0,-1)
(0,2,-1)
(-1,-2,0)
(1,-2,0)
(-2,-1,0)
(2,-1,0)
(-2,1,0)
(2,1,0)
(-1,2,0)
(1,2,0)
(0,-2,1)
(-2,0,1)
(2,0,1)
(0,2,1)
(0,-1,2)
(-1,0,2)
(1,0,2)
(0,1,2)
# d² = 6
(-1,-1,-2)
(1,-1,-2)
(-1,1,-2)
(1,1,-2)
(-1,-2,-1)
(1,-2,-1)
(-2,-1,-1)
(2,-1,-1)
(-2,1,-1)
(2,1,-1)
(-1,2,-1)
(1,2,-1)
(-1,-2,1)
(1,-2,1)
(-2,-1,1)
(2,-1,1)
(-2,1,1)
(2,1,1)
(-1,2,1)
(1,2,1)
(-1,-1,2)
(1,-1,2)
(-1,1,2)
(1,1,2)
# d² = 8
(0,-2,-2)
(-2,0,-2)
(2,0,-2)
(0,2,-2)
(-2,-2,0)
(2,-2,0)
(-2,2,0)
(2,2,0)
(0,-2,2)
(-2,0,2)
(2,0,2)
(0,2,2)
# d² = 9
(-1,-2,-2)
(1,-2,-2)
(-2,-1,-2)
(2,-1,-2)
(-2,1,-2)
(2,1,-2)
(-1,2,-2)
(1,2,-2)
(-2,-2,-1)
(2,-2,-1)
(-2,2,-1)
(2,2,-1)
(-2,-2,1)
(2,-2,1)
(-2,2,1)
(2,2,1)
(-1,-2,2)
(1,-2,2)
(-2,-1,2)
(2,-1,2)
(-2,1,2)
(2,1,2)
(-1,2,2)
(1,2,2)
# d² = 12
(-2,-2,-2)
(2,-2,-2)
(-2,2,-2)
(2,2,-2)
(-2,-2,2)
(2,-2,2)
(-2,2,2)
(2,2,2)
Starting from a solution aaa, then aab, then abc, you can retrieve all other "permutations" like aba, baa, -a-a-b, and so on. So you can keep a < b < c, positive numbers.
Geometrically : one eighth triangle on a globe. For a circle x²+y² one would iterate in a quarter over x and let y be retrieved from r² - x². Happens here to given an a.
Unfortunately the coding is too much to me for answering on a sunny Sunday.
(= hard enough).
Schematically in pseudo-code:
int distance = -1;
int a;
int b;
int c;
PermutationIterator perm = ...
Point next() {
if (perm.atEnd()) { // Initially true.
perm.nextDistance();
++distance;
a = distance;
b = a;
c = a;
// Will return Point(a, a, a);
}
return perm.nextPerm();
}
I have 10 nodes .. each node has known number of transceivers as indicated in this vector:
[8 3 3 3 3 3 2 1 1 1]
where: 8 is the number of transceivers at the 1st node, 3 is the number of transceivers at the 2nd node and so on ..
Each transceiver can only receive from one source at a time .. it is required that all transceivers must be in use simultaneously .. it is permitted for the same node to use more than one transceiver to transmit to another node that has enough transceivers to receive with .. it is not possible for a single transceiver to be considered to be transmitting to multiple transceivers ..
I want to know How can I obtain all possible connections between nodes and each obtained connection matrix identify the number of transceivers used between the connected nodes ?
I don't know how to do this in Matlab.
With Minizinc, I found the following solution (slightly massaged with Microsoft Word):
Note that I used node number 0..9 rather than 1..10 to compact the matrix text.
There is a huge number of possible solutions. This is just one of them.
The Minizinc script:
include "globals.mzn";
set of int: Transceivers = 1..28;
set of int: Nodes = 1..10;
array[Transceivers] of Nodes: node = [1,1,1,1,1,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5,6,6,6,7,7,8,9,10];
array[Transceivers] of Nodes: seq = [1,2,3,4,5,6,7,8,1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,1,2,1,1,1];
array[Transceivers] of var Transceivers: aTo;
array[Nodes, Nodes] of var Transceivers: connections;
% Each transceiver can only receive from one source at a time
constraint all_different([aTo[t] | t in Transceivers]);
% transceivers have to connect to other nodes rather than to their own node
constraint
forall(t in Transceivers)
(node[aTo[t]] != node[t]);
% no more than 1 connection between any pair of nodes
constraint
forall(i in Nodes, j in Nodes where j > i)
(sum([bool2int((node[t] == i) /\ (node[aTo[t]] == j)) | t in Transceivers]) < 2);
solve satisfy;
output [" "] ++ [ show(node[i]-1) ++ "." ++ show(seq[i]) ++ " | " | i in Transceivers] ++
[ if j == 1 then ("\n" ++ show(node[i]-1) ++ "." ++ show(seq[i]) ++ ": ") else "" endif ++
if fix(aTo[i]) == j then " x | " else " | " endif
| i in Transceivers, j in Transceivers ];