# This Python file uses the following encoding: utf-8 # Copyright 2015 Tin Arm Engineering AB # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Capacitated Vehicle Routing Problem with Time Windows (and optional orders). This is a sample using the routing library python wrapper to solve a CVRPTW problem. A description of the problem can be found here: http://en.wikipedia.org/wiki/Vehicle_routing_problem. The variant which is tackled by this model includes a capacity dimension, time windows and optional orders, with a penalty cost if orders are not performed. Too help explore the problem, two classes are provided Customers() and Vehicles(): used to randomly locate orders and depots, and to randomly generate demands, time-window constraints and vehicles. Distances are computed using the Great Circle distances. Distances are in km and times in seconds. A function for the displaying of the vehicle plan display_vehicle_output The optimization engine uses local search to improve solutions, first solutions being generated using a cheapest addition heuristic. """ import math from six.moves import xrange from ortools.constraint_solver import pywrapcp from ortools.constraint_solver import routing_enums_pb2 def distance(x1, y1, x2, y2): # Manhattan distance dist = abs(x1 - x2) + abs(y1 - y2) return dist # Distance callback class CreateDistanceCallback(object): """Create callback to calculate distances and travel times between points.""" def __init__(self, locations): """Initialize distance array.""" num_locations = len(locations) self.matrix = {} for from_node in xrange(num_locations): self.matrix[from_node] = {} for to_node in xrange(num_locations): if from_node == to_node: self.matrix[from_node][to_node] = 0 else: x1 = locations[from_node][0] y1 = locations[from_node][1] x2 = locations[to_node][0] y2 = locations[to_node][1] self.matrix[from_node][to_node] = distance(x1, y1, x2, y2) def Distance(self, from_node, to_node): return self.matrix[from_node][to_node] # Demand callback class CreateDemandCallback(object): """Create callback to get demands at location node.""" def __init__(self, demands): self.matrix = demands def Demand(self, from_node, to_node): return self.matrix[from_node] # Service time (proportional to demand) callback. class CreateServiceTimeCallback(object): """Create callback to get time windows at each location.""" def __init__(self, demands, time_per_demand_unit): self.matrix = demands self.time_per_demand_unit = time_per_demand_unit def ServiceTime(self, from_node, to_node): return self.matrix[from_node] * self.time_per_demand_unit # Create total_time callback (equals service time plus travel time). class CreateTotalTimeCallback(object): def __init__(self, service_time_callback, dist_callback, speed): self.service_time_callback = service_time_callback self.dist_callback = dist_callback self.speed = speed def TotalTime(self, from_node, to_node): service_time = self.service_time_callback(from_node, to_node) travel_time = self.dist_callback(from_node, to_node) / self.speed return service_time + travel_time def main(): # Create the data. data = create_data_array() locations = data[0] demands = data[1] start_times = data[2] num_locations = len(locations) depot = 0 num_vehicles = 5 search_time_limit = 400000 # Create routing model. if num_locations > 0: # The number of nodes of the VRP is num_locations. # Nodes are indexed from 0 to num_locations - 1. By default the start of # a route is node 0. routing = pywrapcp.RoutingModel(num_locations, num_vehicles, depot) search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters() # Setting first solution heuristic: the # method for finding a first solution to the problem. search_parameters.first_solution_strategy = ( routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC) # The 'PATH_CHEAPEST_ARC' method does the following: # Starting from a route "start" node, connect it to the node which produces the # cheapest route segment, then extend the route by iterating on the last # node added to the route. # Put callbacks to the distance function and travel time functions here. dist_between_locations = CreateDistanceCallback(locations) dist_callback = dist_between_locations.Distance routing.SetArcCostEvaluatorOfAllVehicles(dist_callback) demands_at_locations = CreateDemandCallback(demands) demands_callback = demands_at_locations.Demand # Adding capacity dimension constraints. VehicleCapacity = 100; NullCapacitySlack = 0; fix_start_cumul_to_zero = True capacity = "Capacity" routing.AddDimension(demands_callback, NullCapacitySlack, VehicleCapacity, fix_start_cumul_to_zero, capacity) # Adding time dimension constraints. time_per_demand_unit = 300 horizon = 24 * 3600 time = "Time" tw_duration = 5 * 3600 speed = 10 service_times = CreateServiceTimeCallback(demands, time_per_demand_unit) service_time_callback = service_times.ServiceTime total_times = CreateTotalTimeCallback(service_time_callback, dist_callback, speed) total_time_callback = total_times.TotalTime # Add a dimension for time-window constraints and limits on the start times and end times. routing.AddDimension(total_time_callback, # total time function callback horizon, horizon, fix_start_cumul_to_zero, time) # Add limit on size of the time windows. time_dimension = routing.GetDimensionOrDie(time) for order in xrange(1, num_locations): start = start_times[order] time_dimension.CumulVar(order).SetRange(start, start + tw_duration) # Solve displays a solution if any. assignment = routing.SolveWithParameters(search_parameters) if assignment: data = create_data_array() locations = data[0] demands = data[1] start_times = data[2] size = len(locations) # Solution cost. print ("Total distance of all routes: " , str(assignment.ObjectiveValue())) # Inspect solution. capacity_dimension = routing.GetDimensionOrDie(capacity); time_dimension = routing.GetDimensionOrDie(time); for vehicle_nbr in xrange(num_vehicles): index = routing.Start(vehicle_nbr) plan_output = 'Route {0}:'.format(vehicle_nbr) while not routing.IsEnd(index): node_index = routing.IndexToNode(index) load_var = capacity_dimension.CumulVar(index) time_var = time_dimension.CumulVar(index) plan_output += \ " {node_index} Load({load}) Time({tmin}, {tmax}) -> ".format( node_index=node_index, load=assignment.Value(load_var), tmin=str(assignment.Min(time_var)), tmax=str(assignment.Max(time_var))) index = assignment.Value(routing.NextVar(index)) node_index = routing.IndexToNode(index) load_var = capacity_dimension.CumulVar(index) time_var = time_dimension.CumulVar(index) plan_output += \ " {node_index} Load({load}) Time({tmin}, {tmax})".format( node_index=node_index, load=assignment.Value(load_var), tmin=str(assignment.Min(time_var)), tmax=str(assignment.Max(time_var))) print (plan_output) else: print ('No solution found.') else: print ('Specify an instance greater than 0.') def create_data_array(): locations = [[82, 76], [96, 44], [50, 5], [49, 8], [13, 7], [29, 89], [58, 30], [84, 39], [14, 24], [12, 39], [3, 82], [5, 10], [98, 52], [84, 25], [61, 59], [1, 65], [88, 51], [91, 2], [19, 32], [93, 3], [50, 93], [98, 14], [5, 42], [42, 9], [61, 62], [9, 97], [80, 55], [57, 69], [23, 15], [20, 70], [85, 60], [98, 5]] demands = [0, 19, 21, 6, 19, 7, 12, 16, 6, 16, 8, 14, 21, 16, 3, 22, 18, 19, 1, 24, 8, 12, 4, 8, 24, 24, 2, 20, 15, 2, 14, 9] start_times = [28842, 50891, 10351, 49370, 22553, 53131, 8908, 56509, 54032, 10883, 60235, 46644, 35674, 30304, 39950, 38297, 36273, 52108, 2333, 48986, 44552, 31869, 38027, 5532, 57458, 51521, 11039, 31063, 38781, 49169, 32833, 7392] data = [locations, demands, start_times] return data if __name__ == '__main__': main()