492 lines
19 KiB
Python
492 lines
19 KiB
Python
|
|
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} 次整约束移除重解;"
|
|||
|
|
"结果为业务目标反事实差值/区间,不是影子价。"
|
|||
|
|
)
|
|||
|
|
),
|
|||
|
|
}
|