#!/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("
", '' '

每次只展示一个选项。两个旧选项保留7月与11月的三缸两侧对照,' '使用7月模板计算形状距离;10月选项的前段实际也是7月,不是10月采样。' '两个旧选项共用同一批周期,仅横轴相对日期不同,不能作为独立维修证据。' '7号2025年的维修合并为一次停机过程,7月28日仅作末次运行后的时间分界,不是维修日期。' '该过程粗蓝线为7月末运行段中位,红线为11月恢复段中位;细蓝线包含4~7月回溯周期。' '回溯选项的形状距离以4月模板为参照,其他选项仍以事件前模板为参照,数值不可混比。' '新增4号和6号事件先扫描全部有效测点;本页保留全部测点,候选排名按扣除全机共同变化后的局部残差计算。' '6号4月22日至26日视为同一维修过程,过程内周期不进入前后模板。' '5号因当前数据库没有压力功图而暂不展示。展示测点并非已确认异常缸,缺测期间不推断变化发生时刻。

' '
') page = page.replace("", """""") 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()