From a3636b41522d87fbdac5afdadb6d61fa855b927e Mon Sep 17 00:00:00 2001 From: Ondrej Sykora Date: Mon, 4 Dec 2023 08:23:49 +0000 Subject: [PATCH] Updated relax_allowed_vehicle_indices. Changes made: - fixed the behavior; add costs to vehicles that were NOT allowed. - renamed it to `soften_allowed_vehicle_indices`. --- python/cfr/json/transforms.py | 47 +++++++++++++++++------------- python/cfr/json/transforms_test.py | 44 +++++++++++++++++++--------- 2 files changed, 57 insertions(+), 34 deletions(-) diff --git a/python/cfr/json/transforms.py b/python/cfr/json/transforms.py index 8d90bef6..7fe4b1b7 100644 --- a/python/cfr/json/transforms.py +++ b/python/cfr/json/transforms.py @@ -193,46 +193,51 @@ def remove_vehicles( ] -def relax_allowed_vehicle_indices( - shipment: cfr_json.Shipment, cost: float +def soften_allowed_vehicle_indices( + shipment: cfr_json.Shipment, cost: float, num_vehicles: int ) -> None: - """Relaxes the hard vehicle-shipment constraints in the model. + """Softens the hard vehicle-shipment constraints in the model. When `cost > 0`, replaces the hard constraints with equivalent soft constraints where the cost of violating the vehicle-shipment constraint is - `cost`. When `cost == 0`, just removes `allowedVehicleIndices` from the model. + `cost`. When `cost == 0`, removes `allowedVehicleIndices` from the model. Args: - shipment: The shipment in which the allowed vehicle indices are relaxed. + shipment: The shipment in which the allowed vehicle indices are softened. cost: The cost of violating a vehicle-shipment constraint. + num_vehicles: The number of vehicles in the model. Raises: ValueError: When `cost < 0`. """ if cost < 0: raise ValueError("cost must be non-negative.") - allowed_vehicles = shipment.get("allowedVehicleIndices") - shipment.pop("allowedVehicleIndices", None) + allowed_vehicles = shipment.pop("allowedVehicleIndices", None) if allowed_vehicles is None or cost == 0: return costs_per_vehicle = shipment.get("costsPerVehicle", ()) costs_per_vehicle_indices = shipment.get("costsPerVehicleIndices", ()) - all_vehicles_have_cost = costs_per_vehicle and not costs_per_vehicle_indices - if all_vehicles_have_cost: + if costs_per_vehicle and not costs_per_vehicle_indices: + if len(costs_per_vehicle) != num_vehicles: + raise ValueError( + "`shipment['costsPerVehicleIndices']` is not used, but `num_vehicles`" + " is different from`len(shipment['costsPerVehicle'])`." + ) costs_per_vehicle_indices = range(len(costs_per_vehicle)) costs_per_vehicle_map = collections.defaultdict( float, zip(costs_per_vehicle_indices, costs_per_vehicle) ) - for vehicle in allowed_vehicles: - costs_per_vehicle_map[vehicle] += cost - # NOTE(ondrasej): The following relies on Python's deterministic iteration - # order in dicts, where both keys() and values() iterate return the items from - # the dict in insertion order. - if all_vehicles_have_cost: - shipment["costsPerVehicle"] = list(costs_per_vehicle_map.values()) + for vehicle in range(num_vehicles): + if vehicle not in allowed_vehicles: + costs_per_vehicle_map[vehicle] += cost + + indices, costs = zip(*sorted(costs_per_vehicle_map.items())) + if num_vehicles == len(costs_per_vehicle_map): + shipment["costsPerVehicle"] = list(costs) + shipment.pop("costsPerVehicleIndices", None) else: - shipment["costsPerVehicleIndices"] = list(costs_per_vehicle_map.keys()) - shipment["costsPerVehicle"] = list(costs_per_vehicle_map.values()) + shipment["costsPerVehicleIndices"] = list(indices) + shipment["costsPerVehicle"] = list(costs) def remove_load_limits(model: cfr_json.ShipmentModel) -> None: @@ -250,10 +255,10 @@ def remove_pickups(model: cfr_json.ShipmentModel) -> None: this earliest pickup time. When there are pickup visit costs, adds the minimal pickup visit cost to all - delivery visit costs to preserve the pickup costs in the relaxed model to some - extent. + delivery visit costs to preserve the pickup costs in the modified model to + some extent. - The result of the function is a relaxed version of the original model that is + The result of the function is a modified version of the original model that is not necessarily equivalent to the original model, but is very likely easier to solve. diff --git a/python/cfr/json/transforms_test.py b/python/cfr/json/transforms_test.py index 7fde74bf..ccf57caa 100644 --- a/python/cfr/json/transforms_test.py +++ b/python/cfr/json/transforms_test.py @@ -334,18 +334,20 @@ def test_infeasible_shipment(self): transforms.remove_vehicles(model, (1,)) -class RelaxAllowedVehicleIndicesTest(unittest.TestCase): - """Tests for relax_allowed_vehicle_indices.""" +class SoftenAllowedVehicleIndicesTest(unittest.TestCase): + """Tests for soften_allowed_vehicle_indices.""" maxDiff = None def test_negative_cost(self): with self.assertRaises(ValueError): - transforms.relax_allowed_vehicle_indices({}, -1) + transforms.soften_allowed_vehicle_indices({}, cost=-1, num_vehicles=2) def test_no_allowed_vehicle_indices(self): shipment: cfr_json.Shipment = {"label": "S002"} - transforms.relax_allowed_vehicle_indices(shipment, 100) + transforms.soften_allowed_vehicle_indices( + shipment, cost=100, num_vehicles=2 + ) self.assertEqual(shipment, {"label": "S002"}) def test_zero_cost(self): @@ -353,21 +355,21 @@ def test_zero_cost(self): "label": "S001", "allowedVehicleIndices": [0, 1, 2, 3], } - transforms.relax_allowed_vehicle_indices(shipment, 0) + transforms.soften_allowed_vehicle_indices(shipment, cost=0, num_vehicles=10) self.assertEqual(shipment, {"label": "S001"}) def test_with_cost_and_no_existing_costs(self): shipment: cfr_json.Shipment = { "label": "S003", - "allowedVehicleIndices": [2, 3, 10], + "allowedVehicleIndices": [2, 3], } - transforms.relax_allowed_vehicle_indices(shipment, 10) + transforms.soften_allowed_vehicle_indices(shipment, cost=10, num_vehicles=5) self.assertEqual( shipment, { "label": "S003", "costsPerVehicle": [10, 10, 10], - "costsPerVehicleIndices": [2, 3, 10], + "costsPerVehicleIndices": [0, 1, 4], }, ) @@ -378,13 +380,13 @@ def test_with_cost_and_existing_costs_with_indices(self): "costsPerVehicle": [100, 300, 400], "costsPerVehicleIndices": [1, 3, 4], } - transforms.relax_allowed_vehicle_indices(shipment, 10) + transforms.soften_allowed_vehicle_indices(shipment, cost=10, num_vehicles=7) self.assertEqual( shipment, { "label": "S003", - "costsPerVehicle": [100, 310, 400, 10, 10], - "costsPerVehicleIndices": [1, 3, 4, 2, 5], + "costsPerVehicle": [10, 110, 300, 410, 10], + "costsPerVehicleIndices": [0, 1, 3, 4, 6], }, ) @@ -394,12 +396,28 @@ def test_with_cost_and_existing_costs_without_indices(self): "allowedVehicleIndices": [2, 3], "costsPerVehicle": [100, 200, 300, 400, 500], } - transforms.relax_allowed_vehicle_indices(shipment, 10) + transforms.soften_allowed_vehicle_indices(shipment, cost=10, num_vehicles=5) + self.assertEqual( + shipment, + { + "label": "S003", + "costsPerVehicle": [110, 210, 300, 400, 510], + }, + ) + + def test_adding_cost_to_all_vehicles(self): + shipment: cfr_json.Shipment = { + "label": "S003", + "allowedVehicleIndices": [2, 3], + "costsPerVehicle": [200, 300, 400], + "costsPerVehicleIndices": [2, 3, 4], + } + transforms.soften_allowed_vehicle_indices(shipment, cost=10, num_vehicles=5) self.assertEqual( shipment, { "label": "S003", - "costsPerVehicle": [100, 200, 310, 410, 500], + "costsPerVehicle": [10, 10, 200, 300, 410], }, )