Prechádzať zdrojové kódy

oem_frequency_scan 行骨架 + 分母列 2,420/2,420 逐值复现; 只差 ratio 三对列的分子

用户令"继续"的落地(把这张表的骨架与分母先落地, 只留分子等口径):

· reference/rudong/oem_scan_plan.json 增 row_skeleton: 窗集合(6 个) + 每窗机组集(37/37/37/37/37/35)
  ⇒ 行骨架 = 6 窗 × 机组 × 11 部件 = 2,420 行。★ 该节明确标注"复刻自样件、不是由 data/raw 推出"
  (那 5 个窗的索引与谱库都不在本机), note 里写明要真由数据推出需取料单第 8 项的 5 窗导出件。
· scripts/rudong_tcm_oem_scan.py 增 build_oem_skeleton() + --skeleton-oem / --verify-skeleton-oem:
  实测 样件 2420 行 · 本器骨架 2420 行 · 同键 2420 · 仅样件 0 · 仅本器 0;
  dx_hz / BPFI_hz / BPFO_hz / BSF_hz **逐值一致 2420/2420**; 三对 ratio 列本器非空 0 行(样件 2420)—— 等口径。
· 顺手修一处自造的怪数: ratio 列两边同名, merge 后会带 _样/_本 后缀, 第一版按裸名取 ⇒ 打印出
  "2421/2420" 这种不可能的数; 改为按后缀名取并分别报"本器非空 / 样件非空"。

成分账(oem_frequency_scan): 配置 ✅ · 行骨架 ✅(复刻, 可逐键对拍) · 分母 ✅(11/11) · 分子 ⭕(唯一开口)。
docs/振动六层链_接口规格与缺口_v0.1.md 增 §11(附对拍输出原文与性质标注)。
自检: --verify-plan rc=0(11/11) · --verify-skeleton-oem rc=0(2420/2420) · --skeleton-verify 如实出结论。
zhouyang.xie 3 týždňov pred
rodič
commit
31129f2f84

+ 24 - 1
docs/振动六层链_接口规格与缺口_v0.1.md

@@ -320,4 +320,27 @@ option 串 → oem_bearing_freqs.json 条目:
 3. **只能从样件复刻**:**窗集合**(`w0127/w0707/w0711/w0724/w0805/w0811`)与"哪些窗有哪几台" ——
 3. **只能从样件复刻**:**窗集合**(`w0127/w0707/w0711/w0724/w0805/w0811`)与"哪些窗有哪几台" ——
    其余 5 窗的索引与谱库都不在本机(盘上只有 w0127 与 w0316 的索引、只有 w0316 的谱)。
    其余 5 窗的索引与谱库都不在本机(盘上只有 w0127 与 w0316 的索引、只有 w0316 的谱)。
    取料单第 7 项的 1 月导出件只解决 w0127;要复现全部 6 窗,还需要 **w0707/w0711/w0724/w0805/w0811
    取料单第 7 项的 1 月导出件只解决 w0127;要复现全部 6 窗,还需要 **w0707/w0711/w0724/w0805/w0811
-   这 5 个窗的 CMS 导出件**。
+   这 5 个窗的 CMS 导出件**。
+
+---
+
+## 11. `oem_frequency_scan`:行骨架 + 分母列**已 2,420/2,420 逐值复现**(2026-09-17)
+
+`python scripts/rudong_tcm_oem_scan.py --verify-skeleton-oem`:
+
+```
+样件 2420 行 · 本器骨架 2420 行 · 同键 2420 · 仅样件 0 · 仅本器 0
+dx_hz / BPFI_hz / BPFO_hz / BSF_hz 逐值一致: 2420/2420
+三对 ratio 列: 本器非空 0 行(等口径填分子)· 样件非空 2420 行
+结论: 行骨架与分母列逐键逐值一致(只差 ratio 三对列的分子)  rc=0
+```
+
+**行骨架** = 6 窗 × 每窗 37/37/37/37/37/35 台 × 11 个部件 = **2,420 行**(每 (窗,机组) 每部件 1 行)。
+其中**分母列**(`dx_hz` + `BPFI_hz`/`BPFO_hz`/`BSF_hz`)来自 `--verify-plan` 验过的映射(11/11)。
+
+**性质标注(不许混)**:窗集合与每窗机组集**复刻自样件**(写进 `oem_scan_plan.json` 的 `row_skeleton.note`)——
+那 5 个窗(w0707/w0711/w0724/w0805/w0811)的索引与谱库都不在本机,所以这一节**不是**"由 data/raw 推出",
+要真由数据推出需取料单第 8 项的 5 窗导出件。**比值三对列(分子)留空**,等那一句峰值拾取口径。
+
+**至此 `oem_frequency_scan` 的成分账**:配置 ✅ · 行骨架 ✅(复刻,可对拍)· 分母 ✅(11/11)·
+**分子 ⭕(唯一开口)**。

