curtail.py 5.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. # -*- coding: utf-8 -*-
  2. """L1 工况门: 限电识别与运行状态分类 (统一技术栈前置硬门; 口径=零位专项 20260821 已审: 未被卡=PowerRef>1.15×实发).
  3. 自校验锚: 专项实测全场限电时段占比 2025=19.7% / 2026(1-7月)=76.9% — 复现不出即分类器错.
  4. 边界: 停机型限电须状态码/调度令 (报警轴已有 远程停机-业主 1007×359条, 接入待 M4c)。
  5. 锁页互证 (dsh_learn D运行知识): ActRegSt 恒 NotActive 非限电码; 场级受限同步(中位31/38)=park调度; 受限时间 2025≈0.20/2026≈0.79 与本门命令面锚一致; 七态逐台锁表=后训练_创新_如东工况归属.csv."""
  6. import pandas as pd, numpy as np
  7. from src.windscada.config import farm
  8. from app_ETL.app_ETL_guanlan.api import load_10min
  9. CUTIN_MS = 3.0 # 切入风速 (SWT-4.0-130); 低风待机门
  10. STATES = ['正常发电', '限电命令·未绑定', '限电·绑定', '停机', '低风待机']
  11. # 双口径 (2026-08-24 数据裁决, WTG13 单台: 命令面/全行 0.160/0.726 ≈ 专项锚 0.197/0.769; 绑定面/发电行 0.104/0.511):
  12. # 命令面 = PowerRef<0.95×额定 (被调度限的时段, 专项统计口径); 绑定面 = 命令面 ∧ ref≤1.15×实发 (损失真发生, 专项剥离口径的补集)
  13. def classify(d, rated_kw=4000):
  14. p = d['grd_wtc_ActPower_mean']; ref = d['tur_wtc_PowerRef_endvalue']; rpm = d['tur_wtc_GenRpm_mean']
  15. # ★2026-08-26 修 (云端外审逮→根因): 低风门原用 tur_wtc_PriAnemo_mean = **全场恒0.01死哨兵**,
  16. # 且该列因恒值未进契约 → d.get() 返回 None → 低风门**静默失效**, "低风待机"从未产生,
  17. # 切入风速以下的待机(实测占停机行 32%)全被算成"停机" ⇒ 可用率/MTBF/停机帕累托系统性偏。
  18. # 修法: 用活风源 SecAnemo; 风通道缺失必须**响亮失败**, 不许静默跳过判据。
  19. ws = d.get('tur_wtc_SecAnemo_mean')
  20. if ws is None:
  21. raise KeyError('classify: 缺 tur_wtc_SecAnemo_mean — 低风门不可跳过 (守护失败必响亮; '
  22. 'PriAnemo 是恒0.01死哨兵不可作风源)')
  23. st = pd.Series('正常发电', index=d.index)
  24. cmd = ref < 0.95 * rated_kw
  25. bind = cmd & (ref <= 1.15 * p.clip(lower=1)) & (p > 100)
  26. st[cmd & (p > 100) & ~bind] = '限电命令·未绑定'
  27. st[bind] = '限电·绑定'
  28. stop = (p <= 0) & (rpm < 100)
  29. lowwind = stop & ws.notna() & (ws < CUTIN_MS) # 切入风速以下 = 待机, 非停机
  30. st[stop] = '停机'
  31. st[lowwind] = '低风待机'
  32. return st
  33. def farm_daily(cfg=None, turbines=None):
  34. cfg = cfg or farm()
  35. rows = []
  36. for t in (turbines or cfg['turbines']):
  37. try:
  38. d = load_10min(t, cfg, groups=['A.功率', 'A.风况', 'A.转速'])
  39. except FileNotFoundError:
  40. continue
  41. d['state'] = classify(d, cfg['rated_kw'])
  42. d['date'] = d['ts'].dt.date
  43. g = d.groupby('date')['state'].value_counts().unstack(fill_value=0)
  44. g['turbine'] = t
  45. rows.append(g.reset_index())
  46. return pd.concat(rows, ignore_index=True).fillna(0)
  47. def self_check(cfg=None, sample=6):
  48. """自校验: 抽样台复现专项限电占比 (2025≈19.7%, 2026≈76.9%)."""
  49. cfg = cfg or farm()
  50. ts_ = cfg['turbines'][::max(1, len(cfg['turbines']) // sample)][:sample]
  51. out = {}
  52. for t in ts_:
  53. d = load_10min(t, cfg, groups=['A.功率', 'A.风况', 'A.转速'])
  54. d['state'] = classify(d, cfg['rated_kw'])
  55. d['year'] = d['ts'].dt.year
  56. cmd_share = d.groupby('year').apply(lambda x: float((x['tur_wtc_PowerRef_endvalue'] < 0.95 * cfg['rated_kw']).mean()), include_groups=False)
  57. op = d[d['grd_wtc_ActPower_mean'] > 100]
  58. bind_share = op.groupby('year')['state'].apply(lambda s: (s == '限电·绑定').mean())
  59. out[t] = {int(y): dict(命令面=round(float(cmd_share.get(y, float('nan'))), 3), 绑定面=round(float(bind_share.get(y, float('nan'))), 3)) for y in cmd_share.index}
  60. return out
  61. DISPATCH_CODES = {'1007': '远程停机-业主', '1008': '远程停机-西门子'} # M4a 自动发现, 与 dsh_learn 记忆互证
  62. def dispatch_intervals(cfg=None):
  63. """调度令停机区间 (报警事件层). 用于把'停机'细分出'停机(调度令)' — 停机型限电的证据轴."""
  64. import pandas as pd
  65. cfg = cfg or farm()
  66. al = pd.read_parquet(cfg['store'] / 'alarms.parquet')
  67. d = al[al.code.isin(DISPATCH_CODES)].dropna(subset=['t_on', 't_off'])
  68. return d[['turbine', 'code', 't_on', 't_off']].rename(columns={'t_on': 'time_on', 't_off': 'time_off'})
  69. def refine_stop(d, turbine, intervals):
  70. """把 classify() 的'停机'行细分: 落在调度令区间内 → '停机(调度令)'."""
  71. iv = intervals[intervals.turbine == turbine]
  72. if not len(iv):
  73. return d
  74. stop = d['state'] == '停机'
  75. if not stop.any():
  76. return d
  77. ts = d.loc[stop, 'ts'].values
  78. hit = np.zeros(stop.sum(), dtype=bool)
  79. for _, r in iv.iterrows():
  80. hit |= (ts >= np.datetime64(r.time_on)) & (ts <= np.datetime64(r.time_off))
  81. idx = d.index[stop][hit]
  82. d.loc[idx, 'state'] = '停机(调度令)'
  83. return d