new_unit_events.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. """Sample and rank 4/6 unit valve events without scanning wave_sample_one."""
  2. from __future__ import annotations
  3. from collections import defaultdict
  4. from datetime import datetime, timedelta
  5. import numpy as np
  6. from extract_from_db import fetch_cycles, fetch_file_rows, point_name, sample_per_day
  7. from indicator_features.indicator_features import CYLINDER_BORE_MM, _plateau_levels, smooth, volume_from_angle
  8. CYLINDERS = ("一", "二", "三", "四", "五", "六")
  9. SIDES = ("盖侧", "轴侧")
  10. BAD = {
  11. "4": {("二", "盖侧"), ("四", "盖侧"), ("六", "盖侧")},
  12. "6": {("二", "盖侧"), ("四", "盖侧"), ("六", "盖侧")},
  13. }
  14. EVENTS = [
  15. {
  16. "key": "4号 2026-06-10 气阀事件",
  17. "unit": "4", "center": "2026-06-10", "pre_end": "2026-06-10", "post_start": "2026-06-10",
  18. "start": "2026-05-11", "end": "2026-07-11", "markers": ["2026-06-10"],
  19. },
  20. {
  21. "key": "6号 2026-04-22~04-26 气阀维修过程",
  22. "unit": "6", "center": "2026-04-24", "pre_end": "2026-04-22", "post_start": "2026-04-26",
  23. "start": "2026-03-23", "end": "2026-05-27", "markers": ["2026-04-22", "2026-04-26"],
  24. },
  25. ]
  26. def _median(items, key):
  27. return np.median([item[key] for item in items], axis=0).round(6).tolist() if items else []
  28. def collect_events(connection, per_day=6, bins=90):
  29. result = {}
  30. for config in EVENTS:
  31. center = datetime.fromisoformat(config["center"])
  32. pre_end = datetime.fromisoformat(config["pre_end"])
  33. post_start = datetime.fromisoformat(config["post_start"])
  34. points = {}
  35. for cylinder in CYLINDERS:
  36. for side in SIDES:
  37. if (cylinder, side) in BAD[config["unit"]]:
  38. continue
  39. files = fetch_file_rows(
  40. connection, point_name(config["unit"], cylinder, side), config["start"], config["end"]
  41. )
  42. picked = sample_per_day(files, per_day)
  43. cycles = fetch_cycles(connection, [int(row["id"]) for row in picked], 100) if picked else {}
  44. samples = []
  45. for row in picked:
  46. signal = cycles.get(int(row["id"]))
  47. if signal is None or len(signal) < 60 or not np.all(np.isfinite(signal)):
  48. continue
  49. pressure = smooth(signal, 21)
  50. suction, discharge, _ = _plateau_levels(pressure)
  51. span = discharge - suction
  52. if not np.isfinite(span) or span < .3:
  53. continue
  54. indices = np.arange(bins) * len(pressure) / bins
  55. absolute = np.interp(indices, np.arange(len(pressure)), pressure)
  56. profile = (absolute - suction) / span
  57. volume = np.interp(
  58. indices, np.arange(len(pressure)),
  59. volume_from_angle(len(pressure), CYLINDER_BORE_MM[cylinder]),
  60. )
  61. time = row["sample_time"]
  62. if not isinstance(time, datetime):
  63. time = datetime.fromisoformat(str(time))
  64. if time < pre_end:
  65. phase = "修前"
  66. elif time >= post_start:
  67. phase = "修后"
  68. else:
  69. phase = "过程内"
  70. samples.append({
  71. "date": time.strftime("%Y-%m-%d %H:%M:%S"),
  72. "day": (time.date() - center.date()).days,
  73. "phase": phase,
  74. "profile": profile.round(6).tolist(),
  75. "absolute": absolute.round(6).tolist(),
  76. "volume": volume.round(6).tolist(),
  77. })
  78. samples.sort(key=lambda sample: sample["date"])
  79. pre = [sample for sample in samples if sample["phase"] == "修前"]
  80. post = [sample for sample in samples if sample["phase"] == "修后"]
  81. if not pre or not post:
  82. continue
  83. before = np.median([sample["profile"] for sample in pre], axis=0)
  84. after = np.median([sample["profile"] for sample in post], axis=0)
  85. points[(cylinder, side)] = {
  86. "samples": samples, "pre": pre, "post": post,
  87. "before": before, "after": after, "delta": after - before,
  88. }
  89. common = np.median([point["delta"] for point in points.values()], axis=0)
  90. cylinder_scores = defaultdict(list)
  91. for (cylinder, _), point in points.items():
  92. point["total"] = float(np.linalg.norm(point["delta"]))
  93. point["residual"] = float(np.linalg.norm(point["delta"] - common))
  94. cylinder_scores[cylinder].append(point["residual"])
  95. ranking = sorted(
  96. ((cylinder, max(scores)) for cylinder, scores in cylinder_scores.items()),
  97. key=lambda item: item[1], reverse=True,
  98. )
  99. event = {
  100. "label": config["key"], "unit": config["unit"], "markers": config["markers"],
  101. "ranking": [{"cylinder": cylinder, "residual": round(score, 5)} for cylinder, score in ranking],
  102. "sides": {},
  103. }
  104. for (cylinder, side), point in points.items():
  105. template = point["before"]
  106. for sample in point["samples"]:
  107. sample["distance"] = round(float(np.linalg.norm(np.asarray(sample["profile"]) - template)), 5)
  108. name = f"{cylinder}缸{side}"
  109. event["sides"][name] = {
  110. "samples": point["samples"],
  111. "counts": {"pre": len(point["pre"]), "post": len(point["post"])},
  112. "preMedian": point["before"].round(6).tolist(),
  113. "postMedian": point["after"].round(6).tolist(),
  114. "preAbsoluteMedian": _median(point["pre"], "absolute"),
  115. "postAbsoluteMedian": _median(point["post"], "absolute"),
  116. "trend": [{"date": sample["date"], "day": sample["day"], "distance": sample["distance"]}
  117. for sample in point["samples"]],
  118. "totalChange": round(point["total"], 5),
  119. "localResidual": round(point["residual"], 5),
  120. "markerDays": [
  121. (datetime.fromisoformat(marker).date() - center.date()).days
  122. for marker in config["markers"]
  123. ],
  124. }
  125. coverage = sorted({sample["date"][:10] for point in event["sides"].values() for sample in point["samples"]})
  126. event["label"] += f" · 实际功图 {coverage[0]} 至 {coverage[-1]}"
  127. result[config["key"]] = event
  128. return result
  129. def top_cylinder_event(event, limit=3):
  130. cylinders = {item["cylinder"] for item in event["ranking"][:limit]}
  131. selected = dict(event)
  132. selected["sides"] = {
  133. name: point for name, point in event["sides"].items()
  134. if name[0] in cylinders
  135. }
  136. return selected