reliability.py 6.0 KB

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