diff --git a/examples/python/CMakeLists.txt b/examples/python/CMakeLists.txt index 52fb7c6f06..f767a2761b 100644 --- a/examples/python/CMakeLists.txt +++ b/examples/python/CMakeLists.txt @@ -9,6 +9,10 @@ foreach(TEST linear_programming pyflow_example tsp + vrp + vrpgs + cvrp + cvrptw ) add_test(py${TEST}_venv ${VENV_BIN_DIR}/python ${CMAKE_CURRENT_SOURCE_DIR}/${TEST}.py) set_tests_properties(py${TEST}_venv PROPERTIES DEPENDS build_venv) diff --git a/examples/python/cvrp.py b/examples/python/cvrp.py new file mode 100755 index 0000000000..1656c8e3de --- /dev/null +++ b/examples/python/cvrp.py @@ -0,0 +1,259 @@ +#!/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() diff --git a/examples/python/cvrptw.py b/examples/python/cvrptw.py old mode 100644 new mode 100755 index 7c3927c10a..27d882fc2b --- a/examples/python/cvrptw.py +++ b/examples/python/cvrptw.py @@ -1,6 +1,7 @@ +#!/usr/bin/env python # This Python file uses the following encoding: utf-8 # Copyright 2015 Tin Arm Engineering AB -# Copyright 2017 Google LLC +# 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 @@ -13,36 +14,33 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Capacitated Vehicle Routing Problem with Time Windows. - - This is a sample using the routing library python wrapper to solve a - CVRPTW problem. +"""Capacitated Vehicle Routing Problem with Time Windows (CVRPTW). + 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 - and time windows. - Distances are computed using the Manhattan distances. Distances are in km - and times in seconds. - The optimization engine uses local search to improve solutions, first - solutions being generated using a cheapest addition heuristic. + Distances are in meters and time in minutes. + + 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 -import sys from six.moves import xrange from ortools.constraint_solver import pywrapcp from ortools.constraint_solver import routing_enums_pb2 -# Problem Data Definition +########################### +# Problem Data Definition # +########################### class Vehicle(): """Stores the property of a vehicle""" def __init__(self): """Initializes the vehicle properties""" - self._capacity = 100 - - # Travel speed: 80km/h to convert in km/s - self._speed = 80 / 3600. + self._capacity = 15 + # Travel speed: 5km/h to convert in m/min + self._speed = 5 * 60 / 3.6 @property def capacity(self): @@ -54,50 +52,65 @@ class Vehicle(): """Gets the average travel speed of a vehicle""" return self._speed +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 = 5 + 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 city block dimension + city_block = CityBlock() + self._locations = [( + loc[0]*city_block.width, + loc[1]*city_block.height) for loc in locations] - self._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]] self._depot = 0 self._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] - # Time to deliver a package to a customer: 3min/unit - self._time_per_demand_unit = 3 * 60 + [0, # depot + 1, 1, # 1, 2 + 2, 4, # 3, 4 + 2, 4, # 5, 6 + 8, 8, # 7, 8 + 1, 2, # 9,10 + 1, 2, # 11,12 + 4, 4, # 13, 14 + 8, 8] # 15, 16 - start_times = \ - [0, 5080, 1030, 4930, 2250, 5310, 890, 5650, - 5400, 1080, 6020, 4660, 3560, 3030, 3990, 3820, - 3620, 5210, 230, 4890, 4450, 3180, 3800, 550, - 5740, 5150, 1100, 3100, 3870, 4910, 3280, 730] - - # The width of the time window: 5 hours. - tw_duration = 5 * 60 * 60 - - # In this example, the time window widths is the same at each location, so we define the end - # times to be start times + tw_duration. - # For problems in which the time window widths vary by location, you can explicitly define - # the list of end_times, as we have done for start_times. - self._time_windows = [(start, start + tw_duration) for start in start_times] - - # Check data coherency - if self.num_locations == 0: - raise ValueError('Locations must be greater than 0.') - - if (len(self._locations) != len(self._demands) or - len(self._locations) != len(self._time_windows)): - raise RuntimeError("Inconsistent data problem!") + self._time_windows = \ + [(0, 0), + (75, 85), (75, 85), # 1, 2 + (60, 70), (45, 55), # 3, 4 + (0, 8), (50, 60), # 5, 6 + (0, 10), (10, 20), # 7, 8 + (0, 10), (75, 85), # 9, 10 + (85, 95), (5, 15), # 11, 12 + (15, 25), (10, 20), # 13, 14 + (45, 55), (30, 40)] # 15, 16 @property def vehicle(self): @@ -119,11 +132,6 @@ class DataProblem(): """Gets number of locations""" return len(self.locations) - def manhattan_distance(self, from_node, to_node): - """Computes the Manhattan distance between two nodes""" - return (abs(self.locations[from_node][0] - self.locations[to_node][0]) + - abs(self.locations[from_node][1] - self.locations[to_node][1])) - @property def depot(self): """Gets depot location index""" @@ -131,59 +139,71 @@ class DataProblem(): @property def demands(self): - """Gets demands for each locations""" + """Gets demands at each location""" return self._demands @property def time_per_demand_unit(self): - """Gets the average time per demand unit""" - return self._time_per_demand_unit + """Gets the time (in min) to load a demand""" + return 5 # 5 minutes/unit @property def time_windows(self): """Gets (start time, end time) for each locations""" return self._time_windows - @property - def horizon(self): - """Maximum times to perform all deliveries""" - return 24 * 3600 +####################### +# 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])) -# Distance callback -class CreateDistanceCallback(object): # pylint: disable=too-few-public-methods +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._distance = {} + self._distances = {} # precompute distance between location to have distance callback in O(1) for from_node in xrange(data.num_locations): - self._distance[from_node] = {} + self._distances[from_node] = {} for to_node in xrange(data.num_locations): if from_node == to_node: - self._distance[from_node][to_node] = 0 + self._distances[from_node][to_node] = 0 else: - self._distance[from_node][to_node] = ( - data.manhattan_distance(from_node, to_node)) + self._distances[from_node][to_node] = ( + manhattan_distance( + data.locations[from_node], + data.locations[to_node])) - def distance(self, from_node, to_node): + def distance_evaluator(self, from_node, to_node): """Returns the manhattan distance between the two nodes""" - return self._distance[from_node][to_node] + return self._distances[from_node][to_node] -# Demand callback -class CreateDemandCallback(object): # pylint: disable=too-few-public-methods +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(self, from_node, to_node): + def demand_evaluator(self, from_node, to_node): """Returns the demand of the current node""" del to_node return self._demands[from_node] -# Time callback (equals to: service time + travel time). -class CreateTimeCallback(object): +def add_capacity_constraints(routing, data, demand_evaluator): + """Adds capacity constraint""" + capacity = "Capacity" + routing.AddDimension( + demand_evaluator, + 0, # null capacity slack + data.vehicle.capacity, # vehicle maximum capacity + True, # start cumul to zero + capacity) + +class CreateTimeEvaluator(object): """Creates callback to get total times between locations.""" @staticmethod def service_time(data, node): @@ -196,13 +216,14 @@ class CreateTimeCallback(object): if from_node == to_node: travel_time = 0 else: - travel_time = data.manhattan_distance(from_node, to_node) / data.vehicle.speed + travel_time = manhattan_distance( + data.locations[from_node], + data.locations[to_node]) / data.vehicle.speed return travel_time def __init__(self, data): """Initializes the total time matrix.""" self._total_time = {} - # precompute total time to have time callback in O(1) for from_node in xrange(data.num_locations): self._total_time[from_node] = {} @@ -210,109 +231,125 @@ class CreateTimeCallback(object): if from_node == to_node: self._total_time[from_node][to_node] = 0 else: - self._total_time[from_node][to_node] = ( + self._total_time[from_node][to_node] = int( self.service_time(data, from_node) + self.travel_time(data, from_node, to_node)) - def time(self, from_node, to_node): + def time_evaluator(self, from_node, to_node): """Returns the total time between the two nodes""" return self._total_time[from_node][to_node] -def print_assignment(data, routing, assignment, capacity, time): - """Prints solution""" - # Solution cost. - print("Total distance of all routes: {0}\n".format(assignment.ObjectiveValue())) - # Inspect solution. - capacity_dimension = routing.GetDimensionOrDie(capacity) +def add_time_window_constraints(routing, data, time_evaluator): + """Add Global Span constraint""" + time = "Time" + horizon = 120 + routing.AddDimension( + time_evaluator, + horizon, # allow waiting time + horizon, # maximum time per vehicle + True, # start cumul to zero + time) time_dimension = routing.GetDimensionOrDie(time) + for location_idx, time_window in enumerate(data.time_windows): + time_dimension.CumulVar(location_idx).SetRange(time_window[0], time_window[1]) - for vehicle_id in xrange(data.num_vehicles): - index = routing.Start(vehicle_id) - plan_output = 'Route for vehicle {0}:\n'.format(vehicle_id) - route_dist = 0 +########### +# 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 - while not routing.IsEnd(index): - node_index = routing.IndexToNode(index) - next_node_index = routing.IndexToNode(assignment.Value(routing.NextVar(index))) - route_dist += data.manhattan_distance(node_index, next_node_index) + @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. + capacity_dimension = self.routing.GetDimensionOrDie('Capacity') + time_dimension = self.routing.GetDimensionOrDie('Time') + total_dist = 0 + total_time = 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 + 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]) + load_var = capacity_dimension.CumulVar(index) + route_load = self.assignment.Value(load_var) + time_var = time_dimension.CumulVar(index) + time_min = self.assignment.Min(time_var) + time_max = self.assignment.Max(time_var) + plan_output += ' {0} Load({1}) Time({2},{3}) ->'.format(node_index, route_load, time_min, time_max) + index = self.assignment.Value(self.routing.NextVar(index)) + + node_index = self.routing.IndexToNode(index) load_var = capacity_dimension.CumulVar(index) + route_load = self.assignment.Value(load_var) 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})\n'.format( - node_index=node_index, - load=assignment.Value(load_var), - tmin=str(assignment.Min(time_var)), - tmax=str(assignment.Max(time_var))) - plan_output += 'Distance of the route {0}: {dist}\n'.format( - vehicle_id, - dist=route_dist) - plan_output += 'Demand met by vehicle {0}: {load}\n'.format( - vehicle_id, - load=assignment.Value(load_var)) - print(plan_output, '\n') + route_time = self.assignment.Value(time_var) + time_min = self.assignment.Min(time_var) + time_max = self.assignment.Max(time_var) + total_dist += route_dist + total_time += route_time + plan_output += ' {0} Load({1}) Time({2},{3})\n'.format(node_index, route_load, time_min, time_max) + plan_output += 'Distance of the route: {0}m\n'.format(route_dist) + plan_output += 'Load of the route: {0}\n'.format(route_load) + plan_output += 'Time of the route: {0}min\n'.format(route_time) + print(plan_output) + print('Total Distance of all routes: {0}m'.format(total_dist)) + print('Total Time of all routes: {0}min'.format(total_time)) +######## +# Main # +######## def main(): """Entry point of the program""" - # Instanciate the data problem. + # Instantiate the data problem. data = DataProblem() - # Create routing model. - # 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. + # Create Routing Model routing = pywrapcp.RoutingModel(data.num_locations, data.num_vehicles, data.depot) - - # Adding the custom distance function. - dist_callback = CreateDistanceCallback(data).distance - routing.SetArcCostEvaluatorOfAllVehicles(dist_callback) - - # Adding a Capacity dimension constraints. - demands_callback = CreateDemandCallback(data).demand - null_capacity_slack = 0 - fix_start_cumul_to_zero = True - capacity = "Capacity" - routing.AddDimension(demands_callback, - null_capacity_slack, - data.vehicle.capacity, - fix_start_cumul_to_zero, - capacity) - - # Adding a Time dimension for time-window constraints. - time_callback = CreateTimeCallback(data).time - time = "Time" - routing.AddDimension(time_callback, - data.horizon, - data.horizon, - fix_start_cumul_to_zero, - time) - - time_dimension = routing.GetDimensionOrDie(time) - for count, time_window in enumerate(data.time_windows): - time_dimension.CumulVar(count).SetRange(time_window[0], time_window[1]) + # 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) + # Add Time Window constraint + time_evaluator = CreateTimeEvaluator(data).time_evaluator + add_time_window_constraints(routing, data, time_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) - - # Display a solution if any. - if assignment: - print_assignment(data, routing, assignment, capacity, time) - else: - print('No solution found.') - sys.exit(2) + printer = ConsolePrinter(data, routing, assignment) + printer.print() if __name__ == '__main__': main() diff --git a/examples/python/transit_time.py b/examples/python/transit_time.py new file mode 100755 index 0000000000..be37dc6391 --- /dev/null +++ b/examples/python/transit_time.py @@ -0,0 +1,215 @@ +#!/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. + +"""Display Transit Time + Distances are in meters and time in minutes. + + 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 + # Travel speed: 5km/h to convert in m/min + self._speed = 5 * 60 / 3.6 + + @property + def speed(self): + """Gets the average travel speed of a vehicle""" + return self._speed + +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() + + # 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 city block dimension + 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, # 1, 2 + 2, 4, # 3, 4 + 2, 4, # 5, 6 + 8, 8, # 7, 8 + 1, 2, # 9,10 + 1, 2, # 11,12 + 4, 4, # 13, 14 + 8, 8] # 15, 16 + + self._time_windows = \ + [(0, 0), + (75, 85), (75, 85), # 1, 2 + (60, 70), (45, 55), # 3, 4 + (0, 8), (50, 60), # 5, 6 + (0, 10), (10, 20), # 7, 8 + (0, 10), (75, 85), # 9, 10 + (85, 95), (5, 15), # 11, 12 + (15, 25), (10, 20), # 13, 14 + (45, 55), (30, 40)] # 15, 16 + + @property + def vehicle(self): + """Gets a vehicle""" + return self._vehicle + + @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 + + @property + def time_per_demand_unit(self): + """Gets the time (in min) to load a demand""" + return 5 # 5 minutes/unit + + @property + def time_windows(self): + """Gets (start time, end time) for each locations""" + return self._time_windows + +####################### +# 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 CreateTimeEvaluator(object): + """Creates callback to get total times between locations.""" + @staticmethod + def service_time(data, node): + """Gets the service time for the specified location.""" + return data.demands[node] * data.time_per_demand_unit + + @staticmethod + def travel_time(data, from_node, to_node): + """Gets the travel times between two locations.""" + if from_node == to_node: + travel_time = 0 + else: + travel_time = manhattan_distance( + data.locations[from_node], + data.locations[to_node]) / data.vehicle.speed + return travel_time + + def __init__(self, data): + """Initializes the total time matrix.""" + self._total_time = {} + # precompute total time to have time callback in O(1) + for from_node in xrange(data.num_locations): + self._total_time[from_node] = {} + for to_node in xrange(data.num_locations): + if from_node == to_node: + self._total_time[from_node][to_node] = 0 + else: + self._total_time[from_node][to_node] = int( + self.service_time(data, from_node) + + self.travel_time(data, from_node, to_node)) + + def time_evaluator(self, from_node, to_node): + """Returns the total time between the two nodes""" + return self._total_time[from_node][to_node] + +def print_transit_time(route, time_evaluator): + """Print transit time between nodes of a route""" + total_time = 0 + for i, j in route: + total_time += time_evaluator(i, j) + print('{0} -> {1}: {2}min'.format(i, j, time_evaluator(i, j))) + print('Total time: {0}min\n'.format(total_time)) + +######## +# Main # +######## +def main(): + """Entry point of the program""" + # Instantiate the data problem. + data = DataProblem() + + # Print Transit Time + time_evaluator = CreateTimeEvaluator(data).time_evaluator + print('Route 0:') + print_transit_time([[0, 5], [5, 8], [8, 6], [6, 2], [2, 0]], time_evaluator) + + print('Route 1:') + print_transit_time([[0, 9], [9, 14], [14, 16], [16, 10], [10, 0]], time_evaluator) + + print('Route 2:') + print_transit_time([[0, 12], [12, 13], [13, 15], [15, 11], [11, 0]], time_evaluator) + + print('Route 3:') + print_transit_time([[0, 7], [7, 4], [4, 3], [3, 1], [1, 0]], time_evaluator) + +if __name__ == '__main__': + main() diff --git a/examples/python/vrp.py b/examples/python/vrp.py new file mode 100755 index 0000000000..7726fece3f --- /dev/null +++ b/examples/python/vrp.py @@ -0,0 +1,199 @@ +#!/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. + +"""Vehicle Routing Problem (VRP). + This is a sample using the routing library python wrapper to solve a VRP 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 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._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 + + @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 + +####################### +# 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] + +########### +# 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 + 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]) + plan_output += ' {0} -> '.format(node_index) + index = self.assignment.Value(self.routing.NextVar(index)) + + node_index = self.routing.IndexToNode(index) + total_dist += route_dist + plan_output += ' {0}\n'.format(node_index) + plan_output += 'Distance of the route: {0}m\n'.format(route_dist) + 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) + + # 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() diff --git a/examples/python/vrpgs.py b/examples/python/vrpgs.py new file mode 100755 index 0000000000..3e49df11be --- /dev/null +++ b/examples/python/vrpgs.py @@ -0,0 +1,214 @@ +#!/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. + +"""Vehicle Routing Problem (VRP). + This is a sample using the routing library python wrapper to solve a VRP 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 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._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 + + @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 + +####################### +# 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] + +def add_distance_dimension(routing, distance_evaluator): + """Add Global Span constraint""" + distance = "Distance" + routing.AddDimension( + distance_evaluator, + 0, # null slack + 3000, # maximum distance per vehicle + True, # start cumul to zero + distance) + distance_dimension = routing.GetDimensionOrDie(distance) + # Try to minimize the max distance among vehicles. + # /!\ It doesn't mean the standard deviation is minimized + distance_dimension.SetGlobalSpanCostCoefficient(100) + +########### +# 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 + 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]) + plan_output += ' {0} -> '.format(node_index) + index = self.assignment.Value(self.routing.NextVar(index)) + + node_index = self.routing.IndexToNode(index) + total_dist += route_dist + plan_output += ' {0}\n'.format(node_index) + plan_output += 'Distance of the route: {0}m\n'.format(route_dist) + 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_distance_dimension(routing, distance_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()