| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- # -*- coding: utf-8 -*-
- """B翼 偏航系统 (windscada M6c v3). 判据蓝本=偏航分册四维度 + 报警族按码归维 + 真动态轴.
- ★2026-08-25 边界订正 (旧版错误声明"对风差无模拟通道/泵压无数据轴"): 那是只看 1min 转发层与
- 契约内 134 列得出的, **未扫全 10min 原生层 598 列**。原生层实有且全场活:
- tur_wtc_NacelPos_* 机舱位置 (0-360°) → 对风误差可重构 err=wrap(风向−机舱位置)
- tur_wtc_ScYawPos_mean 偏航累计位置 (-380~754) → 扭缆角轴
- flg_wtc_YawStat_endvalue 命令位 ('|'静止/'>'顺/'<'逆)
- flg_wtc_ScYawUnw_counts 解缆动作
- prs_wtc_YawPmPrs_* 偏航泵压 (小站泵压)
- 1min 转发层的 cabin_position/twisted_cable_angle/yaw_error1 确为全场 38/38 死零 (转发层死列≠信号死)。
- 维度:
- D1 偏航轴承与润滑: 润滑泵时长×fleet + 柱塞出脂反馈 + 10201
- D2 驱动装置: 偏航动作次数/行程 + 10100/10125/10105
- D3 液压制动: 卡钳压水平/带宽 + 泵压 + 10113/7122
- D4 对风控制: 静态偏置(符号均值) + 动态散布σ(清洗后) + 报警轴 (10400/10401 + 风传感族)
- A类 数据质量: 机舱位置通道归零/拖偏 (判据=corr(err,机舱位置) 负相关 ∧ |err|>45°行占比≥2%)
- 边界 (skill yaw-dynamic 铁律): 10min 层测不到**死区/偏航速率/滞环** (需≤30s), 该三项 INSUFFICIENT;
- err 为 vane 口径 → 相对/上限, 非可回收 MWh; 行程=活动量代理非真磨损量。"""
- import numpy as np, pandas as pd, pathlib
- from src.windscada.config import farm
- from app_ETL.app_ETL_guanlan.api import load_10min
- DIMS = ['偏航轴承与润滑', '驱动装置', '液压制动', '对风控制']
- FAM = {
- 'D1润滑不可能': ['10201'],
- 'D2过热': ['10100'], 'D2变流器警告': ['10125'], 'D2解缆': ['10105'],
- 'D3制动液压错误': ['10113'], 'D3油位低': ['7122'],
- 'D4偏航失败': ['10400', '10401'],
- 'D4风传感': ['8108', '8110', '8184', '8185', '8186', '8187', '8171'],
- }
- def build_store(cfg=None, span=None, write=True):
- """偏航面登记件。`span=(起, 止)` 给了就按**所选时间窗**(含两端)重算(用户令 2026-09-21)。
- span 模式下数据源换成 `slim10min` 窄仓(同一份 `load_10min` 的列子集 ⇒ 同列同值),
- 并 **不写盘**(`write=False`):按窗重算是"服务时算给页面看",不能覆盖正式产物
- `yaw_daily.parquet`(那是"数据末端往前 90 天"的口径,缺了它其它消费者会变味)。
- """
- cfg = cfg or farm()
- rows = []
- for t in cfg['turbines']:
- rec = dict(turbine=t)
- try:
- if span:
- from app_ETL.app_ETL_guanlan.api import slim
- cur = slim.load(t, cfg, span=span,
- columns=['grd_wtc_ActPower_mean', 'prs_wtc_YawPress_mean',
- 'prs_wtc_YawPress_max', 'prs_wtc_YawPress_min',
- 'dot_wtc_YawLubPu_timeon', 'din_wtc_Y1LubPis_timeon',
- 'flg_wtc_ScYawOpe_counts', 'cnt_wtc_WindFauS_endvalue',
- 'cnt_wtc_WindFauT_endvalue'])
- else:
- d = load_10min(t, cfg, groups=['A.功率', 'B.偏航', 'B.润滑液压'])
- cur = d[d.ts >= str(d.ts.max() - pd.Timedelta(days=90))[:10]]
- op = cur[cur['grd_wtc_ActPower_mean'] > 100]
- byday = cur.groupby(cur.ts.dt.date)
- rec['yawpress_med'] = float(op['prs_wtc_YawPress_mean'].median())
- rec['yawpress_band'] = float((op['prs_wtc_YawPress_max'] - op['prs_wtc_YawPress_min']).median())
- rec['yawlub_s_day'] = float(byday['dot_wtc_YawLubPu_timeon'].sum().mean())
- rec['y1pis_s_day'] = float(byday['din_wtc_Y1LubPis_timeon'].sum().mean())
- rec['scyaw_cnt_day'] = float(byday['flg_wtc_ScYawOpe_counts'].sum().mean())
- for c, k in (('cnt_wtc_WindFauS_endvalue', 'windfau_s'), ('cnt_wtc_WindFauT_endvalue', 'windfau_t')):
- v = cur[c].dropna()
- rec[k] = float(v.iloc[-1] - v.iloc[0]) if len(v) > 2 else np.nan # 计数器窗内增量
- rec['n_op'] = int(len(op))
- except Exception as e:
- rec['err10'] = str(e)[:60]
- rows.append(rec)
- if write:
- print(t, flush=True)
- df = pd.DataFrame(rows)
- al = pd.read_parquet(pathlib.Path(cfg['store']) / 'alarms.parquet')
- if span: # ★按窗:报警族计数也只看窗内(原口径 = 2026 年全年)
- a_w = al[(al['t_on'] >= str(span[0])) & (al['t_on'] <= str(span[1]) + ' 23:59:59')]
- else:
- a_w = al[al['t_on'].dt.year == 2026]
- for name, codes in FAM.items():
- cnt = a_w[a_w.code.isin(codes)].groupby('turbine').size()
- df[name] = df['turbine'].map(cnt).fillna(0).astype(int)
- if write:
- st = pathlib.Path(cfg['store']); df.to_parquet(st / 'yaw_daily.parquet')
- print('→ yaw_daily.parquet', df.shape)
- return df
- def registry(cfg=None, span=None):
- """偏航面判级。`span=(起, 止)` 给了就按**所选时间窗**重算(判据/阈值不动,只换数据切片)。"""
- cfg = cfg or farm()
- if span:
- df = build_store(cfg, span=span, write=False)
- else:
- df = pd.read_parquet(pathlib.Path(cfg['store']) / 'yaw_daily.parquet')
- ep = pathlib.Path(cfg['store']) / 'yaw_err_clean.parquet'
- ERR = pd.read_parquet(ep) if ep.exists() else None
- med = df.median(numeric_only=True)
- rows, detail = [], {}
- for _, r in df.iterrows():
- t = r['turbine']; d = dict(维度={}, 依据={})
- # D1 轴承润滑: 泵时长×fleet + 柱塞出脂反馈 + 10201
- lubx = r['yawlub_s_day'] / max(med['yawlub_s_day'], 1e-9) if r['yawlub_s_day'] == r['yawlub_s_day'] else np.nan
- pisx = r['y1pis_s_day'] / max(med['y1pis_s_day'], 1e-9) if r['y1pis_s_day'] == r['y1pis_s_day'] else np.nan
- st1 = '不可判'
- if lubx == lubx:
- st1 = '报警' if (lubx >= 1.8 or r['D1润滑不可能'] >= 5 or (pisx == pisx and pisx <= 0.3 and lubx >= 1.0)) else \
- '良好' if (lubx >= 1.4 or r['D1润滑不可能'] >= 1) else '优秀'
- d['依据'][DIMS[0]] = (f"润滑泵 {r['yawlub_s_day']:.0f}s/日 ({lubx:.2f}×fleet), 柱塞反馈 {r['y1pis_s_day']:.0f}s/日 ({pisx:.2f}×), "
- f"润滑不可能报警{int(r['D1润滑不可能'])}")
- # D2 驱动: 动作次数×fleet + 过热/变流器/解缆报警
- mvx = r['scyaw_cnt_day'] / max(med['scyaw_cnt_day'], 1e-9) if r['scyaw_cnt_day'] == r['scyaw_cnt_day'] else np.nan
- st2 = '不可判'
- if mvx == mvx:
- st2 = '报警' if (r['D2过热'] >= 5 or r['D2变流器警告'] >= 10 or mvx >= 1.8) else \
- '良好' if (r['D2过热'] >= 1 or r['D2变流器警告'] >= 3 or mvx >= 1.4 or r['D2解缆'] >= med['D2解缆'] * 2 + 5) else '优秀'
- d['依据'][DIMS[1]] = (f"偏航动作 {r['scyaw_cnt_day']:.0f}次/日 ({mvx:.2f}×fleet), 解缆{int(r['D2解缆'])}次, "
- f"过热{int(r['D2过热'])}, 变流器警告{int(r['D2变流器警告'])}")
- # D3 液压制动: 卡钳压水平/带宽 + 制动液压错误/油位低
- px = r['yawpress_med']; bandx = r['yawpress_band'] / max(med['yawpress_band'], 1e-9)
- st3 = '报警' if (r['D3制动液压错误'] >= 3 or r['D3油位低'] >= 3 or bandx >= 2.0) else \
- '良好' if (r['D3制动液压错误'] >= 1 or r['D3油位低'] >= 1 or bandx >= 1.4 or abs(px - med['yawpress_med']) > 15) else '优秀'
- d['依据'][DIMS[2]] = (f"卡钳压中位 {px:.0f} ({px - med['yawpress_med']:+.0f} vs fleet), 带宽 {r['yawpress_band']:.0f} ({bandx:.2f}×), "
- f"制动液压错误{int(r['D3制动液压错误'])}, 油位低{int(r['D3油位低'])}")
- # D4 对风: 真对风误差轴 (机舱位置重构, 清洗后) + 报警轴
- wf = (r['windfau_s'] if r['windfau_s'] == r['windfau_s'] else 0) + (r['windfau_t'] if r['windfau_t'] == r['windfau_t'] else 0)
- e = ERR.loc[t] if ERR is not None and t in ERR.index else None
- st4 = '报警' if (r['D4偏航失败'] >= 10 or r['D4风传感'] >= 30 or wf >= 100) else \
- '良好' if (r['D4偏航失败'] >= 2 or r['D4风传感'] >= 5 or wf >= 30) else '优秀'
- etxt = '对风误差轴不可用'
- if e is not None:
- if abs(e['bias_clean']) >= 2.0:
- st4 = '报警'; etxt = f"静态偏置 {e['bias_clean']:+.2f}° ≥2° → 零位候选(转静态偏航专项)"
- else:
- etxt = (f"静态偏置 {e['bias_clean']:+.2f}° (全场|中位|{ERR['bias_clean'].abs().median():.2f}), "
- f"动态散布σ {e['sd_clean']:.2f}° (全场{ERR['sd_clean'].median():.2f}), |误差|>10°占时 {e['gt10_clean']:.1%}")
- d['依据'][DIMS[3]] = (f"{etxt}; 偏航失败{int(r['D4偏航失败'])}, 风传感报警{int(r['D4风传感'])}, 计数增量{wf:.0f} "
- f"[死区/速率/滞环: 10min层不可测=INSUFFICIENT]")
- # A类 数据质量 (不入四维评级, 单列: 它污染一切依赖机舱位置的分析)
- if e is not None and e['drop'] >= 0.02 and e['corr_pos'] <= -0.15:
- d['数据质量'] = (f"A类·机舱位置通道归零/拖偏: |误差|>45°行占 {e['drop']:.1%}, "
- f"corr(误差,机舱位置)={e['corr_pos']:+.2f} (健康台≈0) → 偏航编码器/PLC读回现场核; "
- f"清洗前σ {e['sd_raw']:.1f}° 清洗后 {e['sd_clean']:.1f}°")
- for dim, st in zip(DIMS, (st1, st2, st3, st4)):
- d['维度'][dim] = st
- judged = [v for v in d['维度'].values() if v != '不可判']
- detail_extra = d.get('数据质量')
- order = {'报警': 0, '良好': 1, '优秀': 2}
- rows.append(dict(机组=t, 系统级=sorted(judged, key=lambda x: order[x])[0] if judged else '不可判',
- **d['维度'], 数据质量=detail_extra or ''))
- detail[t] = d
- return pd.DataFrame(rows).sort_values('机组').reset_index(drop=True), detail
|