From 2aae5c9eea0467cb0401b80d1d2f7d4898380161 Mon Sep 17 00:00:00 2001 From: torressa <23246013+torressa@users.noreply.github.com> Date: Fri, 28 Apr 2023 17:13:13 +0100 Subject: [PATCH] [#147] Add routes w different vehicle types when possible --- tests/test_issue147.py | 31 +++++++++++++++++++++++++ tests/test_issue99.py | 4 ++-- vrpy/subproblem_cspy.py | 30 +++++++++++++------------ vrpy/vrp.py | 50 +++++++++++++++++++++++++++++++++++------ 4 files changed, 92 insertions(+), 23 deletions(-) create mode 100644 tests/test_issue147.py diff --git a/tests/test_issue147.py b/tests/test_issue147.py new file mode 100644 index 0000000..3d9f4ad --- /dev/null +++ b/tests/test_issue147.py @@ -0,0 +1,31 @@ +from networkx import DiGraph +from vrpy import VehicleRoutingProblem +import networkx as nx + +from networkx import DiGraph +from vrpy import VehicleRoutingProblem + + +class TestIssue147: + def setup(self): + G = DiGraph() + for v in [1, 2, 3, 4]: + G.add_edge("Source", v, cost=[10, 11, 10, 10]) + G.add_edge(v, "Sink", cost=[10, 11, 10, 10]) + G.nodes[v]["demand"] = 10 + G.add_edge(1, 2, cost=[10, 11, 10, 10]) + G.add_edge(2, 3, cost=[10, 11, 10, 10]) + G.add_edge(3, 4, cost=[15, 16, 10, 10]) + + self.prob = VehicleRoutingProblem( + G, mixed_fleet=True, load_capacity=[10, 10, 10, 10], use_all_vehicles=True + ) + + def test(self): + self.prob.solve() + assert self.prob.best_routes_type == {1: 0, 2: 1, 3: 2, 4: 3} + + def test_set_num_vehicles(self): + self.prob.num_vehicles = [1, 1, 0, 2] + self.prob.solve() + assert set(self.prob.best_routes_type.values()) == set([0, 1, 3]) diff --git a/tests/test_issue99.py b/tests/test_issue99.py index 0ee8745..fc7e126 100644 --- a/tests/test_issue99.py +++ b/tests/test_issue99.py @@ -1,4 +1,4 @@ -from networkx import from_numpy_matrix, set_node_attributes, relabel_nodes, DiGraph +from networkx import from_numpy_array, set_node_attributes, relabel_nodes, DiGraph from numpy import array from vrpy import VehicleRoutingProblem @@ -236,7 +236,7 @@ def setup(self): # Transform distance matrix to DiGraph A_ = array(distance_, dtype=[("cost", int)]) - G_ = from_numpy_matrix(A_, create_using=DiGraph()) + G_ = from_numpy_array(A_, create_using=DiGraph()) # Set demand set_node_attributes(G_, values=demands, name="demand") diff --git a/vrpy/subproblem_cspy.py b/vrpy/subproblem_cspy.py index 789f044..a01b48e 100644 --- a/vrpy/subproblem_cspy.py +++ b/vrpy/subproblem_cspy.py @@ -1,10 +1,12 @@ import logging from math import floor -from numpy import zeros -from networkx import DiGraph, add_path +import networkx as nx +from numpy import zeros, unique + from cspy import BiDirectional, REFCallback + from vrpy.subproblem import _SubProblemBase logger = logging.getLogger(__name__) @@ -216,12 +218,12 @@ def solve(self, time_limit): my_callback = self.get_REF() direction = ( "forward" - if ( - self.time_windows - or self.pickup_delivery - or self.distribution_collection - ) - else "both" + # if ( + # self.time_windows + # or self.pickup_delivery + # or self.distribution_collection + # ) + # else "both" ) # Run only twice: Once with `elementary=False` check if route already # exists. @@ -241,7 +243,7 @@ def solve(self, time_limit): else: thr = None logger.debug( - f"Solving subproblem using elementary={elementary}, threshold={thr}, direction={direction}" + f"Solving subproblem using elementary={elementary}, threshold={thr}, direction={direction}, max_res={self.max_res}, min_res={self.min_res}" ) alg = BiDirectional( self.sub_G, @@ -252,6 +254,7 @@ def solve(self, time_limit): time_limit=time_limit - 0.5 if time_limit else None, elementary=elementary, REF_callback=my_callback, + two_cycle_elimination=True # pickup_delivery_pairs=self.pickup_delivery_pairs, ) @@ -268,6 +271,7 @@ def solve(self, time_limit): if alg.total_cost is not None and alg.total_cost < -(1e-3): new_route = self.create_new_route(alg.path) logger.debug(alg.path) + path_len = len(alg.path) if not any( list(new_route.edges()) == list(r.edges()) for r in self.routes @@ -323,8 +327,8 @@ def create_new_route(self, path): """Create new route as DiGraph and add to pool of columns""" e = "elem" if len(set(path)) == len(path) else "non-elem" route_id = "{}_{}".format(len(self.routes) + 1, e) - new_route = DiGraph(name=route_id, path=path) - add_path(new_route, path) + new_route = nx.DiGraph(name=route_id, path=path) + nx.add_path(new_route, path) total_cost = 0 for (i, j) in new_route.edges(): edge_cost = self.sub_G.edges[i, j]["cost"][self.vehicle_type] @@ -376,6 +380,4 @@ def get_REF(self): self.T, self.resources, ) - else: - # Use default - return None + return None diff --git a/vrpy/vrp.py b/vrpy/vrp.py index 11a0690..59f28d5 100644 --- a/vrpy/vrp.py +++ b/vrpy/vrp.py @@ -3,6 +3,7 @@ from time import time from typing import List, Union +import numpy as np from networkx import DiGraph, shortest_path # draw_networkx from vrpy.greedy import _Greedy @@ -950,8 +951,24 @@ def _convert_initial_routes_to_digraphs(self): Converts list of initial routes to list of Digraphs. By default, initial routes are computed with the first feasible vehicle type. """ + + def _check_candidate(k): + if ( + sum(self.G.nodes[v]["demand"] for v in r) <= self.load_capacity[k] + ) and types_used[k] + 1 <= vehicles_available[k]: + G.graph["vehicle_type"] = k + types_used[k] += 1 + return True + return False + self._routes = [] self._routes_with_node = {} + types_used = np.zeros(self._vehicle_types) + + if not self.num_vehicles: + vehicles_available = np.array([len(self.G.nodes) - 2] * self._vehicle_types) + else: + vehicles_available = np.array(self.num_vehicles) for route_id, r in enumerate(self._initial_routes, start=1): total_cost = 0 G = DiGraph(name=route_id) @@ -962,13 +979,32 @@ def _convert_initial_routes_to_digraphs(self): total_cost += edge_cost G.graph["cost"] = total_cost if self.load_capacity: - for k in range(len(self.load_capacity)): - if ( - sum(self.G.nodes[v]["demand"] for v in r) - <= self.load_capacity[k] - ): - G.graph["vehicle_type"] = k - break + if self.mixed_fleet: + # Check vehicle_types but first try the ones that are not + # already assigned to a route. + diff = vehicles_available - types_used + candidates = np.argwhere(diff == np.amax(diff)).flatten().tolist() + vehicle_type_found = False + for k in candidates: + vehicle_type_found = _check_candidate(k) + if vehicle_type_found: + break + # In case this is not enough, now try the rest + if not vehicle_type_found: + candidates = [ + k for k in range(self._vehicle_types) if k not in candidates + ] + for k in candidates: + vehicle_type_found = _check_candidate(k) + if vehicle_type_found: + break + else: + for k in range(self._vehicle_types): + if ( + sum(self.G.nodes[v]["demand"] for v in r) + <= self.load_capacity[k] + ): + G.graph["vehicle_type"] = k else: G.graph["vehicle_type"] = 0 if "vehicle_type" not in G.graph: