Przeglądaj źródła

2.8.2 消缺:扫描器把类目库/按月提取件当'变化'——raw_scan 新增 upstream 与 IGNORE_PATTERNS 口径

- scada_mdb: 现场月度通道组库(*-<类>.mdb)列为'上游归档'(信息级)⇒ 不再算缺口/不计变化,
  并写清'先转成同台 10min 补充件 <台号>-<YYYYMM>.csv 才被消费'
- IGNORE_PATTERNS: scada_<N>min_<YYYYMM>.csv 是按月提取/核对件(用户指定位置)⇒ 不报未归类、不计变化
- 实测: 修前 --check 每轮 rc=4('2 处变化')⇒ --auto/raw_watch 每轮误判有新数据; 修后 rc=0, --selftest 通过
- docs/输入数据放置指导 §4.1 记落位与转换口径(12 组中文目录↔类目码、9 类↔主件 595 通道一一对应、float32+ISO 对齐)
- detail 页面依赖台账随本轮重算(3,509 件)重写
zhouyang.xie 2 tygodni temu
rodzic
commit
432d83a449

+ 11 - 11
docs/detail_页面依赖与重算台账_v0.1.md

@@ -6,7 +6,7 @@
 <!-- DETAIL-DEPS:BEGIN -->
 # /detail 页面依赖与重算台账 (自动生成, 禁手改)
 
-> 生成端: `python scripts/detail_deps.py --write`|场站 `rudong`|产物仓 `outputs/rudong`|盘上 1,802 件
+> 生成端: `python scripts/detail_deps.py --write`|场站 `rudong`|产物仓 `outputs/rudong`|盘上 3,509 件
 >
 > 用户令 (2026-09-18): **/detail 各页面不许用旧版产出补**, 只能参照旧版产物的呈现样式与内容, 数值必须由 `data/raw` 重算。本表把每页的取数接口与产物件逐条列清, 缺的写明怎么补。
 
@@ -52,7 +52,7 @@
 |  | `ontology/turbine_params.parquet` ○可选 | 1 | 0.1 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
 |  | `m5_cms_tcm/handoff_vibration_v2.json` ○可选 | 1 | 0.1 MB | raw-derived | scripts/rudong_fusion_handoff.py (vib_fusion_handoff) | 在位 |
 |  | `m5_cms_tcm/fleet_scalar_z.parquet` ○可选 | 1 | 0.7 MB | raw-derived | scripts/rudong_fusion_run.py (vib_fleet_scalar_z) | 在位 |
-|  | `pitch/pitch_daily.parquet` ○可选 | 1 | 0.3 MB | raw-derived | scripts/pitch_face_build.py (pitch_face) | 在位 |
+|  | `pitch/pitch_daily.parquet` ○可选 | 1 | 0.4 MB | raw-derived | scripts/pitch_face_build.py (pitch_face) | 在位 |
 |  | `pitch/pitch_zero_monthly.parquet` ○可选 | 1 | 0.0 MB | raw-derived | scripts/pitch_face_build.py (pitch_face) | 在位 |
 | /api/curves | `windscada/powercurve_bins.parquet` ✅必需 | 1 | 0.0 MB | raw-derived | src/windscada/perf/powercurve.py (scada_derived) | 在位 |
 | /api/curves | `windscada/curve_lenses.parquet` ○可选 | 1 | 0.1 MB | raw-derived | src/windscada/perf/powercurve.py (scada_derived) | 在位 |
@@ -60,20 +60,20 @@
 |  | `windscada/control_profile.parquet` ○可选 | 1 | 0.0 MB | raw-derived | src/windscada/perf/powercurve.py (scada_derived) | 在位 |
 |  | `windscada/hydraulic_accum.parquet` ○可选 | 1 | 0.0 MB | raw-derived | src/windscada/perf/powercurve.py (scada_derived) | 在位 |
 | /api/problem | `windscada/alarms.parquet` ✅必需 | 1 | 0.9 MB | raw-derived | scripts/windscada_alarms_ingest.py (scada_alarms) | 在位 |
-| /api/problem | `ontology/objects.json` ○可选 | 1 | 4.3 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
+| /api/problem | `ontology/objects.json` ○可选 | 1 | 4.5 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
 |  | `windscada/workorders.parquet` ○可选 | 1 | 0.5 MB | raw-derived | scripts/windscada_workorder_ingest.py (scada_workorders) | 在位 |
 | /api/turbine | `windscada/alarms.parquet` ✅必需 | 1 | 0.9 MB | raw-derived | scripts/windscada_alarms_ingest.py (scada_alarms) | 在位 |
