spectra_lookup.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. r"""谱库查表:**记录 → npz 分片**(2026-09-17 用户令"把映射建起来")。
  4. ## 结论先说:这个映射**已经是产品的一部分**,不需要重建
  5. `scripts/rudong_tcm_spectra.py` 落盘时同时写了 `spectra_meta.parquet`,每行一条谱:
  6. turbine · sensor · meas_name · trigger_time · shard · shard_row · x_offset · x_delta · …
  7. 取数配方(该脚本头部原文):
  8. v = np.load(<store>/<shard> + '.npz')['values'][shard_row]
  9. x = x_offset + arange(len(v)) * x_delta
  10. 所以"某条记录对应哪个 npz 的哪一行"**直接查表就有**;本器把它封装成一条命令,
  11. 让"在特征频率附近取幅值"这类下一步工作只需几行代码。
  12. ## 用法
  13. python scripts/spectra_lookup.py --window w0316 --turbine WTG01 --sensor Gear_HS_generator_side \
  14. --meas FFT_10000_Tr [--at 0.223] [--list]
  15. python scripts/spectra_lookup.py --window w0316 --stats # 各 meas 的谱件/记录数、频率轴分辨率
  16. 退出码: 0 找到 · 2 窗/记录/谱件缺(会指明缺哪一件)
  17. """
  18. from __future__ import annotations
  19. import argparse
  20. import pathlib
  21. import sys
  22. import numpy as np
  23. import pandas as pd
  24. ROOT = pathlib.Path(__file__).resolve().parents[1]
  25. sys.path.insert(0, str(ROOT))
  26. from src import paths as P # noqa: E402
  27. def meta_path(win: str) -> pathlib.Path:
  28. """window → spectra_meta.parquet(两份落点都认:窗根与 spectra/ 下)。"""
  29. m5 = P.m5()
  30. if win == 'w0127': # 首窗特例: 索引在 m5 根, 谱在 m5/spectra
  31. cands = [m5 / 'spectra' / 'spectra_meta.parquet', m5 / 'spectra_meta.parquet']
  32. else:
  33. w = m5 / 'windows' / win
  34. cands = [w / 'spectra_meta.parquet', w / 'spectra' / 'spectra_meta.parquet']
  35. for c in cands:
  36. if c.is_file():
  37. return c
  38. return cands[0]
  39. def store_of(mp: pathlib.Path) -> pathlib.Path:
  40. """npz 分片所在根:meta 里 shard 是**相对该根**的(含 p<NN>/ 子目录)。
  41. ★ 两份 meta 落点对应同一个根:`<窗>/spectra_meta.parquet` 与 `<窗>/spectra/spectra_meta.parquet`
  42. 的 shard 都是相对 **`<窗>/spectra/`** 的(实测:meta 记 `p00/FFT_4_Tr_n401_0000.npz`,
  43. 文件在 `<窗>/spectra/p00/…`)。第一版按 "meta 的父目录" 解析,于是报"分片不在位"。
  44. """
  45. return mp.parent if mp.parent.name == 'spectra' else (mp.parent / 'spectra')
  46. def load(win: str, turbine: str, sensor: str, meas: str, trigger: str | None = None):
  47. mp = meta_path(win)
  48. if not mp.is_file():
  49. raise SystemExit(f'[X] 没有谱库元数据: {P.rel(mp)} —— 该窗的谱还没摄入 '
  50. f'(scripts/rudong_tcm_spectra.py);注意 w0127(1 月窗) 的谱库当前不在盘上。')
  51. m = pd.read_parquet(mp)
  52. sel = m[(m['turbine'] == turbine) & (m['sensor'] == sensor) & (m['meas_name'] == meas)]
  53. if sel.empty:
  54. raise SystemExit(f'[X] 表里没有 {turbine}/{sensor}/{meas} 的谱')
  55. if trigger:
  56. t = pd.to_datetime(sel['trigger_time'], errors='coerce')
  57. sel = sel.loc[[(t - pd.to_datetime(trigger)).abs().idxmin()]]
  58. r = sel.iloc[0]
  59. npz = store_of(mp) / str(r['shard'])
  60. npz = npz.with_suffix('.npz') if npz.suffix != '.npz' else npz
  61. if not npz.is_file():
  62. raise SystemExit(f'[X] 分片不在位: {P.rel(npz)}(meta 记的 shard={r["shard"]})')
  63. with np.load(npz, allow_pickle=False) as z:
  64. v = z['values'][int(r['shard_row'])]
  65. x = float(r['x_offset']) + np.arange(len(v)) * float(r['x_delta'])
  66. return dict(row=r, npz=npz, v=np.asarray(v, dtype=float), x=x,
  67. x_offset=float(r['x_offset']), x_delta=float(r['x_delta']))
  68. def main() -> int:
  69. ap = argparse.ArgumentParser(description='谱库查表: 记录 → npz 分片 (取数配方见脚本头)')
  70. ap.add_argument('--window', default='w0316')
  71. ap.add_argument('--turbine', default=None)
  72. ap.add_argument('--sensor', default=None)
  73. ap.add_argument('--meas', default=None)
  74. ap.add_argument('--trigger', default=None, help='指定 trigger_time(不指定取该组合第一条)')
  75. ap.add_argument('--at', type=float, default=None, help='看这个频率(Hz)处的幅值(最近线)')
  76. ap.add_argument('--stats', action='store_true', help='只打印该窗谱库的概况')
  77. a = ap.parse_args()
  78. mp = meta_path(a.window)
  79. if not mp.is_file():
  80. print(f'[X] 谱库元数据不在位: {P.rel(mp)}')
  81. print(f' 该窗的谱还没摄入。重新摄入: python scripts/rudong_tcm_index.py --root <decode 目录> '
  82. f'--out <窗>/index.parquet 然后 python scripts/rudong_tcm_spectra.py --root <同> --out <窗>/spectra')
  83. return 2
  84. m = pd.read_parquet(mp)
  85. if a.stats or not a.turbine:
  86. print(f'== 谱库概况 · 窗 {a.window} · {P.rel(mp)} ==')
  87. print(f' 谱件记录 {len(m)} 条 · 分片 {m["shard"].nunique()} 个 · 测点 {m["sensor"].nunique()} 个'
  88. f' · 机组 {m["turbine"].nunique()} 台')
  89. g = m.groupby('meas_name').agg(条数=('shard_row', 'size'), 分片=('shard', 'nunique'),
  90. dx=('x_delta', 'first'), x0=('x_offset', 'first'))
  91. print(g.sort_values('条数', ascending=False).head(18).to_string())
  92. print(f' 取数配方: v = npz["values"][shard_row]; x = x_offset + arange(len(v))*x_delta')
  93. return 0
  94. got = load(a.window, a.turbine, a.sensor, a.meas, a.trigger)
  95. r = got['row']
  96. print(f'== 记录 → npz ==')
  97. print(f' {r["turbine"]} / {r["sensor"]} / {r["meas_name"]} @ {r["trigger_time"]}')
  98. print(f' 分片 {P.rel(got["npz"])} · 第 {int(r["shard_row"])} 行 · {len(got["v"])} 点'
  99. f' · x: {got["x_offset"]} + i*{got["x_delta"]} Hz ⇒ {got["x"][0]:.3f} ~ {got["x"][-1]:.3f} Hz')
  100. print(f' 幅值: min={got["v"].min():.6g} max={got["v"].max():.6g} 均值={got["v"].mean():.6g}')
  101. if a.at is not None:
  102. i = int(np.argmin(np.abs(got['x'] - a.at)))
  103. print(f' @{a.at} Hz(最近线 {got["x"][i]:.3f} Hz, 第 {i} 条): {got["v"][i]:.6g}')
  104. return 0
  105. if __name__ == '__main__':
  106. for _s in (sys.stdout, sys.stderr):
  107. try:
  108. _s.reconfigure(errors='replace')
  109. except Exception:
  110. pass
  111. sys.exit(main())