hydraulic_validation.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. # -*- coding: utf-8 -*-
  2. """hydraulic_validation.py — 液压校核: 齿轮箱双压通道 Δp 共模/差模机制分型 (候选级筛查).
  3. 裁决对象 = per 机组 **液压回路机制分型候选** (滤芯堵 vs 泵退化) — 区别于 contract_gate (per点清洗) /
  4. temp NBM (部件发热)。本模块 = 压力轴: 单通道 fleet-NBM 能检出压力异常但**分不开机制** (EXP-M3-17 尸检),
  5. 双压通道共模/差模分解才是压力的对的工具。
  6. 来源: gym M 赛道 a22_dp_discriminator (EXP-M3-21, 2026-07-12 ESTABLISHED → 忠实内联并回 src/ 2026-07-15,
  7. 逐字段对拍相等, 见 EXP 文档 §8)。阈值 = EXP-M3-21 预注册值 (Z_FLAG=2.0 / DIFF_HI=0.60 / DIFF_LO=0.40 /
  8. 基线窗前20% / 当前窗后40% / 每窗每台 ≥200 gen 样本), **未重调**。
  9. DP 验收 = tests/sop/test_hydraulic_dp_dp.py。
  10. ★物理 (First Principles): 液压回路 油泵(yidukou 中位≈3.49bar) → 滤芯 → 齿轮箱入口(≈1.94bar),
  11. 滤芯两侧压降 Δp = pump − inlet ≈ +1.5bar (稳态)。
  12. - **滤芯堵塞** = 流阻升 → Δp 增大 → 入口(下游)与泵(上游)**分离** = **差模** (两通道残差方向/幅度不同)。
  13. - **泵退化** = 建压能力降 → 整回路压力**同向整体移** (Δp≈不变) = **共模**。
  14. 分型键 = |D| vs |C| 物理量纲(bar)占比, 对符号约定不敏感。
  15. ★机制 (no black box): per台 (inlet, pump) 各做**自参考时间差分** (当前窗中位 − 基线窗中位, 消台间良性
  16. 静态标定偏置 — 泵压台间良性 spread MAD≈0.33bar, 静态 fleet 残差会把它误判差模) → **fleet 去共模**
  17. (减 fleet 中位 delta, 消季节整场同漂) → 分解 共模 C=(r_in+r_pump)/2 / 差模 D=r_in−r_pump →
  18. 检出 score = max(|z_C|,|z_D|) (fleet-MAD z), flag = score ≥ z_flag; 分型 (仅 flagged 台有意义)
  19. diff_ratio = |D|/(|D|+|C|): ≥ diff_hi → 'filter' | ≤ diff_lo → 'pump' | 之间 → 'ambiguous'。
  20. ★验证状态 (诚实, 必读):
  21. - **候选级筛查, 非确诊** (SOP §0.2 候选): 无液压 gold (无确诊故障标签场) → **INJ-only 验证**。
  22. gym 台架 (yidukou 25台×整年, 2场景×3seed): recall 1.0 / target_pctl 94 / 分型 11/12 显式正确
  23. + 1 保守 ambiguous (diff_ratio 0.594 差 0.006 触 HI), **零 filter↔pump 混淆** (簇 0.59-0.78 vs
  24. 0.06-0.14, 间隙 ≥0.45)。真实故障形态未经 gold 验证。
  25. - **A6 真盘候选未裁**: yidukou 未注入数据上 A6 泵侧 +1.76bar / score 16.9 / diff_ratio 0.637 →
  26. filter 型签名 (滤芯背压)。无 gold / 无独立量 → 候选, 不定论。
  27. - **需双压通道场, 覆盖面窄**: cleaned 正本实测**仅 yidukou** (契约 dec_g2000_127) 同时有
  28. 齿轮箱入口压力+齿轮箱油泵压力; dongqi/dongfang 点表配置含同名通道 (未清洗落地)。用前必核场契约。
  29. - INJ 台架 fp_rate 0.04–0.22, 主体 = 真实未标注漂移 (即 A6), 非算法噪声 — 无 gold 分母虚高。
  30. ★诚实边界 (算法结构性):
  31. - **只检观察窗内发生的变化**: t0 起已存在的恒定故障 (基线窗前) 被自参考差分消掉 → 不可见
  32. (同 S 赛道静态恒偏教义: 无外部绝对参照 INSUFFICIENT)。
  33. - onset 落基线窗内或跨窗 → delta 被稀释 → 漏检/低估; 窗设计假设早期(前 base_frac)近似健康平稳。
  34. - **fleet 去共模前提 = 机群多数健康**: 批次共性液压故障 (>~50% 台同机制同期) 会被 fleet 中位
  35. 减掉 → 盲 (相对判据批次盲区, 场型识别先于判据)。
  36. - 泵退化的 INJ 建模 = 两通道**等幅**同降 (一阶均匀压损); 真实泵退化 inlet/pump 耦合比 ≠1 时
  37. diff_ratio 偏离 pump 簇 → 落 ambiguous (保守方向, 不误报 filter)。
  38. - diff_hi=0.60 对 filter 侧偏保守 (INJ 1/6 target 落 ambiguous), 但从不错分为 pump。
  39. - **score = fleet-相对 z (序统计门)**: 健康/极安静 fleet 上 top-order 台可机会性过 z=2 (物理量级
  40. 仅 ~0.01bar; INJ 台架 fp 0.04-0.22, EXP H3 本就 PARTIAL) → "干净零 flag" 不是本算法性质;
  41. 判读必须结合物理量级 (C_bar/D_bar: 真机制 ~0.5-1.8bar vs 噪声 <0.05bar) 与 fleet 分布。
  42. - mechanism = 机制**分型候选**, 非部件确诊; 收口需独立量 (滤芯压差表读数/油样/泵电流/现场检查)。
  43. - 停机态压力语义不同 → 只用 gen 态样本; fleet 双窗过门 <min_fleet 台 → **全场显式 abstain**
  44. (note, 非静默"干净"); 缺列同理 abstain。
  45. 纯核心 (hydraulic_dp_verdict) 无 I/O 可测; per-farm 加载在调用方 (skill/report 层)。
  46. """
  47. from __future__ import annotations
  48. import numpy as np
  49. import pandas as pd
  50. def _mad_scale(x: np.ndarray) -> float:
  51. """1.4826×MAD (稳健标度); <3 有限样本 → 0.0 (调用侧 or 1e-9 守零)。忠实自 gym _mad_scale。"""
  52. x = x[np.isfinite(x)]
  53. if x.size < 3:
  54. return 0.0
  55. return float(1.4826 * np.median(np.abs(x - np.median(x))))
  56. def hydraulic_dp_verdict(
  57. df: pd.DataFrame, *, inlet_col: str, pump_col: str, time_col: str, turbine_col: str,
  58. power_col: str | None = None, gen_flag_col: str | None = None, gen_power_min: float = 50.0,
  59. base_frac: float = 0.2, cur_frac: float = 0.4, min_per_win: int = 200,
  60. z_flag: float = 2.0, diff_hi: float = 0.60, diff_lo: float = 0.40, min_fleet: int = 5,
  61. ) -> dict:
  62. """per-机组 液压 Δp 共模/差模机制分型 (候选级)。
  63. 返回 {tid: {"flag": bool, "score": max(|z_C|,|z_D|), "mechanism": 'filter'|'pump'|'ambiguous'|None,
  64. "diff_ratio": |D|/(|D|+|C|) 或 None(弃权), "r_inlet_bar"/"r_pump_bar": 去共模后自参考
  65. 残差(bar), "C_bar"/"D_bar": 共模/差模分量(bar), "z_C"/"z_D": fleet-MAD z, "note"?: 弃权原因}}。
  66. mechanism 仅 flagged 台非 None (未 flag → None, 分型无意义); diff_ratio 对所有过双窗门的台报
  67. (含未 flag, 供排查), 弃权台为 None。
  68. gen 态: gen_flag_col (≥0.5 为运行) 优先, 否则 power_col > gen_power_min, 都缺 → 全行参与
  69. (调用方须自行保证已滤停机; 停机压力语义不同)。time_col 须 datetime64。
  70. 阈值为 EXP-M3-21 预注册值, 勿因单场重调 (变更走 gym 重验)。
  71. """
  72. # NaN-safe: Arrow str backend 下 astype(str) 留 float NaN → sorted 混型崩; dropna + 显式 str()
  73. tids = sorted({str(x) for x in df[turbine_col].dropna().unique()})
  74. NULL = {"flag": False, "score": 0.0, "mechanism": None, "diff_ratio": None,
  75. "r_inlet_bar": None, "r_pump_bar": None, "C_bar": None, "D_bar": None,
  76. "z_C": None, "z_D": None}
  77. if inlet_col not in df.columns or pump_col not in df.columns:
  78. return {t: {**NULL, "note": "双压通道列缺; abstain"} for t in tids}
  79. # gen 态过滤 (压力回路随负载; 停机压力语义不同) — 忠实自 gym run()
  80. if gen_flag_col and gen_flag_col in df.columns:
  81. gen = df[gen_flag_col].astype(float) >= 0.5
  82. elif power_col and power_col in df.columns:
  83. gen = df[power_col] > float(gen_power_min)
  84. else:
  85. gen = pd.Series(True, index=df.index)
  86. g = df[gen]
  87. if len(g) == 0:
  88. return {t: {**NULL, "note": "gen 态样本为 0; abstain"} for t in tids}
  89. # 双窗: 基线 = 时间跨度前 base_frac, 当前 = 后 cur_frac (跳过中段过渡, 早晚清晰对比)
  90. t0 = g[time_col].min()
  91. t1 = g[time_col].max()
  92. b_end = t0 + (t1 - t0) * base_frac
  93. c_start = t1 - (t1 - t0) * cur_frac
  94. base_mask = g[time_col] < b_end
  95. cur_mask = g[time_col] >= c_start
  96. # 自参考 delta (per 台, 每通道): 当前窗中位 − 基线窗中位 (消台间恒偏置)
  97. def _delta(col):
  98. # count = 非 NaN 计数 (RV-1 must-fix: size 含 NaN 行 → 全 NaN 死列可过门并静默输出
  99. # flag=False/score=nan 无 note, 违弃权契约; 列 liveness 失效是已知 SCADA 故障模式)
  100. b = g[base_mask].groupby(turbine_col)[col].agg(["median", "count"])
  101. c = g[cur_mask].groupby(turbine_col)[col].agg(["median", "count"])
  102. j = b.join(c, lsuffix="_b", rsuffix="_c")
  103. ok = (j["count_b"] >= min_per_win) & (j["count_c"] >= min_per_win)
  104. return (j["median_c"] - j["median_b"])[ok]
  105. di = _delta(inlet_col)
  106. dp = _delta(pump_col)
  107. common_tids = di.index.intersection(dp.index)
  108. if len(common_tids) < min_fleet:
  109. return {t: {**NULL, "note": f"仅 {len(common_tids)} 台满足双窗样本门 (<{min_fleet}); "
  110. "fleet 去共模不可靠, abstain"} for t in tids}
  111. di = di.loc[common_tids]
  112. dp = dp.loc[common_tids]
  113. # fleet 去共模: 减 fleet 中位 delta (消季节整场同漂; 留台特异变化) → 共模/差模分解
  114. ri = di - di.median()
  115. rp = dp - dp.median()
  116. C = (ri + rp) / 2.0 # 共模残差 (bar)
  117. D = ri - rp # 差模残差 (bar) = Δp 残差
  118. sC = _mad_scale(C.to_numpy()) or 1e-9
  119. sD = _mad_scale(D.to_numpy()) or 1e-9
  120. out = {t: {**NULL, "note": "双窗有效(非NaN)样本不足 (<min_per_win, 含死列/全NaN通道); abstain"}
  121. for t in tids}
  122. for tid in common_tids:
  123. cbar = float(C.loc[tid])
  124. dbar = float(D.loc[tid])
  125. zc = cbar / sC
  126. zd = dbar / sD
  127. score = max(abs(zc), abs(zd))
  128. denom = abs(dbar) + abs(cbar)
  129. diff_ratio = (abs(dbar) / denom) if denom > 1e-9 else 0.0
  130. flag = score >= z_flag
  131. if not flag:
  132. mech = None # gym 'none' → None (未 flag 分型无意义)
  133. elif diff_ratio >= diff_hi:
  134. mech = "filter"
  135. elif diff_ratio <= diff_lo:
  136. mech = "pump"
  137. else:
  138. mech = "ambiguous"
  139. out[str(tid)] = {
  140. "flag": bool(flag), "score": round(float(score), 3), "mechanism": mech,
  141. "diff_ratio": round(diff_ratio, 3),
  142. "r_inlet_bar": round(float(ri.loc[tid]), 3), "r_pump_bar": round(float(rp.loc[tid]), 3),
  143. "C_bar": round(cbar, 3), "D_bar": round(dbar, 3),
  144. "z_C": round(zc, 2), "z_D": round(zd, 2),
  145. }
  146. return out