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] algo_name: str = ""
[docs] execution_time: float = 0.0
[docs] provenance: dict[str, Any] = field(default_factory=dict)
@dataclass
[docs] class PickPosition:
[docs] order_number: int
[docs] article_id: int
[docs] amount: int
[docs] pick_node: tuple[int, int]
[docs] in_store: int
[docs] article_name: Optional[str] = None
# dynamic info
[docs] picked: Optional[bool] = None
@dataclass(frozen=True)
[docs] class WarehouseOrder:
[docs] order_id: int
[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] batch_id: int
[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:
[docs] picker: Resource
[docs] pick_list: PickList
@dataclass
[docs] class AssignmentSolution(AlgorithmSolution):
[docs] assignments: list[PickerAssignment] = field(default_factory=list)
[docs] class NodeType(Enum):
[docs] PICK = auto()
[docs] ROUTE = auto()
[docs] class RouteNode(NamedTuple):
[docs] position: tuple[int, int]
[docs] node_type: NodeType
@dataclass
[docs] class Route:
[docs] route: list[tuple[int, int]] | None = None
[docs] item_sequence: list[tuple[int, int]] | None = None
[docs] distance: float = 0.0
[docs] pick_list: Optional[PickList] = None
[docs] annotated_route: Optional[list[RouteNode]] = None
@dataclass
[docs] class PickTour:
[docs] pick_list: PickList
[docs] route: Route
[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] PENDING = "pending"
[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). """
[docs] tour_id: int
# original plan (copied from Route)
[docs] order_numbers: list[int]
[docs] original_route: Route
[docs] pick_list: PickList
# 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
[docs] class Assignment:
[docs] tour_id: int
[docs] picker_id: int
@dataclass
[docs] class Job:
[docs] batch_idx: int
[docs] picker_id: int
[docs] start_time: float
[docs] end_time: float
[docs] release_time: float
[docs] distance: float
[docs] n_picks: int
[docs] travel_time: float
[docs] handling_time: float
[docs] route: Route
@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. """ ...