| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- #!/usr/bin/env python3
- """Plot the per-angular-bin statistics before/after a chosen event (read-only).
- Uses the same running-segment selection as ``event_bins.py`` (rpm>0 segments, at
- least ``--pre-days`` / ``--post-days`` running days). Two figures per event:
- * profile: for every measurement point, the pre-event median and post-event
- median of a chosen metric across the bins (whole profile shift vs shape);
- * series : for every point, the time trace of the bin whose local (shape) change
- is the largest for a chosen metric, pre and post coloured differently.
- """
- from __future__ import annotations
- import argparse
- import sys
- from collections import defaultdict
- from datetime import datetime
- from pathlib import Path
- import numpy as np
- import matplotlib
- matplotlib.use("Agg")
- import matplotlib.pyplot as plt # noqa: E402
- matplotlib.rcParams["font.sans-serif"] = ["Arial Unicode MS", "Heiti TC", "Hiragino Sans GB", "DejaVu Sans"]
- matplotlib.rcParams["axes.unicode_minus"] = False
- BACKEND = Path(__file__).resolve().parents[1]
- sys.path.insert(0, str(BACKEND))
- from event_bins import load as load_rows, collect_before, collect_after, robust_scale # noqa: E402
- from run_segments import unit_segments # noqa: E402
- CYLINDERS = ("一", "二", "三", "四", "五", "六")
- SIDES = ("盖侧", "轴侧")
- VOLATILE = ("std", "mad", "range", "roughness")
- def median_at(rows, metric, bin_index):
- values = [r[metric] for r in rows if r["bin"] == bin_index and r[metric] is not None]
- return float(np.median(values)) if values else None
- def main() -> None:
- parser = argparse.ArgumentParser(description="每角度箱 前后剖面 / 时间序列图(运行段口径)")
- parser.add_argument("--csv", required=True, help="angle_bins.py 长表 CSV")
- parser.add_argument("--unit", required=True, choices=("7", "8", "9"))
- parser.add_argument("--date", required=True, help="事件日期 YYYY-MM-DD")
- parser.add_argument("--pre-days", type=int, default=15)
- parser.add_argument("--post-days", type=int, default=15)
- parser.add_argument("--merge-gap", type=int, default=2)
- parser.add_argument("--max-gap", type=int, default=30)
- parser.add_argument("--profile-metric", default="mean")
- parser.add_argument("--series-metric", default="std")
- parser.add_argument("--outdir", default="cache/indicator/bins/plots")
- args = parser.parse_args()
- rows = load_rows(args.csv)
- t0 = datetime.fromisoformat(args.date)
- segments = unit_segments(args.unit, args.merge_gap)
- bins = sorted({r["bin"] for r in rows})
- bin_start = {r["bin"]: r["bin_start"] for r in rows}
- bin_deg = round(bin_start[bins[1]] - bin_start[bins[0]], 6) if len(bins) > 1 else 10.0
- points = [(c, s) for c in CYLINDERS for s in SIDES if any(r["cylinder"] == c and r["side"] == s for r in rows)]
- outdir = Path(args.outdir)
- outdir.mkdir(parents=True, exist_ok=True)
- # pre/post rows per point via running segments
- selected = {}
- for cylinder, side in points:
- point_rows = [r for r in rows if r["cylinder"] == cylinder and r["side"] == side]
- pre, _ = collect_before(point_rows, t0, segments, args.pre_days)
- post, _ = collect_after(point_rows, t0, segments, args.post_days, args.max_gap)
- selected[(cylinder, side)] = (pre, post)
- cols = 3
- rows_n = (len(points) + cols - 1) // cols
- # ---- profile figure ----
- fig, axes = plt.subplots(rows_n, cols, figsize=(4.2 * cols, 2.6 * rows_n), squeeze=False)
- for ax, key in zip(axes.ravel(), points):
- pre, post = selected[key]
- xs = [bin_start[b] + bin_deg / 2 for b in bins]
- prof_pre = [median_at(pre, args.profile_metric, b) for b in bins]
- prof_post = [median_at(post, args.profile_metric, b) for b in bins]
- ax.plot(xs, prof_pre, color="crimson", lw=1.4, label="pre")
- ax.plot(xs, prof_post, color="royalblue", lw=1.4, label="post")
- ax.set_title(f"{key[0]}缸{key[1]}", fontsize=10)
- ax.set_xlim(0, 360)
- ax.set_xticks([0, 90, 180, 270, 360])
- ax.tick_params(labelsize=8)
- ax.grid(alpha=0.25)
- for ax in axes.ravel()[len(points):]:
- ax.axis("off")
- axes.ravel()[0].legend(fontsize=7)
- fig.suptitle(f"{args.unit}号机 {args.date} 每角度箱 {args.profile_metric}(pre红 post蓝)")
- fig.tight_layout()
- p1 = outdir / f"{args.unit}_{args.date}_profile_{args.profile_metric}.png"
- fig.savefig(p1, dpi=110)
- plt.close(fig)
- print("图:", p1)
- # ---- series figure: strongest local shape change per point ----
- fig, axes = plt.subplots(rows_n, cols, figsize=(4.2 * cols, 2.6 * rows_n), squeeze=False)
- for ax, key in zip(axes.ravel(), points):
- pre, post = selected[key]
- best_bin, best_score = None, 0.0
- for b in bins:
- a = [r[args.series_metric] for r in pre if r["bin"] == b and r[args.series_metric] is not None]
- c = [r[args.series_metric] for r in post if r["bin"] == b and r[args.series_metric] is not None]
- if len(a) < 3 or len(c) < 3:
- continue
- scale = robust_scale(np.asarray(a))
- if scale <= 1e-12:
- continue
- score = abs(float(np.median(c)) - float(np.median(a))) / scale
- if score > best_score:
- best_bin, best_score = b, score
- if best_bin is None:
- ax.set_title(f"{key[0]}缸{key[1]} 无数据", fontsize=9)
- ax.axis("off")
- continue
- trace = sorted([r for r in rows if r["cylinder"] == key[0] and r["side"] == key[1]
- and r["bin"] == best_bin and r[args.series_metric] is not None], key=lambda r: r["time"])
- for r in trace:
- color = "royalblue" if r["time"] >= t0 else "crimson"
- ax.plot(r["time"], r[args.series_metric], ".", color=color, markersize=3)
- ax.axvline(t0, color="k", lw=0.8, ls="--")
- start = bin_start[best_bin]
- ax.set_title(f"{key[0]}缸{key[1]} 箱{start:g}-{start + bin_deg:g}° ({best_score:.1f}MAD)", fontsize=9)
- ax.tick_params(labelsize=7, rotation=30)
- ax.grid(alpha=0.25)
- for ax in axes.ravel()[len(points):]:
- ax.axis("off")
- fig.suptitle(f"{args.unit}号机 {args.date} 各点局部变化最大的箱 {args.series_metric} 时间序列")
- fig.tight_layout()
- p2 = outdir / f"{args.unit}_{args.date}_series_{args.series_metric}.png"
- fig.savefig(p2, dpi=110)
- plt.close(fig)
- print("图:", p2)
- if __name__ == "__main__":
- main()
|