reactive_validation.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. # -*- coding: utf-8 -*-
  2. """reactive_validation.py — 无功功率控制校核: per机组无功调度偏差 (变流器无功调节异常候选).
  3. 裁决对象 = fleet 内 **单机无功 Q (VAr) 显著偏离机群共同调度水平** 的候选 (变流器/无功控制侧) —
  4. 与有功 P (W) **物理正交** (Q=VAr 无功 vs P=W 有功)。区别 sensor 三相 imbalance (仪表) / gear (机械)。
  5. 来源: gym RQ 赛道 (EXP-RQ1-01 bootstrap TRUE, 五门 H0-H4 齐过: 判别 RCV 1.158 / 无功偏差 recovery 1.0×3seed /
  6. 反臆造 0; 真盘 hailesi 台17 median Q −76 kVAr ≈ 8.6× fleet)。gym 分数≠交付, 本件 = 走设计 §7 四闸后的生产实现。
  7. ★物理/统计 trace (第一性, no black box):
  8. 健康机群近-unity 功率因数调度 (园区 AVC/PF 指令趋一致) → per台 loaded 态中位无功 Q 近机群共同水平;
  9. 单机变流器无功调节异常 (指令跟踪差/无功环故障) → 该台 Q 强偏 → dev=|medQ−fleet_common| 抬 → flag (候选级筛查)。
  10. metric = per台 loaded (P>loaded_min) 中位 Q; fleet_common = 各台 medQ 中位; dev = |medQ−fleet_common|;
  11. |Q|/S (无功占视在功率比) = 调度强度探针 (S=√(P²+Q²))。
  12. ★别名前提已核 (承 LESSONS #50/#51, 数据面第0步硬门; RV-1 逮修数): p_reactive 逐台 corr vs 已用通道
  13. (hailesi 真盘逐台 |corr(Q,P)| 中位 **0.111** / max **0.302**; vs grid电流 中位 0.107) — **≪0.5 → 真分立传感通道**
  14. (非 actual_torque=P/ω 那类 0.9998 派生别名)。**注**: gym 早报 "0.042/<0.13" 系 pooled/全局 corr (偏乐观), 逐台真值
  15. 0.11/0.30, 结论(分立非别名)稳但**引用取 <0.5 界非具体小数**; 调用方跨场须逐台重核 (新场 Q vs P/S)。
  16. ★Q-P 变化判别 (RV-1 逮 + 真盘证, in-data 部分排调度混杂): per台 med Q **随功率 P 强变** (Q≈P·tanφ, 台17
  17. binQ −10→−54kVAr/P升) = **恒定功率因数/电压droop 调度签名** → 偏调度侧 (印证候选非确诊); **平** (正常台 ±3kVAr
  18. across P) = 恒偏 (config/故障型)。本件出 qp_span (P分箱 med Q 极差) + dispatch_like → **像变桨电机温度信道那样
  19. in-data 部分辨调度vs故障** (但 suggestive 非 dispositive, 终判仍需 Q-setpoint 日志)。
  20. ★诚实界 (headline confound + 3原则审):
  21. - **调度混杂 (headline, 最重要)**: 无功部分是**电网/园区调度指令** (AVC/PF setpoint) → fleet-relative 天然离群
  22. **可能是执行逐台 Q 指令 (非故障)** 而非变流器异常 → **候选非确诊, 分离控制差异 vs 故障需园区 Q-setpoint 日志**
  23. (无则 INSUFFICIENT-by-归因)。检出器效度 (注入 recovery / 反臆造, gym H3/H4) 与调度**无关** (对合成偏置有效),
  24. 但真盘天然离群的**归因**承此混杂 (同 fatigue TI-exposure / yaw environment / 电机 measurement-side 型: 信号真、归因需外部数据)。
  25. - **相对非绝对**: fleet-relative 排序/离群, 非绝对无功健康值; 全场一致偏 (整场同 AVC 指令) → fleet_common 吸收 → **批次盲**
  26. (承 relative-criterion-batch-blindness) → 批次嫌疑场须绝对基准/园区指令对照。
  27. - **loaded 条件化**: idle/停机态 Q = standby 无功 (非发电调度) → 必 loaded (P>loaded_min) 条件化; loaded 样本不足 → 弃权。
  28. - **符号双向**: Q 可正 (过励供无功) 或负 (欠励吸无功); dev=|medQ−fleet| 两向离群皆捕; sign_hint 报偏向。
  29. - 需 reactive_col + power_col; turbine id 列全空 → INSUFFICIENT (承 yaw turbine_hash 坏列, §48 F3)。
  30. DP 验收 = tests/sop/test_reactive_power_dp.py。纯核心无 I/O 可测; per-farm 加载在调用方。
  31. """
  32. from __future__ import annotations
  33. import numpy as np
  34. import pandas as pd
  35. def _robust_hi(vals, cv_floor: float, k_mad: float):
  36. """fleet 高离群阈: hi = median + k·**地板 scale** (max(1.4826·MAD, cv_floor·|median|)) — 承 yaw §48 F2
  37. 地板 scale 防 MAD≈0 紧 bulk 漏 clean 离群; 批次盲仍保 (全场一致 → 无值超阈)。返回 (median, hi_thresh, robust_cv)。"""
  38. v = np.asarray([x for x in vals if x is not None and np.isfinite(x)], float)
  39. if v.size < 5:
  40. return None, None, None
  41. med = float(np.median(v)); mad = float(np.median(np.abs(v - med)))
  42. rcv = (1.4826 * mad) / abs(med) if med != 0 else np.inf
  43. scale = max(1.4826 * mad, cv_floor * abs(med)) if med != 0 else (1.4826 * mad or float(np.std(v)))
  44. return med, med + k_mad * scale, round(rcv, 4)
  45. def reactive_power_verdict(
  46. df: pd.DataFrame, *, reactive_col: str, power_col: str, turbine_col: str,
  47. loaded_min: float = 200.0, min_loaded_n: int = 500,
  48. k_mad: float = 3.5, uniform_cv_floor: float = 0.03,
  49. ) -> dict:
  50. """per-机组 **无功功率调度偏差** 校核 (变流器无功调节异常候选; 候选级非确诊).
  51. 返回 {tid: {"med_q": loaded中位无功, "dev": |medQ−fleet_common|, "flag": bool, "z": fleet稳健z,
  52. "absq_over_s": |Q|/S 调度强度, "sign_hint": over|under (供/吸无功), "n_loaded": 样本, "note"?}}。
  53. 另 {"_fleet": {"fleet_common_q": ..., "hi_thresh": ..., "robust_cv": ..., "verdict": CANDIDATE_RANKING|INSUFFICIENT,
  54. "honesty": [...]}}。
  55. 机制: per台 loaded(P>loaded_min) 中位 Q → fleet_common(各台medQ中位) → dev=|medQ−fleet_common|;
  56. flag = dev > 地板scale稳健阈 (承 yaw §48 F2)。**headline: 调度混杂** — 见模块诚实界。
  57. """
  58. HON = [
  59. "(a) 调度混杂 headline: 无功部分=电网/园区AVC/PF指令 → fleet离群可能执行逐台Q指令非故障 → 候选非确诊, "
  60. "分离控制差异vs故障需园区Q-setpoint日志(无则INSUFFICIENT-by-归因); 检出器效度(注入recovery)与调度无关但归因承混杂。",
  61. "(b) 相对非绝对: fleet-relative离群非绝对无功健康值; 全场一致偏(同AVC指令)→fleet_common吸收→批次盲(须绝对基准/指令对照)。",
  62. "(c) loaded条件化: idle/停机Q=standby非发电调度→必P>loaded_min条件化; loaded样本不足→弃权。",
  63. "(d) 别名前提(承#50/#51): p_reactive须真分立通道(逐台corr vs P/S<0.5, 非P/ω派生别名); 跨场重核。",
  64. "(e) 需reactive+power列 ∧ 有效turbine id列(全空→INSUFFICIENT); 符号双向(过励+/欠励−, dev两向捕)。",
  65. ]
  66. # NaN-safe: Arrow str backend 下 astype(str) 留 float NaN → sorted 混型崩; dropna + 显式 str()
  67. tids = sorted({str(x) for x in df[turbine_col].dropna().unique()})
  68. NULL = {"med_q": None, "dev": None, "flag": False, "z": None, "absq_over_s": None,
  69. "sign_hint": None, "n_loaded": 0}
  70. def _wrap(reason, per=None):
  71. base = per or {t: {**NULL, "note": reason} for t in tids}
  72. base["_fleet"] = {"fleet_common_q": None, "hi_thresh": None, "robust_cv": None,
  73. "verdict": "INSUFFICIENT", "honesty": HON, "note": reason}
  74. return base
  75. if reactive_col not in df.columns or power_col not in df.columns:
  76. return _wrap("无 reactive/power 列; abstain (INSUFFICIENT)")
  77. if df[turbine_col].notna().sum() == 0 or (len(tids) == 1 and tids[0] in ("nan", "None", "")):
  78. return _wrap("turbine id 列全空/坏列 (如 turbine_hash 全 NaN) → 无法分组; INSUFFICIENT")
  79. Q = pd.to_numeric(df[reactive_col], errors="coerce").to_numpy(float)
  80. P = pd.to_numeric(df[power_col], errors="coerce").to_numpy(float)
  81. tv = df[turbine_col].astype(str).to_numpy()
  82. loaded = np.isfinite(Q) & np.isfinite(P) & (P > loaded_min)
  83. S = np.sqrt(P ** 2 + Q ** 2)
  84. per = {}
  85. for t in tids:
  86. m = loaded & (tv == t)
  87. if m.sum() < min_loaded_n:
  88. per[t] = {**NULL, "n_loaded": int(m.sum()), "note": "loaded(发电态)样本不足; INSUFFICIENT"}
  89. continue
  90. medq = float(np.median(Q[m]))
  91. with np.errstate(all="ignore"):
  92. aqs = float(np.median(np.abs(Q[m]) / S[m]))
  93. # Q-P 变化判别 (RV-1): P 分箱 med Q 极差; 强变=调度PF签名(偏调度), 平=恒偏(config/故障型)
  94. qb = [float(np.median(Q[m & (P >= lo) & (P < hi)]))
  95. for lo, hi in ((loaded_min, 600.0), (600.0, 1200.0), (1200.0, 1e12))
  96. if (m & (P >= lo) & (P < hi)).sum() >= 50]
  97. qp_span = round(max(qb) - min(qb), 1) if len(qb) >= 2 else None
  98. per[t] = {"med_q": round(medq, 2), "dev": None, "flag": False, "z": None,
  99. "absq_over_s": round(aqs, 4), "sign_hint": ("over" if medq > 0 else "under"),
  100. "qp_span": qp_span, "dispatch_like": (qp_span is not None and abs(qp_span) > 30.0),
  101. "n_loaded": int(m.sum()),
  102. "note": ("med_q 随 loaded_min 变 (Q-P 强变工况台, qp_span 大); flag 稳但标量非稳定物理量, "
  103. "qp_span 大→调度PF签名偏调度侧" if (qp_span is not None and abs(qp_span) > 30.0) else None)}
  104. good = {t: per[t]["med_q"] for t in tids if per[t]["med_q"] is not None}
  105. if len(good) < 5:
  106. return _wrap("合格台数 <5 → fleet 稳健离群不可估; INSUFFICIENT", per)
  107. fleet_common = float(np.median(list(good.values())))
  108. devs = {t: abs(good[t] - fleet_common) for t in good}
  109. med_d, hi_thr, rcv = _robust_hi(list(devs.values()), uniform_cv_floor, k_mad)
  110. scale = max(1.4826 * float(np.median(np.abs(np.array(list(devs.values())) - med_d))),
  111. uniform_cv_floor * abs(med_d) if med_d else 0.0) or 1.0
  112. for t in good:
  113. per[t]["dev"] = round(devs[t], 2)
  114. per[t]["z"] = round((devs[t] - med_d) / scale, 2) if scale > 0 else 0.0
  115. per[t]["flag"] = bool(devs[t] > hi_thr)
  116. n_flag = sum(1 for t in good if per[t]["flag"])
  117. per["_fleet"] = {"fleet_common_q": round(fleet_common, 2), "hi_thresh": round(hi_thr, 2),
  118. "robust_cv": rcv, "verdict": "CANDIDATE_RANKING", "honesty": HON,
  119. "note": f"无功调度偏差 (候选级, 调度混杂 caveat 见 honesty a): fleet_common={fleet_common:.1f}kVAr; "
  120. f"高偏离 flag {n_flag} 台 (归因须 Q-setpoint 日志)。"}
  121. return per