scenario_29.py 8.9 KB

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