#!/usr/bin/env python # This Python file uses the following encoding: utf-8 # Copyright 2015 Tin Arm Engineering AB # Copyright 2018 Google LLC # 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 (CVRP). This is a sample using the routing library python wrapper to solve a CVRP problem. A description of the problem can be found here: http://en.wikipedia.org/wiki/Vehicle_routing_problem. Distances are in meters and time in seconds. Manhattan average block: 750ft x 264ft -> 228m x 80m src: https://nyti.ms/2GDoRIe "NY Times: Know Your distance" here we use: 114m x 80m city block """ from __future__ import print_function from six.moves import xrange from ortools.constraint_solver import pywrapcp from ortools.constraint_solver import routing_enums_pb2 ########################### # Problem Data Definition # ########################### class Vehicle(): """Stores the property of a vehicle""" def __init__(self): """Initializes the vehicle properties""" self._capacity = 15 @property def capacity(self): """Gets vehicle capacity""" return self._capacity class CityBlock(): """City block definition""" @property def width(self): """Gets Block size West to East""" return 228/2 @property def height(self): """Gets Block size North to South""" return 80 class DataProblem(): """Stores the data for the problem""" def __init__(self): """Initializes the data for the problem""" self._vehicle = Vehicle() self._num_vehicles = 4 # Locations in block unit locations = \ [(4, 4), # depot (2, 0), (8, 0), # row 0 (0, 1), (1, 1), (5, 2), (7, 2), (3, 3), (6, 3), (5, 5), (8, 5), (1, 6), (2, 6), (3, 7), (6, 7), (0, 8), (7, 8)] # locations in meters using the block dimension defined city_block = CityBlock() self._locations = [( loc[0]*city_block.width, loc[1]*city_block.height) for loc in locations] self._depot = 0 self._demands = \ [0, # depot 1, 1, # row 0 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8] @property def vehicle(self): """Gets a vehicle""" return self._vehicle @property def num_vehicles(self): """Gets number of vehicles""" return self._num_vehicles @property def locations(self): """Gets locations""" return self._locations @property def num_locations(self): """Gets number of locations""" return len(self.locations) @property def depot(self): """Gets depot location index""" return self._depot @property def demands(self): """Gets demands at each location""" return self._demands ####################### # Problem Constraints # ####################### def manhattan_distance(position_1, position_2): """Computes the Manhattan distance between two points""" return (abs(position_1[0] - position_2[0]) + abs(position_1[1] - position_2[1])) class CreateDistanceEvaluator(object): # pylint: disable=too-few-public-methods """Creates callback to return distance between points.""" def __init__(self, data): """Initializes the distance matrix.""" self._distances = {} # precompute distance between location to have distance callback in O(1) for from_node in xrange(data.num_locations): self._distances[from_node] = {} for to_node in xrange(data.num_locations): if from_node == to_node: self._distances[from_node][to_node] = 0 else: self._distances[from_node][to_node] = ( manhattan_distance( data.locations[from_node], data.locations[to_node])) def distance_evaluator(self, from_node, to_node): """Returns the manhattan distance between the two nodes""" return self._distances[from_node][to_node] class CreateDemandEvaluator(object): # pylint: disable=too-few-public-methods """Creates callback to get demands at each location.""" def __init__(self, data): """Initializes the demand array.""" self._demands = data.demands def demand_evaluator(self, from_node, to_node): """Returns the demand of the current node""" del to_node return self._demands[from_node] def add_capacity_constraints(routing, data, demand_evaluator): """Adds capacity constraint""" capacity = "Capacity" routing.AddDimension( demand_evaluator, 0, # null capacity slack data.vehicle.capacity, True, # start cumul to zero capacity) ########### # Printer # ########### class ConsolePrinter(): """Print solution to console""" def __init__(self, data, routing, assignment): """Initializes the printer""" self._data = data self._routing = routing self._assignment = assignment @property def data(self): """Gets problem data""" return self._data @property def routing(self): """Gets routing model""" return self._routing @property def assignment(self): """Gets routing model""" return self._assignment def print(self): """Prints assignment on console""" # Inspect solution. total_dist = 0 for vehicle_id in xrange(self.data.num_vehicles): index = self.routing.Start(vehicle_id) plan_output = 'Route for vehicle {0}:\n'.format(vehicle_id) route_dist = 0 route_load = 0 while not self.routing.IsEnd(index): node_index = self.routing.IndexToNode(index) next_node_index = self.routing.IndexToNode( self.assignment.Value(self.routing.NextVar(index))) route_dist += manhattan_distance( self.data.locations[node_index], self.data.locations[next_node_index]) route_load += self.data.demands[node_index] plan_output += ' {0} Load({1}) -> '.format(node_index, route_load) index = self.assignment.Value(self.routing.NextVar(index)) node_index = self.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)) ######## # Main # ######## def main(): """Entry point of the program""" # Instantiate the data problem. data = DataProblem() # Create Routing Model routing = pywrapcp.RoutingModel(data.num_locations, data.num_vehicles, data.depot) # Define weight of each edge distance_evaluator = CreateDistanceEvaluator(data).distance_evaluator routing.SetArcCostEvaluatorOfAllVehicles(distance_evaluator) # Add Capacity constraint demand_evaluator = CreateDemandEvaluator(data).demand_evaluator add_capacity_constraints(routing, data, demand_evaluator) # 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) printer = ConsolePrinter(data, routing, assignment) printer.print() if __name__ == '__main__': main()