| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- # -*- coding: utf-8 -*-
- """29# 修泵排期沙盘 — ontology Scenario 机制第一次实跑 (2026-08-28 用户令).
- Palantir 思路的 Scenario: 在本体对象上开"如果"分支推演, 不动真实状态; 输出=建议级 (L3 动作人裁).
- 四情景: A 立即修(下一 CTV 窗) / B 推迟1月 / C 推迟3月 / D 拖到轴承失效(吊装级).
- 诚实边界: 轴承损伤升级概率无该机型标定数据 → 不给伪精确概率, 给结构+量级+方向 (INSUFFICIENT 处显式标注).
- 跑法: .venv/bin/python -m src.ontology.scenario_29"""
- from src import paths as P
- import json, pathlib, sys
- import pandas as pd
- from .store import Store
- ST = P.store()
- ONT = P.ont()
- # 电价区间 (合同电价未知 → 区间口径, 不冒充精确): 江苏燃煤基准 0.391 / 2016-17 核准海上标杆 0.85 元/kWh
- PRICE = (0.391, 0.85)
- REPAIR_DAYS_CTV = 1 # 修泵 = CTV 级小修, 按 1 个作业日估 (润滑泵舱内更换, 无吊装)
- REPLACE_DAYS = 14 # 轴承更换 = 吊装级, 按 2 周停机估 (蒋家沙同类作业量级, 不含等船)
- REPLACE_COST_RMB = (2e6, 5e6) # 大部件更换单次数百万级 (grade D 弱源, parts_cost_public.json)
- def monthly_profile():
- """29# 逐月: 发电态占比 + 正常发电均功率 → '停机1天的机会成本' (限电稀释后)."""
- lm = pd.read_parquet(ST / 'loss_monthly.parquet')
- r = lm[lm.turbine == 'WTG29']
- piv = r.pivot_table(index='month', columns='state', values='rows_', aggfunc='sum').fillna(0)
- tot = piv.sum(axis=1)
- gen_share = piv.get('正常发电', 0) / tot
- gen = r[r.state == '正常发电'].groupby('month').agg(kwh=('act', 'sum'), rows=('rows_', 'sum'))
- mean_kw = gen.kwh / (gen.rows / 6)
- out = pd.DataFrame(dict(gen_share=gen_share, mean_kw=mean_kw)).dropna()
- # 停机1天期望少发 kWh = 均功率 × 24h × 发电态占比 (发电态占比已内含限电/低风/停机的稀释)
- out['day_kwh'] = out.mean_kw * 24 * out.gen_share
- return out
- def weather():
- m = pd.read_parquet(ONT / 'weather_windows_monthly.parquet').set_index('month')
- return m
- def _yuan(kwh):
- return f'{kwh*PRICE[0]/1e4:.1f}~{kwh*PRICE[1]/1e4:.1f} 万元'
- def build():
- prof = monthly_profile()
- wx = weather()
- def month_ref(m):
- """目标月的参考画像: 优先去年同月 (2026-07 后无实测, 用 2025 同月; 限电水平按 2026 常态化上修标注)."""
- y, mm = m.split('-')
- prev = f'2025-{mm}'
- src = m if m in prof.index else prev
- return prof.loc[src], src
- def ctv_days(m):
- y, mm = m.split('-')
- src = m if m in wx.index else f'2025-{mm}'
- r = wx.loc[src]
- return int(r['CTV人员转运_可作业日']), int(r['吊装作业_可作业日']), int(r['天数']), src
- scenarios = []
- for sid, m, wait_mo, note in [('A_立即修', '2026-09', 0, '下一个 CTV 窗'),
- ('B_推迟1月', '2026-10', 1, ''),
- ('C_推迟3月', '2026-12', 3, '')]:
- p, psrc = month_ref(m)
- ctv, lift, dtot, wsrc = ctv_days(m)
- loss_kwh = float(p.day_kwh) * REPAIR_DAYS_CTV
- scenarios.append(dict(
- id=sid, 修理窗=m, 作业级='CTV小修(修泵, 无吊装)',
- 天气窗=f'{m} CTV 可作业 {ctv}/{dtot} 天 (历史{wsrc}窗, 几乎不构成约束)',
- 停机损失=dict(少发电量=f'{loss_kwh:.0f} kWh ({REPAIR_DAYS_CTV}作业日, 画像月={psrc}, '
- f'发电态占比{p.gen_share:.0%}已含限电稀释)',
- 电费口径=_yuan(loss_kwh) + f' (电价区间 {PRICE[0]}~{PRICE[1]} 元/kWh, 合同价未知)'),
- 风险敞口=dict(等待月数=wait_mo,
- 定性=('无新增暴露' if wait_mo == 0 else
- f'{wait_mo} 个月金属接触暴露 — 尖峰幅度已在爬 (9.15→29.13 m/s², 截至2026-08-11振动窗), '
- '升级概率无本机型标定数据 = INSUFFICIENT, 方向明确幅度未知'),
- 对冲='升级即落入情景 D 的成本结构'),
- ))
- # 情景 D: 拖到轴承失效 (发生时点不可知 → 按"冬季发生"与"台风季发生"两个子况给窗)
- p12, _ = month_ref('2026-12')
- lift12 = ctv_days('2026-12')
- lift08 = ctv_days('2026-08')
- loss_replace = float(p12.day_kwh) * REPLACE_DAYS
- scenarios.append(dict(
- id='D_拖到失效', 修理窗='不可控 (失效时点决定)', 作业级='吊装级主轴承更换',
- 天气窗=(f'吊装窗受限: 12月历史 {lift12[1]}/{lift12[2]} 天可吊装, 台风季8月仅 {lift08[1]}/{lift08[2]} 天; '
- '且需大型起重船船期 (响水T1同类按3年框架采购, 等船周期额外)'),
- 停机损失=dict(少发电量=f'≈{loss_replace:.0f} kWh ({REPLACE_DAYS}天停机, 不含等船期; 冬季画像)',
- 电费口径=_yuan(loss_replace) + ' (仅停机侧)',
- 大部件成本=f'{REPLACE_COST_RMB[0]/1e6:.0f}~{REPLACE_COST_RMB[1]/1e6:.0f} 百万元级 '
- '(grade D 弱源; 蒋家沙21#同类作业2025-08实际发生)'),
- 风险敞口=dict(定性='情景成立即最坏情形; 修泵窗口意义 = 用万元级小修对冲百万元级大修'),
- ))
- # key_insight 数字全部机器回抓 (headline 教训第2处: 初稿手写"~8千kWh"实算2万, 连环同坑后全 f-string 化)
- p25 = prof.loc['2025-01':'2025-12']; p26 = prof.loc['2026-01':'2026-06'] # 2026-07 窗不满月剔除
- key_insight = (
- f'限电常态化改变检修机会成本结构: 29# 发电态占比 2026 年 {p26.gen_share.min():.0%}~{p26.gen_share.max():.0%} '
- f'(2025 年 {p25.gen_share.min():.0%}~{p25.gen_share.max():.0%}), '
- f'停机 1 天的少发电量: 2025 年 {p25.day_kwh.min()/1e3:.0f}~{p25.day_kwh.max()/1e3:.0f} 千kWh/天 → '
- f'2026 限电月 {p26.day_kwh.min()/1e3:.0f}~{p26.day_kwh.max()/1e3:.0f} 千kWh/天 — '
- '**限电深的月份检修机会成本大幅下降**; 修泵(CTV级)天气窗全年几乎不受限, '
- '真正受窗约束的是拖到失效后的吊装级更换 (台风季 15~18 天/月 + 等船期)。'
- '⇒ 排期决策的不对称性: 早修的成本被限电稀释, 晚修的成本被窗口与船期放大。')
- a_cost = scenarios[0]['停机损失']['电费口径'] # 建议数字从情景实算回抓, 禁手写 (headline 教训)
- a_ctv = scenarios[0]['天气窗']
- recommendation = (
- f'建议 (建议级, L3 动作人裁): 情景 A — 2026-09 首个 CTV 窗执行修泵。'
- f'依据: ①停机损失 {a_cost} — 且画像月用 2025-09 (限电0%) 属保守高估, '
- f'2026 限电常态化下实际更低 ②{a_ctv} '
- '③每推迟 1 个月 = 1 个月金属接触暴露 (尖峰幅度在爬, 截至2026-08-11), '
- '升级即落入 D 的百万级结构 ④修后验收树已备 (TCM-16 三分支, 柱塞回带+尖峰消判据)。'
- '推迟情景 B/C 省不下钱 (损失同为万元级) 只增加暴露 — 无收益纯风险。')
- s = Store(ONT / 'objects.json')
- s.put(dict(id='scenario/WTG29/repair_scheduling_v1', type='Decision',
- props=dict(name='29# 修泵排期沙盘 v1 (Scenario 推演, 建议级)',
- scenarios=scenarios, key_insight=key_insight,
- recommendation=recommendation,
- boundaries=['轴承升级概率无本机型标定 = INSUFFICIENT, 只给方向',
- '电价用区间口径 (合同价未知)',
- '2026-07 后无 SCADA 实测, 画像月用 2025 同月 — 2025-09 限电0%, '
- '2026 限电常态化下实际损失比沙盘值更低 (保守方向, 强化情景A)',
- '天气窗为公开再分析统计, 非派工依据; 修泵作业时长 1 天为估计',
- '大部件成本 grade D 弱源'],
- claim_window='数据窗: SCADA 至2026-07-07 / 振动至2026-08-11 / 天气至2026-08-25'),
- links=dict(basedOn=['verdict/WTG29/TCM-16', 'mwindow/rudong/climatology'],
- informs=['action/WTG29/repair_pump'])))
- s.save()
- out = ONT / 'scenario_29_repair.json'
- out.write_text(json.dumps(dict(scenarios=scenarios, key_insight=key_insight,
- recommendation=recommendation), ensure_ascii=False, indent=1),
- encoding='utf-8') # 中文 JSON: 缺 encoding 在中文 Windows 上按 GBK 写
- return scenarios, key_insight, recommendation
- if __name__ == '__main__':
- sc, ki, rec = build()
- for x in sc:
- print(f"[{x['id']}] 窗={x['修理窗']} | {x['停机损失'].get('电费口径','')}")
- print('\n★', ki, '\n\n→', rec)
|