|
|
@@ -0,0 +1,125 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+"""Compare indicator features between a pre-repair and a post-repair segment.
|
|
|
+
|
|
|
+For each cylinder/side it reports the segment medians and, importantly, the
|
|
|
+*peer-relative* value (deviation from the same-stage/side median), so a repair
|
|
|
+that only changes one cylinder can be separated from a process-wide shift.
|
|
|
+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, cycle_features # noqa: E402
|
|
|
+
|
|
|
+FEATURES = ("p_discharge", "pressure_ratio", "discharge_frac", "indicated_work", "efficiency", "n_comp")
|
|
|
+
|
|
|
+
|
|
|
+def extract(connection, unit, cylinders, sides, start, end, per_day):
|
|
|
+ rows = []
|
|
|
+ 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_to_time = {int(r["id"]): r for r in picked}
|
|
|
+ for file_id, signal in fetch_cycles(connection, list(id_to_time), 200).items():
|
|
|
+ feats = cycle_features(signal, cylinder)
|
|
|
+ if feats:
|
|
|
+ rows.append({"cylinder": cylinder, "side": side, "stage": CYLINDER_STAGE[cylinder], **feats})
|
|
|
+ return rows
|
|
|
+
|
|
|
+
|
|
|
+def summary(rows):
|
|
|
+ table = {}
|
|
|
+ groups = defaultdict(list)
|
|
|
+ for row in rows:
|
|
|
+ groups[(row["cylinder"], row["side"])].append(row)
|
|
|
+ for key, items in groups.items():
|
|
|
+ table[key] = {feature: float(np.median([r[feature] for r in items if r[feature] is not None]))
|
|
|
+ for feature in FEATURES}
|
|
|
+ return table
|
|
|
+
|
|
|
+
|
|
|
+def peer_relative(table, feature):
|
|
|
+ """Deviation from the same-stage/side median, per cylinder."""
|
|
|
+ by_stage = defaultdict(dict)
|
|
|
+ for (cylinder, side), values in table.items():
|
|
|
+ stage = CYLINDER_STAGE[cylinder]
|
|
|
+ by_stage[(stage, side)][cylinder] = values[feature]
|
|
|
+ result = {}
|
|
|
+ for (stage, side), members in by_stage.items():
|
|
|
+ clean = {c: v for c, v in members.items() if v is not None}
|
|
|
+ if len(clean) < 2:
|
|
|
+ continue
|
|
|
+ med = float(np.median(list(clean.values())))
|
|
|
+ if abs(med) < 1e-9:
|
|
|
+ continue
|
|
|
+ for cylinder, value in clean.items():
|
|
|
+ result[(cylinder, side)] = (value - med) / med
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
+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, metavar=("START", "END"))
|
|
|
+ parser.add_argument("--post", nargs=2, required=True, metavar=("START", "END"))
|
|
|
+ parser.add_argument("--per-day", type=int, default=5)
|
|
|
+ args = parser.parse_args()
|
|
|
+
|
|
|
+ connection = connect()
|
|
|
+ try:
|
|
|
+ pre_rows = extract(connection, args.unit, args.cylinders, args.sides, args.pre[0], args.pre[1], args.per_day)
|
|
|
+ post_rows = extract(connection, args.unit, args.cylinders, args.sides, args.post[0], args.post[1], args.per_day)
|
|
|
+ finally:
|
|
|
+ connection.close()
|
|
|
+
|
|
|
+ pre, post = summary(pre_rows), summary(post_rows)
|
|
|
+ print(f"{args.unit}号机 修前 {args.pre[0]}~{args.pre[1]} ({len(pre_rows)}行) 修后 {args.post[0]}~{args.post[1]} ({len(post_rows)}行)")
|
|
|
+ if not pre_rows or not post_rows:
|
|
|
+ print("某一段无数据"); return
|
|
|
+
|
|
|
+ print(f"\n{'缸侧':>6} " + ' '.join(f"{f[:11]:>18}" for f in FEATURES))
|
|
|
+ for key in sorted(set(pre) & set(post)):
|
|
|
+ cells = []
|
|
|
+ for feature in FEATURES:
|
|
|
+ a, b = pre[key].get(feature), post[key].get(feature)
|
|
|
+ if a and b and abs(a) > 1e-9:
|
|
|
+ cells.append(f"{a:7.3f}->{b:6.3f}({(b-a)/a*100:+5.1f}%)")
|
|
|
+ else:
|
|
|
+ cells.append(f"{'':>18}")
|
|
|
+ print(f"{key[0]+key[1]:>6} " + ' '.join(f"{c:>18}" for c in cells))
|
|
|
+
|
|
|
+ print("\n同级 peer 相对(修前 -> 修后):")
|
|
|
+ for feature in ("pressure_ratio", "p_discharge", "efficiency"):
|
|
|
+ rel_pre = peer_relative(pre, feature)
|
|
|
+ rel_post = peer_relative(post, feature)
|
|
|
+ print(f" {feature}:")
|
|
|
+ for key in sorted(set(rel_pre) & set(rel_post)):
|
|
|
+ print(f" {key[0]}缸{key[1]}: {rel_pre[key]*100:+.1f}% -> {rel_post[key]*100:+.1f}%")
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|