aps-agent/tests/golden/test_cp_engine.py

178 lines
6.9 KiB
Python
Raw Permalink Normal View History

# ============================================================
# SC-03 CP-SAT 引擎黄金测试
# ============================================================
from __future__ import annotations
import pytest
from server.engines import get_engine
from server.engines.base import EngineParams
from server.engines.cp_engine import CpSatEngine
from server.engines.rule_engine import RuleEngine
from server.state.seed import seed_world
from server.timeutil import add_minutes, fmt_date, parse_dt, today0
def _next_id_factory():
counters: dict[str, int] = {}
def next_id(kind: str) -> int:
counters[kind] = counters.get(kind, 0) + 1
return counters[kind]
return next_id
def _run(world, strategy="COMPREHENSIVE", engine="CP", time_limit=8.0):
start = fmt_date(add_minutes(today0(), 24 * 60))
params = EngineParams(
orderIds=[], engineType=engine, strategyTemplate=strategy,
planningHorizonDays=14, startDate=start, timeLimitSeconds=time_limit,
)
return get_engine(engine).solve(world, params, _next_id_factory())
def test_get_engine_cp_is_cpsat():
eng = get_engine("CP")
assert isinstance(eng, CpSatEngine)
assert eng.name == "CP"
assert eng.supports_anytime is True
def test_cp_engine_type_and_solver_meta():
world = seed_world()
result = _run(world)
assert result.engineType == "CP"
assert world["scheduleVersions"][0]["engineType"] == "CP"
meta = world["scheduleVersions"][0].get("solverMeta") or {}
assert meta.get("backend") == "OR-Tools CP-SAT"
assert result.solveStatus in ("OPTIMAL", "FEASIBLE", "UNKNOWN", "INFEASIBLE")
assert result.solveTimeSec is not None
assert result.poCount == 7
assert result.woCount == 30
def test_cp_no_workstation_overlap():
world = seed_world()
_run(world)
by_ws: dict[int, list] = {}
for wo in world["workOrders"]:
by_ws.setdefault(wo["workstationId"], []).append(
(parse_dt(wo["plannedStartTime"]), parse_dt(wo["plannedEndTime"])))
for ws_id, ivs in by_ws.items():
ivs.sort()
for (s1, e1), (s2, e2) in zip(ivs, ivs[1:]):
assert e1 <= s2, f"工位 {ws_id} 双占"
def test_cp_tardiness_not_worse_than_rule():
"""可行 CP 结果直接物化经父进程验证的 operation slots。"""
start = fmt_date(add_minutes(today0(), 24 * 60))
params_c = EngineParams(
orderIds=[], engineType="CP", strategyTemplate="COMPREHENSIVE",
planningHorizonDays=14, startDate=start, timeLimitSeconds=10.0,
)
w_c = seed_world()
c = CpSatEngine().solve(w_c, params_c, _next_id_factory())
version = w_c["scheduleVersions"][-1]
solver_meta = version["solverMeta"]
c3_validation = solver_meta["materializedC3Validation"]
work_orders = [
row for row in w_c["workOrders"]
if row.get("schedulingVersionId") == c.versionId
]
calendar_conflicts = [
row for row in w_c["conflicts"]
if row.get("versionId") == c.versionId and row.get("conflictType") == "CALENDAR"
]
assert c.solveStatus in {"OPTIMAL", "FEASIBLE"}
assert solver_meta["directlyConsumedByMaterializer"] is True
assert solver_meta["operationTimingValidation"]["passed"] is True
assert c3_validation["passed"] is True
assert c3_validation["cpTimingApplied"] is True
assert solver_meta["materializedC7Validation"]["passed"] is True
assert len(work_orders) == c.woCount
assert all(row.get("cpTimingSource") == "operationSlots" for row in work_orders)
assert all(row.get("plannedSegments") for row in work_orders)
assert version["totalTardiness"] == c.totalTardiness
assert calendar_conflicts == []
def test_cp_timeout_returns_feasible_or_status():
"""极短时限:必须返回带 status 的结果,不得静默变 RULE。"""
world = seed_world()
result = _run(world, time_limit=0.5)
assert result.engineType == "CP"
assert result.solveStatus is not None
meta = world["scheduleVersions"][0]["solverMeta"]
assert meta["backend"] == "OR-Tools CP-SAT"
# anytime:通常至少 FEASIBLE/OPTIMAL/UNKNOWN;若 INFEASIBLE 则有 fallback 留痕
assert result.poCount >= 0
def test_cp_infeasible_solver_meta_exposes_native_assumption_core():
world = seed_world()
start = fmt_date(add_minutes(today0(), 24 * 60))
params = EngineParams(
orderIds=[], engineType="CP", strategyTemplate="COMPREHENSIVE",
planningHorizonDays=14, startDate=start, timeLimitSeconds=3.0,
freezeWindowHours=100_000,
)
result = CpSatEngine().solve(world, params, _next_id_factory())
meta = world["scheduleVersions"][-1]["solverMeta"]
assert result.solveStatus == "INFEASIBLE"
core = meta["nativeIis"]
assert core["native"] is True
assert core["method"] == "SufficientAssumptionsForInfeasibility"
assert core["minimality"] == "sufficient-assumption-core"
assert core["isMinimalIis"] is False
assert "C11_freeze" in core["constraintIds"]
from server.aps_domain.constraints import get_constraint
assert all(get_constraint(world, cid) is not None for cid in core["constraintIds"])
@pytest.mark.parametrize("requested", ["RULE", "GA"])
def test_non_cp_still_rule_proxy(requested):
world = seed_world()
result = _run(world, engine=requested)
assert result.engineType == requested
assert isinstance(get_engine(requested), RuleEngine)
def test_hybrid_is_real_pipeline():
from server.engines.cp_engine import HybridEngine
eng = get_engine("HYBRID")
assert isinstance(eng, HybridEngine)
world = seed_world()
result = _run(world, engine="HYBRID")
assert result.engineType == "HYBRID"
meta = world["scheduleVersions"][0].get("solverMeta") or {}
assert meta.get("warmStart") == "RULE"
assert "RULE→CP-SAT" in str(meta.get("pipeline") or "")
assert result.solveStatus is not None
assert result.poCount == 7
def test_hybrid_tardiness_not_worse_than_rule():
start = fmt_date(add_minutes(today0(), 24 * 60))
w_h = seed_world()
h = get_engine("HYBRID").solve(w_h, EngineParams(
orderIds=[], engineType="HYBRID", strategyTemplate="COMPREHENSIVE",
planningHorizonDays=14, startDate=start, timeLimitSeconds=10.0,
), _next_id_factory())
version = w_h["scheduleVersions"][-1]
solver_meta = version["solverMeta"]
work_orders = [
row for row in w_h["workOrders"]
if row.get("schedulingVersionId") == h.versionId
]
assert h.solveStatus in {"OPTIMAL", "FEASIBLE"}
assert solver_meta["directlyConsumedByMaterializer"] is True
assert solver_meta["operationTimingValidation"]["passed"] is True
assert solver_meta["materializedC3Validation"]["passed"] is True
assert solver_meta["materializedC7Validation"]["passed"] is True
assert len(work_orders) == h.woCount
assert all(row.get("cpTimingSource") == "operationSlots" for row in work_orders)
assert all(row.get("plannedSegments") for row in work_orders)
assert version["totalTardiness"] == h.totalTardiness