| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- # -*- coding: utf-8 -*-
- """B翼 结构件: 塔架与基础 + 叶片与叶根连接 (windscada M16).
- ★补齐交付 9 分册中先前缺的两册 (系统不全的实质: windscada 只做了 7 系统, 交付是 9 册).
- 塔架与基础 四维:
- 塔筒频率 — TowerFrq_Frequenc_mean 月度中位 Theil-Sen 年变化; 告警限 10mHz (分册口径);
- 判据 = 年降 ≥ 本台噪声 且 触限年数 < 10 → 报警; < 20 → 良好
- 塔筒振动 — 塔振模块CAN通讯故障 ≥5 次 → **监测不可用(不给等级)**; 强烈振动停机/振动检测计数 → 良好
- 塔筒湿度 — 湿度高告警 ≥2 或 >80%占时 ≥1% → 良好
- 基础与法兰 — **无监测** (SCADA 无该轴, 如实)
- 叶片与叶根 四维:
- 叶片A/B/C — 工单点名该叶的断裂/维修记录 (窗前累计) + 窗内 3418 轮毂松动
- 叶根螺栓 — 窗内 3418 (新发嫌疑) + 工单累计断裂条数
- 边界: 工单覆盖 2020-2024 (2025-26 现场未提供) ⇒ "窗前累计"可信, "窗内新发"只能靠报警码;
- 塔筒频率与分册口径差异 (分册 13# -2.20mHz/yr vs 本件 -1.48, 同台同号同排名第1, 窗口/聚合不同)."""
- import numpy as np, pandas as pd, pathlib
- from src.windscada.config import farm
- TOWER_DIMS = ['塔筒频率', '塔筒振动', '塔筒湿度', '基础与法兰']
- BLADE_DIMS = ['叶片A', '叶片B', '叶片C', '叶根螺栓']
- FREQ_LIMIT_MHZ = 10.0
- def _slope_mHz_yr(series_str):
- if not series_str: return np.nan, 0
- kv = [x.split(':') for x in series_str.split(';')]
- s = pd.Series({k: float(v) for k, v in kv}).sort_index()
- if len(s) < 12: return np.nan, len(s)
- sl = np.median([(s.iloc[j] - s.iloc[i]) / (j - i) for i in range(len(s)) for j in range(i + 1, len(s))])
- return float(sl * 12 * 1000), len(s)
- def _blade_orders(cfg):
- f = pathlib.Path(cfg['store']) / 'workorders.parquet'
- if not f.exists(): return {}
- w = pd.read_parquet(f)
- tx = (w['故障名称'].astype(str) + ' ' + w['故障描述'].astype(str) + ' ' + w['排查项目'].astype(str)
- + ' ' + w['维修对象'].astype(str) + ' ' + w['元器件名称'].astype(str))
- w = w.assign(tx=tx)
- bolt = w[w.tx.str.contains('叶根螺栓|叶片螺栓|螺栓断裂|断栓', na=False)]
- out = {}
- for t, g in bolt.groupby('turbine'):
- rec = dict(累计=int(len(g)), 末次=str(g.t_report.max())[:10])
- for b in 'ABC':
- rec[b] = int(g.tx.str.contains(f'{b}叶片|叶片{b}|{b}叶', na=False).sum())
- out[t] = rec
- return out
- def registry(cfg=None):
- cfg = cfg or farm()
- df = pd.read_parquet(pathlib.Path(cfg['store']) / 'structure.parquet').set_index('turbine')
- bo = _blade_orders(cfg)
- rows, detail = [], {}
- for t, r in df.iterrows():
- d = {}
- # ---- 塔架 ----
- sl, nm = _slope_mHz_yr(r.freq_series)
- noise = float(r.freq_noise_mHz)
- yrs = FREQ_LIMIT_MHZ / abs(sl) if sl == sl and sl < 0 else np.inf
- real = sl == sl and abs(sl) >= 0.8 * noise
- st_f = '不可判' if sl != sl else ('报警' if (real and yrs < 10) else '良好' if (real and yrs < 25) else '优秀')
- why_f = (f"年变化 {sl:+.2f} mHz/yr ({sl/(r.freq_med*1000)*100:+.3f}%/yr), 本台噪声 {noise:.2f} mHz "
- f"({abs(sl)/max(noise,1e-9):.2f}×), 按此速率触 {FREQ_LIMIT_MHZ:.0f}mHz 限需 {yrs:.1f} 年, 月数 {nm}"
- if sl == sl else '月数不足')
- can = int(r['塔振模块CAN'])
- vib_n = int(r['强烈振动停机']) + int(r['叶片振动检测'])
- st_v = '监测不可用' if can >= 5 else ('良好' if vib_n >= 3 else '优秀')
- why_v = f"塔振模块CAN通讯故障 {can} 次, 强烈振动停机 {int(r['强烈振动停机'])} 次, 叶片振动检测 {int(r['叶片振动检测'])} 次" + \
- (' → 该台塔振无有效监测, 不给等级' if can >= 5 else '')
- hum_n = int(r['塔筒湿度高'])
- st_h = '良好' if (hum_n >= 2 or (r.twr_hum_gt80 == r.twr_hum_gt80 and r.twr_hum_gt80 >= 0.01)) else '优秀'
- why_h = f"湿度高告警 {hum_n} 次, 中位 {r.twr_hum_med:.1f}%, p95 {r.twr_hum_p95:.1f}%, >80%占时 {r.twr_hum_gt80:.2%}"
- tower = {'塔筒频率': st_f, '塔筒振动': st_v, '塔筒湿度': st_h, '基础与法兰': '无监测'}
- # ---- 叶片 ----
- b = bo.get(t, {})
- hub = int(r['轮毂松动'])
- cum = b.get('累计', 0)
- # 2026-08-26 双模审(Gemini claim6)采纳: 窗前工单累计(≤2024)不得抬 2025-26 现窗等级 (claim窗≠evidence窗);
- # 等级只由窗内报警(3418 轮毂松动)驱动, 工单累计降为史注记
- st_bolt = '报警' if hub >= 2 else ('良好' if hub >= 1 else '优秀')
- why_bolt = (f"窗内轮毂松动报警 {hub} 次 (松动嫌疑·代理轴, 非螺栓断裂确诊); 工单累计断裂/换栓 {cum} 条"
- + (f", 末次 {b['末次']}" if cum else '')
- + " [口径: 本件为代理轴, 与分册'窗内确诊断裂'不同源 — 工单止于2024, 2025-26现场未提供]")
- blade = {}
- for x in 'ABC':
- n = b.get(x, 0)
- blade[f'叶片{x}'] = '良好' if n >= 1 else '优秀' # 窗前史最多良好(需复查), 不给现窗报警级
- blade['叶根螺栓'] = st_bolt
- why_blade = {f'叶片{x}': f"工单点名该叶断裂/换栓 {b.get(x,0)} 条 (2020-2024窗; 分册另含2025-26窗内记录, 两者非同源)" for x in 'ABC'}
- why_blade['叶根螺栓'] = why_bolt
- order = {'报警': 0, '良好': 1, '监测不可用': 2, '无监测': 3, '不可判': 3, '优秀': 4}
- tj = [v for v in tower.values() if v in ('报警', '良好', '优秀')]
- bj = [v for v in blade.values() if v in ('报警', '良好', '优秀')]
- rows.append(dict(机组=t,
- 塔架系统级=sorted(tj, key=lambda x: order[x])[0] if tj else '不可判',
- 叶片系统级=sorted(bj, key=lambda x: order[x])[0] if bj else '不可判',
- **tower, **blade))
- detail[t] = dict(塔架={'塔筒频率': why_f, '塔筒振动': why_v, '塔筒湿度': why_h, '基础与法兰': 'SCADA 无该轴 (需水下检测/法兰螺栓力矩记录)'},
- 叶片=why_blade)
- return pd.DataFrame(rows).sort_values('机组').reset_index(drop=True), detail
|