+ 342 - 23
reference/rudong/oem_scan_plan.json

@@ -1,23 +1,342 @@
-{
- "_来源": "逆向工程产物 (2026-09-17): 从随包件 outputs/rudong/m5_cms_tcm/oem_frequency_scan.parquet 提取 —— 它每行带 (component, sensor, shard, option, dx_hz), 是**机型级扫描配置**(该轴承用哪条谱、什么分辨率), 与 data/raw 无关。",
- "_性质": "机型常量(哪条谱扫哪个部件), 不是原始数据的函数; 唯一权威来源就是那份随包件, 故原样固化在此, 便于实现端读取。",
- "_与理论表的关系": "option 串 → reference/rudong/oem_bearing_freqs.json 的条目: 先取位置 token(Pos.150R→150r; 139.1/1060 f-576853.PRL→1391/1060; 6338 M/C3→6338m/c3), 同位置多候选(NSK/FAG)时用 option 里的型号 token 定夺。实测 11/11 组件的 BPFI/BPFO/BSF 与样件 *_hz 列逐值一致。",
- "components": [
-  {"component": "generator_de_6338",       "sensor": "Generator_DE",          "shard": "Env_6000_4000_850_Tr", "dx_hz": 1.0625, "option": "6338 M/C3"},
-  {"component": "generator_nde_6338",      "sensor": "Generator_NDE",         "shard": "Env_6000_4000_850_Tr", "dx_hz": 1.0625, "option": "6338 M/C3"},
-  {"component": "hs_generator_left",       "sensor": "Gear_HS_generator_side", "shard": "FFT_6000_Tr",          "dx_hz": 0.9375, "option": "NSK/FAG Pos.150L"},
-  {"component": "hs_generator_right_fag",  "sensor": "Gear_HS_generator_side", "shard": "FFT_6000_Tr",          "dx_hz": 0.9375, "option": "FAG F-581491.01.TR2SK Pos.150R"},
-  {"component": "hs_generator_right_nsk",  "sensor": "Gear_HS_generator_side", "shard": "FFT_6000_Tr",          "dx_hz": 0.9375, "option": "NSK Pos.150R"},
-  {"component": "hs_rotor",                "sensor": "Gear_HS_rotor_side",     "shard": "FFT_6000_Tr",          "dx_hz": 0.9375, "option": "NSK/FAG Pos.151"},
-  {"component": "ims_generator_fag",       "sensor": "Gear_IMS",               "shard": "FFT_2000_fast_Tr",     "dx_hz": 0.625,  "option": "FAG F-581492.TR2S Pos.450"},
-  {"component": "ims_generator_nsk",       "sensor": "Gear_IMS",               "shard": "FFT_2000_fast_Tr",     "dx_hz": 0.625,  "option": "NSK Pos.450"},
-  {"component": "ims_rotor",               "sensor": "Gear_IMS",               "shard": "FFT_2000_fast_Tr",     "dx_hz": 0.625,  "option": "NSK/FAG Pos.451"},
-  {"component": "main_front",              "sensor": "Main_bearing_front",     "shard": "FFT_62_Tr",            "dx_hz": 0.03875, "option": "139.1/1060 f-576853.PRL"},
-  {"component": "main_rear",               "sensor": "Main_bearing_rear",      "shard": "FFT_62_Tr",            "dx_hz": 0.03875, "option": "240.1/800 F-605082.PRL"}
- ],
- "windows_seen_in_sample": ["w0127", "w0707", "w0711", "w0724", "w0805", "w0811"],
- "ratio_columns": {
-  "BPFI_fleet_ratio": "观测 BPFI 频率 / BPFI_hz(舰队基线)—— 分子待峰值拾取口径",
-  "BPFI_local_ratio": "观测 BPFI 频率 / BPFI_hz(本机局部基线)—— 分子待峰值拾取口径"
- }
-}
+{
+ "_来源": "逆向工程产物 (2026-09-17): 从随包件 outputs/rudong/m5_cms_tcm/oem_frequency_scan.parquet 提取 —— 它每行带 (component, sensor, shard, option, dx_hz), 是**机型级扫描配置**(该轴承用哪条谱、什么分辨率), 与 data/raw 无关。",
+ "_性质": "机型常量(哪条谱扫哪个部件), 不是原始数据的函数; 唯一权威来源就是那份随包件, 故原样固化在此, 便于实现端读取。",
+ "_与理论表的关系": "option 串 → reference/rudong/oem_bearing_freqs.json 的条目: 先取位置 token(Pos.150R→150r; 139.1/1060 f-576853.PRL→1391/1060; 6338 M/C3→6338m/c3), 同位置多候选(NSK/FAG)时用 option 里的型号 token 定夺。实测 11/11 组件的 BPFI/BPFO/BSF 与样件 *_hz 列逐值一致。",
+ "components": [
+  {
+   "component": "generator_de_6338",
+   "sensor": "Generator_DE",
+   "shard": "Env_6000_4000_850_Tr",
+   "dx_hz": 1.0625,
+   "option": "6338 M/C3"
+  },
+  {
+   "component": "generator_nde_6338",
+   "sensor": "Generator_NDE",
+   "shard": "Env_6000_4000_850_Tr",
+   "dx_hz": 1.0625,
+   "option": "6338 M/C3"
+  },
+  {
+   "component": "hs_generator_left",
+   "sensor": "Gear_HS_generator_side",
+   "shard": "FFT_6000_Tr",
+   "dx_hz": 0.9375,
+   "option": "NSK/FAG Pos.150L"
+  },
+  {
+   "component": "hs_generator_right_fag",
+   "sensor": "Gear_HS_generator_side",
+   "shard": "FFT_6000_Tr",
+   "dx_hz": 0.9375,
+   "option": "FAG F-581491.01.TR2SK Pos.150R"
+  },
+  {
+   "component": "hs_generator_right_nsk",
+   "sensor": "Gear_HS_generator_side",
+   "shard": "FFT_6000_Tr",
+   "dx_hz": 0.9375,
+   "option": "NSK Pos.150R"
+  },
+  {
+   "component": "hs_rotor",
+   "sensor": "Gear_HS_rotor_side",
+   "shard": "FFT_6000_Tr",
+   "dx_hz": 0.9375,
+   "option": "NSK/FAG Pos.151"
+  },
+  {
+   "component": "ims_generator_fag",
+   "sensor": "Gear_IMS",
+   "shard": "FFT_2000_fast_Tr",
+   "dx_hz": 0.625,
+   "option": "FAG F-581492.TR2S Pos.450"
+  },
+  {
+   "component": "ims_generator_nsk",
+   "sensor": "Gear_IMS",
+   "shard": "FFT_2000_fast_Tr",
+   "dx_hz": 0.625,
+   "option": "NSK Pos.450"
+  },
+  {
+   "component": "ims_rotor",
+   "sensor": "Gear_IMS",
+   "shard": "FFT_2000_fast_Tr",
+   "dx_hz": 0.625,
+   "option": "NSK/FAG Pos.451"
+  },
+  {
+   "component": "main_front",
+   "sensor": "Main_bearing_front",
+   "shard": "FFT_62_Tr",
+   "dx_hz": 0.03875,
+   "option": "139.1/1060 f-576853.PRL"
+  },
+  {
+   "component": "main_rear",
+   "sensor": "Main_bearing_rear",
+   "shard": "FFT_62_Tr",
+   "dx_hz": 0.03875,
+   "option": "240.1/800 F-605082.PRL"
+  }
+ ],
+ "windows_seen_in_sample": [
+  "w0127",
+  "w0707",
+  "w0711",
+  "w0724",
+  "w0805",
+  "w0811"
+ ],
+ "ratio_columns": {
+  "BPFI_fleet_ratio": "观测 BPFI 频率 / BPFI_hz(舰队基线)—— 分子待峰值拾取口径",
+  "BPFI_local_ratio": "观测 BPFI 频率 / BPFI_hz(本机局部基线)—— 分子待峰值拾取口径"
+ },
+ "row_skeleton": {
+  "note": "行骨架 = 窗 × 机组 × 11 个部件(每 (窗,机组) 每部件 1 行 ⇒ 6 窗 × 35~37 台 × 11 = 2,420 行)。★ 窗集合与每窗机组集是**从随包样件 oem_frequency_scan.parquet 复刻**的,不是由 data/raw 推出:w0707/w0711/w0724/w0805/w0811 的索引与谱库都不在本机(盘上只有 w0127 与 w0316 的索引、只有 w0316 的谱)。要真正由数据推出这 6 窗,需要这 5 个窗的 CMS 导出件(取料单第 8 项)。",
+  "windows": [
+   "w0127",
+   "w0707",
+   "w0711",
+   "w0724",
+   "w0805",
+   "w0811"
+  ],
+  "window_turbines": {
+   "w0127": [
+    "WTG01",
+    "WTG03",
+    "WTG04",
+    "WTG05",
+    "WTG06",
+    "WTG07",
+    "WTG08",
+    "WTG09",
+    "WTG10",
+    "WTG11",
+    "WTG12",
+    "WTG13",
+    "WTG14",
+    "WTG15",
+    "WTG16",
+    "WTG17",
+    "WTG18",
+    "WTG19",
+    "WTG20",
+    "WTG21",
+    "WTG22",
+    "WTG23",
+    "WTG24",
+    "WTG25",
+    "WTG26",
+    "WTG27",
+    "WTG28",
+    "WTG29",
+    "WTG30",
+    "WTG31",
+    "WTG32",
+    "WTG33",
+    "WTG34",
+    "WTG35",
+    "WTG36",
+    "WTG37",
+    "WTG38"
+   ],
+   "w0707": [
+    "WTG01",
+    "WTG02",
+    "WTG03",
+    "WTG04",
+    "WTG05",
+    "WTG06",
+    "WTG07",
+    "WTG08",
+    "WTG09",
+    "WTG10",
+    "WTG11",
+    "WTG12",
+    "WTG13",
+    "WTG14",
+    "WTG15",
+    "WTG16",
+    "WTG17",
+    "WTG19",
+    "WTG20",
+    "WTG21",
+    "WTG22",
+    "WTG23",
+    "WTG24",
+    "WTG25",
+    "WTG26",
+    "WTG27",
+    "WTG28",
+    "WTG29",
+    "WTG30",
+    "WTG31",
+    "WTG32",
+    "WTG33",
+    "WTG34",
+    "WTG35",
+    "WTG36",
+    "WTG37",
+    "WTG38"
+   ],
+   "w0711": [
+    "WTG01",
+    "WTG02",
+    "WTG03",
+    "WTG04",
+    "WTG05",
+    "WTG06",
+    "WTG07",
+    "WTG08",
+    "WTG09",
+    "WTG10",
+    "WTG11",
+    "WTG12",
+    "WTG13",
+    "WTG14",
+    "WTG15",
+    "WTG16",
+    "WTG17",
+    "WTG19",
+    "WTG20",
+    "WTG21",
+    "WTG22",
+    "WTG23",
+    "WTG24",
+    "WTG25",
+    "WTG26",
+    "WTG27",
+    "WTG28",
+    "WTG29",
+    "WTG30",
+    "WTG31",
+    "WTG32",
+    "WTG33",
+    "WTG34",
+    "WTG35",
+    "WTG36",
+    "WTG37",
+    "WTG38"
+   ],
+   "w0724": [
+    "WTG01",
+    "WTG02",
+    "WTG03",
+    "WTG04",
+    "WTG05",
+    "WTG06",
+    "WTG07",
+    "WTG08",
+    "WTG09",
+    "WTG10",
+    "WTG11",
+    "WTG12",
+    "WTG13",
+    "WTG14",
+    "WTG15",
+    "WTG16",
+    "WTG17",
+    "WTG19",
+    "WTG20",
+    "WTG21",
+    "WTG22",
+    "WTG23",
+    "WTG24",
+    "WTG25",
+    "WTG26",
+    "WTG27",
+    "WTG28",
+    "WTG29",
+    "WTG30",
+    "WTG31",
+    "WTG32",
+    "WTG33",
+    "WTG34",
+    "WTG35",
+    "WTG36",
+    "WTG37",
+    "WTG38"
+   ],
+   "w0805": [
+    "WTG01",
+    "WTG02",
+    "WTG03",
+    "WTG04",
+    "WTG05",
+    "WTG06",
+    "WTG07",
+    "WTG08",
+    "WTG09",
+    "WTG10",
+    "WTG11",
+    "WTG12",
+    "WTG13",
+    "WTG14",
+    "WTG15",
+    "WTG16",
+    "WTG17",
+    "WTG19",
+    "WTG20",
+    "WTG21",
+    "WTG22",
+    "WTG23",
+    "WTG24",
+    "WTG25",
+    "WTG26",
+    "WTG27",
+    "WTG28",
+    "WTG29",
+    "WTG30",
+    "WTG31",
+    "WTG32",
+    "WTG33",
+    "WTG34",
+    "WTG35",
+    "WTG36",
+    "WTG37",
+    "WTG38"
+   ],
+   "w0811": [
+    "WTG01",
+    "WTG02",
+    "WTG03",
+    "WTG04",
+    "WTG05",
+    "WTG06",
+    "WTG07",
+    "WTG08",
+    "WTG09",
+    "WTG10",
+    "WTG11",
+    "WTG12",
+    "WTG13",
+    "WTG14",
+    "WTG15",
+    "WTG16",
+    "WTG17",
+    "WTG19",
+    "WTG21",
+    "WTG22",
+    "WTG23",
+    "WTG24",
+    "WTG25",
+    "WTG26",
+    "WTG27",
+    "WTG28",
+    "WTG29",
+    "WTG30",
+    "WTG31",
+    "WTG32",
+    "WTG33",
+    "WTG34",
+    "WTG35",
+    "WTG36",
+    "WTG38"
+   ]
+  },
+  "verified_by": "python scripts/rudong_tcm_oem_scan.py --verify-skeleton-oem"
+ }
+}

