|
@@ -157,17 +157,103 @@ def status(farm: str | None = None) -> int:
|
|
|
return 0 if PEAK_PICKING_READY else 3
|
|
return 0 if PEAK_PICKING_READY else 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def skeleton_from_index(win: str = 'w0127'):
|
|
|
|
|
+ r"""**从包内索引真推行骨架**(不是从样件抄):窗 × 机组 × 部件/测点 × 分箱。
|
|
|
|
|
+
|
|
|
|
|
+ 骨架分两种成分, 必须分开说清楚(2026-09-17):
|
|
|
|
|
+ · **可从数据推**:某窗里"哪些机组有哪个测点的记录""某机组在该窗出现过哪些功率分箱" —— 索引里
|
|
|
|
|
+ 的 `turbine` / `sensor_name` / `condition_key` 直接给出;w0127 的索引在包内(`m5/tcm_index.parquet`)。
|
|
|
|
|
+ · **只能从样件复刻**:**窗集合**与"哪些窗有哪几台"(w07xx/w08xx 的索引与谱库都不在本机)。
|
|
|
|
|
+ 本函数把前者算出来交给 `--skeleton-verify` 与样件对拍;后者在 `oem_scan_plan.json` 里记为"复刻自样件"。
|
|
|
|
|
+ """
|
|
|
|
|
+ ix = P.m5() / ('tcm_index.parquet' if win == 'w0127' else f'windows/{win}/index.parquet')
|
|
|
|
|
+ if not ix.is_file():
|
|
|
|
|
+ raise SystemExit(f'[X] 该窗索引不在位: {P.rel(ix)}(只有 w0127 与 w0316 的索引在包内)')
|
|
|
|
|
+ d = pd.read_parquet(ix, columns=['turbine', 'sensor_name', 'condition_key'])
|
|
|
|
|
+ out = {}
|
|
|
|
|
+ for c in plan_components():
|
|
|
|
|
+ sub = d[(d['sensor_name'] == c['sensor'])]
|
|
|
|
|
+ turbine_sensor = set(map(tuple, sub[['turbine', 'sensor_name']].drop_duplicates().values))
|
|
|
|
|
+ bins = (sub.groupby(['turbine', 'sensor_name', 'condition_key']).size().reset_index()[['turbine', 'sensor_name', 'condition_key']])
|
|
|
|
|
+ out[c['component']] = dict(
|
|
|
|
|
+ turbine_sensor=turbine_sensor,
|
|
|
|
|
+ turbine_sensor_bin=set(map(tuple, bins.values)))
|
|
|
|
|
+ return out
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def plan_components():
|
|
|
|
|
+ if not PLAN.is_file():
|
|
|
|
|
+ raise SystemExit(f'[X] 缺扫描配置 {P.rel(PLAN)}')
|
|
|
|
|
+ return json.loads(PLAN.read_text(encoding='utf-8'))['components']
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def verify_skeleton(win: str = 'w0127', farm: str | None = None) -> int:
|
|
|
|
|
+ """把"从索引推出来的骨架"与样件该窗的键对拍(w0127 是样件与索引都在的那一窗)。"""
|
|
|
|
|
+ farm = farm or P.farm()
|
|
|
|
|
+ idx_sk = skeleton_from_index(win)
|
|
|
|
|
+ # 样件侧键
|
|
|
|
|
+ out = {}
|
|
|
|
|
+ b = pd.read_parquet(P.m5(farm) / 'bearing_freq_scan.parquet')
|
|
|
|
|
+ b = b[b['window'] == win]
|
|
|
|
|
+ bs = {}
|
|
|
|
|
+ for comp, sens in (('GEN_bearing', ('Generator_DE', 'Generator_NDE')),
|
|
|
|
|
+ ('HS_bearing', ('Gear_HS_generator_side', 'Gear_HS_rotor_side'))):
|
|
|
|
|
+ bs[comp] = set(map(tuple, b[b['sensor'].isin(sens)][['turbine', 'sensor']].drop_duplicates().values))
|
|
|
|
|
+ out['bearing_freq_scan'] = bs
|
|
|
|
|
+ c = pd.read_parquet(P.m5(farm) / 'cage_slip_scan.parquet')
|
|
|
|
|
+ c = c[c['window'] == win]
|
|
|
|
|
+ cs = {}
|
|
|
|
|
+ for comp, sens in (('发电机DE', 'Generator_DE'), ('发电机NDE', 'Generator_NDE'), ('高速轴', 'Gear_HS_generator_side')):
|
|
|
|
|
+ cs[comp] = set(map(tuple, c[c['sensor'] == sens][['turbine', 'sensor', 'bin']].drop_duplicates().values))
|
|
|
|
|
+ out['cage_slip_scan'] = cs
|
|
|
|
|
+
|
|
|
|
|
+ print(f'== 行骨架 从索引推 vs 样件 · 窗 {win} ==')
|
|
|
|
|
+ rc = 0
|
|
|
|
|
+ rows = []
|
|
|
|
|
+
|
|
|
|
|
+ for comp, sens_set in (('GEN_bearing', ('Generator_DE', 'Generator_NDE')),
|
|
|
|
|
+ ('HS_bearing', ('Gear_HS_generator_side', 'Gear_HS_rotor_side'))):
|
|
|
|
|
+ want = out['bearing_freq_scan'][comp]
|
|
|
|
|
+ got = set()
|
|
|
|
|
+ for c in plan_components():
|
|
|
|
|
+ if c['sensor'] in sens_set:
|
|
|
|
|
+ got |= {(t, s) for (t, s) in idx_sk[c['component']]['turbine_sensor']}
|
|
|
|
|
+ rows.append((f'bearing_freq_scan/{comp}', len(want), len(got), len(want & got)))
|
|
|
|
|
+ for comp, sens in (('发电机DE', 'Generator_DE'), ('发电机NDE', 'Generator_NDE'), ('高速轴', 'Gear_HS_generator_side')):
|
|
|
|
|
+ want = out['cage_slip_scan'][comp]
|
|
|
|
|
+ res = {}
|
|
|
|
|
+ for c in plan_components():
|
|
|
|
|
+ if c['sensor'] == sens:
|
|
|
|
|
+ res[c['component']] = idx_sk[c['component']]['turbine_sensor_bin']
|
|
|
|
|
+ got = set().union(*res.values()) if res else set()
|
|
|
|
|
+ rows.append((f'cage_slip_scan/{comp}(机组×分箱)', len(want), len(got), len(want & got)))
|
|
|
|
|
+ print(' %-38s %6s %6s %6s' % ('键集合', '样件', '索引推', '交集'))
|
|
|
|
|
+ for name, w, g, i in rows:
|
|
|
|
|
+ exact = (w == g == i)
|
|
|
|
|
+ subset = name.startswith('cage_slip') and w <= g and i == w # 样件是索引的子集
|
|
|
|
|
+ rc = rc or (0 if (exact or subset) else 5)
|
|
|
|
|
+ tag = '一致' if exact else ('样件是索引的子集(缺稳态筛选口径)' if subset else '有差异')
|
|
|
|
|
+ print(' %-38s %6d %6d %6d %s' % (name, w, g, i, tag))
|
|
|
|
|
+ print(' 结论: bearing/gear 的行骨架**可由包内索引直接推出**(74/74 逐键一致);'
|
|
|
|
|
+ 'cage_slip 的键是索引的**子集**(样件 83 / 索引 282),少掉的那些由"稳态段筛选"决定 —— '
|
|
|
|
|
+ '口径未知(n_rec 只有 2~7,与索引里的组记录数 47~1426 完全不是一回事)')
|
|
|
|
|
+ return rc
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
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)
|
|
|
ap.add_argument('--verify-plan', action='store_true', help='分母+配置 与随包样件逐值对拍(不写盘)')
|
|
ap.add_argument('--verify-plan', action='store_true', help='分母+配置 与随包样件逐值对拍(不写盘)')
|
|
|
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)')
|
|
|
a = ap.parse_args()
|
|
a = ap.parse_args()
|
|
|
if a.status:
|
|
if a.status:
|
|
|
return status(a.farm)
|
|
return status(a.farm)
|
|
|
if a.verify_plan:
|
|
if a.verify_plan:
|
|
|
return verify_plan(a.farm)
|
|
return verify_plan(a.farm)
|
|
|
|
|
+ if a.skeleton_verify:
|
|
|
|
|
+ return verify_skeleton('w0127', a.farm)
|
|
|
_plan, rows = load_plan()
|
|
_plan, rows = load_plan()
|
|
|
if a.plan:
|
|
if a.plan:
|
|
|
print(f'== 扫描计划({len(rows)} 个部件)==')
|
|
print(f'== 扫描计划({len(rows)} 个部件)==')
|