aps-agent/server/aps_domain/sensitivity.py.bak33

278 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 主控参数敏感性分析(moduleId: domain-sensitivity, SC-06,可重生 ✅)
# 沙盒 one-at-a-time 扰动 + 固定种子蒙特卡洛;永不写主干
# ============================================================
from __future__ import annotations
import copy
import uuid
from typing import Any
from server.aps_domain.params import get_schedule_params
from server.aps_domain.robustness import run_monte_carlo
from server.contracts import UIBlock
from server.engines import get_engine
from server.engines.base import EngineParams
from server.timeutil import add_minutes, fmt_date, today0
World = dict[str, Any]
def _sandbox_counter():
counters: dict[str, int] = {}
def next_id(kind: str) -> int:
counters[kind] = counters.get(kind, 0) + 2000000
return counters[kind]
return next_id
def _kpi_from_result(result, sandbox: World) -> dict[str, float]:
ver = sandbox["scheduleVersions"][-1] if sandbox.get("scheduleVersions") else {}
return {
"tardiness": round(float(result.totalTardiness), 2),
"conflicts": float(result.conflictCount),
"utilization": round(float(result.avgUtilization), 4),
"changeoverMin": float(ver.get("totalChangeoverMin") or 0),
"poCount": float(result.poCount),
"woCount": float(result.woCount),
}
def _run_sandbox(
world: World,
*,
strategy: str,
horizon: int,
start: str,
vip_weight: float | None = None,
delivery_buffer: float | None = None,
freeze_hours: float | None = None,
efficiency_scale: float | None = None,
) -> dict[str, float]:
sandbox = copy.deepcopy(world)
sandbox["scheduleVersions"] = []
sandbox["productionOrders"] = [p for p in sandbox.get("productionOrders", []) if p.get("status") == "PUBLISHED"]
sandbox["workOrders"] = [w for w in sandbox.get("workOrders", []) if w.get("status") not in ("PENDING", "DRAFT", None)]
# 清空草稿工单/PO,避免干扰;简化:只保留非本沙盒相关——种子通常无已发布,直接清空排产结果
sandbox["productionOrders"] = []
sandbox["workOrders"] = []
sandbox["conflicts"] = []
sp = sandbox.setdefault("scheduleParams", {})
if vip_weight is not None:
levels = dict(sp.get("customerLevelWeights") or {})
levels["VIP"] = float(vip_weight)
sp["customerLevelWeights"] = levels
if efficiency_scale is not None and efficiency_scale > 0:
for ln in sandbox.get("lines") or []:
base = float(ln.get("efficiencyFactor") or 1.0)
ln["efficiencyFactor"] = round(base * float(efficiency_scale), 4)
params = EngineParams(
orderIds=[],
engineType="RULE",
strategyTemplate=strategy,
planningHorizonDays=horizon,
startDate=start,
deliveryBufferRatio=delivery_buffer,
freezeWindowHours=freeze_hours,
name=f"sensitivity-{uuid.uuid4().hex[:6]}",
constraints={
"materialKit": False, "equipment": True, "personnel": True, "changeover": True,
"capacity": True, "dueDate": True, "tooling": True, "freeze": True,
},
)
result = get_engine("RULE").solve(sandbox, params, _sandbox_counter())
return _kpi_from_result(result, sandbox)
def run_sensitivity(
world: World,
*,
strategy: str = "COMPREHENSIVE",
) -> dict[str, Any]:
"""
SC-06:Tornado 单因子排序 + 固定种子蒙特卡洛鲁棒性。
经统一 Explore 通道执行(矩阵 55 行):fn 只拿深拷贝沙盒,主干永不外泄写引用;
_run_sandbox 内部再做子沙盒扰动。
"""
from server.aps_domain.explore_boundary import run_explore
return run_explore(world, lambda sandbox: _sensitivity_impl(
sandbox, strategy=strategy,
))
def _sensitivity_impl(world: World, *, strategy: str) -> dict[str, Any]:
"""Tornado/蒙特卡洛核心实现(在统一通道沙盒内运行)。"""
sp = get_schedule_params(world)
base_horizon = int(sp.get("planningHorizonDays") or 14)
base_vip = float((sp.get("customerLevelWeights") or {}).get("VIP") or 3.0)
base_buffer = float(sp.get("deliveryBufferRatio") or 0.95)
base_freeze = float(sp.get("freezeWindowHours") or 24)
start = fmt_date(add_minutes(today0(), 24 * 60))
baseline = _run_sandbox(
world, strategy=strategy, horizon=base_horizon, start=start,
vip_weight=base_vip, delivery_buffer=base_buffer, freeze_hours=base_freeze,
efficiency_scale=1.0,
)
# 每因子:低/高 两档(相对基线)
factors: list[dict[str, Any]] = [
{
"id": "planningHorizonDays", "label": "展望期(天)",
"baseline": base_horizon,
"low": {"label": f"{max(3, base_horizon // 2)} 天", "kwargs": {"horizon": max(3, base_horizon // 2)}},
"high": {"label": f"{min(60, base_horizon * 2)} 天", "kwargs": {"horizon": min(60, base_horizon * 2)}},
},
{
"id": "vipWeight", "label": "VIP 等级权重",
"baseline": base_vip,
"low": {"label": "VIP=1", "kwargs": {"vip_weight": 1.0}},
"high": {"label": "VIP=10", "kwargs": {"vip_weight": 10.0}},
},
{
"id": "deliveryBufferRatio", "label": "交期缓冲比",
"baseline": base_buffer,
"low": {"label": "缓冲 0.85", "kwargs": {"delivery_buffer": 0.85}},
"high": {"label": "缓冲 1.00", "kwargs": {"delivery_buffer": 1.0}},
},
{
"id": "freezeWindowHours", "label": "冻结窗口(时)",
"baseline": base_freeze,
"low": {"label": "冻结 0h", "kwargs": {"freeze_hours": 0.0}},
"high": {"label": "冻结 48h", "kwargs": {"freeze_hours": 48.0}},
},
{
"id": "lineEfficiency", "label": "产线效率系数",
"baseline": 1.0,
"low": {"label": "效率×0.8", "kwargs": {"efficiency_scale": 0.8}},
"high": {"label": "效率×1.2", "kwargs": {"efficiency_scale": 1.2}},
},
]
rows: list[dict[str, Any]] = []
for fac in factors:
common = {
"strategy": strategy, "horizon": base_horizon, "start": start,
"vip_weight": base_vip, "delivery_buffer": base_buffer,
"freeze_hours": base_freeze, "efficiency_scale": 1.0,
}
low_kw = {**common, **fac["low"]["kwargs"]}
high_kw = {**common, **fac["high"]["kwargs"]}
# kwargs 用 horizon 键
def _call(kw: dict) -> dict[str, float]:
return _run_sandbox(
world,
strategy=kw["strategy"],
horizon=int(kw["horizon"]),
start=kw["start"],
vip_weight=kw.get("vip_weight"),
delivery_buffer=kw.get("delivery_buffer"),
freeze_hours=kw.get("freeze_hours"),
efficiency_scale=kw.get("efficiency_scale"),
)
low_kpi = _call(low_kw)
high_kpi = _call(high_kw)
d_low = round(low_kpi["tardiness"] - baseline["tardiness"], 2)
d_high = round(high_kpi["tardiness"] - baseline["tardiness"], 2)
rows.append({
"factorId": fac["id"],
"label": fac["label"],
"baselineValue": fac["baseline"],
"lowLabel": fac["low"]["label"],
"highLabel": fac["high"]["label"],
"low": {
"tardiness": low_kpi["tardiness"],
"conflicts": low_kpi["conflicts"],
"utilization": low_kpi["utilization"],
"deltaTardiness": d_low,
"deltaConflicts": round(low_kpi["conflicts"] - baseline["conflicts"], 1),
},
"high": {
"tardiness": high_kpi["tardiness"],
"conflicts": high_kpi["conflicts"],
"utilization": high_kpi["utilization"],
"deltaTardiness": d_high,
"deltaConflicts": round(high_kpi["conflicts"] - baseline["conflicts"], 1),
},
"swing": round(abs(d_low) + abs(d_high), 2),
"impactMetric": "tardiness",
})
rows.sort(key=lambda r: r["swing"], reverse=True)
monte_carlo = run_monte_carlo(
world,
strategy=strategy,
engine_type="RULE",
baseline_kpi=baseline,
planning_horizon_days=base_horizon,
start_date=start,
delivery_buffer_ratio=base_buffer,
freeze_window_hours=base_freeze,
constraints={
"materialKit": False, "equipment": True, "personnel": True, "changeover": True,
"capacity": True, "dueDate": True, "tooling": True, "freeze": True,
},
reset_schedule_products=True,
)
md_lines = [
"# 敏感性分析(Tornado)",
"",
f"- 策略基线:`{strategy}`",
f"- 基线 KPI:延期 **{baseline['tardiness']}** h · 冲突 **{int(baseline['conflicts'])}** · "
f"利用率 **{round(baseline['utilization'] * 100, 1)}%** · 换型 **{baseline['changeoverMin']}** 分",
"- 说明:沙盒 one-at-a-time,不写主干;超时秒数对 RULE 无意义,未纳入。",
"",
"| 因子 | 低档 Δ延期 | 高档 Δ延期 | 摆幅 |",
"| --- | ---: | ---: | ---: |",
]
for r in rows:
md_lines.append(
f"| {r['label']}({r['lowLabel']} / {r['highLabel']}) | "
f"{r['low']['deltaTardiness']:+} | {r['high']['deltaTardiness']:+} | {r['swing']} |"
)
md_lines.append("")
md_lines.append("摆幅 = |低档Δ延期| + |高档Δ延期|;越大表示该参数对延期越敏感。")
md_lines.extend([
"",
"## 固定种子蒙特卡洛鲁棒性",
"",
f"- 种子:`{monte_carlo['seed']}` · 样本:**{monte_carlo['trials']}** 次",
f"- 鲁棒性:**{round(monte_carlo['robustness'] * 100, 1)}%**",
f"- 延期分布:P50 **{monte_carlo['distribution']['tardinessP50']}h** · "
f"P90 **{monte_carlo['distribution']['tardinessP90']}h**",
])
return {
"strategy": strategy,
"baseline": baseline,
"rows": rows,
"monteCarlo": monte_carlo,
"markdown": "\n".join(md_lines),
"hint": "敏感性只跑沙盒。若要固化参数,请到设置 → 排产参数 改权重/展望期并确认(P2)。",
}
def sensitivity_as_block(report: dict[str, Any]) -> UIBlock:
"""产出 report UI 块(可预览/下载)。"""
return UIBlock(
blockId=f"sensitivity-{uuid.uuid4().hex[:8]}",
type="report",
props={
"reportId": uuid.uuid4().hex[:10],
"title": f"敏感性分析 Tornado({report.get('strategy')})",
"markdown": report.get("markdown") or "",
"reportType": "sensitivity-tornado",
"baseline": report.get("baseline"),
"rows": report.get("rows"),
"monteCarlo": report.get("monteCarlo"),
},
)