101 lines
6.6 KiB
Python
101 lines
6.6 KiB
Python
# ============================================================
|
||
# 方案生成 · Explore 沙盒多策略对比(moduleId: domain-scenario, 可重生 ✅)
|
||
# plan.md §9.7 + §5.1:一次"对比"在深拷贝沙盒里并行试排多个策略,
|
||
# 永不触碰主干世界状态(黄金测试 test_m2_state 固化此不变式);
|
||
# 产出方案卡组(ScenarioCard 的 M2 子集),计划员"看验收选方案"。
|
||
# ============================================================
|
||
from __future__ import annotations # 前向类型引用
|
||
|
||
import copy # 沙盒深拷贝
|
||
import uuid # 方案/块 ID
|
||
from typing import Any # 类型标注
|
||
|
||
from server.contracts import ScheduleResult, UIAction, 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]
|
||
|
||
# 对比的策略组合(§9.7 硬规则 2:策略维度多样化,不凑数)
|
||
_STRATEGIES: list[tuple[str, str]] = [
|
||
("DELIVERY_FIRST", "交期优先"), # EDD 内核:保交期
|
||
("CAPACITY_BALANCE", "产能均衡"), # 均衡负荷:保产线
|
||
("CHANGEOVER_MIN", "换型最小化"), # SC-07:矩阵贪心序
|
||
("CAMPAIGN", "战役合并"), # SC-08:同品窗口合并
|
||
("COMPREHENSIVE", "综合优化"), # 默认权衡
|
||
]
|
||
|
||
|
||
def _sandbox_counter():
|
||
"""沙盒专用发号器:与主干 WorldStore 计数器完全隔离(防串号)。"""
|
||
counters: dict[str, int] = {} # 沙盒内独立计数
|
||
def next_id(kind: str) -> int: # 闭包发号
|
||
counters[kind] = counters.get(kind, 0) + 1000000 # 加大基数以便肉眼区分沙盒对象
|
||
return counters[kind] # 返回号
|
||
return next_id # 返回闭包
|
||
|
||
|
||
def compare_scenarios(world: World, engine_type: str = "RULE") -> tuple[str, UIBlock]:
|
||
"""在沙盒中并行试排多策略并产出方案卡组(权力等级 P1:只读主干 + 写沙盒)。
|
||
|
||
Args:
|
||
world: 主干世界状态(只读,函数内部深拷贝)
|
||
engine_type: 引擎类型(M1/M2 由 RULE 承接)
|
||
Returns:
|
||
(回复文案, scenario-cards UI 块)
|
||
"""
|
||
baseline = world["scheduleVersions"][-1] if world["scheduleVersions"] else None # 基准=当前最新版本(diff 对照)
|
||
start = fmt_date(add_minutes(today0(), 24 * 60)) # 统一起排日:明天(可比性)
|
||
cards: list[dict[str, Any]] = [] # 方案卡集合
|
||
for strategy, label in _STRATEGIES: # 逐策略沙盒试排
|
||
sandbox = copy.deepcopy(world) # Explore 沙盒:深拷贝隔离(§5.1 铁律)
|
||
params = EngineParams(orderIds=[], engineType=engine_type, strategyTemplate=strategy,
|
||
planningHorizonDays=14, startDate=start) # 试排参数
|
||
result: ScheduleResult = get_engine(engine_type).solve(sandbox, params, _sandbox_counter()) # 沙盒求解
|
||
card = { # 方案卡(ScenarioCard 的 M2 子集)
|
||
"scenarioId": uuid.uuid4().hex[:8], # 方案 ID(M2:方案即"可采用的策略",M3 起挂真分支)
|
||
"label": label, # 中文名
|
||
"strategy": strategy, # 策略模板(采用时重跑用——引擎确定性保证结果一致)
|
||
"engine": engine_type, # 引擎类型
|
||
"kpi": { # KPI 组(对比维度)
|
||
"poCount": result.poCount, "woCount": result.woCount,
|
||
"conflictCount": result.conflictCount,
|
||
"totalTardiness": round(result.totalTardiness, 1),
|
||
"avgUtilization": round(result.avgUtilization, 3),
|
||
"totalCost": round(result.totalCost),
|
||
"totalChangeoverMin": float(
|
||
(sandbox["scheduleVersions"][-1] or {}).get("totalChangeoverMin") or 0
|
||
) if sandbox.get("scheduleVersions") else 0.0,
|
||
"poSaved": int(
|
||
((sandbox["scheduleVersions"][-1] or {}).get("campaign") or {}).get("poSaved") or 0
|
||
) if sandbox.get("scheduleVersions") else 0,
|
||
},
|
||
# 相对基准版本的差异摘要(§9.7 diff_vs_baseline 的 M2 简化)
|
||
"diffVsBaseline": ({
|
||
"tardiness": round(result.totalTardiness - baseline["totalTardiness"], 1),
|
||
"conflicts": result.conflictCount - baseline["conflictCount"],
|
||
"utilization": round(result.avgUtilization - baseline["avgUtilization"], 3),
|
||
} if baseline else None),
|
||
# 风险提示:从沙盒冲突里提炼要点(最多 2 条)
|
||
"risks": [c["description"] for c in sandbox["conflicts"][-result.conflictCount:][:2]] if result.conflictCount else [],
|
||
}
|
||
cards.append(card) # 收集方案卡
|
||
# 组装方案卡组 UI 块(§6.2 scenario-cards 类型;每卡带"采用"动作 P1)
|
||
block = UIBlock(
|
||
blockId=f"scenarios-{uuid.uuid4().hex[:8]}", # 块 ID
|
||
type="scenario-cards", # 块类型
|
||
props={"cards": cards, # 卡组数据
|
||
"baseline": baseline["versionNo"] if baseline else None}, # 基准版本号(对照展示)
|
||
actions=[UIAction(actionId="scenario.apply", label="采用此方案", power="P1", # 采用动作(前端逐卡渲染)
|
||
payload={})],
|
||
)
|
||
# 回复文案:一句话对比结论(找延迟最小者作为推荐)
|
||
best = min(cards, key=lambda c: (c["kpi"]["totalTardiness"], c["kpi"]["conflictCount"])) # 简单推荐规则
|
||
text = (f"已在沙盒中并行试排 {len(cards)} 种策略(未影响当前方案):\n" +
|
||
"\n".join(f"· {c['label']}:延迟 {c['kpi']['totalTardiness']}h / 冲突 {c['kpi']['conflictCount']} / "
|
||
f"利用率 {round(c['kpi']['avgUtilization'] * 100)}%" for c in cards) +
|
||
f"\n综合看【{best['label']}】表现最好。点方案卡上的“采用此方案”即可正式排产。") # 推荐话术
|
||
return text, block # 返回文案与卡组块
|