| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- #!/usr/bin/env python3
- """Shape-based indicator-diagram comparison (pre/post repair, peer, chamber).
- Uses the process-invariant normalised P-theta profile so load/level changes
- cancel and only waveform shape remains. Reports L2 shape distances and saves a
- profile figure. Read-only.
- """
- from __future__ import annotations
- import argparse
- import sys
- from collections import defaultdict
- from pathlib import Path
- import numpy as np
- BACKEND = Path(__file__).resolve().parents[1]
- sys.path.insert(0, str(BACKEND))
- from indicator_features.extract_from_db import ( # noqa: E402
- CYLINDERS,
- SIDES,
- connect,
- fetch_cycles,
- fetch_file_rows,
- point_name,
- sample_per_day,
- )
- from indicator_features.indicator_features import CYLINDER_STAGE, normalized_profile # noqa: E402
- def collect(connection, unit, cylinders, sides, start, end, per_day, bins):
- profiles = defaultdict(list)
- for cylinder in cylinders:
- for side in sides:
- point = point_name(unit, cylinder, side)
- files = fetch_file_rows(connection, point, start, end)
- picked = sample_per_day(files, per_day)
- if not picked:
- continue
- id_map = {int(r["id"]): r for r in picked}
- for file_id, signal in fetch_cycles(connection, list(id_map), 200).items():
- prof = normalized_profile(signal, cylinder, bins=bins)
- if prof is not None:
- profiles[(cylinder, side)].append(prof)
- return profiles
- def median_profile(items):
- return np.median(np.vstack(items), axis=0) if items else None
- STAGE_MEMBERS = {"一级": ("二", "四", "六"), "二级": ("三", "五"), "三级": ("一",)}
- def peer_median(medians, cylinder, side):
- peers = STAGE_MEMBERS[CYLINDER_STAGE[cylinder]]
- same = [m for (c, s), m in medians.items() if s == side and c in peers and m is not None]
- return np.median(np.vstack(same), axis=0) if same else None
- def main():
- parser = argparse.ArgumentParser(description="功图波形形状对照(修前/修后、同级、双腔)")
- parser.add_argument("--unit", required=True, choices=("7", "8", "9"))
- parser.add_argument("--cylinders", nargs="+", default=list(CYLINDERS), choices=CYLINDERS)
- parser.add_argument("--sides", nargs="+", default=list(SIDES), choices=SIDES)
- parser.add_argument("--pre", nargs=2, required=True)
- parser.add_argument("--post", nargs=2, required=True)
- parser.add_argument("--per-day", type=int, default=6)
- parser.add_argument("--bins", type=int, default=180)
- parser.add_argument("--out-dir", default=str(BACKEND / "cache" / "indicator" / "shape"))
- args = parser.parse_args()
- connection = connect()
- try:
- pre = collect(connection, args.unit, args.cylinders, args.sides, args.pre[0], args.pre[1], args.per_day, args.bins)
- post = collect(connection, args.unit, args.cylinders, args.sides, args.post[0], args.post[1], args.per_day, args.bins)
- finally:
- connection.close()
- pre_med = {k: median_profile(v) for k, v in pre.items() if v}
- post_med = {k: median_profile(v) for k, v in post.items() if v}
- print(f"{args.unit}号机 形状对照 修前 {args.pre[0]}~{args.pre[1]} 修后 {args.post[0]}~{args.post[1]}")
- print(f"{'缸侧':>6}{'样本前/后':>12}{'修前/后 L2':>12}{'peer前':>9}{'peer后':>9}{'双腔前':>9}{'双腔后':>9}")
- for key in sorted(set(pre_med) | set(post_med)):
- cylinder, side = key
- pm, qm = pre_med.get(key), post_med.get(key)
- pp = peer_median(pre_med, cylinder, side)
- qp = peer_median(post_med, cylinder, side)
- peer_pre = np.linalg.norm(pm - pp) if pm is not None and pp is not None else float("nan")
- peer_post = np.linalg.norm(qm - qp) if qm is not None and qp is not None else float("nan")
- other = "轴侧" if side == "盖侧" else "盖侧"
- ch_pre = np.linalg.norm(pm - pre_med[(cylinder, other)]) if pm is not None and (cylinder, other) in pre_med and pre_med[(cylinder, other)] is not None else float("nan")
- ch_post = np.linalg.norm(qm - post_med[(cylinder, other)]) if qm is not None and (cylinder, other) in post_med and post_med[(cylinder, other)] is not None else float("nan")
- cross = np.linalg.norm(pm - qm) if pm is not None and qm is not None else float("nan")
- print(f"{cylinder+side:>6}{len(pre.get(key,[])):>6}/{len(post.get(key,[])):<5}{cross:>12.3f}{peer_pre:>9.3f}{peer_post:>9.3f}{ch_pre:>9.3f}{ch_post:>9.3f}")
- # Save a figure (best effort).
- try:
- import matplotlib
- matplotlib.use("Agg")
- import matplotlib.pyplot as plt
- out = Path(args.out_dir)
- out.mkdir(parents=True, exist_ok=True)
- keys = [k for k in sorted(set(pre_med) | set(post_med))]
- if keys:
- cols = 3
- rows = (len(keys) + cols - 1) // cols
- fig, axes = plt.subplots(rows, cols, figsize=(4 * cols, 3 * rows), squeeze=False)
- for ax, key in zip(axes.ravel(), keys):
- if pre_med.get(key) is not None:
- ax.plot(pre_med[key], color="crimson", label="pre")
- if post_med.get(key) is not None:
- ax.plot(post_med[key], color="royalblue", label="post")
- ax.set_title(f"{key[0]}缸{key[1]}", fontsize=10)
- ax.set_ylim(-0.2, 1.4)
- ax.legend(fontsize=7)
- for ax in axes.ravel()[len(keys):]:
- ax.axis("off")
- fig.suptitle(f"{args.unit}号机 归一化功图剖面 pre vs post")
- fig.tight_layout()
- path = out / f"{args.unit}_{args.pre[0]}_{args.post[0]}.png"
- fig.savefig(path, dpi=110)
- print("图:", path)
- except Exception as error:
- print("绘图跳过:", error)
- if __name__ == "__main__":
- main()
|