| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- # -*- 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
|