| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- """全时间段标签的 HTML:选中一个标签,看该时刻的首周期多缸归一化波形。"""
- from __future__ import annotations
- import json
- from pathlib import Path
- import numpy as np
- import pandas as pd
- from . import config, data
- COLORS = {
- "一缸": "#c0392b",
- "二缸": "#e67e22",
- "三缸": "#16a085",
- "四缸": "#2980b9",
- "五缸": "#8e44ad",
- "六缸": "#7f8c8d",
- }
- MAX_POINTS = 520
- def _nearest_row(frame: pd.DataFrame, cylinder: str, target: pd.Timestamp, tolerance=pd.Timedelta(hours=2)):
- group = frame[frame["cylinder"] == cylinder]
- if group.empty:
- return None
- distance = (group["sample_time"] - target).abs()
- index = distance.idxmin()
- if distance.loc[index] > tolerance:
- return None
- return group.loc[index]
- def _baseline_rms(meta: dict) -> float:
- median = float(meta.get("rms_log", [0.0, 1.0])[0])
- return float(10 ** median)
- def export_waveforms(frame: pd.DataFrame, intervals: pd.DataFrame, baseline: dict) -> None:
- if frame.empty or intervals.empty:
- return
- out_dir = config.OUTPUT_DIR / "html"
- out_dir.mkdir(parents=True, exist_ok=True)
- records: list[dict] = []
- id_to_row: dict[int, pd.Series] = {}
- for row in intervals.itertuples(index=False):
- seed_time = pd.Timestamp(row.max_time)
- rows: dict[str, pd.Series] = {}
- for name in config.CYLINDERS:
- item = _nearest_row(frame, name, seed_time)
- if item is not None:
- rows[name] = item
- id_to_row[int(item["id"])] = item
- if not rows:
- continue
- records.append(
- {
- "id": len(records),
- "cylinder": row.cylinder,
- "start": str(row.start_time),
- "end": str(row.end_time),
- "duration": float(row.duration_hours),
- "max_score": float(row.max_score),
- "direction": str(row.direction),
- "level": str(row.level),
- "confirmed": bool(row.multi_feature_confirmed),
- "seed_time": str(seed_time),
- "rows": {name: int(item["id"]) for name, item in rows.items()},
- }
- )
- samples = data.fetch_samples(list(id_to_row), chunk_size=200)
- for record in records:
- series = []
- for name, file_id in record.pop("rows").items():
- signal = samples.get(int(file_id))
- if signal is None or signal.size == 0:
- continue
- item = id_to_row[int(file_id)]
- scale = _baseline_rms(baseline.get(name, {}))
- if not np.isfinite(scale) or scale <= 0:
- scale = float(np.sqrt(np.mean(signal ** 2))) or 1.0
- fs = float(item["sample_frequency_hz"] or 0) or 25600.0
- step = max(1, int(np.ceil(signal.size / MAX_POINTS)))
- chosen = np.arange(0, signal.size, step)
- x = chosen / fs * 1000.0
- y = signal[chosen] / scale
- series.append(
- {
- "name": f"cyl{config.CYLINDER_TO_INDEX[name]}",
- "cylinder": name,
- "color": "#c0392b" if name == record["cylinder"] else COLORS[name],
- "width": 2.2 if name == record["cylinder"] else 1.0,
- "data": [[round(float(x[i]), 3), round(float(y[i]), 4)] for i in range(len(chosen))],
- }
- )
- record["series"] = series
- html = """<!doctype html>
- <html lang="zh-CN"><head><meta charset="utf-8"><title>5号机全时段缸体振动标签</title>
- <style>
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif;margin:18px;color:#222;background:#fff}
- h1{font-size:22px;margin:0 0 6px} .note{color:#666;font-size:13px;line-height:1.6;margin-bottom:12px}
- .panel{display:grid;grid-template-columns:300px 1fr;gap:16px}
- select{width:100%;padding:8px;font-size:13px}
- .info{border:1px solid #eee;border-radius:6px;padding:10px;margin-top:10px;font-size:13px;line-height:1.7}
- #chart{height:520px;border:1px solid #eee;border-radius:6px}
- table{border-collapse:collapse;margin-top:10px;font-size:12px;width:100%}
- th,td{border:1px solid #ddd;padding:5px 7px;text-align:left} th{background:#f7f7f7}
- </style></head><body>
- <h1>5号机全时段缸体振动标签 · 首周期多缸归一化波形</h1>
- <div class="note">下拉选择标签;横轴是首周期时间(ms),纵轴是“波形 / 该缸正常基线 RMS”。事件缸加粗红色。缺数据的缸不画。</div>
- <div class="panel">
- <div>
- <select id="pick"></select>
- <div id="info" class="info"></div>
- <table id="table"><thead><tr><th>开始</th><th>方向</th><th>等级</th><th>确认</th></tr></thead><tbody></tbody></table>
- </div>
- <div id="chart"></div>
- </div>
- <script src="echarts.min.js"></script>
- <script>
- const records = __RECORDS__;
- const pick = document.getElementById('pick');
- const chart = echarts.init(document.getElementById('chart'));
- for (const r of records) {
- const opt = document.createElement('option');
- opt.value = r.id;
- opt.text = `${r.start} | ${r.cylinder} | ${r.direction} | ${r.level} | 分数 ${r.max_score}${r.confirmed ? ' | 确认' : ''}`;
- pick.appendChild(opt);
- }
- function render(id) {
- const r = records.find(x => x.id === Number(id));
- if (!r) return;
- document.getElementById('info').innerHTML = `<b>${r.start} ~ ${r.end}</b><br>缸:${r.cylinder}<br>方向:${r.direction}<br>等级:${r.level}<br>多特征确认:${r.confirmed ? '是' : '否'}<br>最大分数:${r.max_score}<br>种子时刻:${r.seed_time}`;
- const tbody = document.querySelector('#table tbody');
- tbody.innerHTML = `<tr><td>${r.start}</td><td>${r.direction}</td><td>${r.level}</td><td>${r.confirmed ? '是' : '否'}</td></tr>`;
- chart.setOption({
- tooltip: {trigger: 'axis'},
- legend: {top: 0},
- grid: {left: 55, right: 20, top: 35, bottom: 45},
- xAxis: {type: 'value', name: 'ms', scale: true},
- yAxis: {type: 'value', name: 'waveform / baseline RMS', scale: true},
- dataZoom: [{type: 'inside'}, {type: 'slider', height: 18, bottom: 10}],
- series: r.series.map(s => ({
- name: s.name, type: 'line', showSymbol: false, sampling: 'lttb',
- lineStyle: {width: s.width}, itemStyle: {color: s.color}, data: s.data
- }))
- }, true);
- }
- pick.addEventListener('change', () => render(pick.value));
- const first = records.filter(r => r.confirmed).sort((a, b) => b.max_score - a.max_score)[0] || records[0];
- if (first) { pick.value = first.id; render(first.id); }
- window.addEventListener('resize', () => chart.resize());
- </script></body></html>"""
- html = html.replace("__RECORDS__", json.dumps(records, ensure_ascii=False))
- (out_dir / "waveforms.html").write_text(html, encoding="utf-8")
- (out_dir / "index.html").write_text(
- """<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>5号机缸体振动</title>
- <style>body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif;margin:24px}a{color:#1a73e8}</style>
- </head><body><h1>5号机缸体振动</h1><p><a href="waveforms.html">打开全时段标签波形</a></p></body></html>""",
- encoding="utf-8",
- )
- print(f"[html] {out_dir / 'waveforms.html'} ({len(records)} labels)")
|