|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
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 = 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, |
|
|
bucket_capacity=BUCKET_CAPACITY, |
|
|
location=LOCATION_A1_LEFT_5 |
|
|
) |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
constraint_verifier.verify_that(minimize_distance_from_last_step_to_origin).given( |
|
|
trolley, step |
|
|
).penalizes_by(3) |
|
|
|