-| /api/turbine | `ontology/objects.json` ○可选 | 1 | 4.3 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
+| /api/turbine | `ontology/objects.json` ○可选 | 1 | 4.5 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
 |  | `windscada/workorders.parquet` ○可选 | 1 | 0.5 MB | raw-derived | scripts/windscada_workorder_ingest.py (scada_workorders) | 在位 |
-| /api/vibcms | `windcms/报告_CMS振动状态评估报告_*.md` ✅必需 | 1 | 0.0 MB | raw-derived | src/windcms/report_std.py (windcms_report_std) | 在位 |
+| /api/vibcms | `windcms/报告_CMS振动状态评估报告_*.md` ✅必需 | 2 | 0.0 MB | raw-derived | src/windcms/report_std.py (windcms_report_std) | 在位 |
 | /api/vibcms | `m5_cms_tcm/handoff_vibration_v2.json` ○可选 | 1 | 0.1 MB | raw-derived | scripts/rudong_fusion_handoff.py (vib_fusion_handoff) | 在位 |
 |  | `windcms/cache/*` ○可选 | 1 | 2.2 MB | raw-derived | src/windcms/data.py (windcms_cache) | 在位 |
-| /api/maint_survey | `ontology/objects.json` ✅必需 | 1 | 4.3 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
-| /api/maint_std | `ontology/objects.json` ✅必需 | 1 | 4.3 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
-| /api/maint_framework | `ontology/objects.json` ✅必需 | 1 | 4.3 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
-| /api/dq_findings | `ontology/objects.json` ✅必需 | 1 | 4.3 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
-| /api/ont_chain | `ontology/objects.json` ✅必需 | 1 | 4.3 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
-| /api/ontology | `ontology/objects.json` ✅必需 | 1 | 4.3 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
+| /api/maint_survey | `ontology/objects.json` ✅必需 | 1 | 4.5 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
+| /api/maint_std | `ontology/objects.json` ✅必需 | 1 | 4.5 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
+| /api/maint_framework | `ontology/objects.json` ✅必需 | 1 | 4.5 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
+| /api/dq_findings | `ontology/objects.json` ✅必需 | 1 | 4.5 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
+| /api/ont_chain | `ontology/objects.json` ✅必需 | 1 | 4.5 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
+| /api/ontology | `ontology/objects.json` ✅必需 | 1 | 4.5 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
 | /api/facts | `guanlan/facts_contract_v0.json` ✅必需 | 1 | 0.0 MB | raw-derived | scripts/guanlan_facts_contract.py (guanlan_contract) | 在位 |
 |  | `guanlan/derived/detail_cards.json` ✅必需 | 1 | 0.0 MB | raw-derived | scripts/guanlan_facts_contract.py (guanlan_contract) | 在位 |
 | /api/facts | `guanlan/derived/qa_refs.json` ○可选 | 1 | 0.0 MB | raw-derived | scripts/guanlan_facts_contract.py (guanlan_contract) | 在位 |

Plik diff jest za duży
+ 4 - 3
docs/版本记录.md


+ 19 - 0
docs/输入数据放置指导_v0.1.md

@@ -85,6 +85,25 @@ python scripts\rebuild_all.py --auto    # 先扫一遍再重算: 新数据落在
 
 ★ **尾月样本很少也如实出**(用户令 2026-09-20 裁决「保持现状」):实测补充件最后一个时间戳是
 `2026-09-01 00:00`,于是 `temp_monthly` 里会多出一个**只有 1 行的 2026-09**,时间窗里也就多一格。
