Spaces:
Sleeping
Sleeping
File size: 9,180 Bytes
e40294e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 |
from dataclasses import dataclass, field
from typing import Annotated, Optional, List, Union
from solverforge_legacy.solver import SolverStatus
from solverforge_legacy.solver.score import HardSoftDecimalScore
from solverforge_legacy.solver.domain import (
planning_entity,
planning_solution,
PlanningId,
PlanningScore,
PlanningListVariable,
PlanningEntityCollectionProperty,
ValueRangeProvider,
InverseRelationShadowVariable,
PreviousElementShadowVariable,
NextElementShadowVariable,
CascadingUpdateShadowVariable,
)
from .warehouse import WarehouseLocation, Side
from .json_serialization import JsonDomainBase
from pydantic import Field
# =============================================================================
# Domain Classes (used internally by solver - @dataclass for performance)
# =============================================================================
@dataclass
class Product:
"""A store product that can be included in an order."""
id: str
name: str
volume: int # in cm3
location: WarehouseLocation
@dataclass
class Order:
"""Represents an order submitted by a customer."""
id: str
items: list["OrderItem"] = field(default_factory=list)
@dataclass
class OrderItem:
"""An indivisible product added to an order."""
id: str
order: Order
product: Product
@property
def volume(self) -> int:
return self.product.volume
@property
def order_id(self) -> str:
return self.order.id if self.order else None
@property
def location(self) -> WarehouseLocation:
return self.product.location
@planning_entity
@dataclass
class TrolleyStep:
"""
Represents a 'stop' in a Trolley's path where an order item is to be picked.
Shadow variables automatically track the trolley assignment and position in the list.
The distance_from_previous is a cascading shadow variable that precomputes distance.
"""
id: Annotated[str, PlanningId]
order_item: OrderItem
# Shadow variables - automatically maintained by solver
trolley: Annotated[
Optional["Trolley"],
InverseRelationShadowVariable(source_variable_name="steps")
] = None
previous_step: Annotated[
Optional["TrolleyStep"],
PreviousElementShadowVariable(source_variable_name="steps")
] = None
next_step: Annotated[
Optional["TrolleyStep"],
NextElementShadowVariable(source_variable_name="steps")
] = None
# Cascading shadow variable - precomputes distance from previous element
# This is updated automatically when the step is assigned/moved
distance_from_previous: Annotated[
Optional[int],
CascadingUpdateShadowVariable(target_method_name="update_distance_from_previous")
] = None
def update_distance_from_previous(self):
"""Called automatically by solver when step is assigned/moved."""
from .warehouse import calculate_distance
if self.trolley is None:
self.distance_from_previous = None
elif self.previous_step is None:
# First step - distance from trolley start
self.distance_from_previous = calculate_distance(
self.trolley.location, self.location
)
else:
# Distance from previous step
self.distance_from_previous = calculate_distance(
self.previous_step.location, self.location
)
@property
def location(self) -> WarehouseLocation:
return self.order_item.location
@property
def is_last(self) -> bool:
return self.next_step is None
@property
def trolley_id(self) -> Optional[str]:
return self.trolley.id if self.trolley else None
def __str__(self) -> str:
return f"TrolleyStep({self.id})"
def __repr__(self) -> str:
return f"TrolleyStep({self.id})"
@planning_entity
@dataclass
class Trolley:
"""
A trolley that will be filled with order items.
The steps list is the planning variable that the solver modifies.
"""
id: Annotated[str, PlanningId]
bucket_count: int
bucket_capacity: int # in cm3
location: WarehouseLocation
# Planning variable - solver assigns TrolleySteps to this list
steps: Annotated[list[TrolleyStep], PlanningListVariable] = field(default_factory=list)
def total_capacity(self) -> int:
"""Total volume capacity of this trolley."""
return self.bucket_count * self.bucket_capacity
def calculate_total_volume(self) -> int:
"""Sum of volumes of all items assigned to this trolley."""
return sum(step.order_item.volume for step in self.steps)
def calculate_excess_volume(self) -> int:
"""Volume exceeding capacity (0 if within capacity)."""
excess = self.calculate_total_volume() - self.total_capacity()
return max(0, excess)
def calculate_required_buckets(self) -> int:
"""
Calculate total buckets needed for all orders on this trolley.
Buckets are NOT shared between orders - each order needs its own buckets.
"""
if len(self.steps) == 0:
return 0
# Group steps by order and calculate buckets per order
order_volumes: dict = {}
for step in self.steps:
order = step.order_item.order
order_volumes[order.id] = order_volumes.get(order.id, 0) + step.order_item.volume
# Sum up required buckets (ceiling division for each order)
total_buckets = 0
for volume in order_volumes.values():
total_buckets += (volume + self.bucket_capacity - 1) // self.bucket_capacity
return total_buckets
def calculate_excess_buckets(self) -> int:
"""Buckets needed beyond capacity (0 if within capacity)."""
excess = self.calculate_required_buckets() - self.bucket_count
return max(0, excess)
def calculate_order_split_penalty(self) -> int:
"""
Penalty for orders split across trolleys.
Returns 1000 per unique order on this trolley (will be summed across all trolleys).
"""
if len(self.steps) == 0:
return 0
unique_orders = set(step.order_item.order.id for step in self.steps)
return len(unique_orders) * 1000
def calculate_total_distance(self) -> int:
"""
Calculate total distance for this trolley's route.
Uses precomputed distance_from_previous shadow variable for speed.
"""
if len(self.steps) == 0:
return 0
from .warehouse import calculate_distance
# Sum precomputed distances (already includes start -> first step)
total = 0
for step in self.steps:
if step.distance_from_previous is not None:
total += step.distance_from_previous
# Add return trip from last step to origin
last_step = self.steps[-1]
total += calculate_distance(last_step.location, self.location)
return total
def __str__(self) -> str:
return f"Trolley({self.id})"
def __repr__(self) -> str:
return f"Trolley({self.id})"
@planning_solution
@dataclass
class OrderPickingSolution:
"""The planning solution containing trolleys and steps to be optimized."""
trolleys: Annotated[list[Trolley], PlanningEntityCollectionProperty]
trolley_steps: Annotated[
list[TrolleyStep],
PlanningEntityCollectionProperty,
ValueRangeProvider
]
score: Annotated[Optional[HardSoftDecimalScore], PlanningScore] = None
solver_status: SolverStatus = SolverStatus.NOT_SOLVING
# =============================================================================
# Pydantic API Models (for REST serialization only)
# =============================================================================
class WarehouseLocationModel(JsonDomainBase):
shelving_id: str = Field(..., alias="shelvingId")
side: str
row: int
class ProductModel(JsonDomainBase):
id: str
name: str
volume: int
location: WarehouseLocationModel
class OrderItemModel(JsonDomainBase):
id: str
order_id: Optional[str] = Field(None, alias="orderId")
product: ProductModel
class OrderModel(JsonDomainBase):
id: str
items: List[OrderItemModel] = Field(default_factory=list)
class TrolleyStepModel(JsonDomainBase):
id: str
order_item: OrderItemModel = Field(..., alias="orderItem")
trolley: Optional[Union[str, "TrolleyModel"]] = None
trolley_id: Optional[str] = Field(None, alias="trolleyId")
class TrolleyModel(JsonDomainBase):
id: str
bucket_count: int = Field(..., alias="bucketCount")
bucket_capacity: int = Field(..., alias="bucketCapacity")
location: WarehouseLocationModel
steps: List[Union[str, TrolleyStepModel]] = Field(default_factory=list)
class OrderPickingSolutionModel(JsonDomainBase):
trolleys: List[TrolleyModel]
trolley_steps: List[TrolleyStepModel] = Field(..., alias="trolleySteps")
score: Optional[str] = None
solver_status: Optional[str] = Field(None, alias="solverStatus")
|