"""Optimize scheduling engine integrated with the APS V2 closed loop. The engine owns algorithm selection and provenance. APS still owns the world, admission, candidate validation, version materialization, and audit trail. """ from __future__ import annotations from typing import Any, Callable from server.contracts import ScheduleResult from server.engines.base import EngineParams, ISchedulingEngine from server.engines.pool_engine import PoolEngine DISPATCH_RULES = frozenset({"EDD", "SPT", "PRIORITY", "FIFO", "LPT", "CR", "ATC"}) def normalize_dispatch_rule(value: str | None) -> str: rule = str(value or "EDD").strip().upper().replace("-", "_") aliases = { "DELIVERY_FIRST": "EDD", "EARLIEST_DUE_DATE": "EDD", "FIRST_IN_FIRST_OUT": "FIFO", "APPARENT_TARDINESS_COST": "ATC", } rule = aliases.get(rule, rule) return rule if rule in DISPATCH_RULES else "EDD" class OptimizeEngine(ISchedulingEngine): """Python-native Optimize entry point for APS scheduling. The first integration reuses PoolEngine's already validated flex materializer. Its ordering policy is supplied by ``dispatch_rule`` so the seven optimize rules share APS calendars, teams, tooling, and rollback semantics while the V2 runtime remains the authority for validation. """ name = "OPTIMIZE" supports_anytime = False def solve( self, world: dict[str, Any], params: EngineParams, next_id: Callable[[str], int], ) -> ScheduleResult: rule = normalize_dispatch_rule(params.strategyTemplate) solved = PoolEngine().solve( world, next_id, sort_mode="ASC", order_ids=params.orderIds or None, start_date=params.startDate, name=params.name, window=None, enforce_teams=params.constraints.get("personnel") if params.constraints else None, dispatch_rule=rule, ) return _summary_to_result(solved, rule) def solve_flex( self, world: dict[str, Any], next_id: Callable[[str], int], *, dispatch_rule: str | None = None, order_ids: list[int] | None = None, start_date: str | None = None, name: str | None = None, window: str | None = None, enforce_teams: bool | None = None, ) -> dict[str, Any]: """Materialize an Optimize candidate for the closed-loop V2 adapter.""" rule = normalize_dispatch_rule(dispatch_rule) solved = PoolEngine().solve( world, next_id, sort_mode="ASC", order_ids=order_ids, start_date=start_date, name=name, window=window, enforce_teams=enforce_teams, dispatch_rule=rule, ) solved.update({ "engineType": "OPTIMIZE", "algorithmId": f"optimize.{rule.lower()}", "algorithmVersion": "1.0.0", "solverId": "optimize-dispatch", "solverVersion": "1.0.0", "dispatchRule": rule, }) return solved def _summary_to_result(solved: dict[str, Any], rule: str) -> ScheduleResult: return ScheduleResult( versionId=int(solved["versionId"]), versionNo=str(solved["versionNo"]), engineType="OPTIMIZE", strategy=rule, status="DRAFT", orderCount=int(solved.get("orderCount") or 0), poCount=int(solved.get("vlCount") or 0), woCount=int(solved.get("woCount") or 0), conflictCount=int(solved.get("conflictCount") or 0), totalTardiness=float(solved.get("totalTardiness") or 0), avgUtilization=float(solved.get("avgUtilization") or 0), evidenceRefs=[f"algorithm:optimize.{rule.lower()}", f"run:{solved['versionId']}"], solveStatus="FEASIBLE" if not solved.get("conflictCount") else "PARTIAL", )