Source code for ware_ops_algos.algorithms.algorithm
from __future__ import annotations
from abc import ABC, abstractmethod
from collections import deque
from dataclasses import dataclass, field
from enum import Enum, auto
from itertools import count
from typing import Generic, TypeVar, Optional, Any, NamedTuple, Deque
import time
import logging
from ware_ops_algos.domain_models import Order, ResolvedOrderPosition, OrderPosition, Resource
[docs]
I = TypeVar("I") # input type
[docs]
O = TypeVar("O") # output type
@dataclass
[docs]
class AlgorithmSolution:
[docs]
execution_time: float = 0.0
[docs]
provenance: dict[str, Any] = field(default_factory=dict)
@dataclass
[docs]
class PickPosition:
[docs]
pick_node: tuple[int, int]
[docs]
article_name: Optional[str] = None
# dynamic info
[docs]
picked: Optional[bool] = None
@dataclass(frozen=True)
[docs]
class WarehouseOrder:
[docs]
due_date: Optional[float] = None
[docs]
order_date: Optional[float] = None
[docs]
pick_positions: Optional[list[PickPosition]] = None
# dynamic info
[docs]
fulfilled: Optional[bool] = None
@dataclass
[docs]
class ItemAssignmentSolution(AlgorithmSolution):
[docs]
resolved_orders: list[WarehouseOrder] = field(default_factory=list)
@dataclass
[docs]
class BatchObject:
[docs]
orders: list[WarehouseOrder]
@dataclass
[docs]
class BatchingSolution(AlgorithmSolution):
[docs]
batches: list[BatchObject] | None = None
# pick_lists: list[list[PickPosition]] = None
[docs]
pick_lists: list[PickList] = None
@dataclass
[docs]
class PickList:
[docs]
pick_positions: list[PickPosition]
[docs]
orders: list[WarehouseOrder]
[docs]
release: Optional[float] = None
[docs]
earliest_due_date: Optional[float] = None
[docs]
id: int = field(default_factory=count().__next__)
@property
[docs]
def order_numbers(self) -> list[int]:
if self.pick_positions is None:
return []
return list({pp.order_number for pp in self.pick_positions})
@dataclass
[docs]
class PickerAssignment:
@dataclass
[docs]
class AssignmentSolution(AlgorithmSolution):
[docs]
assignments: list[PickerAssignment] = field(default_factory=list)
[docs]
class RouteNode(NamedTuple):
[docs]
position: tuple[int, int]
@dataclass
[docs]
class Route:
[docs]
route: list[tuple[int, int]] | None = None
[docs]
item_sequence: list[tuple[int, int]] | None = None
[docs]
pick_list: Optional[PickList] = None
[docs]
annotated_route: Optional[list[RouteNode]] = None
@dataclass
[docs]
class PickTour:
[docs]
assigned_picker: Resource
[docs]
starts_after: Optional[int] = None
[docs]
starts_before: Optional[int] = None
[docs]
planned_start: Optional[float] = None # if scheduled
[docs]
planned_end: Optional[float] = None
[docs]
Node = tuple[float, float]
[docs]
class TourStates(str, Enum):
[docs]
PLANNED = "planned" # PickList is generated
[docs]
ASSIGNED = "assigned" # Is assigned to a picker
[docs]
SCHEDULED = "scheduled" # Is scheduled for a point in time
[docs]
STARTED = "started" # Tour has started picking
[docs]
DONE = "done" # Tour is done
@dataclass
[docs]
class TourPlanningState:
"""
Thin wrapper around a Route object.
Keeps track of the planning state for a single tour.
- route_nodes / pick_sequence are copies from the plan (immutable intent).
- cursor / picks_left / version are the mutable execution state.
- original_route is kept only for debugging/inspection (do not mutate).
"""
# original plan (copied from Route)
[docs]
order_numbers: list[int]
# pick_nodes: list[Node]
[docs]
annotated_route: list[RouteNode]
[docs]
assigned_resource: Optional[int] = None
[docs]
start_time: Optional[float] = None
[docs]
end_time: Optional[float] = None
[docs]
end_time_planned: Optional[float] = None
# execution state, mutable during picking
[docs]
cursor: int = 0 # index into route_nodes
[docs]
picks_left: Deque[Node] = field(default_factory=deque)
[docs]
open_pick_positions: list = field(default_factory=list)
[docs]
status: str = TourStates.PLANNED
[docs]
def current_node(self) -> RouteNode:
return self.annotated_route[self.cursor]
[docs]
def at_end(self) -> bool:
"""True if cursor is on the final node (typically the depot)."""
return self.cursor >= len(self.annotated_route) - 1
[docs]
def next_node(self) -> RouteNode:
return self.annotated_route[self.cursor + 1]
@dataclass
[docs]
class RoutingSolution(AlgorithmSolution):
[docs]
route: Optional[Route] = None
@dataclass
[docs]
class CombinedRoutingSolution(AlgorithmSolution):
[docs]
routes: Optional[list[Route]] = None
@dataclass
[docs]
class Sequencing:
[docs]
pick_list_sequence: list[int]
@dataclass
@dataclass
@dataclass
[docs]
class SchedulingSolution(AlgorithmSolution):
[docs]
jobs: list[Job] | None = None
@dataclass
[docs]
class OrderSelectionSolution(AlgorithmSolution):
[docs]
selected_orders: list[WarehouseOrder] = field(default_factory=list)
@dataclass
[docs]
class PlanningState:
[docs]
item_assignment: Optional[ItemAssignmentSolution] = None
[docs]
batching_solutions: Optional[BatchingSolution] = None
[docs]
assignment_solutions: Optional[AssignmentSolution] = None
[docs]
routing_solutions: Optional[list[RoutingSolution]] = field(default_factory=list)
[docs]
sequencing_solutions: Optional[SchedulingSolution] = None
[docs]
order_selection_solutions: Optional[OrderSelectionSolution] = None
[docs]
provenance: dict[str, Any] = field(default_factory=dict)
[docs]
class Algorithm(ABC, Generic[I, O]):
"""
Abstract base class for all algorithms (routing, batching, etc.).
Handles timing, algo naming, and ensures consistent result metadata.
"""
[docs]
algo_name: str = "Algorithm"
def __init__(self, seed: Optional[int] = None):
self._seed = seed
[docs]
self.logger = logging.getLogger(self.__class__.__name__)
[docs]
def solve(self, input_data: I) -> O:
start_time = time.perf_counter()
try:
result: O = self._run(input_data)
except Exception as e:
raise RuntimeError(f"Algorithm '{self}' failed: {e}") from e
elapsed = time.perf_counter() - start_time
if not result.algo_name:
result.algo_name = self.algo_name
result.execution_time = elapsed
return result
@abstractmethod
def _run(self, input_data: I) -> O:
"""
Concrete algorithms implement this method.
Must return a subclass of AlgorithmResult.
"""
...