aps-agent/server/engines/optimize_engine.py

115 lines
3.9 KiB
Python
Raw Permalink Normal View History

"""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",
)