trend_ingest.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. # -*- coding: utf-8 -*-
  2. """趋势与闭环证据入图谱 (2026-08-28 用户裁: 把"在升"补进图谱)。
  3. 此前"哪些机组风险在上升"检索不到 —— 不是排序问题, 是这类信息从未作为本体对象存在:
  4. 在升三台与闭环三例都只活在 windscada 的链盘与 component_history.json 里,
  5. 图谱侧完全没有对应节点, 于是 ont_search 检索不到、模型只能靠专用工具 ont_risk 才答得上。
  6. 本模块把两类实证补成 Evidence 对象:
  7. evidence/trend/<台>/<部件> 在升 — 末三窗/早三窗比值 ≥1.3, 属"未闭环"的证据
  8. evidence/loop/<台>/<部件> 闭环 — 检出→换件→复测回落, 属"算法端到端验证"的正面实证
  9. 两类都挂 about→turbine、affects→component, 使检索与图遍历都能到达。
  10. 幂等: 同 id 重跑覆盖, 不新增。
  11. 跑法: python -m src.ontology.trend_ingest
  12. """
  13. try:
  14. from app_common.app_common_guanlan.api import install_root as _install_root
  15. except ImportError: # 理论不可达;包结构异常时回退到按位置上跳
  16. from pathlib import Path as _P
  17. def _install_root(_f): return _P(_f).resolve().parents[4]
  18. from src import paths as P
  19. import json, pathlib, sys
  20. ROOT = _install_root(__file__)
  21. ONT = P.ont()
  22. HIST = P.m5() / 'component_history.json'
  23. # component_history 的部件名 → 图谱 Component id 后缀
  24. COMP_MAP = {'齿轮箱': 'gearbox', '发电机': 'generator', '主轴承': 'main_bearing',
  25. '变桨': 'pitch', '偏航': 'yaw', '变流器': 'converter'}
  26. def _comp_id(t, name):
  27. # 2026-08-28 门禁逮: 原来直接拼 component/<中文名> — 图谱里根本没有这种 id
  28. # (真实格式 component/fleet/<英文键>), 造出 6 条悬空引用。
  29. # 引用别人的 id 前必须去图谱核格式, 不能照直觉拼。
  30. key = COMP_MAP.get(name)
  31. return f'component/fleet/{key}' if key else None
  32. def _links(t, comp):
  33. # 挂 about 到台; 部件映射不到就不写 affects — 宁可少一条边, 不留悬空引用。
  34. # ★值必须是**列表**: 写成字符串会被逐字符当目标 (2026-09-12 跑 src.ontology.audit 逮到
  35. # 78 条链接指向 't'/'u'/'r'/'b'/'i' —— 与 chain_ingest 同一处笔误)。
  36. L = dict(about=[f'turbine/{t}'])
  37. cid = _comp_id(t, comp)
  38. if cid:
  39. L['affects'] = [cid]
  40. return L
  41. def build(dry=False):
  42. from .store import Store
  43. if not HIST.exists():
  44. print(f'源缺失: {HIST}'); return {}
  45. h = json.loads(HIST.read_text(encoding='utf-8'))
  46. summ = h.get('summary') or {}
  47. meta = h.get('meta') or {}
  48. built = meta.get('built', '')
  49. s = Store(ONT / 'objects.json')
  50. stat = dict(trend=0, loop=0)
  51. for key, rows in summ.items():
  52. if not isinstance(rows, list):
  53. continue
  54. # ── 在升 ──
  55. if key.startswith('★当前在升'):
  56. for r in rows:
  57. t, comp = r.get('turbine'), r.get('component')
  58. if not t or not comp:
  59. continue
  60. oid = f'evidence/trend/{t}/{comp}'
  61. wo = r.get('工单可对')
  62. s.put(dict(id=oid, type='Evidence', props=dict(
  63. name=f'{t[-2:]}# {comp} 振动在升 {r.get("ratio")}×',
  64. turbine=t, component=comp, facet='果侧振动稳态', facet_detail='标量趋势(末三窗/早三窗)',
  65. source=P.rel(P.m5() / 'component_history.json') + ' ★当前在升',
  66. direction='上升', ratio=r.get('ratio'), early=r.get('early'), late=r.get('late'),
  67. n_events=r.get('n_events'), work_order_matchable=wo,
  68. claim_window=f'末三窗/早三窗 (component_history {built})',
  69. status='在升·未闭环',
  70. summary=(f'{t[-2:]}# {comp} 振动标量末三窗/早三窗 {r.get("ratio")}× '
  71. f'({r.get("early")}→{r.get("late")}), 判为在升未闭环。'
  72. # ★2026-09-19 修: 原先这里写死"台账止 2024-11"——那是随包旧快照的口径,
  73. # 本机台账实测已到 2026-07-08 ⇒ 该断言的日期会过期、把"数据边界"
  74. # 说成错的年份。改成不写具体日期的诚实说法(边界由台账跨度决定)。
  75. + ('工单台账在该证据窗内没有可对的工单 —— "无工单"是**数据边界**'
  76. '(台账跨度所限), 不等于未处置。' if wo is False else '')),
  77. note='阈值=末三窗/早三窗 ≥1.3; 该判据只描述方向不定级'),
  78. links=_links(t, comp)), force=True)
  79. stat['trend'] += 1
  80. # ── 换件闭环 ──
  81. if key.startswith('★换件闭环'):
  82. for r in rows:
  83. t, comp = r.get('turbine'), r.get('component')
  84. if not t or not comp:
  85. continue
  86. oid = f'evidence/loop/{t}/{comp}'
  87. s.put(dict(id=oid, type='Evidence', props=dict(
  88. name=f'{t[-2:]}# {comp} 换件闭环 {r.get("ratio")}×',
  89. turbine=t, component=comp, facet='果侧振动稳态', facet_detail='换件前后对比(端到端闭环)',
  90. source=P.rel(P.m5() / 'component_history.json') + ' ★换件闭环',
  91. direction='下降', ratio=r.get('ratio'), pre=r.get('pre'), post=r.get('post'),
  92. replace_date=r.get('replace_date'), title=r.get('title'),
  93. verdict=r.get('verdict'), status='已闭环·恢复',
  94. claim_window=f'换件日 {r.get("replace_date")} 前后 (component_history {built})',
  95. summary=(f'{t[-2:]}# {comp}{("·" + r["title"]) if r.get("title") else ""} '
  96. f'{r.get("replace_date")} 换件后振动 {r.get("pre")}→{r.get("post")} '
  97. f'= {r.get("ratio")}×, 判定恢复。该台正是算法当初的检出台 ⇒ '
  98. f'故障前能看见、换件后能看见恢复。'),
  99. note='端到端闭环实证, 用于算法可检出性论证'),
  100. links=_links(t, comp)), force=True)
  101. stat['loop'] += 1
  102. if not dry:
  103. s.save()
  104. print(f'trend_ingest: {stat}')
  105. return stat
  106. if __name__ == '__main__':
  107. build(dry='--dry' in sys.argv)