+
+### 4.1 现场按**类目**交付的月度通道组(2026-09-21 实测 7-8 月交付)
+
+现场会按"月份 × 通道组"打包交付:`7月/风机数据.zip` → `2026-07-tur.mdb`,共 12 组
+(中文目录 ↔ 类目码:`风机数据`=tur、`温度数据`=tmp、`压力数据`=prs、`电网数据`=grd、`DigiIn数据`=din、
+`DigiOut数据`=dot、`计算数据`=cnt、`标志数据`=flg、`内部数据`=int、`统计数据`=scd、`标准数据`=std、`汇总数据`=sum)。
+实测结论(2026-09-21,7 月交付):
+
+| 事 | 结论 |
+|---|---|
+| 落位 | `data/raw/<场站>/scada_mdb/<年>年/<月>月/<年>-<月>-<类>.mdb`(与既有 `2026-07-tur.mdb` 同规) |
+| 谁是消费者 | **取数层不直接读类目库**(它只认 `*-10min.mdb`/`*-1min.mdb`)⇒ 类目库是**上游归档**,扫描器按信息级列出、不计变化 |
+| 怎么才能被摄入 | 逐台类目表按 `TimeStamp+StationId` 对齐合成 **同台 10min 补充件** `scada_10min/<台号>-<YYYYMM>.csv`(列与主件逐列同名同序、时间戳 `YYYY-MM-DD HH:MM:SS`)⇒ 第 ③/④/④c 步自动纳入 |
+| 通道对应 | 9 个逐台类目与主件的**前缀块一一对应**(tur 91 / din 93 / dot 69 / cnt 69 / tmp 116 / prs 52 / grd 50 / flg 42 / int 13 = 595 通道,0 缺 0 多);`scd`/`std`/`sum` 是 farm 级与台账级,不在逐台 10min 布局内 |
+| 精度/格式 | 现场 CSV 导出是 **float32** 落盘、时间类通道写 `YYYY-MM-DD HH:MM:SS+00:00`;MDB 存 double / `MM/DD/YYYY HH:MM:SS` ⇒ 合成补充件时按 **float32 + ISO** 对齐,避免同一列跨日分层(实测重叠 19,555,865 格,两侧都有值却不等 = **0** 格) |
+
+★ **`<场站>/scada_10min_<YYYYMM>.csv`(按月提取/核对件)**:用户指定放在场站目录下的交付核对件,
+**不是摄入源** ⇒ 扫描器按名字模式忽略(`IGNORE_PATTERNS`),不报"未归类"、不计入变化;
+真正进产物的是 `scada_10min/<台号>.csv` + 同台补充件。
 口径 = **产物是当前 `data/raw` 的函数** —— 源件里有这个时间戳就该看得见;加"最少样本"门会让
 "数据到没到"变成猜的。要少这一格,把那个时间戳裁掉、或让现场别导出月初那一行即可。
 

+ 35 - 4
scripts/raw_scan.py

@@ -40,6 +40,7 @@ import hashlib
 import json
 import os
 import pathlib
+import re
 import sys
 import time
 
@@ -78,6 +79,12 @@ TECH_STEPS = ('⑦ 本体: 码表/手册/文档',)
 TECH_NOTE = '厂商技术资料(故障处理手册/维护 WI/图纸/对译表)→ 本体对象库/检索索引/实机参数表'
 # raw 根下**随包发的说明件**(不是数据): 别把它们报成"未归类"。口径: 只有名字在白名单里且不参与摄入。
 IGNORE_FILES = {'README_把原始数据放这里.txt'}
+# ★2026-09-21 用户令「按月 10min 提取件就放在 <场站>/ 下」: 它是**给人核对/交接的导出件**, 不是摄入源
+#   (摄入走 `<台号>.csv` + 同台补充件)。不加这条, 每次扫描都会把它报成"未归类 + 一处变化" ⇒
+#   `--auto` / raw_watch 会永远以为"来了新数据", 每轮都触发一次全量重算。
+IGNORE_PATTERNS: tuple[tuple[str, str], ...] = (
+    (r'^scada_\d+min_\d{6}\.csv$', '按月提取/核对件(非摄入源;摄入走 <台号>.csv + 同台补充件)'),
+)
 
 # ── 消费者取数口径(用户令 2026-09-20 步骤 C:让"放了没人读"当场可见)────────────────────────
 # ★为什么必须有它:族表原来只按**扩展名白名单**计件 —— 往 `scada_10min/` 里放 `WTG01-B2.csv`(856 列、