+ 79 - 0
scripts/rudong_tcm_oem_scan.py

@@ -240,6 +240,72 @@ def verify_skeleton(win: str = 'w0127', farm: str | None = None) -> int:
     return rc
     return rc
 
 
 
 
+def build_oem_skeleton():
+    """`oem_frequency_scan` 的**行骨架 + 分母列**(不含三对 ratio 列 —— 那要口径)。
+
+    行骨架 = 窗 × 机组 × 11 个部件(每 (窗,机组) 每部件 1 行 ⇒ 2,420 行);
+    窗集合与每窗机组集**复刻自样件**(见 `oem_scan_plan.json` 的 `row_skeleton.note`:那 5 个窗的
+    索引与谱库都不在本机,所以这一节不是"由 data/raw 推出");
+    分母列(`dx_hz`/`BPFI_hz`/`BPFO_hz`/`BSF_hz`)由 `--verify-plan` 验过的映射给出(11/11 逐值一致)。
+    """
+    plan = json.loads(PLAN.read_text(encoding='utf-8'))
+    _p, comps = load_plan()
+    sk = plan.get('row_skeleton') or {}
+    wt = sk.get('window_turbines') or {}
+    if not wt:
+        raise SystemExit('[X] 缺行骨架(oem_scan_plan.json 的 row_skeleton)')
+    rows = []
+    for win in sorted(wt):
+        for t in wt[win]:
+            for c in comps:
+                rows.append(dict(component=c['component'], sensor=c['sensor'], shard=c['shard'],
+                                 option=c['option'], window=win, turbine=t,
+                                 dx_hz=c['dx_hz'], BPFI_hz=c['BPFI_hz'], BPFO_hz=c['BPFO_hz'],
+                                 BSF_hz=c['BSF_hz'],
+                                 BPFI_fleet_ratio=None, BPFI_local_ratio=None,
+                                 BPFO_fleet_ratio=None, BPFO_local_ratio=None,
+                                 BSF_fleet_ratio=None, BSF_local_ratio=None))
+    return pd.DataFrame(rows)
+
+
+def verify_skeleton_oem(farm: str | None = None) -> int:
+    """行骨架 + 分母列 与随包样件**逐键逐值**对拍(三对 ratio 列留空,不参与)。"""
+    farm = farm or P.farm()
+    sample = P.m5(farm) / 'oem_frequency_scan.parquet'
+    if not sample.is_file():
+        print(f'[X] 没有样件可对拍: {P.rel(sample)}')
+        return 2
+    want = pd.read_parquet(sample)
+    got = build_oem_skeleton()
+    key = ['component', 'sensor', 'shard', 'option', 'window', 'turbine']
+    val = ['dx_hz', 'BPFI_hz', 'BPFO_hz', 'BSF_hz']
+    m = want[key + val + ['BPFI_fleet_ratio']].merge(got, on=key, how='outer', suffixes=('_样', '_本'),
+                                                     indicator=True)
+    both = m[m['_merge'] == 'both']
+    only_w = int((m['_merge'] == 'left_only').sum())
+    only_g = int((m['_merge'] == 'right_only').sum())
+    bad, badcols = 0, {}
+    for c in val:
+        d = (pd.to_numeric(both[f'{c}_样'], errors='coerce') - pd.to_numeric(both[f'{c}_本'], errors='coerce')).abs()
+        n = int((d > 1e-9).sum())
+        bad += n
+        if n:
+            badcols[c] = n
+    # ★ 列名注意: 两边都有 BPFI_fleet_ratio ⇒ merge 后会带 _样/_本 后缀(第一版按裸名取, 于是算出 2421/2420 这种怪数)
+    rc_w = 'BPFI_fleet_ratio_样' if 'BPFI_fleet_ratio_样' in both.columns else 'BPFI_fleet_ratio'
+    rc_g = 'BPFI_fleet_ratio_本' if 'BPFI_fleet_ratio_本' in both.columns else 'BPFI_fleet_ratio'
+    filled_w = int(both[rc_w].notna().sum()) if rc_w in both.columns else 0
+    filled_g = int(both[rc_g].notna().sum()) if rc_g in both.columns else 0
+    print(f'== oem_frequency_scan 行骨架 + 分母列 对拍 · 场站 {farm} ==')
+    print(f'   样件 {len(want)} 行 · 本器骨架 {len(got)} 行 · 同键 {len(both)} · 仅样件 {only_w} · 仅本器 {only_g}')
+    print(f'   {" / ".join(val)} 逐值一致: {len(both) - bad}/{len(both)}' + (f'  不一致: {badcols}' if badcols else ''))
+    print(f'   三对 ratio 列: 本器非空 {filled_g} 行(等口径填分子)· 样件非空 {filled_w} 行')
+    ok = (not bad and only_w == 0 and only_g == 0 and len(both) == len(want) == len(got))
+    print(f'   结论: {"行骨架与分母列逐键逐值一致(只差 ratio 三对列的分子)" if ok else "有差异 —— 见上"}  '
+          f'rc={0 if ok else 5}')
+    return 0 if ok else 5
+
+
 def main() -> int:
 def main() -> int:
     ap = argparse.ArgumentParser(description='六层链 oem_scan 步(逆向实现: 分母已就位/分子待口径)')
     ap = argparse.ArgumentParser(description='六层链 oem_scan 步(逆向实现: 分母已就位/分子待口径)')
     ap.add_argument('--farm', default=None)
     ap.add_argument('--farm', default=None)
