#!/usr/bin/env python3 # -*- coding: utf-8 -*- r"""谱库查表:**记录 → npz 分片**(2026-09-17 用户令"把映射建起来")。 ## 结论先说:这个映射**已经是产品的一部分**,不需要重建 `scripts/rudong_tcm_spectra.py` 落盘时同时写了 `spectra_meta.parquet`,每行一条谱: turbine · sensor · meas_name · trigger_time · shard · shard_row · x_offset · x_delta · … 取数配方(该脚本头部原文): v = np.load(/ + '.npz')['values'][shard_row] x = x_offset + arange(len(v)) * x_delta 所以"某条记录对应哪个 npz 的哪一行"**直接查表就有**;本器把它封装成一条命令, 让"在特征频率附近取幅值"这类下一步工作只需几行代码。 ## 用法 python scripts/spectra_lookup.py --window w0316 --turbine WTG01 --sensor Gear_HS_generator_side \ --meas FFT_10000_Tr [--at 0.223] [--list] python scripts/spectra_lookup.py --window w0316 --stats # 各 meas 的谱件/记录数、频率轴分辨率 退出码: 0 找到 · 2 窗/记录/谱件缺(会指明缺哪一件) """ from __future__ import annotations import argparse import pathlib import sys import numpy as np import pandas as pd ROOT = pathlib.Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from src import paths as P # noqa: E402 def meta_path(win: str) -> pathlib.Path: """window → spectra_meta.parquet(两份落点都认:窗根与 spectra/ 下)。""" m5 = P.m5() if win == 'w0127': # 首窗特例: 索引在 m5 根, 谱在 m5/spectra cands = [m5 / 'spectra' / 'spectra_meta.parquet', m5 / 'spectra_meta.parquet'] else: w = m5 / 'windows' / win cands = [w / 'spectra_meta.parquet', w / 'spectra' / 'spectra_meta.parquet'] for c in cands: if c.is_file(): return c return cands[0] def store_of(mp: pathlib.Path) -> pathlib.Path: """npz 分片所在根:meta 里 shard 是**相对该根**的(含 p/ 子目录)。 ★ 两份 meta 落点对应同一个根:`<窗>/spectra_meta.parquet` 与 `<窗>/spectra/spectra_meta.parquet` 的 shard 都是相对 **`<窗>/spectra/`** 的(实测:meta 记 `p00/FFT_4_Tr_n401_0000.npz`, 文件在 `<窗>/spectra/p00/…`)。第一版按 "meta 的父目录" 解析,于是报"分片不在位"。 """ return mp.parent if mp.parent.name == 'spectra' else (mp.parent / 'spectra') def load(win: str, turbine: str, sensor: str, meas: str, trigger: str | None = None): mp = meta_path(win) if not mp.is_file(): raise SystemExit(f'[X] 没有谱库元数据: {P.rel(mp)} —— 该窗的谱还没摄入 ' f'(scripts/rudong_tcm_spectra.py);注意 w0127(1 月窗) 的谱库当前不在盘上。') m = pd.read_parquet(mp) sel = m[(m['turbine'] == turbine) & (m['sensor'] == sensor) & (m['meas_name'] == meas)] if sel.empty: raise SystemExit(f'[X] 表里没有 {turbine}/{sensor}/{meas} 的谱') if trigger: t = pd.to_datetime(sel['trigger_time'], errors='coerce') sel = sel.loc[[(t - pd.to_datetime(trigger)).abs().idxmin()]] r = sel.iloc[0] npz = store_of(mp) / str(r['shard']) npz = npz.with_suffix('.npz') if npz.suffix != '.npz' else npz if not npz.is_file(): raise SystemExit(f'[X] 分片不在位: {P.rel(npz)}(meta 记的 shard={r["shard"]})') with np.load(npz, allow_pickle=False) as z: v = z['values'][int(r['shard_row'])] x = float(r['x_offset']) + np.arange(len(v)) * float(r['x_delta']) return dict(row=r, npz=npz, v=np.asarray(v, dtype=float), x=x, x_offset=float(r['x_offset']), x_delta=float(r['x_delta'])) def main() -> int: ap = argparse.ArgumentParser(description='谱库查表: 记录 → npz 分片 (取数配方见脚本头)') ap.add_argument('--window', default='w0316') ap.add_argument('--turbine', default=None) ap.add_argument('--sensor', default=None) ap.add_argument('--meas', default=None) ap.add_argument('--trigger', default=None, help='指定 trigger_time(不指定取该组合第一条)') ap.add_argument('--at', type=float, default=None, help='看这个频率(Hz)处的幅值(最近线)') ap.add_argument('--stats', action='store_true', help='只打印该窗谱库的概况') a = ap.parse_args() mp = meta_path(a.window) if not mp.is_file(): print(f'[X] 谱库元数据不在位: {P.rel(mp)}') print(f' 该窗的谱还没摄入。重新摄入: python scripts/rudong_tcm_index.py --root ' f'--out <窗>/index.parquet 然后 python scripts/rudong_tcm_spectra.py --root <同> --out <窗>/spectra') return 2 m = pd.read_parquet(mp) if a.stats or not a.turbine: print(f'== 谱库概况 · 窗 {a.window} · {P.rel(mp)} ==') print(f' 谱件记录 {len(m)} 条 · 分片 {m["shard"].nunique()} 个 · 测点 {m["sensor"].nunique()} 个' f' · 机组 {m["turbine"].nunique()} 台') g = m.groupby('meas_name').agg(条数=('shard_row', 'size'), 分片=('shard', 'nunique'), dx=('x_delta', 'first'), x0=('x_offset', 'first')) print(g.sort_values('条数', ascending=False).head(18).to_string()) print(f' 取数配方: v = npz["values"][shard_row]; x = x_offset + arange(len(v))*x_delta') return 0 got = load(a.window, a.turbine, a.sensor, a.meas, a.trigger) r = got['row'] print(f'== 记录 → npz ==') print(f' {r["turbine"]} / {r["sensor"]} / {r["meas_name"]} @ {r["trigger_time"]}') print(f' 分片 {P.rel(got["npz"])} · 第 {int(r["shard_row"])} 行 · {len(got["v"])} 点' f' · x: {got["x_offset"]} + i*{got["x_delta"]} Hz ⇒ {got["x"][0]:.3f} ~ {got["x"][-1]:.3f} Hz') print(f' 幅值: min={got["v"].min():.6g} max={got["v"].max():.6g} 均值={got["v"].mean():.6g}') if a.at is not None: i = int(np.argmin(np.abs(got['x'] - a.at))) print(f' @{a.at} Hz(最近线 {got["x"][i]:.3f} Hz, 第 {i} 条): {got["v"][i]:.6g}') return 0 if __name__ == '__main__': for _s in (sys.stdout, sys.stderr): try: _s.reconfigure(errors='replace') except Exception: pass sys.exit(main())