|
|
from solverforge_legacy.solver.score import ( |
|
|
constraint_provider, |
|
|
ConstraintFactory, |
|
|
HardSoftDecimalScore, |
|
|
) |
|
|
|
|
|
from .domain import Trolley |
|
|
|
|
|
|
|
|
REQUIRED_NUMBER_OF_BUCKETS = "Required number of buckets" |
|
|
MINIMIZE_ORDER_SPLIT = "Minimize order split by trolley" |
|
|
MINIMIZE_DISTANCE = "Minimize total distance" |
|
|
|
|
|
|
|
|
@constraint_provider |
|
|
def define_constraints(factory: ConstraintFactory): |
|
|
return [ |
|
|
|
|
|
required_number_of_buckets(factory), |
|
|
|
|
|
minimize_order_split_by_trolley(factory), |
|
|
minimize_total_distance(factory), |
|
|
] |
|
|
|
|
|
|
|
|
def required_number_of_buckets(factory: ConstraintFactory): |
|
|
""" |
|
|
Hard: Ensure trolley has enough buckets for all orders. |
|
|
""" |
|
|
return ( |
|
|
factory.for_each(Trolley) |
|
|
.filter(lambda trolley: trolley.calculate_excess_buckets() > 0) |
|
|
.penalize( |
|
|
HardSoftDecimalScore.ONE_HARD, |
|
|
lambda trolley: trolley.calculate_excess_buckets() |
|
|
) |
|
|
.as_constraint(REQUIRED_NUMBER_OF_BUCKETS) |
|
|
) |
|
|
|
|
|
|
|
|
def minimize_order_split_by_trolley(factory: ConstraintFactory): |
|
|
""" |
|
|
Soft: Orders should ideally be on the same trolley. |
|
|
""" |
|
|
return ( |
|
|
factory.for_each(Trolley) |
|
|
.filter(lambda trolley: len(trolley.steps) > 0) |
|
|
.penalize( |
|
|
HardSoftDecimalScore.ONE_SOFT, |
|
|
lambda trolley: trolley.calculate_order_split_penalty() |
|
|
) |
|
|
.as_constraint(MINIMIZE_ORDER_SPLIT) |
|
|
) |
|
|
|
|
|
|
|
|
def minimize_total_distance(factory: ConstraintFactory): |
|
|
""" |
|
|
Soft: Minimize total distance traveled by all trolleys. |
|
|
Aggregated at Trolley level (like vehicle-routing) for performance. |
|
|
""" |
|
|
return ( |
|
|
factory.for_each(Trolley) |
|
|
.penalize( |
|
|
HardSoftDecimalScore.ONE_SOFT, |
|
|
lambda trolley: trolley.calculate_total_distance() |
|
|
) |
|
|
.as_constraint(MINIMIZE_DISTANCE) |
|
|
) |
|
|
|