@@ -247,6 +313,10 @@ def main() -> int:
     ap.add_argument('--plan', action='store_true', help='打印扫描计划')
     ap.add_argument('--plan', action='store_true', help='打印扫描计划')
     ap.add_argument('--status', action='store_true', help='机器可读状态(JSON)')
     ap.add_argument('--status', action='store_true', help='机器可读状态(JSON)')
     ap.add_argument('--skeleton-verify', action='store_true', help='行骨架: 由索引推 vs 样件逐键对拍(w0127)')
     ap.add_argument('--skeleton-verify', action='store_true', help='行骨架: 由索引推 vs 样件逐键对拍(w0127)')
+    ap.add_argument('--skeleton-oem', action='store_true',
+                    help='打印 oem_frequency_scan 的行骨架+分母列(不含 ratio)')
+    ap.add_argument('--verify-skeleton-oem', action='store_true',
+                    help='oem_frequency_scan 行骨架+分母列 与样件逐键逐值对拍')
     a = ap.parse_args()
     a = ap.parse_args()
     if a.status:
     if a.status:
         return status(a.farm)
         return status(a.farm)
@@ -254,6 +324,15 @@ def main() -> int:
         return verify_plan(a.farm)
         return verify_plan(a.farm)
     if a.skeleton_verify:
     if a.skeleton_verify:
         return verify_skeleton('w0127', a.farm)
         return verify_skeleton('w0127', a.farm)
+    if a.verify_skeleton_oem:
+        return verify_skeleton_oem(a.farm)
+    if a.skeleton_oem:
+        df = build_oem_skeleton()
+        print(df.head(12).to_string(index=False))
+        print('… 共 %d 行(窗 %d × 机组 %d × 部件 %d)'
+              % (len(df), df['window'].nunique(), df['turbine'].nunique(), df['component'].nunique()))
+        print('三对 ratio 列留空(分子=观测峰, 待口径); 分母列已由 --verify-plan 验过 11/11')
+        return 0
     _plan, rows = load_plan()
     _plan, rows = load_plan()
     if a.plan:
     if a.plan:
         print(f'== 扫描计划({len(rows)} 个部件)==')
         print(f'== 扫描计划({len(rows)} 个部件)==')