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