| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- r"""六层链 `energy_share` 步 —— **谱线能量占比**(最后补上的那一步脚本)。
- ## 它在链里的位置与口径
- `src/windcms/pipeline.py::analyze` 的步序是 `oem_scan → **energy_share** → model_run → fusion → report`:
- 扫线(哪些特征频率有线)之后,本步回答"**这条线占了多少能量**"。此前这一步脚本没随包
- (`chain_gap_check` 报"1 个步骤脚本未随包"),于是链上少一环、也没有任何件回答这个问题。
- 口径(写死在这里,供复核):
- band = 目标特征频率 ±tol_bins·dx(默认 ±2 bin,与 model_run 的取峰窗同宽)
- 峰能量 = Σ y[i]² (i ∈ band)
- 总能量 = Σ y[j]² (整条谱,去掉 DC 前 3 bin)
- energy_pct = 峰能量 ÷ 总能量 × 100
- ★只报**占比**,不定级:占比小不等于没问题(宽带抬升会摊薄单线占比),
- 故消费端(model_run 的 `vib_verdict_and_writeback`)把 energy_pct<0.01% 的结论降为「参考」。
- 输入: `m5_cms_tcm/windows/<窗>/index.parquet` + `spectra/*`(振动摄入的产物)
- 候选线来自 `reference/rudong/oem_scan_plan.json`(与 `rudong_tcm_oem_scan.py` 同一份配置)
- 输出: `m5_cms_tcm/line_energy_share.parquet`
- 列: window, turbine, sensor, meas, component, line, hz, band_hz, energy_pct, peak, total, n_bins
- 用法: python scripts/rudong_line_energy_share.py [--window w0316] [--limit-turbines N]
- """
- from __future__ import annotations
- import argparse
- import pathlib
- import sys
- import time
- import numpy as np
- import pandas as pd
- ROOT = pathlib.Path(__file__).resolve().parents[1]
- sys.path.insert(0, str(ROOT))
- sys.path.insert(0, str(ROOT / 'scripts'))
- from src import paths as P # noqa: E402
- import rudong_tcm_oem_scan as OS # noqa: E402 候选线(分母)同一实现
- import spectra_lookup as SL # noqa: E402
- LINES = (('BPFI', 'BPFI_hz'), ('BPFO', 'BPFO_hz'), ('BSF', 'BSF_hz'))
- def windows_of(explicit=None) -> list[str]:
- m5 = P.m5()
- got = sorted(d.name for d in (m5 / 'windows').glob('w[0-9][0-9][0-9][0-9]')
- if (d / 'index.parquet').is_file())
- return [explicit] if explicit else got
- def main() -> int:
- ap = argparse.ArgumentParser(description='六层链 energy_share 步: 谱线能量占比')
- ap.add_argument('--window', default=None)
- ap.add_argument('--tol-bins', type=int, default=2)
- ap.add_argument('--limit-turbines', type=int, default=0)
- ap.add_argument('--out', default=None)
- a = ap.parse_args()
- _plan, comps = OS.load_plan()
- wins = windows_of(a.window)
- if not wins:
- print('[跳过] 没有窗索引(先跑振动摄入 scripts/vib_raw_build.py)')
- return 0
- rows = []
- t0 = time.time()
- for w in wins:
- mp = SL.meta_path(w)
- if not mp.is_file():
- continue
- meta = pd.read_parquet(mp, columns=['turbine', 'sensor', 'meas_name'])
- avail = set(map(tuple, meta.drop_duplicates().values))
- turbines = sorted({t for t, _s, _m in avail})
- if a.limit_turbines:
- turbines = turbines[:a.limit_turbines]
- for c in comps:
- sen, meas = c['sensor'], c['shard']
- for t in turbines:
- if (t, sen, meas) not in avail:
- continue
- try:
- rec = SL.load(w, t, sen, meas)
- except BaseException:
- continue
- if not rec:
- continue
- y = np.asarray(rec['v'], dtype=float)
- dx = float(rec.get('x_delta') or c.get('dx_hz') or 0)
- if not dx or y.size < 8:
- continue
- y2 = y[3:] ** 2 # 去前 3 bin(DC/趋势), 与"总能量"口径一致
- total = float(np.nansum(y2))
- if not np.isfinite(total) or total <= 0:
- continue
- for label, col in LINES:
- hz = c.get(col)
- if not hz or not np.isfinite(hz) or hz <= 0:
- continue
- k = int(round(float(hz) / dx))
- lo, hi = max(k - a.tol_bins - 3, 0), min(k + a.tol_bins - 3, y2.size)
- if hi <= lo:
- continue
- band = y2[lo:hi]
- peak = float(np.nansum(band))
- rows.append(dict(window=w, turbine=t, sensor=sen, meas=meas,
- component=c['component'], line=label, hz=round(float(hz), 3),
- band_hz=round(2 * a.tol_bins * dx, 3),
- energy_pct=round(100.0 * peak / total, 6),
- peak=peak, total=total, n_bins=int(hi - lo)))
- if not rows:
- print(f'[X] 一条占比也没算出来(窗 {wins} 的谱库不可用?)—— 不写空文件')
- return 2
- df = pd.DataFrame(rows).sort_values(['window', 'turbine', 'component', 'line'])
- out = pathlib.Path(a.out) if a.out else (P.m5() / 'line_energy_share.parquet')
- out.parent.mkdir(parents=True, exist_ok=True)
- df.to_parquet(out, index=False)
- top = df.nlargest(3, 'energy_pct')[['turbine', 'component', 'line', 'energy_pct']].to_dict('records')
- print(f'已写 {P.rel(out)}: {len(df)} 行 × {len(df.columns)} 列(窗 {",".join(wins)},{time.time()-t0:.0f}s)· '
- f'占比最高: {top}')
- try:
- from src import derived_manifest as DM
- DM.record(P.out_root(), {out.relative_to(P.out_root()).as_posix():
- 'scripts/rudong_line_energy_share.py (线能量占比 = ±2bin 带内能量 ÷ 全谱能量; 只报占比不定级)'},
- by='rudong_line_energy_share')
- print(' 已自登记 → _derived_manifest.json')
- except Exception as e:
- print(f' [i] 自登记跳过: {type(e).__name__}: {e}')
- return 0
- if __name__ == '__main__':
- for _s in (sys.stdout, sys.stderr):
- try:
- _s.reconfigure(errors='replace')
- except Exception:
- pass
- sys.exit(main())
|