from __future__ import annotations import copy import hashlib import json from collections.abc import Callable, Mapping from datetime import date from typing import Any from server.aps_domain.constraints import engine_constraint_flags from server.aps_domain.params import get_schedule_params from server.engines.base import EngineParams from server.engines.cp_engine import CP_DIAGNOSTIC_RELAXABLE_CONSTRAINTS, CpSatEngine from server.engines.solver_process import ( SolverProcessError, run_cp_constraint_diagnostic, ) World = dict[str, Any] _METHOD = "cp-one-constraint-at-a-time-resolve.v1" _OBJECTIVE_UNIT = "weighted-tardiness-minute" _LABELS = { "C1_precedence": "工艺先后序", "C2_no_overlap": "工位/设备独占", "C3_calendar": "班次日历窗口", "C7_capacity": "产线日产能上限", "C10_changeover": "顺序相关换型", "C11_freeze": "冻结窗口", "C12_team": "班组并发容量", "C12_tooling": "工装并发容量", } def _resolve_start_date(world: World, start_date: str | None) -> tuple[str, str]: source = "request" if start_date is not None else "world.businessDate" raw = start_date if start_date is not None else world.get("businessDate") if not isinstance(raw, str) or not raw.strip(): raise ValueError("startDate 必填;仅可省略于 world.businessDate 已设置时") value = raw.strip() try: parsed = date.fromisoformat(value) except ValueError as exc: raise ValueError("startDate 须为 YYYY-MM-DD") from exc if parsed.isoformat() != value: raise ValueError("startDate 须为 YYYY-MM-DD") return value, source def _digest(payload: Any) -> str: text = json.dumps( payload, ensure_ascii=False, allow_nan=False, sort_keys=True, separators=(",", ":"), default=str, ) return hashlib.sha256(text.encode("utf-8")).hexdigest() def _solver_snapshot(meta: dict[str, Any]) -> dict[str, Any]: process = meta.get("solverProcess") or {} raw_c3 = meta.get("c3Calendar") c3_calendar = ( { key: raw_c3.get(key) for key in ( "schemaVersion", "modelMode", "pausePolicy", "anchor", "horizonMinutes", "coverageStart", "coverageEnd", "coverageComplete", "normalizedCalendarDigest", "calendarBucketCount", "lineWindowCounts", "segmentIntervalCount", "segmentIntervalLimit", "maxSegmentsPerOperation", "selectedMode", "active", ) } if isinstance(raw_c3, Mapping) else None ) return { "status": meta.get("status"), "objective": meta.get("objective"), "bestBound": meta.get("bestBound"), "gap": meta.get("gap"), "wallTimeSec": meta.get("wallTimeSec"), "timeLimitSec": meta.get("timeLimitSec"), "assumptionConstraints": meta.get("assumptionConstraints") or [], "activeAssumptionConstraints": meta.get("activeAssumptionConstraints") or [], "enforcedAssumptionConstraints": meta.get("enforcedAssumptionConstraints") or [], "relaxedConstraintIds": meta.get("relaxedConstraintIds") or [], "constraintInstanceCounts": meta.get("constraintInstanceCounts") or {}, "diagnosticMode": meta.get("diagnosticMode"), "numSearchWorkers": meta.get("numSearchWorkers"), "randomSeed": meta.get("randomSeed"), "requestId": process.get("requestId"), "invocationId": process.get("invocationId"), "operation": process.get("operation"), "runtimeSafe": (process.get("runtimeIdentity") or {}).get("safe"), "c3Calendar": c3_calendar, } def _model_identity(snapshot: dict[str, Any]) -> dict[str, Any]: c3_calendar = snapshot.get("c3Calendar") c3_topology = ( { key: c3_calendar.get(key) for key in ( "schemaVersion", "modelMode", "pausePolicy", "anchor", "horizonMinutes", "coverageStart", "coverageEnd", "coverageComplete", "normalizedCalendarDigest", "calendarBucketCount", "lineWindowCounts", "segmentIntervalCount", "segmentIntervalLimit", "maxSegmentsPerOperation", ) } if isinstance(c3_calendar, Mapping) else None ) return { "assumptionConstraints": snapshot.get("assumptionConstraints") or [], "activeAssumptionConstraints": snapshot.get("activeAssumptionConstraints") or [], "constraintInstanceCounts": snapshot.get("constraintInstanceCounts") or {}, "c3Topology": c3_topology, "objectiveUnit": _OBJECTIVE_UNIT, } def run_cp_marginal_resolve( world: World, *, start_date: str | None = None, strategy: str = "COMPREHENSIVE", planning_horizon_days: int = 14, time_limit_seconds: float = 4.0, constraint_ids: list[str] | None = None, cancel_check: Callable[[], None] | None = None, ) -> dict[str, Any]: """Compare the CP business objective after whole-constraint removal, one at a time.""" raw_constraint_ids = ( list(CP_DIAGNOSTIC_RELAXABLE_CONSTRAINTS) if constraint_ids is None else list(constraint_ids) ) if not raw_constraint_ids: raise ValueError("constraintIds 不能为空") if len(raw_constraint_ids) != len(set(raw_constraint_ids)): raise ValueError("constraintIds 不得重复") requested = sorted(raw_constraint_ids) unknown = set(requested) - CP_DIAGNOSTIC_RELAXABLE_CONSTRAINTS if unknown: raise ValueError(f"不支持 CP 诊断约束:{sorted(unknown)}") if not 1 <= int(planning_horizon_days) <= 90: raise ValueError("planningHorizonDays 须在 1~90") if not 0.5 <= float(time_limit_seconds) <= 10.0: raise ValueError("timeLimitSeconds 须在 0.5~10") resolved_start, start_source = _resolve_start_date(world, start_date) sandbox = copy.deepcopy(world) schedule_params = get_schedule_params(sandbox) params = EngineParams( orderIds=[], engineType="CP", strategyTemplate=strategy, planningHorizonDays=int(planning_horizon_days), startDate=resolved_start, constraints=engine_constraint_flags(sandbox), deliveryBufferRatio=float(schedule_params.get("deliveryBufferRatio") or 0.95), freezeWindowHours=float(schedule_params.get("freezeWindowHours") or 0.0), timeLimitSeconds=float(time_limit_seconds), name="cp-marginal-diagnostic", ) entries, _, source_count = CpSatEngine().collect_and_order(sandbox, params) world_digest = _digest(sandbox) entries_digest = _digest(entries) params_digest = _digest(params.model_dump(mode="json")) objective_spec = { "kind": "min-weighted-tardiness", "unit": _OBJECTIVE_UNIT, "late": "max(0, jobEndMin-dueMin)", "weight": "customerLevelWeight*100 + rush/forecast adjustments", } objective_spec_digest = _digest(objective_spec) digest = _digest({ "worldDigest": world_digest, "entriesDigest": entries_digest, "paramsDigest": params_digest, "objectiveSpecDigest": objective_spec_digest, "constraintIds": requested, }) common = { "method": _METHOD, "modelScope": "operation-level-cp-sat-re-solve", "counterfactualMode": "one-constraint-at-a-time-whole-removal", "objectiveUnit": _OBJECTIVE_UNIT, "isDualValue": False, "isUnitMarginalValue": False, "nonAdditiveAcrossConstraints": True, "cancellationGranularity": "between-solves", "cancellationDoesNotInterruptActiveSolve": True, "activeSolveSupervisionTimeoutSeconds": max( 15.0, float(time_limit_seconds) + 12.0, ), "cancellationCleanupTimeoutSeconds": 11.0, "cancellationLatencyUpperBoundSeconds": ( max(15.0, float(time_limit_seconds) + 12.0) + 11.0 ), "track": "fixed", "strategy": strategy, "startDate": resolved_start, "startDateSource": start_source, "planningHorizonDays": int(planning_horizon_days), "timeLimitSeconds": float(time_limit_seconds), "inputDigest": digest, "worldDigest": world_digest, "entriesDigest": entries_digest, "paramsDigest": params_digest, "objectiveSpec": objective_spec, "objectiveSpecDigest": objective_spec_digest, "entryCount": len(entries), "sourceOrderCount": source_count, } if not entries: return { **common, "status": "no_demand", "evaluations": 0, "baseline": None, "rows": [], "summary": "当前 fixed 轨没有可进入 CP-SAT 的待排订单,未执行重解。", } if cancel_check is not None: cancel_check() try: _, baseline_meta = run_cp_constraint_diagnostic( sandbox, entries, params, pipeline_label="CP-MARGINAL-BASELINE", ) except SolverProcessError as exc: return { **common, "status": "baseline_solver_error", "evaluations": 1, "baseline": None, "rows": [], "summary": f"CP 基线求解失败:{exc.code}", "error": exc.as_dict(), } if cancel_check is not None: cancel_check() baseline = _solver_snapshot(baseline_meta) active = set(baseline_meta.get("activeAssumptionConstraints") or []) baseline_status = str(baseline.get("status") or "") baseline_objective = baseline.get("objective") if baseline_status not in {"OPTIMAL", "FEASIBLE", "INFEASIBLE"}: return { **common, "status": "baseline_unavailable", "evaluations": 1, "baseline": baseline, "rows": [], "summary": f"CP 基线状态 {baseline_status or 'UNKNOWN'},无法比较业务目标。", } if baseline_status in {"OPTIMAL", "FEASIBLE"} and not isinstance( baseline_objective, (int, float) ): return { **common, "status": "baseline_unavailable", "evaluations": 1, "baseline": baseline, "rows": [], "summary": "CP 基线缺少可比较 objective。", } rows: list[dict[str, Any]] = [] evaluations = 1 monotonicity_violation = False for constraint_id in requested: base_row = { "constraintId": constraint_id, "constraintName": _LABELS[constraint_id], "wholeConstraintRemoval": True, "isDualValue": False, "isUnitMarginalValue": False, "nonAdditiveAcrossConstraints": True, } if constraint_id not in active: rows.append({ **base_row, "status": "inactive", "reason": "该约束在本次 CP 模型中未启用,未执行反事实重解。", "objectiveImprovement": None, }) continue if cancel_check is not None: cancel_check() evaluations += 1 try: _, relaxed_meta = run_cp_constraint_diagnostic( sandbox, entries, params, pipeline_label=f"CP-MARGINAL-{constraint_id}", relaxed_constraint_id=constraint_id, ) except SolverProcessError as exc: rows.append({ **base_row, "status": "solver_error", "reason": exc.code, "error": exc.as_dict(), "objectiveImprovement": None, }) continue if cancel_check is not None: cancel_check() relaxed_snapshot = _solver_snapshot(relaxed_meta) if _model_identity(relaxed_snapshot) != _model_identity(baseline): rows.append({ **base_row, "status": "solver_error", "reason": "基线与松弛重解的模型实例清单不一致", "relaxed": relaxed_snapshot, "objectiveImprovement": None, }) continue relaxed_status = str(relaxed_snapshot.get("status") or "") relaxed_objective = relaxed_snapshot.get("objective") if baseline_status == "INFEASIBLE": if relaxed_status in {"OPTIMAL", "FEASIBLE"}: rows.append({ **base_row, "status": "restores_feasibility", "comparisonQuality": ( "exact-optimal" if relaxed_status == "OPTIMAL" else "feasible-only" ), "relaxed": relaxed_snapshot, "objectiveImprovement": None, "interpretation": "基线不可行,完整移除该约束后恢复可行;不计算目标差。", }) elif relaxed_status == "INFEASIBLE": rows.append({ **base_row, "status": "does_not_restore_feasibility", "reason": "基线和松弛后均不可行,整约束移除未恢复可行性。", "relaxed": relaxed_snapshot, "objectiveImprovement": None, }) elif relaxed_status == "MODEL_INVALID": rows.append({ **base_row, "status": "solver_error", "reason": "松弛后 CP 模型无效,无法得出可行性结论。", "relaxed": relaxed_snapshot, "objectiveImprovement": None, }) else: rows.append({ **base_row, "status": "unavailable", "reason": f"松弛后状态 {relaxed_status or 'UNKNOWN'},未得出可行性结论。", "relaxed": relaxed_snapshot, "objectiveImprovement": None, }) continue if relaxed_status == "MODEL_INVALID": rows.append({ **base_row, "status": "solver_error", "reason": "松弛后 CP 模型无效,无法比较业务目标。", "relaxed": relaxed_snapshot, "objectiveImprovement": None, }) continue if relaxed_status == "INFEASIBLE": monotonicity_violation = True rows.append({ **base_row, "status": "monotonicity_violation", "reason": "移除约束后反而不可行,违反单调性,整份报告失败关闭。", "relaxed": relaxed_snapshot, "objectiveImprovement": None, }) continue if relaxed_status not in {"OPTIMAL", "FEASIBLE"} or not isinstance( relaxed_objective, (int, float) ): rows.append({ **base_row, "status": "unavailable", "reason": f"松弛后求解状态 {relaxed_status or 'UNKNOWN'}", "relaxed": relaxed_snapshot, "objectiveImprovement": None, }) continue exact = baseline_status == "OPTIMAL" and relaxed_status == "OPTIMAL" incumbent_difference = float(baseline_objective) - float(relaxed_objective) if exact: if incumbent_difference < -1e-6: monotonicity_violation = True rows.append({ **base_row, "status": "monotonicity_violation", "reason": "OPTIMAL 结果显示移除约束后目标恶化,违反单调性。", "relaxed": relaxed_snapshot, "objectiveImprovement": None, }) continue rows.append({ **base_row, "status": "available", "comparisonQuality": "exact-optimal", "isExactCounterfactual": True, "baselineObjective": float(baseline_objective), "relaxedObjective": float(relaxed_objective), "objectiveImprovement": round(incumbent_difference, 6), "improvementLowerBound": round(incumbent_difference, 6), "improvementUpperBound": round(incumbent_difference, 6), "relaxed": relaxed_snapshot, "interpretation": ( "完整移除该约束后 CP 加权延期最优目标的精确 one-at-a-time 改善;" "该值不是对偶,也不是每分钟边际价格。" ), }) continue baseline_bound = float(baseline["bestBound"]) relaxed_bound = float(relaxed_snapshot["bestBound"]) lower = max(0.0, baseline_bound - float(relaxed_objective)) upper = float(baseline_objective) - relaxed_bound if upper < -1e-6: monotonicity_violation = True rows.append({ **base_row, "status": "monotonicity_violation", "reason": "FEASIBLE 界限显示移除约束可能恶化最优目标,报告失败关闭。", "relaxed": relaxed_snapshot, "objectiveImprovement": None, }) continue rows.append({ **base_row, "status": "bounded", "comparisonQuality": "bound-interval", "isExactCounterfactual": False, "objectiveImprovement": None, "incumbentDifference": round(incumbent_difference, 6), "improvementLowerBound": round(lower, 6), "improvementUpperBound": round(max(0.0, upper), 6), "relaxed": relaxed_snapshot, "interpretation": ( "至少一侧仅 FEASIBLE,只报告由 incumbent/bound 推导的改善区间;" "incumbent 差值不用于排序。" ), }) if monotonicity_violation: for row in rows: if row["status"] != "monotonicity_violation": row["suppressedStatus"] = row["status"] row["status"] = "suppressed" row["reason"] = "报告存在单调性违反,本行改善值已抑制。" for field in ( "objectiveImprovement", "objectiveDelta", "incumbentDifference", "improvementLowerBound", "improvementUpperBound", ): row[field] = None rows.sort(key=lambda row: ( 0 if row["status"] == "available" else 1, -(row.get("objectiveImprovement") or 0.0), row["constraintId"], )) if cancel_check is not None: cancel_check() partial = any(row["status"] in {"solver_error", "unavailable"} for row in rows) report_status = ( "monotonicity_violation" if monotonicity_violation else "partial" if partial else "completed" ) return { **common, "status": report_status, "evaluations": evaluations, "baseline": baseline, "rows": rows, "summary": ( "检测到约束移除单调性违反,报告失败关闭。" if monotonicity_violation else "部分约束重解失败或未得出结论;仅保留可验证行。" if partial else ( f"完成 CP 基线 + {evaluations - 1} 次整约束移除重解;" "结果为业务目标反事实差值/区间,不是影子价。" ) ), }