Spaces:
Sleeping
Sleeping
| from solverforge_legacy.solver.test import ConstraintVerifier | |
| from order_picking.domain import ( | |
| Product, Order, OrderItem, Trolley, TrolleyStep, OrderPickingSolution | |
| ) | |
| from order_picking.warehouse import WarehouseLocation, Side, new_shelving_id, Column, Row | |
| from order_picking.constraints import ( | |
| define_constraints, | |
| required_number_of_buckets, | |
| minimize_order_split_by_trolley, | |
| minimize_distance_from_previous_step, | |
| minimize_distance_from_last_step_to_origin, | |
| ) | |
| # Test locations | |
| LOCATION_A1_LEFT_5 = WarehouseLocation( | |
| shelving_id=new_shelving_id(Column.COL_A, Row.ROW_1), | |
| side=Side.LEFT, | |
| row=5 | |
| ) | |
| LOCATION_A1_LEFT_8 = WarehouseLocation( | |
| shelving_id=new_shelving_id(Column.COL_A, Row.ROW_1), | |
| side=Side.LEFT, | |
| row=8 | |
| ) | |
| LOCATION_B1_LEFT_3 = WarehouseLocation( | |
| shelving_id=new_shelving_id(Column.COL_B, Row.ROW_1), | |
| side=Side.LEFT, | |
| row=3 | |
| ) | |
| LOCATION_E3_RIGHT_9 = WarehouseLocation( | |
| shelving_id=new_shelving_id(Column.COL_E, Row.ROW_3), | |
| side=Side.RIGHT, | |
| row=9 | |
| ) | |
| # Bucket capacity for tests (48,000 cm3) | |
| BUCKET_CAPACITY = 48000 | |
| constraint_verifier = ConstraintVerifier.build( | |
| define_constraints, OrderPickingSolution, Trolley, TrolleyStep | |
| ) | |
| def create_product(id: str, volume: int, location: WarehouseLocation) -> Product: | |
| return Product(id=id, name=f"Product {id}", volume=volume, location=location) | |
| def create_order_with_items(order_id: str, products: list[Product]) -> tuple[Order, list[OrderItem]]: | |
| order = Order(id=order_id, items=[]) | |
| items = [] | |
| for i, product in enumerate(products): | |
| item = OrderItem(id=f"{order_id}-{i}", order=order, product=product) | |
| order.items.append(item) | |
| items.append(item) | |
| return order, items | |
| def connect(trolley: Trolley, *steps: TrolleyStep): | |
| """Set up trolley-step relationships.""" | |
| trolley.steps = list(steps) | |
| for i, step in enumerate(steps): | |
| step.trolley = trolley | |
| step.previous_step = steps[i - 1] if i > 0 else None | |
| step.next_step = steps[i + 1] if i < len(steps) - 1 else None | |
| class TestRequiredNumberOfBuckets: | |
| def test_not_penalized_when_under_capacity(self): | |
| """Trolley with enough buckets should not be penalized.""" | |
| trolley = Trolley( | |
| id="1", | |
| bucket_count=4, | |
| bucket_capacity=BUCKET_CAPACITY, | |
| location=LOCATION_A1_LEFT_5 | |
| ) | |
| product = create_product("p1", 10000, LOCATION_B1_LEFT_3) | |
| order, items = create_order_with_items("o1", [product]) | |
| step = TrolleyStep(id="s1", order_item=items[0]) | |
| connect(trolley, step) | |
| constraint_verifier.verify_that(required_number_of_buckets).given( | |
| trolley, step | |
| ).penalizes_by(0) | |
| def test_penalized_when_over_bucket_count(self): | |
| """Trolley with too few buckets should be penalized.""" | |
| trolley = Trolley( | |
| id="1", | |
| bucket_count=1, # Only 1 bucket | |
| bucket_capacity=BUCKET_CAPACITY, | |
| location=LOCATION_A1_LEFT_5 | |
| ) | |
| # Create order with volume requiring 2 buckets | |
| product1 = create_product("p1", 40000, LOCATION_B1_LEFT_3) | |
| product2 = create_product("p2", 40000, LOCATION_A1_LEFT_8) | |
| order, items = create_order_with_items("o1", [product1, product2]) | |
| step1 = TrolleyStep(id="s1", order_item=items[0]) | |
| step2 = TrolleyStep(id="s2", order_item=items[1]) | |
| connect(trolley, step1, step2) | |
| # Total volume: 80000, bucket capacity: 48000 | |
| # Required buckets: ceil(80000/48000) = 2 | |
| # Available: 1, excess: 1 | |
| constraint_verifier.verify_that(required_number_of_buckets).given( | |
| trolley, step1, step2 | |
| ).penalizes_by(1) | |
| class TestMinimizeOrderSplitByTrolley: | |
| def test_single_trolley_per_order(self): | |
| """Order on single trolley should be minimally penalized.""" | |
| trolley = Trolley( | |
| id="1", | |
| bucket_count=4, | |
| bucket_capacity=BUCKET_CAPACITY, | |
| location=LOCATION_A1_LEFT_5 | |
| ) | |
| product = create_product("p1", 10000, LOCATION_B1_LEFT_3) | |
| order, items = create_order_with_items("o1", [product]) | |
| step = TrolleyStep(id="s1", order_item=items[0]) | |
| connect(trolley, step) | |
| # 1 trolley * 1000 = 1000 | |
| constraint_verifier.verify_that(minimize_order_split_by_trolley).given( | |
| trolley, step | |
| ).penalizes_by(1000) | |
| def test_order_split_across_trolleys(self): | |
| """Order split across trolleys should be penalized more.""" | |
| trolley1 = Trolley( | |
| id="1", | |
| bucket_count=4, | |
| bucket_capacity=BUCKET_CAPACITY, | |
| location=LOCATION_A1_LEFT_5 | |
| ) | |
| trolley2 = Trolley( | |
| id="2", | |
| bucket_count=4, | |
| bucket_capacity=BUCKET_CAPACITY, | |
| location=LOCATION_A1_LEFT_5 | |
| ) | |
| product1 = create_product("p1", 10000, LOCATION_B1_LEFT_3) | |
| product2 = create_product("p2", 10000, LOCATION_A1_LEFT_8) | |
| order, items = create_order_with_items("o1", [product1, product2]) | |
| step1 = TrolleyStep(id="s1", order_item=items[0]) | |
| step2 = TrolleyStep(id="s2", order_item=items[1]) | |
| connect(trolley1, step1) | |
| connect(trolley2, step2) | |
| # 2 trolleys * 1000 = 2000 | |
| constraint_verifier.verify_that(minimize_order_split_by_trolley).given( | |
| trolley1, trolley2, step1, step2 | |
| ).penalizes_by(2000) | |
| class TestMinimizeDistanceFromPreviousStep: | |
| def test_distance_from_trolley_start(self): | |
| """Distance from trolley start location to first step.""" | |
| trolley = Trolley( | |
| id="1", | |
| bucket_count=4, | |
| bucket_capacity=BUCKET_CAPACITY, | |
| location=LOCATION_A1_LEFT_5 | |
| ) | |
| product = create_product("p1", 10000, LOCATION_A1_LEFT_8) | |
| order, items = create_order_with_items("o1", [product]) | |
| step = TrolleyStep(id="s1", order_item=items[0]) | |
| connect(trolley, step) | |
| # Same shelving, same side: |8 - 5| = 3 meters | |
| constraint_verifier.verify_that(minimize_distance_from_previous_step).given( | |
| trolley, step | |
| ).penalizes_by(3) | |
| class TestMinimizeDistanceFromLastStepToOrigin: | |
| def test_distance_back_to_origin(self): | |
| """Distance from last step back to trolley start location.""" | |
| trolley = Trolley( | |
| id="1", | |
| bucket_count=4, | |
| bucket_capacity=BUCKET_CAPACITY, | |
| location=LOCATION_A1_LEFT_5 | |
| ) | |
| product = create_product("p1", 10000, LOCATION_A1_LEFT_8) | |
| order, items = create_order_with_items("o1", [product]) | |
| step = TrolleyStep(id="s1", order_item=items[0]) | |
| connect(trolley, step) | |
| # Same shelving, same side: |8 - 5| = 3 meters | |
| constraint_verifier.verify_that(minimize_distance_from_last_step_to_origin).given( | |
| trolley, step | |
| ).penalizes_by(3) | |