pair_plot.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. #!/usr/bin/env python3
  2. """A simple, readable before/after figure for one measurement point.
  3. Three panels, all in Chinese:
  4. 1. 平均压力 对 曲柄角度(停机前红 / 开机后蓝),高亮关注的角度段;
  5. 2. 波动 std 对 曲柄角度(同色);
  6. 3. 关注角度段的 std 随时间变化,停机前红、开机后蓝,并标出事件日。
  7. """
  8. from __future__ import annotations
  9. import argparse
  10. import sys
  11. from collections import defaultdict
  12. from datetime import datetime
  13. from pathlib import Path
  14. import numpy as np
  15. import matplotlib
  16. matplotlib.use("Agg")
  17. import matplotlib.pyplot as plt # noqa: E402
  18. matplotlib.rcParams["font.sans-serif"] = ["Arial Unicode MS", "Heiti TC", "Hiragino Sans GB", "DejaVu Sans"]
  19. matplotlib.rcParams["axes.unicode_minus"] = False
  20. BACKEND = Path(__file__).resolve().parents[1]
  21. sys.path.insert(0, str(BACKEND))
  22. import cn_names as CN # noqa: E402
  23. from event_bins import collect_after, collect_before, load # noqa: E402
  24. from run_segments import unit_segments # noqa: E402
  25. def med(rows, metric, bin_index):
  26. vals = [r[metric] for r in rows if r["bin"] == bin_index and r[metric] is not None]
  27. return float(np.median(vals)) if vals else np.nan
  28. def main() -> None:
  29. parser = argparse.ArgumentParser(description="单点 停机前/开机后 简明对照图")
  30. parser.add_argument("--csv", required=True)
  31. parser.add_argument("--unit", required=True)
  32. parser.add_argument("--date", required=True)
  33. parser.add_argument("--cylinder", required=True)
  34. parser.add_argument("--side", required=True)
  35. parser.add_argument("--highlight", default="", help="关注角度段,如 140-150")
  36. parser.add_argument("--series-metric", default="std", help="右图随时间画哪个指标(默认 std)")
  37. parser.add_argument("--pre-days", type=int, default=15)
  38. parser.add_argument("--post-days", type=int, default=15)
  39. parser.add_argument("--max-gap", type=int, default=30)
  40. parser.add_argument("--merge-gap", type=int, default=2)
  41. parser.add_argument("--out", required=True)
  42. args = parser.parse_args()
  43. rows = load(args.csv)
  44. point = [r for r in rows if r["cylinder"] == args.cylinder and r["side"] == args.side]
  45. t0 = datetime.fromisoformat(args.date)
  46. segments = unit_segments(args.unit, args.merge_gap)
  47. pre, _ = collect_before(point, t0, segments, args.pre_days)
  48. post, _ = collect_after(point, t0, segments, args.post_days, args.max_gap)
  49. bins = sorted({r["bin"] for r in point})
  50. bin_start = {r["bin"]: r["bin_start"] for r in point}
  51. bin_deg = round(bin_start[bins[1]] - bin_start[bins[0]], 6) if len(bins) > 1 else 10.0
  52. centers = [bin_start[b] + bin_deg / 2 for b in bins]
  53. lo = hi = None
  54. if "-" in args.highlight:
  55. lo, hi = (float(x) for x in args.highlight.split("-"))
  56. hi_bins = [b for b in bins if lo is not None and lo <= bin_start[b] < hi]
  57. def daily_series(rows, bin_index, metric):
  58. by_day = defaultdict(list)
  59. for r in rows:
  60. if r["bin"] == bin_index and r[metric] is not None:
  61. by_day[r["time"].date()].append(r[metric])
  62. days = sorted(by_day)
  63. return days, [float(np.median(by_day[d])) for d in days]
  64. fig, axes = plt.subplots(1, 3, figsize=(15, 3.8))
  65. for ax, metric, name in ((axes[0], "mean", "平均压力 (MPa)"), (axes[1], "std", "波动 std (MPa)")):
  66. ax.plot(centers, [med(pre, metric, b) for b in bins], color="crimson", lw=1.8, label="停机前")
  67. ax.plot(centers, [med(post, metric, b) for b in bins], color="royalblue", lw=1.8, label="开机后")
  68. if hi_bins:
  69. ax.axvspan(lo, hi, color="orange", alpha=0.25)
  70. ax.set_xlim(0, 360)
  71. ax.set_xticks([0, 90, 180, 270, 360])
  72. ax.set_xlabel("曲柄角度 (°)")
  73. ax.set_title(name)
  74. ax.grid(alpha=0.25)
  75. axes[0].legend(fontsize=9)
  76. ax = axes[2]
  77. if hi_bins:
  78. b0 = hi_bins[len(hi_bins) // 2]
  79. days, values = daily_series(point, b0, args.series_metric)
  80. for d, v in zip(days, values):
  81. color = "royalblue" if datetime.combine(d, datetime.min.time()) >= t0 else "crimson"
  82. ax.plot(d, v, "o", color=color, ms=4)
  83. ax.axvline(t0, color="k", ls="--", lw=1)
  84. ax.set_title(f"关注段 {lo:g}-{hi:g}° 的 {CN.metric_col(args.series_metric)} 随时间")
  85. else:
  86. ax.set_title("(未指定关注段)")
  87. ax.set_xlabel("日期")
  88. ax.grid(alpha=0.25)
  89. ax.tick_params(axis="x", labelrotation=30)
  90. fig.suptitle(f"{args.unit}号机 {args.date} {args.cylinder}缸{args.side} 停机前 vs 开机后")
  91. fig.tight_layout()
  92. Path(args.out).parent.mkdir(parents=True, exist_ok=True)
  93. fig.savefig(args.out, dpi=120)
  94. print("图:", args.out)
  95. print(f"前段周期 {len({r['wave_file_id'] for r in pre})},后段周期 {len({r['wave_file_id'] for r in post})}")
  96. if hi_bins:
  97. pre_b = [r for r in pre if r["bin"] == b0]
  98. post_b = [r for r in post if r["bin"] == b0]
  99. print(f"关注段 {lo:g}-{hi:g}° {CN.metric_col(args.series_metric)}: 前中位 {np.median([r[args.series_metric] for r in pre_b]):.5g} -> 后中位 {np.median([r[args.series_metric] for r in post_b]):.5g}")
  100. if __name__ == "__main__":
  101. main()