@@ -98,7 +105,15 @@ CONSUME: dict[str, dict] = {
     'scada_1min': dict(consume=('???.csv', '???-*.csv', '???_*.csv'), data_exts=('.csv', '.mdb', '.zip'),
                        how='取数层按内部台号取 `<台号>.csv`(01E.csv)+ 同台补充件'),
     'scada_mdb': dict(consume=('*-10min.mdb', '*-1min.mdb', '*.zip'), data_exts=('.mdb', '.zip'),
-                      how='取数层读取按月的库(2026-08-10min.mdb);CSV 缺失时才回落'),
+                      how='取数层读取按月的库(2026-08-10min.mdb);CSV 缺失时才回落',
+                      # ★2026-09-21: 现场按**类目**交付的月度通道组库(7月-8月交付 zip 里的
+                      #   `2026-0X-<类>.mdb`)。它是 `scada_10min` 同台补充件的**上游**:取数层不直接读它,
+                      #   要先转成 `scada_10min/<台号>-<YYYYMM>.csv`。故列为"上游归档"(信息级),
+                      #   既不当缺口(不算变化/不触发重算),也不假装被摄入。
+                      upstream=tuple(f'*-{c}.mdb' for c in
+                                     ('cnt', 'din', 'dot', 'flg', 'grd', 'int', 'prs', 'scd', 'std', 'sum', 'tmp', 'tur')),
+                      upstream_how='现场月度**通道组**归档(上游件):先转成同台 10min 补充件 '
+                                   '`scada_10min/<台号>-<YYYYMM>.csv` 才被消费(口径见 docs/输入数据放置指导_v0.1.md §4.1)'),
     'windcms': dict(consume=('*_decode.json', '*.pdf', '*.docx', '*.md', '*.json'),
                     data_exts=('.json', '.zip', '.csv', '.mdb'),
                     how='振动摄入读 *_decode.json;厂家报告 PDF 由报告转录侧读'),
@@ -176,12 +191,19 @@ def fingerprint(d: pathlib.Path, patterns: tuple[str, ...], deep: bool = False,
     consumed: set[str] = set()
     for g in cg:
         consumed |= {p.relative_to(d).as_posix() for p in d.rglob(g) if p.is_file()}
+    # ── 上游归档件(客户口径里"已知但不由取数层直接读"的): 归信息级, 不算缺口、不计变化 ──
+    upat = tuple(spec.get('upstream') or ())
+    upstream: set[str] = set()
+    for g in upat:
+        upstream |= {p.relative_to(d).as_posix() for p in d.rglob(g) if p.is_file()}
     not_consumed = [r for r, _s, _m in entries if r not in consumed]
-    gaps = [r for r in not_consumed if pathlib.PurePosixPath(r).suffix.lower() in dex]
+    gaps = [r for r in not_consumed
+            if r not in upstream and pathlib.PurePosixPath(r).suffix.lower() in dex]
     others = [r for r in not_consumed if r not in gaps]
     return dict(files=len(entries), bytes=sum(e[1] for e in entries), newest=newest,
                 matched=len(mset), dirs=len(dirs), unmatched=_unm[:10], unmatched_n=len(_unm),
                 consumed=len(consumed), consume_how=spec.get('how', ''),
+                upstream=len(upstream), upstream_how=spec.get('upstream_how', ''),
                 gaps=gaps[:10], gaps_n=len(gaps), others=others[:10], others_n=len(others),
                 digest=_digest(entries, deep, d, all_files, deep_max, dirs),
                 top=[e[0] for e in sorted(entries, key=lambda x: -x[2])[:3]])
@@ -215,17 +237,21 @@ def scan(raw_root: pathlib.Path, deep: bool = False, station_dir: pathlib.Path |
             continue
         if p.name in IGNORE_FILES:
             continue                       # 随包的放置说明件: 不是数据, 不报"未归类"
+        if any(re.match(rx, p.name) for rx, _why in IGNORE_PATTERNS):
+            continue                       # 交付核对用的导出件(位置由用户指定): 非摄入源, 不报"未归类"
         rel = p.relative_to(raw_root).as_posix()
         unclassified.append(dict(dir=rel, files=1,
                                  note='不在任何已知族的目录里 —— 没有消费者; 是新的输入族就得给它配摄入器'))
     # 已知族目录内**消费者不会读**的件 (步骤 C): 两级 —— 缺口级(gaps, 扩展名像数据) 与 信息级(others)
-    stray, gaps = [], []
+    stray, gaps, upstream = [], [], []
     for name, fp in fam.items():
         if fp.get('others_n'):
             stray.append(dict(family=name, n=fp['others_n'], example=fp.get('others')[:5]))
         if fp.get('gaps_n'):
             gaps.append(dict(family=name, n=fp['gaps_n'], example=fp.get('gaps')[:8],
                              how=fp.get('consume_how', '')))
+        if fp.get('upstream'):
+            upstream.append(dict(family=name, n=fp['upstream'], how=fp.get('upstream_how', '')))
     # 场站目录下**新增的未知子目录**(整目录级; 其文件已在上面逐件登记, 这里给个汇总便于人读)
     extra_dirs = []
     if station.is_dir():
@@ -236,7 +262,7 @@ def scan(raw_root: pathlib.Path, deep: bool = False, station_dir: pathlib.Path |
                                        note='场站目录下的新子目录(族表里没有)'))
     return dict(at=time.strftime('%Y-%m-%d %H:%M:%S'), root=str(raw_root), station=str(station),
                 deep=deep, families=fam, unclassified=unclassified + extra_dirs,
-                stray_in_families=stray, gaps=gaps)
+                stray_in_families=stray, gaps=gaps, upstream=upstream)
 
 
 def diff(old: dict, new: dict) -> list[dict]:
