repair_compare.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. #!/usr/bin/env python3
  2. """Compare indicator features between a pre-repair and a post-repair segment.
  3. For each cylinder/side it reports the segment medians and, importantly, the
  4. *peer-relative* value (deviation from the same-stage/side median), so a repair
  5. that only changes one cylinder can be separated from a process-wide shift.
  6. Read-only.
  7. """
  8. from __future__ import annotations
  9. import argparse
  10. import sys
  11. from collections import defaultdict
  12. from pathlib import Path
  13. import numpy as np
  14. BACKEND = Path(__file__).resolve().parents[1]
  15. sys.path.insert(0, str(BACKEND))
  16. from indicator_features.extract_from_db import ( # noqa: E402
  17. CYLINDERS,
  18. SIDES,
  19. connect,
  20. fetch_cycles,
  21. fetch_file_rows,
  22. point_name,
  23. sample_per_day,
  24. )
  25. from indicator_features.indicator_features import CYLINDER_STAGE, cycle_features # noqa: E402
  26. FEATURES = ("p_discharge", "pressure_ratio", "discharge_frac", "indicated_work", "efficiency", "n_comp")
  27. def extract(connection, unit, cylinders, sides, start, end, per_day):
  28. rows = []
  29. for cylinder in cylinders:
  30. for side in sides:
  31. point = point_name(unit, cylinder, side)
  32. files = fetch_file_rows(connection, point, start, end)
  33. picked = sample_per_day(files, per_day)
  34. if not picked:
  35. continue
  36. id_to_time = {int(r["id"]): r for r in picked}
  37. for file_id, signal in fetch_cycles(connection, list(id_to_time), 200).items():
  38. feats = cycle_features(signal, cylinder)
  39. if feats:
  40. rows.append({"cylinder": cylinder, "side": side, "stage": CYLINDER_STAGE[cylinder], **feats})
  41. return rows
  42. def summary(rows):
  43. table = {}
  44. groups = defaultdict(list)
  45. for row in rows:
  46. groups[(row["cylinder"], row["side"])].append(row)
  47. for key, items in groups.items():
  48. table[key] = {feature: float(np.median([r[feature] for r in items if r[feature] is not None]))
  49. for feature in FEATURES}
  50. return table
  51. def peer_relative(table, feature):
  52. """Deviation from the same-stage/side median, per cylinder."""
  53. by_stage = defaultdict(dict)
  54. for (cylinder, side), values in table.items():
  55. stage = CYLINDER_STAGE[cylinder]
  56. by_stage[(stage, side)][cylinder] = values[feature]
  57. result = {}
  58. for (stage, side), members in by_stage.items():
  59. clean = {c: v for c, v in members.items() if v is not None}
  60. if len(clean) < 2:
  61. continue
  62. med = float(np.median(list(clean.values())))
  63. if abs(med) < 1e-9:
  64. continue
  65. for cylinder, value in clean.items():
  66. result[(cylinder, side)] = (value - med) / med
  67. return result
  68. def main():
  69. parser = argparse.ArgumentParser(description="修前/修后功图特征同级对照")
  70. parser.add_argument("--unit", required=True, choices=("7", "8", "9"))
  71. parser.add_argument("--cylinders", nargs="+", default=list(CYLINDERS), choices=CYLINDERS)
  72. parser.add_argument("--sides", nargs="+", default=list(SIDES), choices=SIDES)
  73. parser.add_argument("--pre", nargs=2, required=True, metavar=("START", "END"))
  74. parser.add_argument("--post", nargs=2, required=True, metavar=("START", "END"))
  75. parser.add_argument("--per-day", type=int, default=5)
  76. args = parser.parse_args()
  77. connection = connect()
  78. try:
  79. pre_rows = extract(connection, args.unit, args.cylinders, args.sides, args.pre[0], args.pre[1], args.per_day)
  80. post_rows = extract(connection, args.unit, args.cylinders, args.sides, args.post[0], args.post[1], args.per_day)
  81. finally:
  82. connection.close()
  83. pre, post = summary(pre_rows), summary(post_rows)
  84. print(f"{args.unit}号机 修前 {args.pre[0]}~{args.pre[1]} ({len(pre_rows)}行) 修后 {args.post[0]}~{args.post[1]} ({len(post_rows)}行)")
  85. if not pre_rows or not post_rows:
  86. print("某一段无数据"); return
  87. print(f"\n{'缸侧':>6} " + ' '.join(f"{f[:11]:>18}" for f in FEATURES))
  88. for key in sorted(set(pre) & set(post)):
  89. cells = []
  90. for feature in FEATURES:
  91. a, b = pre[key].get(feature), post[key].get(feature)
  92. if a and b and abs(a) > 1e-9:
  93. cells.append(f"{a:7.3f}->{b:6.3f}({(b-a)/a*100:+5.1f}%)")
  94. else:
  95. cells.append(f"{'':>18}")
  96. print(f"{key[0]+key[1]:>6} " + ' '.join(f"{c:>18}" for c in cells))
  97. print("\n同级 peer 相对(修前 -> 修后):")
  98. for feature in ("pressure_ratio", "p_discharge", "efficiency"):
  99. rel_pre = peer_relative(pre, feature)
  100. rel_post = peer_relative(post, feature)
  101. print(f" {feature}:")
  102. for key in sorted(set(rel_pre) & set(rel_post)):
  103. print(f" {key[0]}缸{key[1]}: {rel_pre[key]*100:+.1f}% -> {rel_post[key]*100:+.1f}%")
  104. if __name__ == "__main__":
  105. main()