| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- #!/usr/bin/env python3
- """Render actual sampled waveforms for selected events without changing the original page."""
- from __future__ import annotations
- import argparse
- import csv
- from datetime import datetime, timedelta
- from pathlib import Path
- import numpy as np
- from make_event_dashboard import html
- from extract_from_db import connect, fetch_cycles
- from indicator_features.indicator_features import (
- CYLINDER_BORE_MM, _plateau_levels, smooth, volume_from_angle,
- )
- from new_unit_events import collect_events
- EVENTS = [
- ("7号 2026-05-21", "7", "2026-05-21", "高密度特征_7号_2026.csv", 30, 30),
- ("8号 2026-03-29/04-01 合并事件", "8", "2026-03-30", "高密度特征_8号_2026.csv", 30, 30),
- ("8号 2025同期(后段延长至60天)", "8", "2025-03-30", "高密度特征_8号_2025同期.csv", 30, 60),
- ("7号 2025-08-08(7月前段 / 11月后段)", "7", "2025-08-08", "高密度特征_7号_2025气阀事件.csv", 30, 110),
- ("7号 2025-10-07(7月前段 / 11月后段)", "7", "2025-10-07", "高密度特征_7号_2025气阀事件.csv", 90, 50),
- ("7号 2025停机维修合并过程(4~7月回溯 / 11月恢复)", "7", "2025-07-28", "高密度特征_7号_2025维修前回溯.csv", 120, 120),
- ]
- BAD = {("7", "四", "盖侧"), ("7", "六", "盖侧"), ("8", "六", "盖侧")}
- def build_data(root: Path):
- bins = 90
- data = {"angle": (np.arange(bins) * 360 / bins).tolist(), "machines": {}}
- connection = connect()
- cycle_cache = {}
- try:
- for label, unit, date, filename, pre_days, post_days in EVENTS:
- t0 = datetime.fromisoformat(date)
- with (root / filename).open(encoding="utf-8-sig", newline="") as handle:
- rows = list(csv.DictReader(handle))
- if filename in ("高密度特征_7号_2025维修前回溯.csv", "高密度特征_7号_2025气阀事件.csv"):
- with (root / "高密度特征_7号_2025事件后11月.csv").open(encoding="utf-8-sig", newline="") as handle:
- rows.extend(csv.DictReader(handle))
- rows = [r for r in rows if
- t0 - timedelta(days=pre_days) <= datetime.fromisoformat(r["sample_time"]) < t0 + timedelta(days=post_days + 1)
- and float(r.get("p_span") or 0) >= 0.3
- and (unit, r["cylinder"], r["side"]) not in BAD]
- points = sorted({(r["cylinder"], r["side"]) for r in rows})
- # Select one cylinder per event. A dropdown exposes both chambers without
- # presenting unrelated cylinders as peers or duplicate fault samples.
- cylinder = "一" if any(c == "一" for c, _ in points) else "三"
- chosen = [r for r in rows if r["cylinder"] == cylinder]
- if filename == "高密度特征_7号_2025维修前回溯.csv":
- chosen = [r for r in rows if (r["cylinder"], r["side"]) in
- {("三", "盖侧"), ("三", "轴侧"), ("四", "轴侧"), ("五", "盖侧") }]
- ids = [int(r["wave_file_id"]) for r in chosen]
- missing = [i for i in ids if i not in cycle_cache]
- if missing:
- cycle_cache.update(fetch_cycles(connection, missing, 100))
- event = {"label": label, "sides": {}}
- for cylinder, side in sorted({(r["cylinder"], r["side"]) for r in chosen}):
- samples = []
- for row in chosen:
- if (row["cylinder"], row["side"]) != (cylinder, side):
- continue
- signal = cycle_cache.get(int(row["wave_file_id"]))
- if signal is None or len(signal) < 60 or not np.all(np.isfinite(signal)):
- continue
- p = smooth(signal, 21)
- suction, discharge, _ = _plateau_levels(p)
- span = discharge - suction
- if span < 0.3:
- continue
- indices = np.arange(bins) * len(p) / bins
- absolute = np.interp(indices, np.arange(len(p)), p)
- profile = (absolute - suction) / span
- volume = np.interp(indices, np.arange(len(p)), volume_from_angle(len(p), CYLINDER_BORE_MM[cylinder]))
- time = datetime.fromisoformat(row["sample_time"])
- samples.append({
- "date": row["sample_time"], "day": (time.date() - t0.date()).days,
- "phase": "修前" if time < t0 else "修后",
- "profile": profile.round(6).tolist(), "absolute": absolute.round(6).tolist(),
- "volume": volume.round(6).tolist(),
- })
- samples.sort(key=lambda s: s["date"])
- if not samples:
- continue
- pre = [s for s in samples if s["phase"] == "修前"]
- post = [s for s in samples if s["phase"] == "修后"]
- reference = pre or post
- if filename == "高密度特征_7号_2025维修前回溯.csv":
- reference = [s for s in samples if s["date"][:7] == "2025-04"] or reference
- template = np.median([s["profile"] for s in reference], axis=0)
- for sample in samples:
- sample["distance"] = round(float(np.linalg.norm(np.asarray(sample["profile"]) - template)), 5)
- def median(items, key):
- return np.median([s[key] for s in items], axis=0).round(6).tolist() if items else []
- event["sides"][f"{cylinder}缸{side}"] = {
- "samples": samples, "counts": {"pre": len(pre), "post": len(post)},
- "preMedian": median(pre, "profile"), "postMedian": median(post, "profile"),
- "preAbsoluteMedian": median(pre, "absolute"), "postAbsoluteMedian": median(post, "absolute"),
- "trend": [{"date": s["date"], "day": s["day"], "distance": s["distance"]} for s in samples],
- }
- if filename == "高密度特征_7号_2025维修前回溯.csv":
- # Compare the last running period with restart, but measure the
- # lookback trend against the earliest available April period.
- july = [s for s in samples if s["date"][:7] == "2025-07"]
- event["sides"][f"{cylinder}缸{side}"]["preMedian"] = median(july, "profile")
- event["sides"][f"{cylinder}缸{side}"]["preAbsoluteMedian"] = median(july, "absolute")
- coverage = sorted({s["date"][:10] for d in event["sides"].values() for s in d["samples"]})
- if coverage:
- event["label"] += f" · 实际功图 {coverage[0]} 至 {coverage[-1]}"
- data["machines"][label] = event
- data["machines"].update(collect_events(connection, per_day=6, bins=bins))
- finally:
- connection.close()
- return data
- def make_html(data):
- page = html(data)
- # Reuse the style and actual-waveform plots without editing the source dashboard.
- page = page.replace("一缸 P-V 功图事件前后对比", "事件功图扩展对比")
- page = page.replace("图例在图外,可逐时间点勾选", "图例在图外,按四类勾选")
- page = page.replace("P-V图例可按时间点单独勾选", "功图按前段/后段及中位线分组勾选")
- page = page.replace("<div id=\"app\"></div>",
- '<label>选择事件:<select id="event-select"></select></label>'
- '<p class="note">每次只展示一个选项。两个旧选项保留7月与11月的三缸两侧对照,'
- '使用7月模板计算形状距离;10月选项的前段实际也是7月,不是10月采样。'
- '两个旧选项共用同一批周期,仅横轴相对日期不同,不能作为独立维修证据。'
- '7号2025年的维修合并为一次停机过程,7月28日仅作末次运行后的时间分界,不是维修日期。'
- '该过程粗蓝线为7月末运行段中位,红线为11月恢复段中位;细蓝线包含4~7月回溯周期。'
- '回溯选项的形状距离以4月模板为参照,其他选项仍以事件前模板为参照,数值不可混比。'
- '新增4号和6号事件先扫描全部有效测点;本页保留全部测点,候选排名按扣除全机共同变化后的局部残差计算。'
- '6号4月22日至26日视为同一维修过程,过程内周期不进入前后模板。'
- '5号因当前数据库没有压力功图而暂不展示。展示测点并非已确认异常缸,缺测期间不推断变化发生时刻。</p>'
- '<div id="app"></div>')
- page = page.replace("<script>renderPage(DATA);</script>", """<script>
- renderPage(DATA);
- const sections=Array.from(document.querySelectorAll('.machine'));
- const selector=document.getElementById('event-select');
- Object.keys(DATA.machines).forEach((name,index)=>{const option=document.createElement('option');option.value=index;option.textContent=name;selector.append(option)});
- function selectEvent(){sections.forEach((section,index)=>section.hidden=index!==Number(selector.value));requestAnimationFrame(()=>charts.forEach(chart=>chart.resize()))}
- selector.addEventListener('change',selectEvent);selectEvent();
- </script>""")
- return page
- def main():
- parser = argparse.ArgumentParser(description="生成实际周期的事件功图扩展网页")
- parser.add_argument("--root", default="cache/indicator")
- parser.add_argument("--out", required=True)
- args = parser.parse_args()
- data = build_data(Path(args.root))
- out = Path(args.out)
- out.parent.mkdir(parents=True, exist_ok=True)
- out.write_text(make_html(data), encoding="utf-8")
- print(f"对比网页 -> {out}")
- for event in data["machines"].values():
- print(event["label"], {point: d["counts"] for point, d in event["sides"].items()})
- if __name__ == "__main__":
- main()
|