# -*- coding: utf-8 -*- """A翼 部件可靠性概览 (windscada M14; SOP 5.x 口径族). 把"报警条数/停机事件/停机时长"按**部件**归集, 给部件级可靠性视角 (区别于问题总览的按系统现态筛查): R1 部件停机贡献: 事件数 / 停机时长 / 占全场% (归因码=段起点±30min首发码) R2 部件级 MTBF: 全场可运行小时 ÷ 该部件事件数 【参考级】(无工单不可拆计划/故障) R3 影响面: 影响台数 / 全场38 + 报警条数 R4 典型失效模式: 该部件时长 top3 码 (专业中文) R5 当前问题台: 接七系统矩阵的报警台 (链到单问题页) 纪律: 归因码→部件的映射显式列表, 未映射码归"未归类"如实报占比 (不塞进任一部件); MTBF 为 SCADA 全停机口径【参考级】; 外部类(电网/风况/运维操作) 单列不计入设备可靠性.""" import numpy as np, pandas as pd, pathlib from src.windscada.config import farm from src.windscada import i18n # 码 → 部件 (显式表; 前缀规则兜底) CODE2COMP = { '1001': '运维操作', '1015': '运维操作', '1020': '运维操作', '1007': '运维操作', '1008': '运维操作', '1161': '风况(外部)', '5122': '电网/供电(外部)', '63003': '电网/供电(外部)', '63004': '电网/供电(外部)', '2700': '塔架/结构', '4101': '齿轮箱', '4102': '齿轮箱', '4103': '齿轮箱', '63209': '主控与传感网', '63036': '变流器', '3418': '轮毂/叶片', '3145': '轮毂/叶片', '3173': '变桨与液压', '3419': '未归类', } PREFIX = [('101', '偏航'), ('104', '偏航'), ('102', '偏航'), ('712', '偏航'), ('31', '变桨与液压'), ('32', '变桨与液压'), ('34', '变桨与液压'), ('71', '变桨与液压'), ('93', '变桨与液压'), ('41', '齿轮箱'), ('42', '齿轮箱'), ('139', '变流器'), ('19', '变流器'), ('200', '变流器'), ('81', '主控与传感网'), ('82', '主控与传感网'), ('63', '主控与传感网')] EXTERNAL = {'运维操作', '风况(外部)', '电网/供电(外部)'} SYS_OF = {'变桨与液压': '变桨', '偏航': '偏航', '齿轮箱': '齿轮箱', '发电机': '发电机', '变流器': '变流器', '主控与传感网': '主控与传感网', '轮毂/叶片': '变桨'} def comp_of(code): c = str(code) if c in CODE2COMP: return CODE2COMP[c] for p, comp in PREFIX: if c.startswith(p): return comp return '未归类' def overview(cfg=None, since='2026-01-01', span=None): """部件可靠性概览。 `span=(起, 止)` 给了就按**所选时间窗**(含两端)重算(用户令 2026-09-21)—— 停机事件与报警都是**日粒度**(`t_start`/`t_on`),所以按窗是精确切片;不给 = 原口径(`since` 起)。 """ cfg = cfg or farm() st = pathlib.Path(cfg['store']) ev = pd.read_parquet(st / 'stop_events.parquet') al = pd.read_parquet(st / 'alarms.parquet') if span: a, b = str(span[0]), str(span[1]) + ' 23:59:59' ev = ev[(ev.t_start >= a) & (ev.t_start <= b) & (~ev.dispatch)].copy() al = al[(al.t_on >= a) & (al.t_on <= b)].copy() since = str(span[0]) else: ev = ev[(ev.t_start >= since) & (~ev.dispatch)].copy() al = al[al.t_on >= since].copy() ev['comp'] = ev.cause_code.map(lambda c: '无记录停机' if not c else comp_of(c)) al['comp'] = al.code.map(comp_of) n_t = len(cfg['turbines']) months = ev.t_start.dt.to_period('M').nunique() cal_h = months * 30.4 * 24 * n_t stop_h_all = float(ev.dur_h.sum()) rows = [] for comp, g in ev.groupby('comp'): ga = al[al.comp == comp] if comp != '无记录停机' else al.iloc[0:0] top = (g.groupby('cause_code').dur_h.sum().nlargest(3)) modes = '; '.join(f"{c} {i18n.alarm_label(c)[:14]} {v:.0f}h" for c, v in top.items() if c) rows.append(dict(部件=comp, 事件数=int(len(g)), 停机时长h=round(float(g.dur_h.sum()), 0), 占停机=round(float(g.dur_h.sum()) / max(stop_h_all, 1e-9), 4), 影响台数=int(g.turbine.nunique()), 报警条数=int(len(ga)), MTBF_h=(None if comp == '无记录停机' else round((cal_h - stop_h_all) / max(len(g), 1), 0)), # 归因缺口不给可靠性指标 MDT_h=round(float(g.dur_h.mean()), 1), 外部=comp in EXTERNAL, 典型失效模式=modes)) df = pd.DataFrame(rows).sort_values('停机时长h', ascending=False).reset_index(drop=True) # ★必须在 astype(object) 之前算 — astype(object) 把 bool 列变 object 后, # `~df.外部` 走的是整数位取反 (~True=-2, ~False=-1), 结果被当列名索引 → KeyError, # 整个可靠性概览块静默降级成 {"err": ...}。2026-08-28 全系统口径盘点逮出。 dev_h = round(float(df.loc[~df['外部'].astype(bool), '停机时长h'].sum()), 0) ext_h = round(float(df.loc[df['外部'].astype(bool), '停机时长h'].sum()), 0) df = df.astype(object).where(pd.notna(df), None) # DataFrame 会把 None 转 NaN → 出口还原 (NaN 是非法 JSON) return dict(窗=f'时间窗 {since[:7]} 起 {months} 个月 × {n_t} 台', 口径='SCADA全停机(≥1h段, 不含调度令); 归因=段起点±30min首发码; ' '平均无故障间隔(MTBF)=可运行小时÷该部件事件数【参考级】; 外部类不计设备可靠性', 总停机h=round(stop_h_all, 0), 设备类停机h=dev_h, 外部类停机h=ext_h, 表=df.to_dict('records')) def current_problems(cfg=None): """当前问题台 (七系统矩阵报警) 按部件归并 → 链单问题页.""" from .. import taxonomy mx = taxonomy.system_matrix(cfg) out = {} for t, sysd in mx.items(): for s, v in sysd.items(): if v['状态'] == '报警': out.setdefault(s, []).append(dict(t=t, why=i18n.humanize(v['依据'])[:90])) return out