| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495 |
- # -*- coding: utf-8 -*-
- """schedule.py — 柱2「智能排程」核心: 检修停机窗口机会成本最小化 (farm-agnostic).
- B1 检修排程 (docs/B1检修排程_模块设计_v0.1.md) 的可复用内核: 把 scripts/maint_schedule_gjd.py
- 的郭家店写死数学抽成纯函数 + 接柱1 cases 当任务池。
- 一句话: 同样的必要停机, 移到低(风×价)窗口 — 不修任何东西的纯排程收益。
- ★物理内核: 检修窗机会成本 = Σ_窗内(预期功率 × 预期电价)。现货省份两轴合一 = 低(风×价)积窗。
- ★纪律: 输出 best+mean+worst + saving (禁单点最优, 风/价预期都有误差); ¥ 用假定价 → verdict【参考·参数假定】。
- ★柱1×柱2 桥: tasks_from_cases 把柱1 CONFIRMED/预警 case 转成停机任务池 (发现→排程闭环)。
- 纯函数 (optimize_schedule): 不碰文件/时钟; per-farm 数据加载在 runner。
- """
- def optimize_schedule(day_costs, task_turbine_days, per_day_capacity=2):
- """核心: 给每个可选作业日的停机成本 → 排 task_turbine_days 台日 → best/mean/worst 机会成本。
- day_costs: {day_key: 元/台日} — 每个候选作业日停一台的机会成本 (低风×低价日=便宜)。
- task_turbine_days: 总停机台日 (如 偏航治理 59 台日 / D29 大修 3 台日)。
- per_day_capacity: 每日最多同时停几台 (班组约束)。
- 返回 {best_cny, mean_cny, worst_cny, saving_vs_mean, saving_pct, need_days, best_days, worst_days}。
- 禁单点最优: 同时给最差/均匀基线 = 决策者看区间不看点。
- """
- if not day_costs or task_turbine_days <= 0 or per_day_capacity <= 0:
- return {'verdict': 'INSUFFICIENT', 'reason': '空任务池/无候选日/容量非正'}
- import math
- days_sorted = sorted(day_costs.items(), key=lambda kv: kv[1]) # 便宜→贵
- costs = [c for _, c in days_sorted]
- need_days = math.ceil(task_turbine_days / per_day_capacity)
- if need_days > len(costs):
- return {'verdict': 'INSUFFICIENT',
- 'reason': f'需 {need_days} 作业日 > 可选 {len(costs)} 日 (候选窗不足)'}
- def _sum_schedule(day_cost_list):
- """按台日铺进日: 前 need_days-1 日各满容量, 末日铺剩余。"""
- rem, total = task_turbine_days, 0.0
- for c in day_cost_list[:need_days]:
- put = min(per_day_capacity, rem)
- total += c * put
- rem -= put
- return total
- best = _sum_schedule(costs) # 最便宜 need_days 日
- worst = _sum_schedule(costs[::-1]) # 最贵 need_days 日
- mean_day = sum(costs) / len(costs)
- mean = mean_day * task_turbine_days # 均匀随机基线
- saving = mean - best
- return {
- 'verdict': '参考',
- 'best_cny': round(best), 'mean_cny': round(mean), 'worst_cny': round(worst),
- 'saving_vs_mean': round(saving),
- 'saving_pct': round(saving / mean * 100, 1) if mean else None,
- 'need_days': need_days, 'n_candidate_days': len(costs),
- 'best_days': [k for k, _ in days_sorted[:need_days]],
- 'worst_days': [k for k, _ in days_sorted[::-1][:need_days]],
- '_note': 'best/mean/worst 三值 (禁单点最优); ¥ 若用假定价 → 交付标【参考·参数假定·待业主结算价】',
- }
- def day_costs_from_power_price(power_by_day_hour, price_by_hour, work_hours=range(8, 17)):
- """构造 day_costs: 每完整覆盖作业窗的日 → Σ_作业时(场中位功率 kW/1000 × 逐时价 元/MWh)。
- power_by_day_hour: {(day, hour): 场中位功率kW}; price_by_hour: {hour: 元/MWh}。
- 只取作业窗被完整覆盖的日 (缺时段的日不可比)。单台停机成本 = 该日作业窗少发的电 × 价。
- """
- wh = list(work_hours)
- by_day = {}
- for (day, hour), pkw in power_by_day_hour.items():
- # NaN-safe: pkw != pkw 逮 NaN (缺测小时不计入, 该日作业窗覆盖会因此不足而剔)
- if hour in wh and price_by_hour.get(hour) is not None and pkw is not None and pkw == pkw:
- by_day.setdefault(day, {})[hour] = max(pkw, 0.0) / 1000.0 * price_by_hour[hour]
- # 只保留作业窗每小时都在的日
- return {day: round(sum(hc.values()), 2) for day, hc in by_day.items() if len(hc) == len(wh)}
- def tasks_from_cases(cases, states=('CONFIRMED', 'TICKETED', 'RELAPSED'),
- verdicts=('准定论·预警', '定论'), td_per_turbine=1.0):
- """柱1×柱2 桥: 柱1 cases → 停机任务池。默认取 CONFIRMED/工单态 (真需停机的); 可放宽到预警候选。
- states/verdicts: 纳入排程的门槛 (默认只排已确诊/已开工单; 放宽 verdicts 含预警可做"预排程")。
- 返回 {'turbines': [...], 'turbine_days': N, 'basis': ...}。
- """
- picked = []
- for c in (cases or []):
- if not isinstance(c, dict):
- continue
- st_ok = c.get('state') in states
- vd_ok = (c.get('verdict') or '').strip('【】') in verdicts
- if st_ok or vd_ok:
- picked.append({'turbine': c.get('turbine'), 'system': c.get('system'),
- 'case_id': c.get('case_id'), 'state': c.get('state'), 'verdict': c.get('verdict')})
- tds = round(len(picked) * td_per_turbine, 1)
- return {'turbines': picked, 'turbine_days': tds,
- 'basis': f'{len(picked)} 台 (state∈{states} ∨ verdict∈{verdicts}) × {td_per_turbine}台日/台'}
|