@@ -360,6 +386,11 @@ def main() -> int:
             print(f'    {g["family"]:16s} {g["n"]:5d} 件  例: ' + '、'.join(g['example'][:4])
                   + (' …' if g['n'] > 4 else ''))
             print(f'      消费者口径: {g["how"]}')
+    if now.get('upstream'):
+        print('  (信息,不计入"变化")族目录里的**上游归档件**(要先转换才被消费):')
+        for u in now['upstream']:
+            print(f'    {u["family"]:16s} {u["n"]:5d} 件')
+            print(f'      {u["how"]}')
     if now.get('stray_in_families'):
         print('  (信息,不计入"变化")族目录里**消费者不读的附件类**件:')
         for s in now['stray_in_families']:

+ 15 - 1
src/version.py

@@ -19,7 +19,7 @@ import json
 import pathlib
 
 NAME = '观澜·如东样板 v2'
-VERSION = '2.8.1'          # ★ 改版本只改这里
+VERSION = '2.8.2'          # ★ 改版本只改这里
 EDITION = 'offline-single-package'
 PACKAGE_STEM = 'app_guanlang'           # 交付包文件名前缀(用户令 2026-09-17)
 
@@ -49,6 +49,20 @@ BUMP_RULE = ('改动落在"解决方案/架构/核心功能" → 大 +1(中/
 #   level: major/minor/patch 表示这一版**相对上一版**是哪一级变化;legacy 表示该版用的是
 #   旧编号体系(0.x,未按本规则),仅作历史对账用。
 HISTORY: tuple[dict, ...] = (
+    dict(version='2.8.2', date='2026-09-21', level='patch',
+         title='消缺:扫描器的"变化"判据被两类**非摄入件**常年占住 —— 现场按类目交付的月度通道组库'
+               '(`scada_mdb/<年>年/<月>月/<年>-<月>-<类>.mdb`)被当成"数据没人读·缺口",用户指定的按月提取件'
+               '(`<场站>/scada_10min_<YYYYMM>.csv`)被当成"未归类";两者每次扫描都计一次变化 ⇒ '
+               '`raw_scan --check` 永远 rc=4,`rebuild_all.py --auto` 与 raw_watch 会**每轮都以为来了新数据**',
+         note='纯消缺(无功能变化)⇒ 小版本 +1。要点: '
+              '① `CONSUME` 新增 `upstream` 口径(信息级):类目库是 10min 同台补充件的**上游**,'
+              '取数层不直接读它 ⇒ 不再算缺口、不计变化,改按"上游归档件(要先转换才被消费)"列出并写清转换口径; '
+              '② 新增 `IGNORE_PATTERNS`:`scada_\\d+min_\\d{6}\\.csv` 是按月提取/核对件(位置由用户指定、'
+              '不是摄入源)⇒ 不报未归类、不计变化;'
+              '③ 实测(2026-09-21 落位 7-8 月交付后):修前 `--check` rc=4 且每轮都报"2 处变化",'
+              '修后 `--check` rc=0「与上次快照一致,没有新数据」,`--selftest` 通过,24 件类目库如实以信息级列出; '
+              '④ 口径写进 `docs/输入数据放置指导_v0.1.md` §4.1(含 12 组中文目录↔类目码、9 类↔主件 595 通道一一对应、'
+              'float32+ISO 对齐、以及"类目库→同台补充件"的落位转换)'),
     dict(version='2.8.1', date='2026-09-20', level='patch',
          title='消缺:重算链末步「台账等价验收」在全新机器上必然 rc=5(交付包不含产物 ⇒ 没有随包基线),'
                '原先把它判成失败 ⇒ 门户上写"重算失败"(实测服务器 2.8.0 一轮 24/24 步全 OK 却整轮 rc=1)',

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików