Skip to content

Commit

Permalink
Updated relax_allowed_vehicle_indices.
Browse files Browse the repository at this point in the history
Changes made:
- fixed the behavior; add costs to vehicles that were NOT allowed.
- renamed it to `soften_allowed_vehicle_indices`.
  • Loading branch information
ondrasej committed Dec 4, 2023
1 parent 4a229e1 commit a3636b4
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 34 deletions.
47 changes: 26 additions & 21 deletions python/cfr/json/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
44 changes: 31 additions & 13 deletions python/cfr/json/transforms_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,40 +334,42 @@ 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):
shipment: cfr_json.Shipment = {
"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],
},
)

Expand All @@ -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],
},
)

Expand All @@ -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],
},
)

Expand Down

0 comments on commit a3636b4

Please sign in to comment.