From 417bdca46ceb76c2313a1ff7ef9119163011fcd0 Mon Sep 17 00:00:00 2001 From: torressa <23246013+torressa@users.noreply.github.com> Date: Sun, 19 Sep 2021 17:29:21 +0100 Subject: [PATCH 1/9] Add PDP with cspy #15 --- examples/pdp.py | 17 +++++++- tests/test_consistency.py | 10 ++--- tests/test_toy.py | 57 ++++++++++++++++++++------ vrpy/checks.py | 85 ++++++++++++++++++--------------------- vrpy/subproblem_cspy.py | 47 +++++++++++++++++----- 5 files changed, 139 insertions(+), 77 deletions(-) diff --git a/examples/pdp.py b/examples/pdp.py index ef44f2e..e2691bb 100644 --- a/examples/pdp.py +++ b/examples/pdp.py @@ -20,9 +20,22 @@ if __name__ == "__main__": - prob = VehicleRoutingProblem(G, load_capacity=6, pickup_delivery=True, num_stops=6) - prob.solve(cspy=False) + prob = VehicleRoutingProblem(G, + load_capacity=6, + pickup_delivery=True, + num_stops=6) + prob.solve(cspy=True, exact=True, pricing_strategy="Exact") print(prob.best_value) print(prob.best_routes) + for (u, v) in PICKUPS_DELIVERIES: + found = False + for route in prob.best_routes.values(): + if u in route and v in route: + found = True + break + if not found: + print((u, v), "Not present") + assert False + print(prob.node_load) assert prob.best_value == 5980 diff --git a/tests/test_consistency.py b/tests/test_consistency.py index 763e17a..0bbdc08 100644 --- a/tests/test_consistency.py +++ b/tests/test_consistency.py @@ -53,9 +53,6 @@ def test_consistency_parameters(): G = DiGraph() G.add_edge("Source", "Sink", cost=1) prob = VehicleRoutingProblem(G, pickup_delivery=True) - # pickup delivery requires cspy=False - with pytest.raises(NotImplementedError): - prob.solve() # pickup delivery expects at least one request with pytest.raises(KeyError): prob.solve(cspy=False, pricing_strategy="Exact") @@ -88,9 +85,10 @@ def test_mixed_fleet_consistency(): prob.solve() G.edges["Source", "Sink"]["cost"] = [1, 2] with pytest.raises(ValueError): - prob = VehicleRoutingProblem( - G, mixed_fleet=True, load_capacity=[2, 4], fixed_cost=[4] - ) + prob = VehicleRoutingProblem(G, + mixed_fleet=True, + load_capacity=[2, 4], + fixed_cost=[4]) prob.solve() diff --git a/tests/test_toy.py b/tests/test_toy.py index 8e23278..b485e36 100644 --- a/tests/test_toy.py +++ b/tests/test_toy.py @@ -7,6 +7,7 @@ class TestsToy: + def setup(self): """ Creates a toy graph. @@ -57,7 +58,10 @@ def test_cspy_stops_capacity_duration(self): """Tests column generation procedure on toy graph with stop, capacity and duration constraints """ - prob = VehicleRoutingProblem(self.G, num_stops=3, load_capacity=10, duration=62) + prob = VehicleRoutingProblem(self.G, + num_stops=3, + load_capacity=10, + duration=62) prob.solve(exact=False) assert prob.best_value == 85 assert set(prob.best_routes_duration.values()) == {41, 62} @@ -183,9 +187,11 @@ def test_clarke_wright(self): ######### def test_all(self): - prob = VehicleRoutingProblem( - self.G, num_stops=3, time_windows=True, duration=63, load_capacity=10 - ) + prob = VehicleRoutingProblem(self.G, + num_stops=3, + time_windows=True, + duration=63, + load_capacity=10) prob.solve(cspy=False) lp_best = prob.best_value prob.solve(cspy=True) @@ -210,7 +216,9 @@ def test_knapsack(self): def test_pricing_strategies(self): sol = [] - for strategy in ["Exact", "BestPaths", "BestEdges1", "BestEdges2", "Hyper"]: + for strategy in [ + "Exact", "BestPaths", "BestEdges1", "BestEdges2", "Hyper" + ]: prob = VehicleRoutingProblem(self.G, num_stops=4) prob.solve(pricing_strategy=strategy) sol.append(prob.best_value) @@ -246,7 +254,7 @@ def test_extend_preassignment(self): prob.solve(preassignments=routes) assert prob.best_value == 70 - def test_pick_up_delivery(self): + def test_pick_up_delivery_lp(self): self.G.nodes[2]["request"] = 5 self.G.nodes[2]["demand"] = 10 self.G.nodes[3]["demand"] = 10 @@ -263,6 +271,23 @@ def test_pick_up_delivery(self): prob.solve(pricing_strategy="Exact", cspy=False) assert prob.best_value == 65 + def test_pick_up_delivery_cspy(self): + self.G.nodes[2]["request"] = 5 + self.G.nodes[2]["demand"] = 10 + self.G.nodes[3]["demand"] = 10 + self.G.nodes[3]["request"] = 4 + self.G.nodes[4]["demand"] = -10 + self.G.nodes[5]["demand"] = -10 + self.G.add_edge(2, 5, cost=10) + self.G.remove_node(1) + prob = VehicleRoutingProblem( + self.G, + load_capacity=15, + pickup_delivery=True, + ) + prob.solve(pricing_strategy="Exact", cspy=True) + assert prob.best_value == 65 + def test_distribution_collection(self): self.G.nodes[1]["collect"] = 12 self.G.nodes[4]["collect"] = 1 @@ -285,17 +310,20 @@ def test_fixed_cost(self): assert set(prob.best_routes_cost.values()) == {30 + 100, 40 + 100} def test_drop_nodes(self): - prob = VehicleRoutingProblem( - self.G, num_stops=3, num_vehicles=1, drop_penalty=100 - ) + prob = VehicleRoutingProblem(self.G, + num_stops=3, + num_vehicles=1, + drop_penalty=100) prob.solve() assert prob.best_value == 240 assert prob.best_routes == {1: ["Source", 1, 2, 3, "Sink"]} def test_num_vehicles_use_all(self): - prob = VehicleRoutingProblem( - self.G, num_stops=3, num_vehicles=2, use_all_vehicles=True, drop_penalty=100 - ) + prob = VehicleRoutingProblem(self.G, + num_stops=3, + num_vehicles=2, + use_all_vehicles=True, + drop_penalty=100) prob.solve() assert len(prob.best_routes) == 2 prob.num_vehicles = 3 @@ -316,7 +344,10 @@ def test_periodic(self): frequency += 1 assert frequency == 2 assert prob.schedule[0] in [[1], [1, 2]] - prob = VehicleRoutingProblem(self.G, num_stops=2, periodic=2, num_vehicles=1) + prob = VehicleRoutingProblem(self.G, + num_stops=2, + periodic=2, + num_vehicles=1) prob.solve() assert prob.schedule == {} diff --git a/vrpy/checks.py b/vrpy/checks.py index e4fbf6d..e904d5f 100644 --- a/vrpy/checks.py +++ b/vrpy/checks.py @@ -33,16 +33,16 @@ def check_arguments( raise TypeError("Maximum duration must be positive integer.") strategies = ["Exact", "BestEdges1", "BestEdges2", "BestPaths", "Hyper"] if pricing_strategy not in strategies: - raise ValueError( - "Pricing strategy %s is not valid. Pick one among %s" - % (pricing_strategy, strategies) - ) + raise ValueError("Pricing strategy %s is not valid. Pick one among %s" % + (pricing_strategy, strategies)) if mixed_fleet: - if load_capacity and num_vehicles and len(load_capacity) != len(num_vehicles): + if load_capacity and num_vehicles and len(load_capacity) != len( + num_vehicles): raise ValueError( "Input arguments load_capacity and num_vehicles must have same dimension." ) - if load_capacity and fixed_cost and len(load_capacity) != len(fixed_cost): + if load_capacity and fixed_cost and len(load_capacity) != len( + fixed_cost): raise ValueError( "Input arguments load_capacity and fixed_cost must have same dimension." ) @@ -53,28 +53,21 @@ def check_arguments( for (i, j) in G.edges(): if not isinstance(G.edges[i, j]["cost"], list): raise TypeError( - "Cost attribute for edge (%s,%s) should be of type list" - ) + "Cost attribute for edge (%s,%s) should be of type list") if len(G.edges[i, j]["cost"]) != vehicle_types: raise ValueError( "Cost attribute for edge (%s,%s) has dimension %s, should have dimension %s." - % (i, j, len(G.edges[i, j]["cost"]), vehicle_types) - ) + % (i, j, len(G.edges[i, j]["cost"]), vehicle_types)) if use_all_vehicles: if not num_vehicles: logger.warning("num_vehicles = None, use_all_vehicles ignored") -def check_clarke_wright_compatibility( - time_windows, pickup_delivery, distribution_collection, mixed_fleet, periodic -): - if ( - time_windows - or pickup_delivery - or distribution_collection - or mixed_fleet - or periodic - ): +def check_clarke_wright_compatibility(time_windows, pickup_delivery, + distribution_collection, mixed_fleet, + periodic): + if (time_windows or pickup_delivery or distribution_collection or + mixed_fleet or periodic): raise ValueError( "Clarke & Wright heuristic not compatible with time windows, pickup and delivery, simultaneous distribution and collection, mixed fleet, frequencies." ) @@ -85,7 +78,8 @@ def check_vrp(G: DiGraph = None): # if G is not a DiGraph if not isinstance(G, DiGraph): - raise TypeError("Input graph must be of type networkx.classes.digraph.DiGraph.") + raise TypeError( + "Input graph must be of type networkx.classes.digraph.DiGraph.") for v in ["Source", "Sink"]: # If Source or Sink is missing if v not in G.nodes(): @@ -126,7 +120,8 @@ def check_initial_routes(initial_routes: list = None, G: DiGraph = None): for route in initial_routes: if route[0] != "Source" or route[-1] != "Sink": - raise ValueError("Route %s must start at Source and end at Sink" % route) + raise ValueError("Route %s must start at Source and end at Sink" % + route) # Check if every node is in at least one route for v in G.nodes(): if v not in ["Source", "Sink"]: @@ -141,9 +136,8 @@ def check_initial_routes(initial_routes: list = None, G: DiGraph = None): edges = list(zip(route[:-1], route[1:])) for (i, j) in edges: if (i, j) not in G.edges(): - raise KeyError( - "Edge (%s,%s) in route %s missing in graph." % (i, j, route) - ) + raise KeyError("Edge (%s,%s) in route %s missing in graph." % + (i, j, route)) if "cost" not in G.edges[i, j]: raise KeyError("Edge (%s,%s) has no cost attribute." % (i, j)) @@ -161,8 +155,8 @@ def check_preassignments(routes: list = None, G: DiGraph = None): for (i, j) in edges: if (i, j) not in G.edges(): raise ValueError( - "Edge (%s,%s) in locked route %s is not in graph G." % (i, j, route) - ) + "Edge (%s,%s) in locked route %s is not in graph G." % + (i, j, route)) def check_consistency( @@ -175,7 +169,8 @@ def check_consistency( # pickup delivery requires cspy=False if cspy and pickup_delivery: - raise NotImplementedError("pickup_delivery option requires cspy=False.") + pass + # raise NotImplementedError("pickup_delivery option requires cspy=False.") # pickup delivery requires pricing_stragy="Exact" if pickup_delivery and pricing_strategy != "Exact": pricing_strategy = "Exact" @@ -184,35 +179,36 @@ def check_consistency( if pickup_delivery: request = any("request" in G.nodes[v] for v in G.nodes()) if not request: - raise KeyError("pickup_delivery option expects at least one request.") + raise KeyError( + "pickup_delivery option expects at least one request.") -def check_feasibility( - load_capacity: list = None, G: DiGraph = None, duration: int = None -): +def check_feasibility(load_capacity: list = None, + G: DiGraph = None, + duration: int = None): """Checks basic problem feasibility.""" if load_capacity: for v in G.nodes(): if G.nodes[v]["demand"] > max(load_capacity): raise ValueError( - "Demand %s at node %s larger than max capacity %s." - % (G.nodes[v]["demand"], v, max(load_capacity)) - ) + "Demand %s at node %s larger than max capacity %s." % + (G.nodes[v]["demand"], v, max(load_capacity))) if duration: for v in G.nodes(): if v not in ["Source", "Sink"]: - shortest_path_to = shortest_path_length( - G, source="Source", target=v, weight="time" - ) - shortest_path_from = shortest_path_length( - G, source=v, target="Sink", weight="time" - ) + shortest_path_to = shortest_path_length(G, + source="Source", + target=v, + weight="time") + shortest_path_from = shortest_path_length(G, + source=v, + target="Sink", + weight="time") shortest_trip = shortest_path_from + shortest_path_to if shortest_trip > duration: raise ValueError( - "Node %s not reachable with duration constraints" % v - ) + "Node %s not reachable with duration constraints" % v) def check_seed(seed): @@ -234,8 +230,7 @@ def check_pickup_delivery_time_windows(G: DiGraph, edges: list): if (i, j) not in G.edges(): raise ValueError( "Problem Infeasible, request (%s,%s) cannot be done with given time windows." - % (i, j) - ) + % (i, j)) def check_periodic_num_vehicles(periodic=None, num_vehicles=[]): diff --git a/vrpy/subproblem_cspy.py b/vrpy/subproblem_cspy.py index 31c1ef7..a43d930 100644 --- a/vrpy/subproblem_cspy.py +++ b/vrpy/subproblem_cspy.py @@ -3,6 +3,7 @@ from numpy import zeros from networkx import DiGraph, add_path from cspy import BiDirectional, REFCallback + from vrpy.subproblem import _SubProblemBase logger = logging.getLogger(__name__) @@ -14,13 +15,14 @@ class _MyREFCallback(REFCallback): Based on Righini and Salani (2006). """ - def __init__(self, max_res, time_windows, distribution_collection, T, - resources): + def __init__(self, max_res, time_windows, distribution_collection, + pickup_delivery, T, resources): REFCallback.__init__(self) # Set attributes for use in REF functions self._max_res = max_res self._time_windows = time_windows self._distribution_collection = distribution_collection + self._pickup_delivery = pickup_delivery self._T = T self._resources = resources # Set later @@ -74,6 +76,27 @@ def REF_fwd(self, cumul_res, tail, head, edge_res, partial_path, # Delivery new_res[5] = max(new_res[5] + self._sub_G.nodes[j]["demand"], new_res[4]) + + _partial_path = list(partial_path) + if self._pickup_delivery: + open_requests = [ + n for n in _partial_path + if "request" in self._sub_G.nodes[n] and + self._sub_G.nodes[n]["request"] not in _partial_path + ] + if len(open_requests) > 0: + pickup_node = [ + n for n in self._sub_G.nodes() + if "request" in self._sub_G.nodes[n] and + self._sub_G.nodes[n]["request"] == j + ] + if len(pickup_node) == 1: + pickup_node = pickup_node[0] + if ("request" not in self._sub_G.nodes[j] and + pickup_node not in open_requests): + new_res[6] = 1.0 + else: + new_res[6] = 0.0 return new_res def REF_bwd(self, cumul_res, tail, head, edge_res, partial_path, @@ -179,12 +202,8 @@ def __init__(self, *args, exact): self.exact = exact # Resource names self.resources = [ - "stops/mono", - "load", - "time", - "time windows", - "collect", - "deliver", + "stops/mono", "load", "time", "time windows", "collect", "deliver", + "pickup_delivery" ] # Set number of resources as attribute of graph self.sub_G.graph["n_res"] = len(self.resources) @@ -201,6 +220,7 @@ def __init__(self, *args, exact): 1, # time windows total_demand, # pickup total_demand, # deliver + 1, # pickup_delivery ] # Initialize cspy edge attributes for edge in self.sub_G.edges(data=True): @@ -237,7 +257,7 @@ def solve(self, time_limit): self.sub_G, self.max_res, self.min_res, - direction="both", + direction="forward", time_limit=time_limit - 0.5 if time_limit else None, elementary=True, REF_callback=my_callback, @@ -248,7 +268,7 @@ def solve(self, time_limit): self.max_res, self.min_res, threshold=-1, - direction="both", + direction="forward", time_limit=time_limit - 0.5 if time_limit else None, elementary=True, REF_callback=my_callback, @@ -309,6 +329,9 @@ def formulate(self): if self.load_capacity and self.distribution_collection: self.max_res[4] = self.load_capacity[self.vehicle_type] self.max_res[5] = self.load_capacity[self.vehicle_type] + if self.pickup_delivery: + # Time windows feasibility + self.max_res[6] = 0 def add_new_route(self, path): """Create new route as DiGraph and add to pool of columns""" @@ -357,12 +380,14 @@ def add_max_duration(self): self.sub_G.edges[i, j]["res_cost"][2] = travel_time def get_REF(self): - if self.time_windows or self.distribution_collection: + if (self.time_windows or self.distribution_collection or + self.pickup_delivery): # Use custom REF return _MyREFCallback( self.max_res, self.time_windows, self.distribution_collection, + self.pickup_delivery, self.T, self.resources, ) From 731a752f0ee5642c235696518f9f87efc0fe0187 Mon Sep 17 00:00:00 2001 From: torressa <23246013+torressa@users.noreply.github.com> Date: Sun, 3 Oct 2021 19:24:54 +0100 Subject: [PATCH 2/9] Clean up pick and delivery REF #15, add test #101 --- tests/test_issue79.py | 16 ++- vrpy/subproblem_cspy.py | 81 ++++-------- vrpy/vrp.py | 285 +++++++++++++++++++--------------------- 3 files changed, 176 insertions(+), 206 deletions(-) diff --git a/tests/test_issue79.py b/tests/test_issue79.py index ad661af..bcbbd58 100644 --- a/tests/test_issue79.py +++ b/tests/test_issue79.py @@ -3,6 +3,7 @@ class TestIssue79: + def setup(self): G = DiGraph() G.add_edge("Source", 8, cost=0) @@ -24,13 +25,20 @@ def setup(self): G.nodes[6]["collect"] = 1 G.nodes[2]["collect"] = 1 G.nodes[5]["collect"] = 2 - self.prob = VehicleRoutingProblem( - G, load_capacity=15, distribution_collection=True - ) + self.prob = VehicleRoutingProblem(G, + load_capacity=15, + distribution_collection=True) - def test_node_load(self): + def test_node_load_cspy(self): self.prob.solve() assert self.prob.node_load[1][8] == 8 assert self.prob.node_load[1][6] == 5 assert self.prob.node_load[1][2] == 5 assert self.prob.node_load[1][5] == 5 + + def test_node_load_lp(self): + self.prob.solve(cspy=False) + assert self.prob.node_load[1][8] == 8 + assert self.prob.node_load[1][6] == 5 + assert self.prob.node_load[1][2] == 5 + assert self.prob.node_load[1][5] == 5 diff --git a/vrpy/subproblem_cspy.py b/vrpy/subproblem_cspy.py index a43d930..e8e96a8 100644 --- a/vrpy/subproblem_cspy.py +++ b/vrpy/subproblem_cspy.py @@ -16,7 +16,7 @@ class _MyREFCallback(REFCallback): """ def __init__(self, max_res, time_windows, distribution_collection, - pickup_delivery, T, resources): + pickup_delivery, T, resources, pickup_delivery_pairs): REFCallback.__init__(self) # Set attributes for use in REF functions self._max_res = max_res @@ -25,10 +25,12 @@ def __init__(self, max_res, time_windows, distribution_collection, self._pickup_delivery = pickup_delivery self._T = T self._resources = resources + self._pickup_delivery_pairs = pickup_delivery_pairs # Set later self._sub_G = None self._source_id = None self._sink_id = None + self._matched_delivery_to_pickup_nodes = {} def REF_fwd(self, cumul_res, tail, head, edge_res, partial_path, cumul_cost): @@ -38,34 +40,22 @@ def REF_fwd(self, cumul_res, tail, head, edge_res, partial_path, new_res[0] += 1 # load new_res[1] += self._sub_G.nodes[j]["demand"] + # time # Service times theta_i = self._sub_G.nodes[i]["service_time"] - # theta_j = self._sub_G.nodes[j]["service_time"] - # theta_t = self._sub_G.nodes[self._sink_id]["service_time"] # Travel times travel_time_ij = self._sub_G.edges[i, j]["time"] - # try: - # travel_time_jt = self._sub_G.edges[j, self._sink_id]["time"] - # except KeyError: - # travel_time_jt = 0 # Time windows # Lower a_j = self._sub_G.nodes[j]["lower"] - # a_t = self._sub_G.nodes[self._sink_id]["lower"] # Upper b_j = self._sub_G.nodes[j]["upper"] - # b_t = self._sub_G.nodes[self._sink_id]["upper"] new_res[2] = max(new_res[2] + theta_i + travel_time_ij, a_j) # time-window feasibility resource if not self._time_windows or (new_res[2] <= b_j): - # and new_res[2] < self._T - a_j - theta_j and - # a_t <= new_res[2] + travel_time_jt + theta_t <= b_t): - # if not self._time_windows or ( - # new_res[2] <= b_j and new_res[2] < self._T - a_j - theta_j and - # a_t <= new_res[2] + travel_time_jt + theta_t <= b_t): new_res[3] = 0 else: new_res[3] = 1 @@ -85,18 +75,19 @@ def REF_fwd(self, cumul_res, tail, head, edge_res, partial_path, self._sub_G.nodes[n]["request"] not in _partial_path ] if len(open_requests) > 0: - pickup_node = [ - n for n in self._sub_G.nodes() - if "request" in self._sub_G.nodes[n] and - self._sub_G.nodes[n]["request"] == j + pickup_node = None + pickup_nodes = [ + u for (u, v) in self._pickup_delivery_pairs if v == j ] - if len(pickup_node) == 1: - pickup_node = pickup_node[0] - if ("request" not in self._sub_G.nodes[j] and - pickup_node not in open_requests): - new_res[6] = 1.0 - else: - new_res[6] = 0.0 + if len(pickup_nodes) == 1: + pickup_node = pickup_nodes[0] + if (pickup_node is not None and + pickup_node not in open_requests): + new_res[6] = 1.0 + else: + new_res[6] = 0.0 + elif len(open_requests) != 0 and j == self._sink_id: + new_res[6] = 1.0 return new_res def REF_bwd(self, cumul_res, tail, head, edge_res, partial_path, @@ -108,37 +99,24 @@ def REF_bwd(self, cumul_res, tail, head, edge_res, partial_path, new_res[0] -= 1 # load new_res[1] += self._sub_G.nodes[i]["demand"] + # Get relevant service times (thetas) and travel time # Service times theta_i = self._sub_G.nodes[i]["service_time"] theta_j = self._sub_G.nodes[j]["service_time"] - # theta_s = self._sub_G.nodes[self._source_id]["service_time"] # Travel times travel_time_ij = self._sub_G.edges[i, j]["time"] - # try: - # travel_time_si = self._sub_G.edges[self._source_id, i]["time"] - # except KeyError: - # travel_time_si = 0 # Lower time windows - # a_i = self._sub_G.nodes[i]["lower"] a_j = self._sub_G.nodes[j]["lower"] - # a_s = self._sub_G.nodes[self._source_id]["lower"] # Upper time windows b_i = self._sub_G.nodes[i]["upper"] b_j = self._sub_G.nodes[j]["upper"] - # b_s = self._sub_G.nodes[self._source_id]["upper"] new_res[2] = max(new_res[2] + theta_j + travel_time_ij, self._T - b_i - theta_i) # time-window feasibility if not self._time_windows or (new_res[2] <= self._T - a_j - theta_j): - # and new_res[2] < self._T - a_i - theta_i and - # a_s <= new_res[2] + theta_s + travel_time_si <= b_s): - # if not self._time_windows or ( - # new_res[2] <= self._T - a_j and - # new_res[2] < self._T - a_i - theta_i and - # a_s <= new_res[2] + theta_s + travel_time_si <= b_s): new_res[3] = 0 else: new_res[3] = 1 @@ -195,11 +173,12 @@ class _SubProblemCSPY(_SubProblemBase): Inherits problem parameters from `SubproblemBase` """ - def __init__(self, *args, exact): + def __init__(self, *args, exact, pickup_delivery_pairs): """Initializes resources.""" # Pass arguments to base super(_SubProblemCSPY, self).__init__(*args) self.exact = exact + self.pickup_delivery_pairs = pickup_delivery_pairs # Resource names self.resources = [ "stops/mono", "load", "time", "time windows", "collect", "deliver", @@ -251,28 +230,29 @@ def solve(self, time_limit): more_routes = False my_callback = self.get_REF() + direction = "forward" if self.time_windows or self.pickup_delivery else "both" while True: if self.exact: alg = BiDirectional( self.sub_G, self.max_res, self.min_res, - direction="forward", + direction=direction, time_limit=time_limit - 0.5 if time_limit else None, elementary=True, REF_callback=my_callback, - ) + pickup_delivery_pairs=self.pickup_delivery_pairs) else: alg = BiDirectional( self.sub_G, self.max_res, self.min_res, threshold=-1, - direction="forward", + direction=direction, time_limit=time_limit - 0.5 if time_limit else None, elementary=True, REF_callback=my_callback, - ) + pickup_delivery_pairs=self.pickup_delivery_pairs) # Pass processed graph if my_callback is not None: @@ -330,7 +310,6 @@ def formulate(self): self.max_res[4] = self.load_capacity[self.vehicle_type] self.max_res[5] = self.load_capacity[self.vehicle_type] if self.pickup_delivery: - # Time windows feasibility self.max_res[6] = 0 def add_new_route(self, path): @@ -383,14 +362,10 @@ def get_REF(self): if (self.time_windows or self.distribution_collection or self.pickup_delivery): # Use custom REF - return _MyREFCallback( - self.max_res, - self.time_windows, - self.distribution_collection, - self.pickup_delivery, - self.T, - self.resources, - ) + return _MyREFCallback(self.max_res, self.time_windows, + self.distribution_collection, + self.pickup_delivery, self.T, self.resources, + self.pickup_delivery_pairs) else: # Use default return None diff --git a/vrpy/vrp.py b/vrpy/vrp.py index 2d27e39..f425f37 100644 --- a/vrpy/vrp.py +++ b/vrpy/vrp.py @@ -139,6 +139,7 @@ def __init__( self._initial_routes = [] self._preassignments = [] self._dropped_nodes = [] + self._pickup_delivery_pairs = [] # Parameters for final solution self._best_value = None self._best_routes = [] @@ -234,6 +235,12 @@ def solve( if self._pricing_strategy == "Hyper": self.hyper_heuristic = _HyperHeuristic() + # Extract pairs of (pickup, delivery) nodes from graph + if self.pickup_delivery: + self._pickup_delivery_pairs = [(i, self.G.nodes[i]["request"]) + for i in self.G.nodes() + if "request" in self.G.nodes[i]] + self._start_time = time() if preassignments: self._preassignments = preassignments @@ -281,25 +288,23 @@ def best_routes_cost(self): """Returns dict with route ids as keys and route costs as values.""" cost = {} for route in self.best_routes: - edges = list(zip(self.best_routes[route][:-1], self.best_routes[route][1:])) + edges = list( + zip(self.best_routes[route][:-1], self.best_routes[route][1:])) k = self._best_routes_vehicle_type[route] - cost[route] = sum(self._H.edges[i, j]["cost"][k] for (i, j) in edges) + cost[route] = sum( + self._H.edges[i, j]["cost"][k] for (i, j) in edges) return cost @property def best_routes_load(self): """Returns dict with route ids as keys and route loads as values.""" load = {} - if ( - not self.load_capacity - or self.distribution_collection - or self.pickup_delivery - ): + if (not self.load_capacity or self.distribution_collection or + self.pickup_delivery): return load for route in self.best_routes: load[route] = sum( - self._H.nodes[v]["demand"] for v in self.best_routes[route] - ) + self._H.nodes[v]["demand"] for v in self.best_routes[route]) return load @property @@ -312,11 +317,8 @@ def node_load(self): If simultaneous distribution and collection, load refers to truck load when leaving the node. """ load = {} - if ( - not self.load_capacity - and not self.pickup_delivery - and not self.distribution_collection - ): + if (not self.load_capacity and not self.pickup_delivery and + not self.distribution_collection): return load for i in self.best_routes: load[i] = {} @@ -328,9 +330,8 @@ def node_load(self): amount += self._H.nodes[v]["demand"] if self.distribution_collection: # in this case, load = Q + collect - demand - amount += ( - self._H.nodes[v]["collect"] - 2 * self._H.nodes[v]["demand"] - ) + amount += (self._H.nodes[v]["collect"] - + 2 * self._H.nodes[v]["demand"]) load[i][v] = amount return load @@ -341,13 +342,14 @@ def best_routes_duration(self): if not self.duration and not self.time_windows: return duration for route in self.best_routes: - edges = list(zip(self.best_routes[route][:-1], self.best_routes[route][1:])) + edges = list( + zip(self.best_routes[route][:-1], self.best_routes[route][1:])) # Travel times - duration[route] = sum(self._H.edges[i, j]["time"] for (i, j) in edges) + duration[route] = sum( + self._H.edges[i, j]["time"] for (i, j) in edges) # Service times - duration[route] += sum( - self._H.nodes[v]["service_time"] for v in self.best_routes[route] - ) + duration[route] += sum(self._H.nodes[v]["service_time"] + for v in self.best_routes[route]) return duration @@ -367,17 +369,19 @@ def arrival_time(self): for j in range(1, len(route)): tail = route[j - 1] head = route[j] - arrival[i][head] = min( - max( - arrival[i][tail] - + self._H.nodes[tail]["service_time"] - + self._H.edges[tail, head]["time"], - self._H.nodes[head]["lower"], - ), - self._H.nodes[head]["upper"], - ) + arrival[i][head] = max( + arrival[i][tail] + self._H.nodes[tail]["service_time"] + + self._H.edges[tail, head]["time"], + self._H.nodes[head]["lower"]) return arrival + def check_arrival_time(self): + # Check arrival times + for k1, v1 in self.arrival_time.items(): + for k2, v2 in v1.items(): + assert (self.G.nodes[k2]["lower"] <= v2) + assert (v2 <= self.G.nodes[k2]["upper"]) + @property def departure_time(self): """ @@ -401,6 +405,13 @@ def departure_time(self): ) return departure + def check_departure_time(self): + # Check departure times + for k1, v1 in self.departure_time.items(): + for k2, v2 in v1.items(): + assert (self.G.nodes[k2]["lower"] <= v2) + assert (v2 <= self.G.nodes[k2]["upper"]) + @property def schedule(self): """If Periodic CVRP, returns a dict with keys a day number and values @@ -442,12 +453,11 @@ def _pre_solve(self): G=self.G, ) # Check feasibility - check_feasibility( - load_capacity=self.load_capacity, G=self.G, duration=self.duration - ) + check_feasibility(load_capacity=self.load_capacity, + G=self.G, + duration=self.duration) self.num_vehicles, self._num_vehicles_schedule = check_periodic_num_vehicles( - periodic=self.periodic, num_vehicles=self.num_vehicles - ) + periodic=self.periodic, num_vehicles=self.num_vehicles) # Lock preassigned routes if self._preassignments: check_preassignments(self._preassignments, self.G) @@ -456,9 +466,9 @@ def _pre_solve(self): self._prune_graph() # Compute upper bound on number of stops as knapsack problem if self.load_capacity and not self.pickup_delivery: - num_stops = get_num_stops_upper_bound( - self.G, self._max_capacity, self.num_stops, self.distribution_collection - ) + num_stops = get_num_stops_upper_bound(self.G, self._max_capacity, + self.num_stops, + self.distribution_collection) self.num_stops = num_stops logger.info("new upper bound : max num stops = %s" % num_stops) # Set empty route cost if use_all_vehicles = True @@ -502,8 +512,7 @@ def _solve(self, dive, solver): elif len(self.G.nodes()) > 2: # Solve as MIP _, _ = self.masterproblem.solve( - relax=False, time_limit=self._get_time_remaining(mip=True) - ) + relax=False, time_limit=self._get_time_remaining(mip=True)) ( self._best_value, self._best_routes_as_graphs, @@ -515,16 +524,13 @@ def _column_generation(self): # Generate good columns self._find_columns() # Stop if time limit is passed - if ( - isinstance(self._get_time_remaining(), float) - and self._get_time_remaining() == 0.0 - ): + if (isinstance(self._get_time_remaining(), float) and + self._get_time_remaining() == 0.0): logger.info("time up !") break # Stop if no improvement limit is passed or max iter exceeded if self._no_improvement > 1000 or ( - self._max_iter and self._iteration >= self._max_iter - ): + self._max_iter and self._iteration >= self._max_iter): break def _find_columns(self): @@ -532,46 +538,38 @@ def _find_columns(self): # Solve restricted relaxed master problem if self._dive: duals, relaxed_cost = self.masterproblem.solve_and_dive( - time_limit=self._get_time_remaining() - ) + time_limit=self._get_time_remaining()) if self.hyper_heuristic: self.hyper_heuristic.init(relaxed_cost) else: duals, relaxed_cost = self.masterproblem.solve( - relax=True, time_limit=self._get_time_remaining() - ) + relax=True, time_limit=self._get_time_remaining()) logger.info("iteration %s, %.6s" % (self._iteration, relaxed_cost)) pricing_strategy = self._get_next_pricing_strategy(relaxed_cost) # One subproblem per vehicle type for vehicle in range(self._vehicle_types): # Solve pricing problem with randomised greedy algorithm - if ( - self._greedy - and not self.time_windows - and not self.distribution_collection - and not self.pickup_delivery - and not self.minimize_global_span - ): + if (self._greedy and not self.time_windows and + not self.distribution_collection and + not self.pickup_delivery and not self.minimize_global_span): subproblem = self._def_subproblem(duals, vehicle, greedy=True) self.routes, self._more_routes = subproblem.solve(n_runs=20) # Add initial_routes if self._more_routes: - for r in ( - r - for r in self.routes - if r.graph["name"] not in self.masterproblem.y - ): + for r in (r for r in self.routes + if r.graph["name"] not in self.masterproblem.y): self.masterproblem.update(r) # Continue searching for columns self._more_routes = False if not self.minimize_global_span: self._more_routes = self._solve_subproblem_with_heuristic( - pricing_strategy=pricing_strategy, vehicle=vehicle, duals=duals - ) + pricing_strategy=pricing_strategy, + vehicle=vehicle, + duals=duals) else: - for route in self._routes[: len(self._routes)]: + for route in self._routes[:len(self._routes)]: self._more_routes = self._solve_subproblem_with_heuristic( pricing_strategy=pricing_strategy, vehicle=vehicle, @@ -608,39 +606,39 @@ def _solve_subproblem_with_heuristic( more_columns = False if self._pricing_strategy == "Hyper": if pricing_strategy == "BestPaths": - more_columns = self._attempt_solve_best_paths( - vehicle=vehicle, duals=duals, route=route - ) + more_columns = self._attempt_solve_best_paths(vehicle=vehicle, + duals=duals, + route=route) elif pricing_strategy == "BestEdges1": - more_columns = self._attempt_solve_best_edges1( - vehicle=vehicle, duals=duals, route=route - ) + more_columns = self._attempt_solve_best_edges1(vehicle=vehicle, + duals=duals, + route=route) elif pricing_strategy == "BestEdges2": - more_columns = self._attempt_solve_best_edges2( - vehicle=vehicle, duals=duals, route=route - ) + more_columns = self._attempt_solve_best_edges2(vehicle=vehicle, + duals=duals, + route=route) elif pricing_strategy == "Exact": - more_columns = self._attempt_solve_exact( - vehicle=vehicle, duals=duals, route=route - ) + more_columns = self._attempt_solve_exact(vehicle=vehicle, + duals=duals, + route=route) # old approach else: if pricing_strategy == "BestPaths": - more_columns = self._attempt_solve_best_paths( - vehicle=vehicle, duals=duals, route=route - ) + more_columns = self._attempt_solve_best_paths(vehicle=vehicle, + duals=duals, + route=route) elif pricing_strategy == "BestEdges1": - more_columns = self._attempt_solve_best_edges1( - vehicle=vehicle, duals=duals, route=route - ) + more_columns = self._attempt_solve_best_edges1(vehicle=vehicle, + duals=duals, + route=route) elif pricing_strategy == "BestEdges2": - more_columns = self._attempt_solve_best_edges2( - vehicle=vehicle, duals=duals, route=route - ) + more_columns = self._attempt_solve_best_edges2(vehicle=vehicle, + duals=duals, + route=route) if pricing_strategy == "Exact" or not more_columns: - more_columns = self._attempt_solve_exact( - vehicle=vehicle, duals=duals, route=route - ) + more_columns = self._attempt_solve_exact(vehicle=vehicle, + duals=duals, + route=route) return more_columns def _attempt_solve_best_paths(self, vehicle=None, duals=None, route=None): @@ -654,8 +652,7 @@ def _attempt_solve_best_paths(self, vehicle=None, duals=None, route=None): k_shortest_paths, ) self.routes, self._more_routes = subproblem.solve( - self._get_time_remaining() - ) + self._get_time_remaining()) more_columns = self._more_routes if more_columns: break @@ -674,8 +671,7 @@ def _attempt_solve_best_edges1(self, vehicle=None, duals=None, route=None): alpha, ) self.routes, self._more_routes = subproblem.solve( - self._get_time_remaining(), - ) + self._get_time_remaining(),) more_columns = self._more_routes if more_columns: break @@ -707,16 +703,15 @@ def _attempt_solve_best_edges2(self, vehicle=None, duals=None, route=None): def _attempt_solve_exact(self, vehicle=None, duals=None, route=None): subproblem = self._def_subproblem(duals, vehicle, route) - self.routes, self._more_routes = subproblem.solve(self._get_time_remaining()) + self.routes, self._more_routes = subproblem.solve( + self._get_time_remaining()) return self._more_routes def _get_next_pricing_strategy(self, relaxed_cost): "Return the appropriate pricing strategy based on input parameters" pricing_strategy = None - if ( - self._pricing_strategy == "Hyper" - and self._no_improvement != self._run_exact - ): + if (self._pricing_strategy == "Hyper" and + self._no_improvement != self._run_exact): self._no_improvement_iteration = self._iteration if self._iteration == 0: pricing_strategy = "BestPaths" @@ -733,16 +728,17 @@ def _get_next_pricing_strategy(self, relaxed_cost): return pricing_strategy def _update_hyper_heuristic(self, relaxed_cost: float): - best_paths, best_paths_freq = self.masterproblem.get_heuristic_distribution() + best_paths, best_paths_freq = self.masterproblem.get_heuristic_distribution( + ) self.hyper_heuristic.current_performance( new_objective_value=relaxed_cost, produced_column=self._more_routes, active_columns=best_paths_freq, ) self.hyper_heuristic.move_acceptance() - self.hyper_heuristic.update_parameters( - self._iteration, self._no_improvement, self._no_improvement_iteration - ) + self.hyper_heuristic.update_parameters(self._iteration, + self._no_improvement, + self._no_improvement_iteration) def _get_time_remaining(self, mip: bool = False): """ @@ -809,6 +805,7 @@ def _def_subproblem( pricing_strategy, pricing_parameter, exact=self._exact, + pickup_delivery_pairs=self._pickup_delivery_pairs, ) else: # As LP @@ -848,14 +845,12 @@ def _solve_with_clarke_wright(self): ) alg.run() self._best_routes = dict( - zip([r for r in range(len(alg.best_routes))], alg.best_routes) - ) + zip([r for r in range(len(alg.best_routes))], alg.best_routes)) self._best_routes_vehicle_type = dict( zip( [r for r in range(len(alg.best_routes))], [0 for r in range(len(alg.best_routes))], - ) - ) + )) # Preprocessing # @@ -867,13 +862,9 @@ def _get_initial_solution(self): """ self._initial_routes = [] # Run Clarke & Wright if possible - if ( - not self.time_windows - and not self.pickup_delivery - and not self.distribution_collection - and not self.mixed_fleet - and not self.periodic - ): + if (not self.time_windows and not self.pickup_delivery and + not self.distribution_collection and not self.mixed_fleet and + not self.periodic): best_value = 1e10 best_num_vehicles = 1e10 for alpha in [x / 10 for x in range(1, 20)]: @@ -894,17 +885,15 @@ def _get_initial_solution(self): best_value = alg.best_value best_num_vehicles = len(alg.best_routes) logger.info( - "Clarke & Wright solution found with value %s and %s vehicles" - % (best_value, best_num_vehicles) - ) + "Clarke & Wright solution found with value %s and %s vehicles" % + (best_value, best_num_vehicles)) # Run greedy algorithm if possible - alg = _Greedy(self.G, self.load_capacity, self.num_stops, self.duration) + alg = _Greedy(self.G, self.load_capacity, self.num_stops, + self.duration) alg.run() - logger.info( - "Greedy solution found with value %s and %s vehicles" - % (alg.best_value, len(alg.best_routes)) - ) + logger.info("Greedy solution found with value %s and %s vehicles" % + (alg.best_value, len(alg.best_routes))) self._initial_routes += alg.best_routes # TO DO: Run heuristics from VeRyPy for the CVRP @@ -914,7 +903,9 @@ def _get_initial_solution(self): elif self.pickup_delivery: for v in self.G.nodes(): if "request" in self.G.nodes[v]: - obvious_route = ["Source", v, self.G.nodes[v]["request"], "Sink"] + obvious_route = [ + "Source", v, self.G.nodes[v]["request"], "Sink" + ] edges = [ ("Source", v), (v, self.G.nodes[v]["request"]), @@ -948,10 +939,8 @@ def _convert_initial_routes_to_digraphs(self): 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] - ): + if (sum(self.G.nodes[v]["demand"] for v in r) <= + self.load_capacity[k]): G.graph["vehicle_type"] = k break else: @@ -1000,10 +989,8 @@ def _add_fixed_costs(self): def _remove_infeasible_arcs_capacities(self): infeasible_arcs = [] for (i, j) in self.G.edges(): - if ( - self.G.nodes[i]["demand"] + self.G.nodes[j]["demand"] - > self._max_capacity - ): + if (self.G.nodes[i]["demand"] + self.G.nodes[j]["demand"] > + self._max_capacity): infeasible_arcs.append((i, j)) self.G.remove_edges_from(infeasible_arcs) @@ -1022,13 +1009,14 @@ def _remove_infeasible_arcs_time_windows(self): # earliest time is coming straight from depot self.G.nodes[v]["lower"] = max( self.G.nodes[v]["lower"], - self.G.nodes["Source"]["lower"] - + self.G.edges["Source", v]["time"], + self.G.nodes["Source"]["lower"] + + self.G.edges["Source", v]["time"], ) # Latest time is going straight to depot self.G.nodes[v]["upper"] = min( self.G.nodes[v]["upper"], - self.G.nodes["Sink"]["upper"] - self.G.edges[v, "Sink"]["time"], + self.G.nodes["Sink"]["upper"] - + self.G.edges[v, "Sink"]["time"], ) self.G.remove_edges_from(infeasible_arcs) @@ -1055,19 +1043,18 @@ def _set_zero_attributes(self): for v in self.G.nodes(): for attribute in [ - "demand", - "collect", - "service_time", - "lower", - "upper", + "demand", + "collect", + "service_time", + "lower", + "upper", ]: if attribute not in self.G.nodes[v]: self.G.nodes[v][attribute] = 0 # Ignore demand at Source/Sink if v in ["Source", "Sink"] and self.G.nodes[v]["demand"] > 0: - logger.warning( - "Demand %s at node %s is ignored." % (self.G.nodes[v]["demand"], v) - ) + logger.warning("Demand %s at node %s is ignored." % + (self.G.nodes[v]["demand"], v)) self.G.nodes[v]["demand"] = 0 # Set frequency = 1 if missing @@ -1087,11 +1074,9 @@ def _readjust_sink_time_windows(self): if self.G.nodes["Sink"]["upper"] == 0: self.G.nodes["Sink"]["upper"] = max( - self.G.nodes[u]["upper"] - + self.G.nodes[u]["service_time"] - + self.G.edges[u, "Sink"]["time"] - for u in self.G.predecessors("Sink") - ) + self.G.nodes[u]["upper"] + self.G.nodes[u]["service_time"] + + self.G.edges[u, "Sink"]["time"] + for u in self.G.predecessors("Sink")) def _update_dummy_attributes(self): """Adds dummy attributes on nodes and edges if missing.""" @@ -1120,7 +1105,8 @@ def _best_routes_as_node_lists(self): for route in self._best_routes_as_graphs: node_list = shortest_path(route, "Source", "Sink") self._best_routes[route_id] = node_list - self._best_routes_vehicle_type[route_id] = route.graph["vehicle_type"] + self._best_routes_vehicle_type[route_id] = route.graph[ + "vehicle_type"] route_id += 1 # Merge with preassigned complete routes for route in self._preassignments: @@ -1130,7 +1116,8 @@ def _best_routes_as_node_lists(self): best_cost = 1e10 for k in range(self._vehicle_types): # If different vehicles, the cheapest feasible one is accounted for - cost = sum(self._H.edges[i, j]["cost"][k] for (i, j) in edges) + cost = sum( + self._H.edges[i, j]["cost"][k] for (i, j) in edges) load = sum(self._H.nodes[i]["demand"] for i in route) if cost < best_cost: if self.load_capacity: From 65c13ab3c1ba91b2f0d77d8b7a9d90e3d31d33f2 Mon Sep 17 00:00:00 2001 From: torressa <23246013+torressa@users.noreply.github.com> Date: Sun, 3 Oct 2021 19:29:45 +0100 Subject: [PATCH 3/9] Forgot to add the file --- tests/test_issue101.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/test_issue101.py diff --git a/tests/test_issue101.py b/tests/test_issue101.py new file mode 100644 index 0000000..41e97b8 --- /dev/null +++ b/tests/test_issue101.py @@ -0,0 +1,34 @@ +from networkx import DiGraph, read_gpickle +from vrpy import VehicleRoutingProblem + + +class TestIssue101: + + def setup(self): + self.G = DiGraph() + self.G.add_edge("Source", 1, cost=5) + self.G.add_edge("Source", 2, cost=5) + self.G.add_edge(1, "Sink", cost=5) + self.G.add_edge(2, "Sink", cost=5) + self.G.add_edge(1, 2, cost=1) + self.G.nodes[1]["lower"] = 0 + self.G.nodes[1]["upper"] = 20 + self.G.nodes[2]["lower"] = 0 + self.G.nodes[2]["upper"] = 20 + self.G.nodes[1]["service_time"] = 5 + self.G.nodes[2]["service_time"] = 5 + self.G.nodes[1]["demand"] = 8 + self.G.nodes[2]["demand"] = 8 + self.prob = VehicleRoutingProblem(self.G, + load_capacity=10, + time_windows=True) + + def test_cspy(self): + self.prob.solve() + self.prob.check_arrival_time() + self.prob.check_departure_time() + + def test_lp(self): + self.prob.solve(cspy=False) + self.prob.check_arrival_time() + self.prob.check_departure_time() From ef7ec37f9b93816eade9407980b62c23fb1db18b Mon Sep 17 00:00:00 2001 From: torressa <23246013+torressa@users.noreply.github.com> Date: Sun, 13 Mar 2022 20:37:28 +0000 Subject: [PATCH 4/9] Add non-elementary routes. --- benchmarks/tests/pytest.ini | 6 +- benchmarks/tests/test_cvrptw_solomon.py | 38 ++-- benchmarks/tests/test_examples.py | 76 ++++--- tests/pytest.ini | 7 +- tests/test_issue99.py | 12 +- vrpy/checks.py | 82 +++---- vrpy/hyper_heuristic.py | 90 ++++---- vrpy/master_solve_pulp.py | 49 ++-- vrpy/subproblem_cspy.py | 175 +++++++------- vrpy/subproblem_lp.py | 2 +- vrpy/vrp.py | 288 +++++++++++++----------- 11 files changed, 459 insertions(+), 366 deletions(-) diff --git a/benchmarks/tests/pytest.ini b/benchmarks/tests/pytest.ini index 5e0b9f0..ad1f990 100644 --- a/benchmarks/tests/pytest.ini +++ b/benchmarks/tests/pytest.ini @@ -1,5 +1,9 @@ [pytest] +log_cli = 1 +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) +log_cli_date_format=%Y-%m-%d %H:%M:%S python_files = test_*.py #testpaths = tests/ filterwarnings = - ignore::DeprecationWarning \ No newline at end of file + ignore::DeprecationWarning diff --git a/benchmarks/tests/test_cvrptw_solomon.py b/benchmarks/tests/test_cvrptw_solomon.py index 25eee97..af8739a 100644 --- a/benchmarks/tests/test_cvrptw_solomon.py +++ b/benchmarks/tests/test_cvrptw_solomon.py @@ -4,19 +4,18 @@ class TestsSolomon: - def setup(self): """ Solomon instance c101, 25 first nodes only including depot """ - data = SolomonDataSet(path="benchmarks/data/cvrptw/", - instance_name="C101.txt", - n_vertices=25) + data = SolomonDataSet( + path="benchmarks/data/cvrptw/", instance_name="C101.txt", n_vertices=25 + ) self.G = data.G self.n_vertices = 25 - self.prob = VehicleRoutingProblem(self.G, - load_capacity=data.max_load, - time_windows=True) + self.prob = VehicleRoutingProblem( + self.G, load_capacity=data.max_load, time_windows=True + ) initial_routes = [ ["Source", 13, 17, 18, 19, 15, 16, 14, 12, 1, "Sink"], ["Source", 20, 24, 23, 22, 21, "Sink"], @@ -26,7 +25,7 @@ def setup(self): # Set repeating solver arguments self.solver_args = { "pricing_strategy": "BestPaths", - "initial_routes": initial_routes + "initial_routes": initial_routes, } def test_setup_instance_name(self): @@ -40,8 +39,7 @@ def test_setup_nodes(self): assert len(self.G.nodes()) == self.n_vertices + 1 def test_setup_edges(self): - assert len( - self.G.edges()) == self.n_vertices * (self.n_vertices - 1) + 1 + assert len(self.G.edges()) == self.n_vertices * (self.n_vertices - 1) + 1 def test_subproblem_lp(self): # benchmark result @@ -50,33 +48,33 @@ def test_subproblem_lp(self): assert round(self.prob.best_value, -1) in [190, 200] def test_schedule_lp(self): - 'Tests whether final schedule is time-window feasible' + "Tests whether final schedule is time-window feasible" self.prob.solve(**self.solver_args, cspy=False) # Check arrival times for k1, v1 in self.prob.arrival_time.items(): for k2, v2 in v1.items(): - assert (self.G.nodes[k2]["lower"] <= v2) - assert (v2 <= self.G.nodes[k2]["upper"]) + assert self.G.nodes[k2]["lower"] <= v2 + assert v2 <= self.G.nodes[k2]["upper"] # Check departure times for k1, v1 in self.prob.departure_time.items(): for k2, v2 in v1.items(): - assert (self.G.nodes[k2]["lower"] <= v2) - assert (v2 <= self.G.nodes[k2]["upper"]) + assert self.G.nodes[k2]["lower"] <= v2 + assert v2 <= self.G.nodes[k2]["upper"] def test_subproblem_cspy(self): self.prob.solve(**self.solver_args, cspy=True) assert round(self.prob.best_value, -1) in [190, 200] def test_schedule_cspy(self): - 'Tests whether final schedule is time-window feasible' + "Tests whether final schedule is time-window feasible" self.prob.solve(**self.solver_args) # Check departure times for k1, v1 in self.prob.departure_time.items(): for k2, v2 in v1.items(): - assert (self.G.nodes[k2]["lower"] <= v2) - assert (v2 <= self.G.nodes[k2]["upper"]) + assert self.G.nodes[k2]["lower"] <= v2 + assert v2 <= self.G.nodes[k2]["upper"] # Check arrival times for k1, v1 in self.prob.arrival_time.items(): for k2, v2 in v1.items(): - assert (self.G.nodes[k2]["lower"] <= v2) - assert (v2 <= self.G.nodes[k2]["upper"]) + assert self.G.nodes[k2]["lower"] <= v2 + assert v2 <= self.G.nodes[k2]["upper"] diff --git a/benchmarks/tests/test_examples.py b/benchmarks/tests/test_examples.py index 1bd249c..eca3d34 100644 --- a/benchmarks/tests/test_examples.py +++ b/benchmarks/tests/test_examples.py @@ -42,35 +42,39 @@ def setup(self): # Define VRP self.prob = VehicleRoutingProblem(self.G) - def test_cvrp_dive(self): + def test_cvrp_dive_lp(self): self.prob.load_capacity = 15 self.prob.solve(cspy=False, pricing_strategy="BestEdges1", dive=True) - sol_lp = self.prob.best_value - self.prob.solve(pricing_strategy="BestEdges1", dive=True) - sol_cspy = self.prob.best_value - assert int(sol_lp) == 6208 - assert int(sol_cspy) == 6208 + assert int(self.prob.best_value) == 6208 + + def test_cvrp_dive_cspy(self): + self.prob.load_capacity = 15 + self.prob.solve(pricing_strategy="BestEdges1", dive=True, exact=True) + assert int(self.prob.best_value) == 6208 - def test_vrptw_dive(self): + def test_vrptw_dive_lp(self): self.prob.time_windows = True self.prob.solve(cspy=False, dive=True) - sol_lp = self.prob.best_value - self.prob.solve(dive=True) - sol_cspy = self.prob.best_value - assert int(sol_lp) == 6528 - assert int(sol_cspy) == 6528 + assert int(self.prob.best_value) == 6528 - def test_cvrpsdc_dive(self): + def test_vrptw_dive_cspy(self): + self.prob.time_windows = True + self.prob.solve(cspy=True, dive=True) + assert int(self.prob.best_value) == 6528 + + def test_cvrpsdc_dive_lp(self): self.prob.load_capacity = 15 self.prob.distribution_collection = True self.prob.solve(cspy=False, pricing_strategy="BestEdges1", dive=True) - sol_lp = self.prob.best_value + assert int(self.prob.best_value) == 6208 + + def test_cvrpsdc_dive_cspy(self): + self.prob.load_capacity = 15 + self.prob.distribution_collection = True self.prob.solve(pricing_strategy="BestEdges1", dive=True) - sol_cspy = self.prob.best_value - assert int(sol_lp) == 6208 - assert int(sol_cspy) == 6208 + assert int(self.prob.best_value) == 6208 - def test_pdp_dive(self): + def test_pdp_dive_lp(self): # Set demands and requests for (u, v) in PICKUPS_DELIVERIES: self.G.nodes[u]["request"] = v @@ -83,35 +87,39 @@ def test_pdp_dive(self): sol_lp = self.prob.best_value assert int(sol_lp) == 5980 - def test_cvrp(self): + def test_cvrp_lp(self): self.prob.load_capacity = 15 self.prob.solve(cspy=False, pricing_strategy="BestEdges1") - sol_lp = self.prob.best_value + assert int(self.prob.best_value) == 6208 + + def test_cvrp_cspy(self): + self.prob.load_capacity = 15 self.prob.solve(pricing_strategy="BestEdges1") - sol_cspy = self.prob.best_value - assert int(sol_lp) == 6208 - assert int(sol_cspy) == 6208 + assert int(self.prob.best_value) == 6208 - def test_vrptw(self): + def test_vrptw_lp(self): self.prob.time_windows = True self.prob.solve(cspy=False) - sol_lp = self.prob.best_value + assert int(self.prob.best_value) == 6528 + + def test_vrptw_cspy(self): + self.prob.time_windows = True self.prob.solve() - sol_cspy = self.prob.best_value - assert int(sol_lp) == 6528 - assert int(sol_cspy) == 6528 + assert int(self.prob.best_value) == 6528 - def test_cvrpsdc(self): + def test_cvrpsdc_lp(self): self.prob.load_capacity = 15 self.prob.distribution_collection = True self.prob.solve(cspy=False, pricing_strategy="BestEdges1") - sol_lp = self.prob.best_value + assert int(self.prob.best_value) == 6208 + + def test_cvrpsdc_cspy(self): + self.prob.load_capacity = 15 + self.prob.distribution_collection = True self.prob.solve(pricing_strategy="BestEdges1") - sol_cspy = self.prob.best_value - assert int(sol_lp) == 6208 - assert int(sol_cspy) == 6208 + assert int(self.prob.best_value) == 6208 - def test_pdp(self): + def test_pdp_lp(self): # Set demands and requests for (u, v) in PICKUPS_DELIVERIES: self.G.nodes[u]["request"] = v diff --git a/tests/pytest.ini b/tests/pytest.ini index 5e0b9f0..a18e67e 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -1,5 +1,10 @@ [pytest] +log_cli = 1 +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) +log_cli_date_format=%Y-%m-%d %H:%M:%S + python_files = test_*.py #testpaths = tests/ filterwarnings = - ignore::DeprecationWarning \ No newline at end of file + ignore::DeprecationWarning diff --git a/tests/test_issue99.py b/tests/test_issue99.py index 3736134..0ee8745 100644 --- a/tests/test_issue99.py +++ b/tests/test_issue99.py @@ -3,7 +3,7 @@ from vrpy import VehicleRoutingProblem -class TestIssue79: +class TestIssue99: def setup(self): distance_ = [ @@ -247,6 +247,12 @@ def setup(self): # Define VRP self.prob = VehicleRoutingProblem(G_, load_capacity=100) - def test_node_load(self): - self.prob.solve() + def test_lp(self): + self.prob.solve(cspy=False) + print(self.prob.best_routes) + assert self.prob.best_value == 829 + + def test_cspy(self): + self.prob.solve(cspy=True) + print(self.prob.best_routes) assert self.prob.best_value == 829 diff --git a/vrpy/checks.py b/vrpy/checks.py index e904d5f..adb0cdd 100644 --- a/vrpy/checks.py +++ b/vrpy/checks.py @@ -33,16 +33,16 @@ def check_arguments( raise TypeError("Maximum duration must be positive integer.") strategies = ["Exact", "BestEdges1", "BestEdges2", "BestPaths", "Hyper"] if pricing_strategy not in strategies: - raise ValueError("Pricing strategy %s is not valid. Pick one among %s" % - (pricing_strategy, strategies)) + raise ValueError( + "Pricing strategy %s is not valid. Pick one among %s" + % (pricing_strategy, strategies) + ) if mixed_fleet: - if load_capacity and num_vehicles and len(load_capacity) != len( - num_vehicles): + if load_capacity and num_vehicles and len(load_capacity) != len(num_vehicles): raise ValueError( "Input arguments load_capacity and num_vehicles must have same dimension." ) - if load_capacity and fixed_cost and len(load_capacity) != len( - fixed_cost): + if load_capacity and fixed_cost and len(load_capacity) != len(fixed_cost): raise ValueError( "Input arguments load_capacity and fixed_cost must have same dimension." ) @@ -53,21 +53,28 @@ def check_arguments( for (i, j) in G.edges(): if not isinstance(G.edges[i, j]["cost"], list): raise TypeError( - "Cost attribute for edge (%s,%s) should be of type list") + "Cost attribute for edge (%s,%s) should be of type list" + ) if len(G.edges[i, j]["cost"]) != vehicle_types: raise ValueError( "Cost attribute for edge (%s,%s) has dimension %s, should have dimension %s." - % (i, j, len(G.edges[i, j]["cost"]), vehicle_types)) + % (i, j, len(G.edges[i, j]["cost"]), vehicle_types) + ) if use_all_vehicles: if not num_vehicles: logger.warning("num_vehicles = None, use_all_vehicles ignored") -def check_clarke_wright_compatibility(time_windows, pickup_delivery, - distribution_collection, mixed_fleet, - periodic): - if (time_windows or pickup_delivery or distribution_collection or - mixed_fleet or periodic): +def check_clarke_wright_compatibility( + time_windows, pickup_delivery, distribution_collection, mixed_fleet, periodic +): + if ( + time_windows + or pickup_delivery + or distribution_collection + or mixed_fleet + or periodic + ): raise ValueError( "Clarke & Wright heuristic not compatible with time windows, pickup and delivery, simultaneous distribution and collection, mixed fleet, frequencies." ) @@ -78,8 +85,7 @@ def check_vrp(G: DiGraph = None): # if G is not a DiGraph if not isinstance(G, DiGraph): - raise TypeError( - "Input graph must be of type networkx.classes.digraph.DiGraph.") + raise TypeError("Input graph must be of type networkx.classes.digraph.DiGraph.") for v in ["Source", "Sink"]: # If Source or Sink is missing if v not in G.nodes(): @@ -120,8 +126,7 @@ def check_initial_routes(initial_routes: list = None, G: DiGraph = None): for route in initial_routes: if route[0] != "Source" or route[-1] != "Sink": - raise ValueError("Route %s must start at Source and end at Sink" % - route) + raise ValueError("Route %s must start at Source and end at Sink" % route) # Check if every node is in at least one route for v in G.nodes(): if v not in ["Source", "Sink"]: @@ -136,8 +141,9 @@ def check_initial_routes(initial_routes: list = None, G: DiGraph = None): edges = list(zip(route[:-1], route[1:])) for (i, j) in edges: if (i, j) not in G.edges(): - raise KeyError("Edge (%s,%s) in route %s missing in graph." % - (i, j, route)) + raise KeyError( + "Edge (%s,%s) in route %s missing in graph." % (i, j, route) + ) if "cost" not in G.edges[i, j]: raise KeyError("Edge (%s,%s) has no cost attribute." % (i, j)) @@ -155,8 +161,8 @@ def check_preassignments(routes: list = None, G: DiGraph = None): for (i, j) in edges: if (i, j) not in G.edges(): raise ValueError( - "Edge (%s,%s) in locked route %s is not in graph G." % - (i, j, route)) + "Edge (%s,%s) in locked route %s is not in graph G." % (i, j, route) + ) def check_consistency( @@ -179,36 +185,35 @@ def check_consistency( if pickup_delivery: request = any("request" in G.nodes[v] for v in G.nodes()) if not request: - raise KeyError( - "pickup_delivery option expects at least one request.") + raise KeyError("pickup_delivery option expects at least one request.") -def check_feasibility(load_capacity: list = None, - G: DiGraph = None, - duration: int = None): +def check_feasibility( + load_capacity: list = None, G: DiGraph = None, duration: int = None +): """Checks basic problem feasibility.""" if load_capacity: for v in G.nodes(): if G.nodes[v]["demand"] > max(load_capacity): raise ValueError( - "Demand %s at node %s larger than max capacity %s." % - (G.nodes[v]["demand"], v, max(load_capacity))) + "Demand %s at node %s larger than max capacity %s." + % (G.nodes[v]["demand"], v, max(load_capacity)) + ) if duration: for v in G.nodes(): if v not in ["Source", "Sink"]: - shortest_path_to = shortest_path_length(G, - source="Source", - target=v, - weight="time") - shortest_path_from = shortest_path_length(G, - source=v, - target="Sink", - weight="time") + shortest_path_to = shortest_path_length( + G, source="Source", target=v, weight="time" + ) + shortest_path_from = shortest_path_length( + G, source=v, target="Sink", weight="time" + ) shortest_trip = shortest_path_from + shortest_path_to if shortest_trip > duration: raise ValueError( - "Node %s not reachable with duration constraints" % v) + "Node %s not reachable with duration constraints" % v + ) def check_seed(seed): @@ -230,7 +235,8 @@ def check_pickup_delivery_time_windows(G: DiGraph, edges: list): if (i, j) not in G.edges(): raise ValueError( "Problem Infeasible, request (%s,%s) cannot be done with given time windows." - % (i, j)) + % (i, j) + ) def check_periodic_num_vehicles(periodic=None, num_vehicles=[]): diff --git a/vrpy/hyper_heuristic.py b/vrpy/hyper_heuristic.py index 012073d..7350c49 100644 --- a/vrpy/hyper_heuristic.py +++ b/vrpy/hyper_heuristic.py @@ -125,17 +125,14 @@ def pick_heuristic(self): pass # choose according to MAB maxval = max(self.heuristic_points.values()) - best_heuristics = [ - i for i, j in self.heuristic_points.items() if j == maxval - ] + best_heuristics = [i for i, j in self.heuristic_points.items() if j == maxval] if len(best_heuristics) == 1: self.current_heuristic = best_heuristics[0] else: self.current_heuristic = self.random_state.choice(best_heuristics) return self.current_heuristic - def update_scaling_factor(self, no_improvement_count: int, - no_improvement_iteration: int): + def update_scaling_factor(self, no_improvement_count: int): """ Implements Drake et al. (2012) @@ -180,8 +177,8 @@ def current_performance( elif self.performance_measure_type == "weighted_average": self._current_performance_wgtavr( - active_columns=active_columns, - new_objective_value=new_objective_value) + active_columns=active_columns, new_objective_value=new_objective_value + ) else: raise ValueError("performence_measure not set correctly!") self.set_current_objective(objective=new_objective_value) @@ -220,13 +217,11 @@ def reward(self, y, stagnated=True): x *= 0.9 return x - def update_parameters(self, iteration: int, no_improvement_count: int, - no_improvement_iteration: int): + def update_parameters(self, iteration: int, no_improvement_count: int): "Updates the high-level parameters" # measure time and add to weighted average self.iteration = iteration - self.update_scaling_factor(no_improvement_count, - no_improvement_iteration) + self.update_scaling_factor(no_improvement_count) # compute average of runtimes if self.performance_measure_type == "relative_improvement": self._update_params_relimp() @@ -245,40 +240,48 @@ def _compute_last_runtime(self): def _current_performance_relimp(self, produced_column: bool = False): # time measure if self.iteration > self.start_computing_average + 1: - self.d = max((self.average_runtime - self.last_runtime) / - self.average_runtime * 100, 0) - logger.debug("Resolve count %s, improvement %s", produced_column, - self.d) + self.d = max( + (self.average_runtime - self.last_runtime) / self.average_runtime * 100, + 0, + ) + logger.debug("Resolve count %s, improvement %s", produced_column, self.d) if self.d > self.d_max: self.d_max = self.d logger.debug( "Column produced, average runtime %s and last runtime %s", - self.average_runtime, self.last_runtime) + self.average_runtime, + self.last_runtime, + ) else: self.d = 0 - def _current_performance_wgtavr(self, - new_objective_value: float = None, - active_columns: dict = None): + def _current_performance_wgtavr( + self, new_objective_value: float = None, active_columns: dict = None + ): self.active_columns = active_columns # insert new runtime into sorted list bisect.insort(self.runtime_dist, self.last_runtime) self.objective_decrease[self.current_heuristic] += max( - self.current_objective_value - new_objective_value, 0) + self.current_objective_value - new_objective_value, 0 + ) self.total_objective_decrease += max( - self.current_objective_value - new_objective_value, 0) + self.current_objective_value - new_objective_value, 0 + ) # update quality values for heuristic in self.heuristic_options: if self.iterations[heuristic] > 0: self._update_exp(heuristic) - index = bisect.bisect(self.runtime_dist, - self.last_runtime_dict[heuristic]) - self.norm_runtime[heuristic] = (len(self.runtime_dist) - - index) / len(self.runtime_dist) + index = bisect.bisect( + self.runtime_dist, self.last_runtime_dict[heuristic] + ) + self.norm_runtime[heuristic] = (len(self.runtime_dist) - index) / len( + self.runtime_dist + ) if self.total_objective_decrease > 0: self.norm_objective_decrease[heuristic] = ( - self.objective_decrease[heuristic] / - self.total_objective_decrease) + self.objective_decrease[heuristic] + / self.total_objective_decrease + ) def _update_params_relimp(self): "Updates params for relative improvements performance measure" @@ -288,9 +291,10 @@ def _update_params_relimp(self): if reduced_n == 0: self.average_runtime = self.last_runtime else: - self.average_runtime = (self.average_runtime * (reduced_n - 1) / - (reduced_n) + 1 / - (reduced_n) * self.last_runtime) + self.average_runtime = ( + self.average_runtime * (reduced_n - 1) / (reduced_n) + + 1 / (reduced_n) * self.last_runtime + ) heuristic = self.current_heuristic # store old values old_q = self.q[heuristic] @@ -298,19 +302,22 @@ def _update_params_relimp(self): stagnated = old_q == 0 and old_n > 3 # average of improvements self.r[heuristic] = self.r[heuristic] * old_n / (old_n + 1) + 1 / ( - old_n + 1) * self.reward(self.d, stagnated=stagnated) + old_n + 1 + ) * self.reward(self.d, stagnated=stagnated) self.q[heuristic] = (old_q + self.r[heuristic]) / (old_n + 1) # compute heuristic points MAB-style for heuristic in self.heuristic_options: if self.iterations[heuristic] != 0: self._update_exp(heuristic) self.heuristic_points[heuristic] = ( - self.theta * self.q[heuristic] + - self.scaling_factor * self.exp[heuristic]) + self.theta * self.q[heuristic] + + self.scaling_factor * self.exp[heuristic] + ) def _update_exp(self, heuristic): - self.exp[heuristic] = sqrt(2 * log(sum(self.iterations.values())) / - self.iterations[heuristic]) + self.exp[heuristic] = sqrt( + 2 * log(sum(self.iterations.values())) / self.iterations[heuristic] + ) def _update_params_wgtavr(self): "Updates params for Weighted average performance measure" @@ -326,10 +333,11 @@ def _update_params_wgtavr(self): if sum_exp != 0: norm_spread = self.exp[heuristic] / sum_exp self.q[heuristic] = ( - self.weight_col_basic * active_i / active + - self.weight_runtime * norm_runtime + - self.weight_obj * self.norm_objective_decrease[heuristic] + - self.weight_col_total * active_i / total_added) + self.weight_col_basic * active_i / active + + self.weight_runtime * norm_runtime + + self.weight_obj * self.norm_objective_decrease[heuristic] + + self.weight_col_total * active_i / total_added + ) self.heuristic_points[heuristic] = self.theta * self.q[ - heuristic] + self.weight_spread * norm_spread * (1 - - self.theta) + heuristic + ] + self.weight_spread * norm_spread * (1 - self.theta) diff --git a/vrpy/master_solve_pulp.py b/vrpy/master_solve_pulp.py index c9988c3..b00272d 100644 --- a/vrpy/master_solve_pulp.py +++ b/vrpy/master_solve_pulp.py @@ -7,6 +7,7 @@ from vrpy.masterproblem import _MasterProblemBase from vrpy.restricted_master_heuristics import _DivingHeuristic + logger = logging.getLogger(__name__) @@ -50,11 +51,13 @@ def solve(self, relax, time_limit): if pulp.LpStatus[self.prob.status] != "Optimal": raise Exception("problem " + str(pulp.LpStatus[self.prob.status])) - if relax: - for r in self.routes: - val = pulp.value(self.y[r.graph["name"]]) - if val > 0.1: - logger.debug("route %s selected %s" % (r.graph["name"], val)) + # This logging takes time + # if relax: + # for r in self.routes: + # val = pulp.value(self.y[r.graph["name"]]) + # if val > 0.1: + # logger.debug("route %s selected %s" % + # (r.graph["name"], val)) duals = self.get_duals() logger.debug("duals : %s" % duals) return duals, self.prob.objective.value() @@ -123,16 +126,13 @@ def get_total_cost_and_routes(self, relax: bool): for r in self.routes: val = pulp.value(self.y[r.graph["name"]]) if val is not None and val > 0: - logger.debug( - "%s cost %s load %s" - % ( - shortest_path(r, "Source", "Sink"), - r.graph["cost"], - sum(self.G.nodes[v]["demand"] for v in r.nodes()), - ) - ) - best_routes.append(r) + # This logging takes time + # logger.debug("%s cost %s load %s" % ( + # shortest_path(r, "Source", "Sink"), + # r.graph["cost"], + # sum(self.G.nodes[v]["demand"] for v in r.nodes()), + # )) if self.drop_penalty: self.dropped_nodes = [ v for v in self.drop if pulp.value(self.drop[v]) > 0.5 @@ -178,7 +178,9 @@ def _solve(self, relax: bool, time_limit: Optional[int]): and "depot_to" not in self.G.nodes[node] ): for const in self.prob.constraints: - # Modify the self.prob object (the self.set_covering_constrs object cannot be modified (?)) + # Modify the self.prob object (the + # self.set_covering_constrs object cannot be modified + # (?)) if "visit_node" in const: self.prob.constraints[const].sense = pulp.LpConstraintEQ if ( @@ -189,6 +191,10 @@ def _solve(self, relax: bool, time_limit: Optional[int]): self.dummy[node].cat = pulp.LpInteger # Set route variables to integer for var in self.y.values(): + # Disallow routes that visit multiple nodes + # if "non" in var.name: + # var.upBound = 0 + # var.lowBound = 0 var.cat = pulp.LpInteger # Force vehicle bound artificial variable to 0 for var in self.dummy_bound.values(): @@ -303,17 +309,20 @@ def _add_set_covering_constraints(self): ) def _add_route_selection_variable(self, route): + # Added path with the raw path as using `nx.add_path` then + # `graph.nodes()` + # removes repeated nodes, so the coefficients are not correct. + if "path" in route.graph: + nodes = [n for n in route.graph["path"] if n not in ["Source", "Sink"]] + else: + nodes = [n for n in route.nodes() if n not in ["Source", "Sink"]] self.y[route.graph["name"]] = pulp.LpVariable( "y{}".format(route.graph["name"]), lowBound=0, upBound=1, cat=pulp.LpContinuous, e=( - pulp.lpSum( - self.set_covering_constrs[r] - for r in route.nodes() - if r not in ["Source", "Sink"] - ) + pulp.lpSum(self.set_covering_constrs[n] for n in nodes) + pulp.lpSum( self.vehicle_bound_constrs[k] for k in range(len(self.num_vehicles)) diff --git a/vrpy/subproblem_cspy.py b/vrpy/subproblem_cspy.py index e8e96a8..a903522 100644 --- a/vrpy/subproblem_cspy.py +++ b/vrpy/subproblem_cspy.py @@ -1,5 +1,6 @@ import logging from math import floor + from numpy import zeros from networkx import DiGraph, add_path from cspy import BiDirectional, REFCallback @@ -15,8 +16,16 @@ class _MyREFCallback(REFCallback): Based on Righini and Salani (2006). """ - def __init__(self, max_res, time_windows, distribution_collection, - pickup_delivery, T, resources, pickup_delivery_pairs): + def __init__( + self, + max_res, + time_windows, + distribution_collection, + pickup_delivery, + T, + resources, + pickup_delivery_pairs, + ): REFCallback.__init__(self) # Set attributes for use in REF functions self._max_res = max_res @@ -32,8 +41,7 @@ def __init__(self, max_res, time_windows, distribution_collection, self._sink_id = None self._matched_delivery_to_pickup_nodes = {} - def REF_fwd(self, cumul_res, tail, head, edge_res, partial_path, - cumul_cost): + def REF_fwd(self, cumul_res, tail, head, edge_res, partial_path, cumul_cost): new_res = list(cumul_res) i, j = tail, head # stops / monotone resource @@ -64,25 +72,22 @@ def REF_fwd(self, cumul_res, tail, head, edge_res, partial_path, # Pickup new_res[4] += self._sub_G.nodes[j]["collect"] # Delivery - new_res[5] = max(new_res[5] + self._sub_G.nodes[j]["demand"], - new_res[4]) + new_res[5] = max(new_res[5] + self._sub_G.nodes[j]["demand"], new_res[4]) _partial_path = list(partial_path) if self._pickup_delivery: open_requests = [ - n for n in _partial_path - if "request" in self._sub_G.nodes[n] and - self._sub_G.nodes[n]["request"] not in _partial_path + n + for n in _partial_path + if "request" in self._sub_G.nodes[n] + and self._sub_G.nodes[n]["request"] not in _partial_path ] if len(open_requests) > 0: pickup_node = None - pickup_nodes = [ - u for (u, v) in self._pickup_delivery_pairs if v == j - ] + pickup_nodes = [u for (u, v) in self._pickup_delivery_pairs if v == j] if len(pickup_nodes) == 1: pickup_node = pickup_nodes[0] - if (pickup_node is not None and - pickup_node not in open_requests): + if pickup_node is not None and pickup_node not in open_requests: new_res[6] = 1.0 else: new_res[6] = 0.0 @@ -90,8 +95,7 @@ def REF_fwd(self, cumul_res, tail, head, edge_res, partial_path, new_res[6] = 1.0 return new_res - def REF_bwd(self, cumul_res, tail, head, edge_res, partial_path, - cumul_cost): + def REF_bwd(self, cumul_res, tail, head, edge_res, partial_path, cumul_cost): new_res = list(cumul_res) i, j = tail, head @@ -112,8 +116,7 @@ def REF_bwd(self, cumul_res, tail, head, edge_res, partial_path, b_i = self._sub_G.nodes[i]["upper"] b_j = self._sub_G.nodes[j]["upper"] - new_res[2] = max(new_res[2] + theta_j + travel_time_ij, - self._T - b_i - theta_i) + new_res[2] = max(new_res[2] + theta_j + travel_time_ij, self._T - b_i - theta_i) # time-window feasibility if not self._time_windows or (new_res[2] <= self._T - a_j - theta_j): @@ -124,9 +127,8 @@ def REF_bwd(self, cumul_res, tail, head, edge_res, partial_path, if self._distribution_collection: # Delivery new_res[5] += new_res[5] + self._sub_G.nodes[i]["demand"] - # Pickup - new_res[4] = max(new_res[5], - new_res[4] + self._sub_G.nodes[i]["collect"]) + # Pick up + new_res[4] = max(new_res[5], new_res[4] + self._sub_G.nodes[i]["collect"]) return new_res def REF_join(self, fwd_resources, bwd_resources, tail, head, edge_res): @@ -177,25 +179,30 @@ def __init__(self, *args, exact, pickup_delivery_pairs): """Initializes resources.""" # Pass arguments to base super(_SubProblemCSPY, self).__init__(*args) - self.exact = exact + # self.exact = exact self.pickup_delivery_pairs = pickup_delivery_pairs # Resource names self.resources = [ - "stops/mono", "load", "time", "time windows", "collect", "deliver", - "pickup_delivery" + "stops/mono", + "load", + "time", + "time windows", + "collect", + "deliver", + "pickup_delivery", ] # Set number of resources as attribute of graph self.sub_G.graph["n_res"] = len(self.resources) # Default lower and upper bounds self.min_res = [0] * len(self.resources) # Add upper bounds for mono, stops, load and time, and time windows - total_demand = sum( - [self.sub_G.nodes[v]["demand"] for v in self.sub_G.nodes()]) + total_demand = sum([self.sub_G.nodes[v]["demand"] for v in self.sub_G.nodes()]) self.max_res = [ floor(len(self.sub_G.nodes()) / 2), # stop/mono total_demand, # load - sum([self.sub_G.edges[u, v]["time"] for u, v in self.sub_G.edges() - ]), # time + sum( + [self.sub_G.edges[u, v]["time"] for u, v in self.sub_G.edges()] + ), # time 1, # time windows total_demand, # pickup total_demand, # deliver @@ -230,29 +237,29 @@ def solve(self, time_limit): more_routes = False my_callback = self.get_REF() - direction = "forward" if self.time_windows or self.pickup_delivery else "both" - while True: - if self.exact: - alg = BiDirectional( - self.sub_G, - self.max_res, - self.min_res, - direction=direction, - time_limit=time_limit - 0.5 if time_limit else None, - elementary=True, - REF_callback=my_callback, - pickup_delivery_pairs=self.pickup_delivery_pairs) - else: - alg = BiDirectional( - self.sub_G, - self.max_res, - self.min_res, - threshold=-1, - direction=direction, - time_limit=time_limit - 0.5 if time_limit else None, - elementary=True, - REF_callback=my_callback, - pickup_delivery_pairs=self.pickup_delivery_pairs) + direction = ( + "forward" + 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. + for elementary in [False, True]: + alg = BiDirectional( + self.sub_G, + self.max_res, + self.min_res, + threshold=-1e-3, + direction=direction, + time_limit=time_limit - 0.5 if time_limit else None, + elementary=elementary, + REF_callback=my_callback, + # pickup_delivery_pairs=self.pickup_delivery_pairs, + ) # Pass processed graph if my_callback is not None: @@ -263,19 +270,20 @@ def solve(self, time_limit): logger.debug("subproblem") logger.debug("cost = %s", alg.total_cost) logger.debug("resources = %s", alg.consumed_resources) + if alg.total_cost is not None and alg.total_cost < -(1e-3): - more_routes = True - self.add_new_route(alg.path) - logger.debug("new route %s", alg.path) - logger.debug("reduced cost = %s", alg.total_cost) - logger.debug("real cost = %s", self.total_cost) - break - # If not already solved exactly - elif not self.exact: - # Solve exactly from here on - self.exact = True - # Solved heuristically and exactly and no more routes - # Or time out + new_route = self.create_new_route(alg.path) + if not any( + list(new_route.edges()) == list(r.edges()) for r in self.routes + ): + more_routes = True + self.routes.append(new_route) + self.total_cost = new_route.graph["cost"] + logger.debug("reduced cost = %s", alg.total_cost) + logger.debug("real cost = %s", self.total_cost) + break + else: + logger.info("Route already found, finding elementary one") else: break return self.routes, more_routes @@ -302,31 +310,34 @@ def formulate(self): # print("node = ", v, "lb = ", self.sub_G.nodes[v]["lower"], # "ub = ", self.sub_G.nodes[v]["upper"]) - self.T = max(self.sub_G.nodes[v]["upper"] + - self.sub_G.nodes[v]["service_time"] + - self.sub_G.edges[v, "Sink"]["time"] - for v in self.sub_G.predecessors("Sink")) + self.T = max( + self.sub_G.nodes[v]["upper"] + + self.sub_G.nodes[v]["service_time"] + + self.sub_G.edges[v, "Sink"]["time"] + for v in self.sub_G.predecessors("Sink") + ) if self.load_capacity and self.distribution_collection: self.max_res[4] = self.load_capacity[self.vehicle_type] self.max_res[5] = self.load_capacity[self.vehicle_type] if self.pickup_delivery: self.max_res[6] = 0 - def add_new_route(self, path): + def create_new_route(self, path): """Create new route as DiGraph and add to pool of columns""" - route_id = len(self.routes) + 1 - new_route = DiGraph(name=route_id) + 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) - self.total_cost = 0 + total_cost = 0 for (i, j) in new_route.edges(): edge_cost = self.sub_G.edges[i, j]["cost"][self.vehicle_type] - self.total_cost += edge_cost + total_cost += edge_cost new_route.edges[i, j]["cost"] = edge_cost if i != "Source": self.routes_with_node[i].append(new_route) - new_route.graph["cost"] = self.total_cost + new_route.graph["cost"] = total_cost new_route.graph["vehicle_type"] = self.vehicle_type - self.routes.append(new_route) + return new_route def add_max_stops(self): """Updates maximum number of stops.""" @@ -359,13 +370,17 @@ def add_max_duration(self): self.sub_G.edges[i, j]["res_cost"][2] = travel_time def get_REF(self): - if (self.time_windows or self.distribution_collection or - self.pickup_delivery): + if self.time_windows or self.distribution_collection or self.pickup_delivery: # Use custom REF - return _MyREFCallback(self.max_res, self.time_windows, - self.distribution_collection, - self.pickup_delivery, self.T, self.resources, - self.pickup_delivery_pairs) + return _MyREFCallback( + self.max_res, + self.time_windows, + self.distribution_collection, + self.pickup_delivery, + self.T, + self.resources, + self.pickup_delivery_pairs, + ) else: # Use default return None diff --git a/vrpy/subproblem_lp.py b/vrpy/subproblem_lp.py index 84f60c8..73a38a3 100644 --- a/vrpy/subproblem_lp.py +++ b/vrpy/subproblem_lp.py @@ -41,7 +41,7 @@ def solve(self, time_limit, exact=True): logger.debug("Objective %s" % pulp.value(self.prob.objective)) if ( pulp.value(self.prob.objective) is not None - and pulp.value(self.prob.objective) < -(10 ** -3) + and pulp.value(self.prob.objective) < -(10**-3) and pulp.LpStatus[self.prob.status] not in ["Infeasible"] ) or (exact == False and pulp.LpStatus[self.prob.status] in ["Optimal", ""]): more_routes = True diff --git a/vrpy/vrp.py b/vrpy/vrp.py index f425f37..be51730 100644 --- a/vrpy/vrp.py +++ b/vrpy/vrp.py @@ -1,7 +1,9 @@ import logging from time import time from typing import List, Union + from networkx import DiGraph, shortest_path # draw_networkx + from vrpy.greedy import _Greedy from vrpy.master_solve_pulp import _MasterSolvePulp from vrpy.subproblem_lp import _SubProblemLP @@ -237,9 +239,11 @@ def solve( # Extract pairs of (pickup, delivery) nodes from graph if self.pickup_delivery: - self._pickup_delivery_pairs = [(i, self.G.nodes[i]["request"]) - for i in self.G.nodes() - if "request" in self.G.nodes[i]] + self._pickup_delivery_pairs = [ + (i, self.G.nodes[i]["request"]) + for i in self.G.nodes() + if "request" in self.G.nodes[i] + ] self._start_time = time() if preassignments: @@ -288,23 +292,25 @@ def best_routes_cost(self): """Returns dict with route ids as keys and route costs as values.""" cost = {} for route in self.best_routes: - edges = list( - zip(self.best_routes[route][:-1], self.best_routes[route][1:])) + edges = list(zip(self.best_routes[route][:-1], self.best_routes[route][1:])) k = self._best_routes_vehicle_type[route] - cost[route] = sum( - self._H.edges[i, j]["cost"][k] for (i, j) in edges) + cost[route] = sum(self._H.edges[i, j]["cost"][k] for (i, j) in edges) return cost @property def best_routes_load(self): """Returns dict with route ids as keys and route loads as values.""" load = {} - if (not self.load_capacity or self.distribution_collection or - self.pickup_delivery): + if ( + not self.load_capacity + or self.distribution_collection + or self.pickup_delivery + ): return load for route in self.best_routes: load[route] = sum( - self._H.nodes[v]["demand"] for v in self.best_routes[route]) + self._H.nodes[v]["demand"] for v in self.best_routes[route] + ) return load @property @@ -317,8 +323,11 @@ def node_load(self): If simultaneous distribution and collection, load refers to truck load when leaving the node. """ load = {} - if (not self.load_capacity and not self.pickup_delivery and - not self.distribution_collection): + if ( + not self.load_capacity + and not self.pickup_delivery + and not self.distribution_collection + ): return load for i in self.best_routes: load[i] = {} @@ -330,8 +339,9 @@ def node_load(self): amount += self._H.nodes[v]["demand"] if self.distribution_collection: # in this case, load = Q + collect - demand - amount += (self._H.nodes[v]["collect"] - - 2 * self._H.nodes[v]["demand"]) + amount += ( + self._H.nodes[v]["collect"] - 2 * self._H.nodes[v]["demand"] + ) load[i][v] = amount return load @@ -342,14 +352,13 @@ def best_routes_duration(self): if not self.duration and not self.time_windows: return duration for route in self.best_routes: - edges = list( - zip(self.best_routes[route][:-1], self.best_routes[route][1:])) + edges = list(zip(self.best_routes[route][:-1], self.best_routes[route][1:])) # Travel times - duration[route] = sum( - self._H.edges[i, j]["time"] for (i, j) in edges) + duration[route] = sum(self._H.edges[i, j]["time"] for (i, j) in edges) # Service times - duration[route] += sum(self._H.nodes[v]["service_time"] - for v in self.best_routes[route]) + duration[route] += sum( + self._H.nodes[v]["service_time"] for v in self.best_routes[route] + ) return duration @@ -370,17 +379,19 @@ def arrival_time(self): tail = route[j - 1] head = route[j] arrival[i][head] = max( - arrival[i][tail] + self._H.nodes[tail]["service_time"] + - self._H.edges[tail, head]["time"], - self._H.nodes[head]["lower"]) + arrival[i][tail] + + self._H.nodes[tail]["service_time"] + + self._H.edges[tail, head]["time"], + self._H.nodes[head]["lower"], + ) return arrival def check_arrival_time(self): # Check arrival times for k1, v1 in self.arrival_time.items(): for k2, v2 in v1.items(): - assert (self.G.nodes[k2]["lower"] <= v2) - assert (v2 <= self.G.nodes[k2]["upper"]) + assert self.G.nodes[k2]["lower"] <= v2 + assert v2 <= self.G.nodes[k2]["upper"] @property def departure_time(self): @@ -409,8 +420,8 @@ def check_departure_time(self): # Check departure times for k1, v1 in self.departure_time.items(): for k2, v2 in v1.items(): - assert (self.G.nodes[k2]["lower"] <= v2) - assert (v2 <= self.G.nodes[k2]["upper"]) + assert self.G.nodes[k2]["lower"] <= v2 + assert v2 <= self.G.nodes[k2]["upper"] @property def schedule(self): @@ -453,11 +464,12 @@ def _pre_solve(self): G=self.G, ) # Check feasibility - check_feasibility(load_capacity=self.load_capacity, - G=self.G, - duration=self.duration) + check_feasibility( + load_capacity=self.load_capacity, G=self.G, duration=self.duration + ) self.num_vehicles, self._num_vehicles_schedule = check_periodic_num_vehicles( - periodic=self.periodic, num_vehicles=self.num_vehicles) + periodic=self.periodic, num_vehicles=self.num_vehicles + ) # Lock preassigned routes if self._preassignments: check_preassignments(self._preassignments, self.G) @@ -466,9 +478,9 @@ def _pre_solve(self): self._prune_graph() # Compute upper bound on number of stops as knapsack problem if self.load_capacity and not self.pickup_delivery: - num_stops = get_num_stops_upper_bound(self.G, self._max_capacity, - self.num_stops, - self.distribution_collection) + num_stops = get_num_stops_upper_bound( + self.G, self._max_capacity, self.num_stops, self.distribution_collection + ) self.num_stops = num_stops logger.info("new upper bound : max num stops = %s" % num_stops) # Set empty route cost if use_all_vehicles = True @@ -512,11 +524,13 @@ def _solve(self, dive, solver): elif len(self.G.nodes()) > 2: # Solve as MIP _, _ = self.masterproblem.solve( - relax=False, time_limit=self._get_time_remaining(mip=True)) + relax=False, time_limit=self._get_time_remaining(mip=True) + ) ( self._best_value, self._best_routes_as_graphs, ) = self.masterproblem.get_total_cost_and_routes(relax=False) + self._post_process(solver) def _column_generation(self): @@ -524,13 +538,16 @@ def _column_generation(self): # Generate good columns self._find_columns() # Stop if time limit is passed - if (isinstance(self._get_time_remaining(), float) and - self._get_time_remaining() == 0.0): + if ( + isinstance(self._get_time_remaining(), float) + and self._get_time_remaining() == 0.0 + ): logger.info("time up !") break # Stop if no improvement limit is passed or max iter exceeded if self._no_improvement > 1000 or ( - self._max_iter and self._iteration >= self._max_iter): + self._max_iter and self._iteration >= self._max_iter + ): break def _find_columns(self): @@ -538,38 +555,47 @@ def _find_columns(self): # Solve restricted relaxed master problem if self._dive: duals, relaxed_cost = self.masterproblem.solve_and_dive( - time_limit=self._get_time_remaining()) + time_limit=self._get_time_remaining() + ) if self.hyper_heuristic: self.hyper_heuristic.init(relaxed_cost) else: duals, relaxed_cost = self.masterproblem.solve( - relax=True, time_limit=self._get_time_remaining()) + relax=True, time_limit=self._get_time_remaining() + ) logger.info("iteration %s, %.6s" % (self._iteration, relaxed_cost)) pricing_strategy = self._get_next_pricing_strategy(relaxed_cost) + # TODO: parallel # One subproblem per vehicle type for vehicle in range(self._vehicle_types): # Solve pricing problem with randomised greedy algorithm - if (self._greedy and not self.time_windows and - not self.distribution_collection and - not self.pickup_delivery and not self.minimize_global_span): + if ( + self._greedy + and not self.time_windows + and not self.distribution_collection + and not self.pickup_delivery + and not self.minimize_global_span + ): subproblem = self._def_subproblem(duals, vehicle, greedy=True) self.routes, self._more_routes = subproblem.solve(n_runs=20) # Add initial_routes if self._more_routes: - for r in (r for r in self.routes - if r.graph["name"] not in self.masterproblem.y): + for r in ( + r + for r in self.routes + if r.graph["name"] not in self.masterproblem.y + ): self.masterproblem.update(r) # Continue searching for columns self._more_routes = False if not self.minimize_global_span: self._more_routes = self._solve_subproblem_with_heuristic( - pricing_strategy=pricing_strategy, - vehicle=vehicle, - duals=duals) + pricing_strategy=pricing_strategy, vehicle=vehicle, duals=duals + ) else: - for route in self._routes[:len(self._routes)]: + for route in self._routes[: len(self._routes)]: self._more_routes = self._solve_subproblem_with_heuristic( pricing_strategy=pricing_strategy, vehicle=vehicle, @@ -591,7 +617,6 @@ def _find_columns(self): self._no_improvement += 1 else: self._no_improvement = 0 - self._no_improvement_iteration = self._iteration if not self._dive: self._lower_bound.append(relaxed_cost) @@ -606,39 +631,39 @@ def _solve_subproblem_with_heuristic( more_columns = False if self._pricing_strategy == "Hyper": if pricing_strategy == "BestPaths": - more_columns = self._attempt_solve_best_paths(vehicle=vehicle, - duals=duals, - route=route) + more_columns = self._attempt_solve_best_paths( + vehicle=vehicle, duals=duals, route=route + ) elif pricing_strategy == "BestEdges1": - more_columns = self._attempt_solve_best_edges1(vehicle=vehicle, - duals=duals, - route=route) + more_columns = self._attempt_solve_best_edges1( + vehicle=vehicle, duals=duals, route=route + ) elif pricing_strategy == "BestEdges2": - more_columns = self._attempt_solve_best_edges2(vehicle=vehicle, - duals=duals, - route=route) + more_columns = self._attempt_solve_best_edges2( + vehicle=vehicle, duals=duals, route=route + ) elif pricing_strategy == "Exact": - more_columns = self._attempt_solve_exact(vehicle=vehicle, - duals=duals, - route=route) + more_columns = self._attempt_solve_exact( + vehicle=vehicle, duals=duals, route=route + ) # old approach else: if pricing_strategy == "BestPaths": - more_columns = self._attempt_solve_best_paths(vehicle=vehicle, - duals=duals, - route=route) + more_columns = self._attempt_solve_best_paths( + vehicle=vehicle, duals=duals, route=route + ) elif pricing_strategy == "BestEdges1": - more_columns = self._attempt_solve_best_edges1(vehicle=vehicle, - duals=duals, - route=route) + more_columns = self._attempt_solve_best_edges1( + vehicle=vehicle, duals=duals, route=route + ) elif pricing_strategy == "BestEdges2": - more_columns = self._attempt_solve_best_edges2(vehicle=vehicle, - duals=duals, - route=route) + more_columns = self._attempt_solve_best_edges2( + vehicle=vehicle, duals=duals, route=route + ) if pricing_strategy == "Exact" or not more_columns: - more_columns = self._attempt_solve_exact(vehicle=vehicle, - duals=duals, - route=route) + more_columns = self._attempt_solve_exact( + vehicle=vehicle, duals=duals, route=route + ) return more_columns def _attempt_solve_best_paths(self, vehicle=None, duals=None, route=None): @@ -652,7 +677,8 @@ def _attempt_solve_best_paths(self, vehicle=None, duals=None, route=None): k_shortest_paths, ) self.routes, self._more_routes = subproblem.solve( - self._get_time_remaining()) + self._get_time_remaining() + ) more_columns = self._more_routes if more_columns: break @@ -671,7 +697,8 @@ def _attempt_solve_best_edges1(self, vehicle=None, duals=None, route=None): alpha, ) self.routes, self._more_routes = subproblem.solve( - self._get_time_remaining(),) + self._get_time_remaining() + ) more_columns = self._more_routes if more_columns: break @@ -691,7 +718,6 @@ def _attempt_solve_best_edges2(self, vehicle=None, duals=None, route=None): ) self.routes, self._more_routes = subproblem.solve( self._get_time_remaining(), - # exact=False, ) more_columns = self._more_routes if more_columns: @@ -703,16 +729,17 @@ def _attempt_solve_best_edges2(self, vehicle=None, duals=None, route=None): def _attempt_solve_exact(self, vehicle=None, duals=None, route=None): subproblem = self._def_subproblem(duals, vehicle, route) - self.routes, self._more_routes = subproblem.solve( - self._get_time_remaining()) + self.routes, self._more_routes = subproblem.solve(self._get_time_remaining()) return self._more_routes def _get_next_pricing_strategy(self, relaxed_cost): "Return the appropriate pricing strategy based on input parameters" pricing_strategy = None - if (self._pricing_strategy == "Hyper" and - self._no_improvement != self._run_exact): - self._no_improvement_iteration = self._iteration + if ( + self._pricing_strategy == "Hyper" + and self._no_improvement != self._run_exact + ): + self._no_improvement = self._iteration if self._iteration == 0: pricing_strategy = "BestPaths" self.hyper_heuristic.init(relaxed_cost) @@ -721,24 +748,21 @@ def _get_next_pricing_strategy(self, relaxed_cost): self._update_hyper_heuristic(relaxed_cost) pricing_strategy = self.hyper_heuristic.pick_heuristic() elif self._no_improvement == self._run_exact: - self._no_improvement = 0 + # self._no_improvement = 0 pricing_strategy = "Exact" else: pricing_strategy = self._pricing_strategy return pricing_strategy def _update_hyper_heuristic(self, relaxed_cost: float): - best_paths, best_paths_freq = self.masterproblem.get_heuristic_distribution( - ) + best_paths, best_paths_freq = self.masterproblem.get_heuristic_distribution() self.hyper_heuristic.current_performance( new_objective_value=relaxed_cost, produced_column=self._more_routes, active_columns=best_paths_freq, ) self.hyper_heuristic.move_acceptance() - self.hyper_heuristic.update_parameters(self._iteration, - self._no_improvement, - self._no_improvement_iteration) + self.hyper_heuristic.update_parameters(self._iteration, self._no_improvement) def _get_time_remaining(self, mip: bool = False): """ @@ -845,12 +869,14 @@ def _solve_with_clarke_wright(self): ) alg.run() self._best_routes = dict( - zip([r for r in range(len(alg.best_routes))], alg.best_routes)) + zip([r for r in range(len(alg.best_routes))], alg.best_routes) + ) self._best_routes_vehicle_type = dict( zip( [r for r in range(len(alg.best_routes))], [0 for r in range(len(alg.best_routes))], - )) + ) + ) # Preprocessing # @@ -862,9 +888,13 @@ def _get_initial_solution(self): """ self._initial_routes = [] # Run Clarke & Wright if possible - if (not self.time_windows and not self.pickup_delivery and - not self.distribution_collection and not self.mixed_fleet and - not self.periodic): + if ( + not self.time_windows + and not self.pickup_delivery + and not self.distribution_collection + and not self.mixed_fleet + and not self.periodic + ): best_value = 1e10 best_num_vehicles = 1e10 for alpha in [x / 10 for x in range(1, 20)]: @@ -885,15 +915,17 @@ def _get_initial_solution(self): best_value = alg.best_value best_num_vehicles = len(alg.best_routes) logger.info( - "Clarke & Wright solution found with value %s and %s vehicles" % - (best_value, best_num_vehicles)) + "Clarke & Wright solution found with value %s and %s vehicles" + % (best_value, best_num_vehicles) + ) # Run greedy algorithm if possible - alg = _Greedy(self.G, self.load_capacity, self.num_stops, - self.duration) + alg = _Greedy(self.G, self.load_capacity, self.num_stops, self.duration) alg.run() - logger.info("Greedy solution found with value %s and %s vehicles" % - (alg.best_value, len(alg.best_routes))) + logger.info( + "Greedy solution found with value %s and %s vehicles" + % (alg.best_value, len(alg.best_routes)) + ) self._initial_routes += alg.best_routes # TO DO: Run heuristics from VeRyPy for the CVRP @@ -903,9 +935,7 @@ def _get_initial_solution(self): elif self.pickup_delivery: for v in self.G.nodes(): if "request" in self.G.nodes[v]: - obvious_route = [ - "Source", v, self.G.nodes[v]["request"], "Sink" - ] + obvious_route = ["Source", v, self.G.nodes[v]["request"], "Sink"] edges = [ ("Source", v), (v, self.G.nodes[v]["request"]), @@ -939,8 +969,10 @@ def _convert_initial_routes_to_digraphs(self): 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]): + if ( + sum(self.G.nodes[v]["demand"] for v in r) + <= self.load_capacity[k] + ): G.graph["vehicle_type"] = k break else: @@ -989,8 +1021,10 @@ def _add_fixed_costs(self): def _remove_infeasible_arcs_capacities(self): infeasible_arcs = [] for (i, j) in self.G.edges(): - if (self.G.nodes[i]["demand"] + self.G.nodes[j]["demand"] > - self._max_capacity): + if ( + self.G.nodes[i]["demand"] + self.G.nodes[j]["demand"] + > self._max_capacity + ): infeasible_arcs.append((i, j)) self.G.remove_edges_from(infeasible_arcs) @@ -1009,14 +1043,13 @@ def _remove_infeasible_arcs_time_windows(self): # earliest time is coming straight from depot self.G.nodes[v]["lower"] = max( self.G.nodes[v]["lower"], - self.G.nodes["Source"]["lower"] + - self.G.edges["Source", v]["time"], + self.G.nodes["Source"]["lower"] + + self.G.edges["Source", v]["time"], ) # Latest time is going straight to depot self.G.nodes[v]["upper"] = min( self.G.nodes[v]["upper"], - self.G.nodes["Sink"]["upper"] - - self.G.edges[v, "Sink"]["time"], + self.G.nodes["Sink"]["upper"] - self.G.edges[v, "Sink"]["time"], ) self.G.remove_edges_from(infeasible_arcs) @@ -1039,22 +1072,23 @@ def _prune_graph(self): self._remove_infeasible_arcs_time_windows() def _set_zero_attributes(self): - """ Sets attr = 0 if missing """ + """Sets attr = 0 if missing""" for v in self.G.nodes(): for attribute in [ - "demand", - "collect", - "service_time", - "lower", - "upper", + "demand", + "collect", + "service_time", + "lower", + "upper", ]: if attribute not in self.G.nodes[v]: self.G.nodes[v][attribute] = 0 # Ignore demand at Source/Sink if v in ["Source", "Sink"] and self.G.nodes[v]["demand"] > 0: - logger.warning("Demand %s at node %s is ignored." % - (self.G.nodes[v]["demand"], v)) + logger.warning( + "Demand %s at node %s is ignored." % (self.G.nodes[v]["demand"], v) + ) self.G.nodes[v]["demand"] = 0 # Set frequency = 1 if missing @@ -1063,20 +1097,22 @@ def _set_zero_attributes(self): self.G.nodes[v][attribute] = 1 def _set_time_to_zero_if_missing(self): - """ Sets time = 0 if missing """ + """Sets time = 0 if missing""" for (i, j) in self.G.edges(): for attribute in ["time"]: if attribute not in self.G.edges[i, j]: self.G.edges[i, j][attribute] = 0 def _readjust_sink_time_windows(self): - """ Readjusts Sink time windows """ + """Readjusts Sink time windows""" if self.G.nodes["Sink"]["upper"] == 0: self.G.nodes["Sink"]["upper"] = max( - self.G.nodes[u]["upper"] + self.G.nodes[u]["service_time"] + - self.G.edges[u, "Sink"]["time"] - for u in self.G.predecessors("Sink")) + self.G.nodes[u]["upper"] + + self.G.nodes[u]["service_time"] + + self.G.edges[u, "Sink"]["time"] + for u in self.G.predecessors("Sink") + ) def _update_dummy_attributes(self): """Adds dummy attributes on nodes and edges if missing.""" @@ -1105,8 +1141,7 @@ def _best_routes_as_node_lists(self): for route in self._best_routes_as_graphs: node_list = shortest_path(route, "Source", "Sink") self._best_routes[route_id] = node_list - self._best_routes_vehicle_type[route_id] = route.graph[ - "vehicle_type"] + self._best_routes_vehicle_type[route_id] = route.graph["vehicle_type"] route_id += 1 # Merge with preassigned complete routes for route in self._preassignments: @@ -1116,8 +1151,7 @@ def _best_routes_as_node_lists(self): best_cost = 1e10 for k in range(self._vehicle_types): # If different vehicles, the cheapest feasible one is accounted for - cost = sum( - self._H.edges[i, j]["cost"][k] for (i, j) in edges) + cost = sum(self._H.edges[i, j]["cost"][k] for (i, j) in edges) load = sum(self._H.nodes[i]["demand"] for i in route) if cost < best_cost: if self.load_capacity: From 60015dad31390aef5da2ed18fcb37baec76e00a5 Mon Sep 17 00:00:00 2001 From: torressa <23246013+torressa@users.noreply.github.com> Date: Sun, 10 Apr 2022 20:16:07 +0100 Subject: [PATCH 5/9] Added 101 test into benchmarks, WIP with cspy-dev --- benchmarks/tests/graph_issue101 | Bin 0 -> 16631 bytes benchmarks/tests/test_cvrptw_solomon_range.py | 64 ++++++++++++++---- benchmarks/tests/test_issue101.py | 21 ++++++ tests/test_issue101.py | 9 +-- vrpy/subproblem.py | 2 +- vrpy/subproblem_cspy.py | 5 +- 6 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 benchmarks/tests/graph_issue101 create mode 100644 benchmarks/tests/test_issue101.py diff --git a/benchmarks/tests/graph_issue101 b/benchmarks/tests/graph_issue101 new file mode 100644 index 0000000000000000000000000000000000000000..de81fb8054d82cb174291bc9bee9f5834343beb2 GIT binary patch literal 16631 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 b3>yiwivRx#qIKyX`-iI(=HZhgCwKTiHx1}D literal 0 HcmV?d00001 diff --git a/benchmarks/tests/test_cvrptw_solomon_range.py b/benchmarks/tests/test_cvrptw_solomon_range.py index 43c986e..f1bd9d3 100644 --- a/benchmarks/tests/test_cvrptw_solomon_range.py +++ b/benchmarks/tests/test_cvrptw_solomon_range.py @@ -1,10 +1,12 @@ from pytest import fixture +from time import time +import csv from vrpy import VehicleRoutingProblem from benchmarks.solomon_dataset import SolomonDataSet -params = list(range(7, 10)) +params = list(range(7, 70)) @fixture( @@ -16,17 +18,55 @@ def n(request): return request.param +REPS_LP = 1 +REPS_CSPY = 10 + + +def write_avg(n, times_cspy, iter_cspy, times_lp, iter_lp, name="cspy102fwdearly"): + def _avg(l): + return sum(l) / len(l) + + with open(f"benchmarks/results/{name}.csv", "a", newline="") as f: + writer_object = csv.writer(f) + writer_object.writerow( + [n, _avg(times_cspy), _avg(iter_cspy), _avg(times_lp), _avg(iter_lp)] + ) + f.close() + + class TestsSolomon: def test_subproblem(self, n): - data = SolomonDataSet(path="benchmarks/data/cvrptw/", - instance_name="C101.txt", - n_vertices=n) + data = SolomonDataSet( + path="benchmarks/data/cvrptw/", instance_name="C101.txt", n_vertices=n + ) self.G = data.G - self.prob = VehicleRoutingProblem(self.G, - load_capacity=data.max_load, - time_windows=True) - self.prob.solve(cspy=False) - best_value_lp = self.prob.best_value - self.prob.solve(cspy=True) - best_value_cspy = self.prob.best_value - assert int(best_value_lp) == int(best_value_cspy) + best_values_lp = None + lp_iter = [] + times_lp = [] + for r in range(REPS_LP): + prob = VehicleRoutingProblem( + self.G, load_capacity=data.max_load, time_windows=True + ) + start = time() + prob.solve(cspy=False) + best_value_lp = prob.best_value + times_lp.append(time() - start) + lp_iter.append(prob._iteration) + del prob + best_values_cspy = [] + times_cspy = [] + iter_cspy = [] + for r in range(REPS_CSPY): + prob = VehicleRoutingProblem( + self.G, load_capacity=data.max_load, time_windows=True + ) + start = time() + prob.solve(cspy=True, pricing_strategy="Exact") + times_cspy.append(time() - start) + best_values_cspy.append(prob.best_value) + iter_cspy.append(prob._iteration) + prob.check_arrival_time() + prob.check_departure_time() + del prob + assert all(best_value_lp == val_cspy for val_cspy in best_values_cspy) + write_avg(n, times_cspy, iter_cspy, times_lp, lp_iter) diff --git a/benchmarks/tests/test_issue101.py b/benchmarks/tests/test_issue101.py new file mode 100644 index 0000000..24da219 --- /dev/null +++ b/benchmarks/tests/test_issue101.py @@ -0,0 +1,21 @@ +from networkx import DiGraph, read_gpickle +from vrpy import VehicleRoutingProblem + + +class TestIssue101_large: + def setup(self): + G = read_gpickle("benchmarks/tests/graph_issue101") + self.prob = VehicleRoutingProblem(G, load_capacity=80) + self.prob.time_windows = True + + # def test_lp(self): + # self.prob.solve(cspy=False, solver="gurobi") + # self.prob.check_arrival_time() + # self.prob.check_departure_time() + # assert self.prob.best_value == 1832.5756999999999 + + def test_cspy(self): + self.prob.solve(pricing_strategy="Exact") + self.prob.check_arrival_time() + self.prob.check_departure_time() + assert self.prob.best_value == 1832.5756999999999 diff --git a/tests/test_issue101.py b/tests/test_issue101.py index 41e97b8..eaf3d14 100644 --- a/tests/test_issue101.py +++ b/tests/test_issue101.py @@ -1,9 +1,8 @@ -from networkx import DiGraph, read_gpickle +from networkx import DiGraph from vrpy import VehicleRoutingProblem -class TestIssue101: - +class TestIssue101_small: def setup(self): self.G = DiGraph() self.G.add_edge("Source", 1, cost=5) @@ -19,9 +18,7 @@ def setup(self): self.G.nodes[2]["service_time"] = 5 self.G.nodes[1]["demand"] = 8 self.G.nodes[2]["demand"] = 8 - self.prob = VehicleRoutingProblem(self.G, - load_capacity=10, - time_windows=True) + self.prob = VehicleRoutingProblem(self.G, load_capacity=10, time_windows=True) def test_cspy(self): self.prob.solve() diff --git a/vrpy/subproblem.py b/vrpy/subproblem.py index a25b294..308609e 100644 --- a/vrpy/subproblem.py +++ b/vrpy/subproblem.py @@ -198,7 +198,7 @@ def remove_edges_3(self, beta): 4. Remove all edges that do not belong to these paths """ # Normalize weights - max_weight = max([self.G.edges[i, j]["weight"] for (i, j) in self.G.edges()]) + max_weight = max(self.G.edges[i, j]["weight"] for (i, j) in self.G.edges()) min_weight = min(self.G.edges[i, j]["weight"] for (i, j) in self.G.edges()) for edge in self.G.edges(data=True): edge[2]["pos_weight"] = ( diff --git a/vrpy/subproblem_cspy.py b/vrpy/subproblem_cspy.py index a903522..6fb2de8 100644 --- a/vrpy/subproblem_cspy.py +++ b/vrpy/subproblem_cspy.py @@ -248,12 +248,14 @@ def solve(self, time_limit): ) # Run only twice: Once with `elementary=False` check if route already # exists. + thr = min(self.G.edges[i, j]["weight"] for (i, j) in self.G.edges()) + logger.info(f"threshold={thr}") for elementary in [False, True]: alg = BiDirectional( self.sub_G, self.max_res, self.min_res, - threshold=-1e-3, + threshold=thr, direction=direction, time_limit=time_limit - 0.5 if time_limit else None, elementary=elementary, @@ -273,6 +275,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.info(alg.path) if not any( list(new_route.edges()) == list(r.edges()) for r in self.routes ): From 3e2162c79a3ed1191ccdd25e9f15efbb05e8f10e Mon Sep 17 00:00:00 2001 From: torressa <23246013+torressa@users.noreply.github.com> Date: Mon, 11 Apr 2022 20:29:56 +0100 Subject: [PATCH 6/9] Update threshold logic, fix SDC example --- benchmarks/tests/test_cvrptw_solomon.py | 32 ++++--------------------- vrpy/subproblem_cspy.py | 23 +++++++++++++++--- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/benchmarks/tests/test_cvrptw_solomon.py b/benchmarks/tests/test_cvrptw_solomon.py index af8739a..5c05acd 100644 --- a/benchmarks/tests/test_cvrptw_solomon.py +++ b/benchmarks/tests/test_cvrptw_solomon.py @@ -46,35 +46,11 @@ def test_subproblem_lp(self): # e.g., in Feillet et al. (2004) self.prob.solve(**self.solver_args, cspy=False) assert round(self.prob.best_value, -1) in [190, 200] - - def test_schedule_lp(self): - "Tests whether final schedule is time-window feasible" - self.prob.solve(**self.solver_args, cspy=False) - # Check arrival times - for k1, v1 in self.prob.arrival_time.items(): - for k2, v2 in v1.items(): - assert self.G.nodes[k2]["lower"] <= v2 - assert v2 <= self.G.nodes[k2]["upper"] - # Check departure times - for k1, v1 in self.prob.departure_time.items(): - for k2, v2 in v1.items(): - assert self.G.nodes[k2]["lower"] <= v2 - assert v2 <= self.G.nodes[k2]["upper"] + self.prob.check_arrival_time() + self.prob.check_departure_time() def test_subproblem_cspy(self): self.prob.solve(**self.solver_args, cspy=True) assert round(self.prob.best_value, -1) in [190, 200] - - def test_schedule_cspy(self): - "Tests whether final schedule is time-window feasible" - self.prob.solve(**self.solver_args) - # Check departure times - for k1, v1 in self.prob.departure_time.items(): - for k2, v2 in v1.items(): - assert self.G.nodes[k2]["lower"] <= v2 - assert v2 <= self.G.nodes[k2]["upper"] - # Check arrival times - for k1, v1 in self.prob.arrival_time.items(): - for k2, v2 in v1.items(): - assert self.G.nodes[k2]["lower"] <= v2 - assert v2 <= self.G.nodes[k2]["upper"] + self.prob.check_arrival_time() + self.prob.check_departure_time() diff --git a/vrpy/subproblem_cspy.py b/vrpy/subproblem_cspy.py index 6fb2de8..10873a5 100644 --- a/vrpy/subproblem_cspy.py +++ b/vrpy/subproblem_cspy.py @@ -214,6 +214,8 @@ def __init__(self, *args, exact, pickup_delivery_pairs): # Initialize max feasible arrival time self.T = 0 self.total_cost = None + self._avg_path_len = 1 + self._iters = 1 # @profile def solve(self, time_limit): @@ -248,9 +250,18 @@ def solve(self, time_limit): ) # Run only twice: Once with `elementary=False` check if route already # exists. - thr = min(self.G.edges[i, j]["weight"] for (i, j) in self.G.edges()) - logger.info(f"threshold={thr}") - for elementary in [False, True]: + + s = [False, True] if not self.distribution_collection else [True] + for elementary in s: + if elementary: + # Use threshold if non-elementary (safe-guard against large + # instances) + thr = self._avg_path_len * min( + self.G.edges[i, j]["weight"] for (i, j) in self.G.edges() + ) + logger.info(f"threshold={thr}") + else: + thr = None alg = BiDirectional( self.sub_G, self.max_res, @@ -276,6 +287,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.info(alg.path) + path_len = len(alg.path) if not any( list(new_route.edges()) == list(r.edges()) for r in self.routes ): @@ -284,6 +296,11 @@ def solve(self, time_limit): self.total_cost = new_route.graph["cost"] logger.debug("reduced cost = %s", alg.total_cost) logger.debug("real cost = %s", self.total_cost) + if path_len > 2: + self._avg_path_len += ( + path_len - self._avg_path_len + ) / self._iters + self._iters += 1 break else: logger.info("Route already found, finding elementary one") From 2f99cd17abba7356b159033da76612cda9146a53 Mon Sep 17 00:00:00 2001 From: torressa <23246013+torressa@users.noreply.github.com> Date: Wed, 13 Apr 2022 17:29:28 +0100 Subject: [PATCH 7/9] Force non-elementary routes to 0 in MIP --- vrpy/master_solve_pulp.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vrpy/master_solve_pulp.py b/vrpy/master_solve_pulp.py index b00272d..b508c91 100644 --- a/vrpy/master_solve_pulp.py +++ b/vrpy/master_solve_pulp.py @@ -192,9 +192,10 @@ def _solve(self, relax: bool, time_limit: Optional[int]): # Set route variables to integer for var in self.y.values(): # Disallow routes that visit multiple nodes - # if "non" in var.name: - # var.upBound = 0 - # var.lowBound = 0 + if "non" in var.name: + print(var.name) + var.upBound = 0 + var.lowBound = 0 var.cat = pulp.LpInteger # Force vehicle bound artificial variable to 0 for var in self.dummy_bound.values(): From 92d50241ca8ea8d7f94d60dcbe7c35432ea174d6 Mon Sep 17 00:00:00 2001 From: torressa <23246013+torressa@users.noreply.github.com> Date: Fri, 15 Apr 2022 17:31:52 +0100 Subject: [PATCH 8/9] Clean up (remove pickup delivery stuff for cspy) - Replace `exact` option with `elementary` option in solving attributes --- benchmarks/run.py | 4 +- benchmarks/tests/test_examples.py | 2 +- benchmarks/tests/test_issue101.py | 2 - examples/pdp.py | 7 +-- tests/test_toy.py | 74 +++++++++++++------------------ vrpy/checks.py | 3 +- vrpy/master_solve_pulp.py | 1 - vrpy/subproblem_cspy.py | 59 +++++++----------------- vrpy/vrp.py | 41 +++++++---------- 9 files changed, 71 insertions(+), 122 deletions(-) diff --git a/benchmarks/run.py b/benchmarks/run.py index 811fdd7..907c2b6 100644 --- a/benchmarks/run.py +++ b/benchmarks/run.py @@ -4,7 +4,9 @@ from multiprocessing import Pool, cpu_count from pathlib import Path from typing import List, Dict, Union + from networkx import DiGraph + from benchmarks.augerat_dataset import AugeratDataSet from benchmarks.solomon_dataset import SolomonDataSet from benchmarks.utils.csv_table import CsvTable @@ -202,7 +204,7 @@ def _run_single_problem(path_to_instance: Path, **kwargs): def main(): - """ Run parallel or series""" + """Run parallel or series""" if SERIES: run_series() else: diff --git a/benchmarks/tests/test_examples.py b/benchmarks/tests/test_examples.py index eca3d34..dec16a6 100644 --- a/benchmarks/tests/test_examples.py +++ b/benchmarks/tests/test_examples.py @@ -49,7 +49,7 @@ def test_cvrp_dive_lp(self): def test_cvrp_dive_cspy(self): self.prob.load_capacity = 15 - self.prob.solve(pricing_strategy="BestEdges1", dive=True, exact=True) + self.prob.solve(pricing_strategy="BestEdges1", dive=True) assert int(self.prob.best_value) == 6208 def test_vrptw_dive_lp(self): diff --git a/benchmarks/tests/test_issue101.py b/benchmarks/tests/test_issue101.py index 24da219..7a27ebf 100644 --- a/benchmarks/tests/test_issue101.py +++ b/benchmarks/tests/test_issue101.py @@ -12,10 +12,8 @@ def setup(self): # self.prob.solve(cspy=False, solver="gurobi") # self.prob.check_arrival_time() # self.prob.check_departure_time() - # assert self.prob.best_value == 1832.5756999999999 def test_cspy(self): self.prob.solve(pricing_strategy="Exact") self.prob.check_arrival_time() self.prob.check_departure_time() - assert self.prob.best_value == 1832.5756999999999 diff --git a/examples/pdp.py b/examples/pdp.py index e2691bb..6591557 100644 --- a/examples/pdp.py +++ b/examples/pdp.py @@ -20,11 +20,8 @@ if __name__ == "__main__": - prob = VehicleRoutingProblem(G, - load_capacity=6, - pickup_delivery=True, - num_stops=6) - prob.solve(cspy=True, exact=True, pricing_strategy="Exact") + prob = VehicleRoutingProblem(G, load_capacity=6, pickup_delivery=True, num_stops=6) + prob.solve(cspy=False, pricing_strategy="Exact") print(prob.best_value) print(prob.best_routes) for (u, v) in PICKUPS_DELIVERIES: diff --git a/tests/test_toy.py b/tests/test_toy.py index b485e36..694cbf0 100644 --- a/tests/test_toy.py +++ b/tests/test_toy.py @@ -7,7 +7,6 @@ class TestsToy: - def setup(self): """ Creates a toy graph. @@ -42,7 +41,7 @@ def test_cspy_stops(self): ["Source", 4, 5, "Sink"], ] assert set(prob.best_routes_cost.values()) == {30, 40} - prob.solve(exact=False) + prob.solve() assert prob.best_value == 70 def test_cspy_stops_capacity(self): @@ -58,11 +57,8 @@ def test_cspy_stops_capacity_duration(self): """Tests column generation procedure on toy graph with stop, capacity and duration constraints """ - prob = VehicleRoutingProblem(self.G, - num_stops=3, - load_capacity=10, - duration=62) - prob.solve(exact=False) + prob = VehicleRoutingProblem(self.G, num_stops=3, load_capacity=10, duration=62) + prob.solve() assert prob.best_value == 85 assert set(prob.best_routes_duration.values()) == {41, 62} assert prob.node_load[1]["Sink"] in [5, 10] @@ -187,11 +183,9 @@ def test_clarke_wright(self): ######### def test_all(self): - prob = VehicleRoutingProblem(self.G, - num_stops=3, - time_windows=True, - duration=63, - load_capacity=10) + prob = VehicleRoutingProblem( + self.G, num_stops=3, time_windows=True, duration=63, load_capacity=10 + ) prob.solve(cspy=False) lp_best = prob.best_value prob.solve(cspy=True) @@ -216,9 +210,7 @@ def test_knapsack(self): def test_pricing_strategies(self): sol = [] - for strategy in [ - "Exact", "BestPaths", "BestEdges1", "BestEdges2", "Hyper" - ]: + for strategy in ["Exact", "BestPaths", "BestEdges1", "BestEdges2", "Hyper"]: prob = VehicleRoutingProblem(self.G, num_stops=4) prob.solve(pricing_strategy=strategy) sol.append(prob.best_value) @@ -271,22 +263,22 @@ def test_pick_up_delivery_lp(self): prob.solve(pricing_strategy="Exact", cspy=False) assert prob.best_value == 65 - def test_pick_up_delivery_cspy(self): - self.G.nodes[2]["request"] = 5 - self.G.nodes[2]["demand"] = 10 - self.G.nodes[3]["demand"] = 10 - self.G.nodes[3]["request"] = 4 - self.G.nodes[4]["demand"] = -10 - self.G.nodes[5]["demand"] = -10 - self.G.add_edge(2, 5, cost=10) - self.G.remove_node(1) - prob = VehicleRoutingProblem( - self.G, - load_capacity=15, - pickup_delivery=True, - ) - prob.solve(pricing_strategy="Exact", cspy=True) - assert prob.best_value == 65 + # def test_pick_up_delivery_cspy(self): + # self.G.nodes[2]["request"] = 5 + # self.G.nodes[2]["demand"] = 10 + # self.G.nodes[3]["demand"] = 10 + # self.G.nodes[3]["request"] = 4 + # self.G.nodes[4]["demand"] = -10 + # self.G.nodes[5]["demand"] = -10 + # self.G.add_edge(2, 5, cost=10) + # self.G.remove_node(1) + # prob = VehicleRoutingProblem( + # self.G, + # load_capacity=15, + # pickup_delivery=True, + # ) + # prob.solve(pricing_strategy="Exact", cspy=True) + # assert prob.best_value == 65 def test_distribution_collection(self): self.G.nodes[1]["collect"] = 12 @@ -310,20 +302,17 @@ def test_fixed_cost(self): assert set(prob.best_routes_cost.values()) == {30 + 100, 40 + 100} def test_drop_nodes(self): - prob = VehicleRoutingProblem(self.G, - num_stops=3, - num_vehicles=1, - drop_penalty=100) + prob = VehicleRoutingProblem( + self.G, num_stops=3, num_vehicles=1, drop_penalty=100 + ) prob.solve() assert prob.best_value == 240 assert prob.best_routes == {1: ["Source", 1, 2, 3, "Sink"]} def test_num_vehicles_use_all(self): - prob = VehicleRoutingProblem(self.G, - num_stops=3, - num_vehicles=2, - use_all_vehicles=True, - drop_penalty=100) + prob = VehicleRoutingProblem( + self.G, num_stops=3, num_vehicles=2, use_all_vehicles=True, drop_penalty=100 + ) prob.solve() assert len(prob.best_routes) == 2 prob.num_vehicles = 3 @@ -344,10 +333,7 @@ def test_periodic(self): frequency += 1 assert frequency == 2 assert prob.schedule[0] in [[1], [1, 2]] - prob = VehicleRoutingProblem(self.G, - num_stops=2, - periodic=2, - num_vehicles=1) + prob = VehicleRoutingProblem(self.G, num_stops=2, periodic=2, num_vehicles=1) prob.solve() assert prob.schedule == {} diff --git a/vrpy/checks.py b/vrpy/checks.py index adb0cdd..e4fbf6d 100644 --- a/vrpy/checks.py +++ b/vrpy/checks.py @@ -175,8 +175,7 @@ def check_consistency( # pickup delivery requires cspy=False if cspy and pickup_delivery: - pass - # raise NotImplementedError("pickup_delivery option requires cspy=False.") + raise NotImplementedError("pickup_delivery option requires cspy=False.") # pickup delivery requires pricing_stragy="Exact" if pickup_delivery and pricing_strategy != "Exact": pricing_strategy = "Exact" diff --git a/vrpy/master_solve_pulp.py b/vrpy/master_solve_pulp.py index b508c91..3bcbe97 100644 --- a/vrpy/master_solve_pulp.py +++ b/vrpy/master_solve_pulp.py @@ -193,7 +193,6 @@ def _solve(self, relax: bool, time_limit: Optional[int]): for var in self.y.values(): # Disallow routes that visit multiple nodes if "non" in var.name: - print(var.name) var.upBound = 0 var.lowBound = 0 var.cat = pulp.LpInteger diff --git a/vrpy/subproblem_cspy.py b/vrpy/subproblem_cspy.py index 10873a5..789f044 100644 --- a/vrpy/subproblem_cspy.py +++ b/vrpy/subproblem_cspy.py @@ -21,25 +21,20 @@ def __init__( max_res, time_windows, distribution_collection, - pickup_delivery, T, resources, - pickup_delivery_pairs, ): REFCallback.__init__(self) # Set attributes for use in REF functions self._max_res = max_res self._time_windows = time_windows self._distribution_collection = distribution_collection - self._pickup_delivery = pickup_delivery self._T = T self._resources = resources - self._pickup_delivery_pairs = pickup_delivery_pairs # Set later self._sub_G = None self._source_id = None self._sink_id = None - self._matched_delivery_to_pickup_nodes = {} def REF_fwd(self, cumul_res, tail, head, edge_res, partial_path, cumul_cost): new_res = list(cumul_res) @@ -74,25 +69,6 @@ def REF_fwd(self, cumul_res, tail, head, edge_res, partial_path, cumul_cost): # Delivery new_res[5] = max(new_res[5] + self._sub_G.nodes[j]["demand"], new_res[4]) - _partial_path = list(partial_path) - if self._pickup_delivery: - open_requests = [ - n - for n in _partial_path - if "request" in self._sub_G.nodes[n] - and self._sub_G.nodes[n]["request"] not in _partial_path - ] - if len(open_requests) > 0: - pickup_node = None - pickup_nodes = [u for (u, v) in self._pickup_delivery_pairs if v == j] - if len(pickup_nodes) == 1: - pickup_node = pickup_nodes[0] - if pickup_node is not None and pickup_node not in open_requests: - new_res[6] = 1.0 - else: - new_res[6] = 0.0 - elif len(open_requests) != 0 and j == self._sink_id: - new_res[6] = 1.0 return new_res def REF_bwd(self, cumul_res, tail, head, edge_res, partial_path, cumul_cost): @@ -175,12 +151,11 @@ class _SubProblemCSPY(_SubProblemBase): Inherits problem parameters from `SubproblemBase` """ - def __init__(self, *args, exact, pickup_delivery_pairs): + def __init__(self, *args, elementary): """Initializes resources.""" # Pass arguments to base super(_SubProblemCSPY, self).__init__(*args) - # self.exact = exact - self.pickup_delivery_pairs = pickup_delivery_pairs + self.elementary = elementary # Resource names self.resources = [ "stops/mono", @@ -189,7 +164,6 @@ def __init__(self, *args, exact, pickup_delivery_pairs): "time windows", "collect", "deliver", - "pickup_delivery", ] # Set number of resources as attribute of graph self.sub_G.graph["n_res"] = len(self.resources) @@ -206,7 +180,6 @@ def __init__(self, *args, exact, pickup_delivery_pairs): 1, # time windows total_demand, # pickup total_demand, # deliver - 1, # pickup_delivery ] # Initialize cspy edge attributes for edge in self.sub_G.edges(data=True): @@ -214,7 +187,9 @@ def __init__(self, *args, exact, pickup_delivery_pairs): # Initialize max feasible arrival time self.T = 0 self.total_cost = None + # Average length of a path self._avg_path_len = 1 + # Iteration counter self._iters = 1 # @profile @@ -223,10 +198,10 @@ def solve(self, time_limit): Solves the subproblem with cspy. Time limit is reduced by 0.5 seconds as a safety window. - Resolves until: - 1. heuristic algorithm gives a new route (column with -ve reduced cost); - 2. exact algorithm gives a new route; - 3. neither heuristic nor exact give a new route. + Resolves at most twice: + 1. using elementary = False, + 2. using elementary = True, and threshold, if a route has already been + found previously. """ if not self.run_subsolve: return self.routes, False @@ -251,7 +226,11 @@ def solve(self, time_limit): # Run only twice: Once with `elementary=False` check if route already # exists. - s = [False, True] if not self.distribution_collection else [True] + s = ( + [False, True] + if (not self.distribution_collection and not self.elementary) + else [True] + ) for elementary in s: if elementary: # Use threshold if non-elementary (safe-guard against large @@ -259,9 +238,11 @@ def solve(self, time_limit): thr = self._avg_path_len * min( self.G.edges[i, j]["weight"] for (i, j) in self.G.edges() ) - logger.info(f"threshold={thr}") else: thr = None + logger.debug( + f"Solving subproblem using elementary={elementary}, threshold={thr}, direction={direction}" + ) alg = BiDirectional( self.sub_G, self.max_res, @@ -286,7 +267,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.info(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 @@ -326,10 +307,6 @@ def formulate(self): # Time windows feasibility self.max_res[3] = 0 # Maximum feasible arrival time - # for v in self.sub_G.nodes(): - # print("node = ", v, "lb = ", self.sub_G.nodes[v]["lower"], - # "ub = ", self.sub_G.nodes[v]["upper"]) - self.T = max( self.sub_G.nodes[v]["upper"] + self.sub_G.nodes[v]["service_time"] @@ -396,10 +373,8 @@ def get_REF(self): self.max_res, self.time_windows, self.distribution_collection, - self.pickup_delivery, self.T, self.resources, - self.pickup_delivery_pairs, ) else: # Use default diff --git a/vrpy/vrp.py b/vrpy/vrp.py index be51730..883eea9 100644 --- a/vrpy/vrp.py +++ b/vrpy/vrp.py @@ -1,3 +1,4 @@ +import sys import logging from time import time from typing import List, Union @@ -5,12 +6,14 @@ from networkx import DiGraph, shortest_path # draw_networkx from vrpy.greedy import _Greedy -from vrpy.master_solve_pulp import _MasterSolvePulp +from vrpy.schedule import _Schedule from vrpy.subproblem_lp import _SubProblemLP from vrpy.subproblem_cspy import _SubProblemCSPY +from vrpy.hyper_heuristic import _HyperHeuristic +from vrpy.master_solve_pulp import _MasterSolvePulp from vrpy.subproblem_greedy import _SubProblemGreedy +from vrpy.preprocessing import get_num_stops_upper_bound from vrpy.clarke_wright import _ClarkeWright, _RoundTrip -from vrpy.schedule import _Schedule from vrpy.checks import ( check_arguments, check_consistency, @@ -22,12 +25,10 @@ check_clarke_wright_compatibility, check_preassignments, ) -from vrpy.preprocessing import get_num_stops_upper_bound -from vrpy.hyper_heuristic import _HyperHeuristic logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) +logging.basicConfig(level=logging.INFO, stream=sys.stdout) class VehicleRoutingProblem: @@ -122,8 +123,8 @@ def __init__( self._solver: str = None self._time_limit: int = None self._pricing_strategy: str = None - self._exact: bool = None self._cspy: bool = None + self._elementary: bool = None self._dive: bool = None self._greedy: bool = None self._max_iter: int = None @@ -141,7 +142,6 @@ def __init__( self._initial_routes = [] self._preassignments = [] self._dropped_nodes = [] - self._pickup_delivery_pairs = [] # Parameters for final solution self._best_value = None self._best_routes = [] @@ -156,7 +156,7 @@ def solve( preassignments=None, pricing_strategy="BestEdges1", cspy=True, - exact=False, + elementary=False, time_limit=None, solver="cbc", dive=False, @@ -188,10 +188,12 @@ def solve( cspy (bool, optional): True if cspy is used for subproblem. Defaults to True. - exact (bool, optional): - True if only cspy's exact algorithm is used to generate columns. - Otherwise, heuristics will be used until they produce +ve - reduced cost columns, after which the exact algorithm is used. + elementary (bool, optional): + True if only cspy's elementary algorithm is to be used. + Otherwise, a mix is used: only use elementary when a route is + repeated. In this case, also a threshold is used to avoid + running for too long. If dive=True then this is also forced to + be True. Defaults to False. time_limit (int, optional): Maximum number of seconds allowed for solving (for finding columns). @@ -227,8 +229,8 @@ def solve( self._solver = solver self._time_limit = time_limit self._pricing_strategy = pricing_strategy - self._exact = exact self._cspy = cspy + self._elementary = elementary if not dive else True self._dive = False self._greedy = greedy self._max_iter = max_iter @@ -236,15 +238,6 @@ def solve( self._heuristic_only = heuristic_only if self._pricing_strategy == "Hyper": self.hyper_heuristic = _HyperHeuristic() - - # Extract pairs of (pickup, delivery) nodes from graph - if self.pickup_delivery: - self._pickup_delivery_pairs = [ - (i, self.G.nodes[i]["request"]) - for i in self.G.nodes() - if "request" in self.G.nodes[i] - ] - self._start_time = time() if preassignments: self._preassignments = preassignments @@ -568,6 +561,7 @@ def _find_columns(self): # TODO: parallel # One subproblem per vehicle type + for vehicle in range(self._vehicle_types): # Solve pricing problem with randomised greedy algorithm if ( @@ -828,8 +822,7 @@ def _def_subproblem( self.distribution_collection, pricing_strategy, pricing_parameter, - exact=self._exact, - pickup_delivery_pairs=self._pickup_delivery_pairs, + elementary=self._elementary, ) else: # As LP From 7dd4d009f30e7556b43847a3ec4a479243b648ff Mon Sep 17 00:00:00 2001 From: torressa <23246013+torressa@users.noreply.github.com> Date: Fri, 15 Apr 2022 18:17:33 +0100 Subject: [PATCH 9/9] Fix schedule logic #101 --- tests/test_toy.py | 26 ++++++-------------------- vrpy/vrp.py | 9 +++++---- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/tests/test_toy.py b/tests/test_toy.py index 694cbf0..5d7da96 100644 --- a/tests/test_toy.py +++ b/tests/test_toy.py @@ -85,16 +85,10 @@ def test_cspy_schedule(self): time_windows=True, ) prob.solve() - # Check departure times - for k1, v1 in prob.departure_time.items(): - for k2, v2 in v1.items(): - assert self.G.nodes[k2]["lower"] <= v2 - assert v2 <= self.G.nodes[k2]["upper"] - # Check arrival times - for k1, v1 in prob.arrival_time.items(): - for k2, v2 in v1.items(): - assert self.G.nodes[k2]["lower"] <= v2 - assert v2 <= self.G.nodes[k2]["upper"] + assert prob.departure_time[1]["Source"] == 0 + assert prob.arrival_time[1]["Sink"] in [41, 62] + prob.check_arrival_time() + prob.check_departure_time() ############### # subsolve lp # @@ -143,16 +137,8 @@ def test_LP_schedule(self): time_windows=True, ) prob.solve(cspy=False) - # Check departure times - for k1, v1 in prob.departure_time.items(): - for k2, v2 in v1.items(): - assert self.G.nodes[k2]["lower"] <= v2 - assert v2 <= self.G.nodes[k2]["upper"] - # Check arrival times - for k1, v1 in prob.arrival_time.items(): - for k2, v2 in v1.items(): - assert self.G.nodes[k2]["lower"] <= v2 - assert v2 <= self.G.nodes[k2]["upper"] + prob.check_arrival_time() + prob.check_departure_time() def test_LP_stops_elementarity(self): """Tests column generation procedure on toy graph""" diff --git a/vrpy/vrp.py b/vrpy/vrp.py index 883eea9..11a0690 100644 --- a/vrpy/vrp.py +++ b/vrpy/vrp.py @@ -403,9 +403,8 @@ def departure_time(self): for j in range(1, len(route)): tail = route[j - 1] head = route[j] - departure[i][head] = min( - arrival[i][head] + self._H.nodes[head]["service_time"], - self._H.nodes[head]["upper"], + departure[i][head] = ( + arrival[i][head] + self._H.nodes[head]["service_time"] ) return departure @@ -414,7 +413,9 @@ def check_departure_time(self): for k1, v1 in self.departure_time.items(): for k2, v2 in v1.items(): assert self.G.nodes[k2]["lower"] <= v2 - assert v2 <= self.G.nodes[k2]["upper"] + # Upper TW should not be checked as lower + service_time > upper + # in many cases. Also not enforced at the subproblem level. + # assert v2 <= self.G.nodes[k2]["upper"] @property def schedule(self):