indicator_service.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. """压缩机示功图(P-V)。
  2. 读取波库中该测点**最近一次运行**(``rpm>0``)的 ``wave_file`` 首个周期
  3. (``wave_sample_one``),按缸径/行程/连杆/余隙把曲柄转角换算为容积(L),
  4. 返回盖侧 / 轴侧两副压力功图(P-V)。
  5. 首周期本身即一个完整冲程。7/8/9 号机按
  6. ``月度报告/沁水压缩机参数20250102.docx`` 的相位角把共享键相角旋转到各缸自身
  7. 0°(显示角 = 共享角 − 相位角);4/5/6 号机暂无资料,保持原逻辑。
  8. 轴侧容积扣除活塞杆直径 78mm,盖侧用全缸径。
  9. """
  10. from __future__ import annotations
  11. import re
  12. from typing import Any
  13. import numpy as np
  14. from db import connect
  15. CYLINDERS = ("一", "二", "三", "四", "五", "六")
  16. SIDES = ("盖侧", "轴侧")
  17. CYLINDER_BORE_MM = {"一": 360.0, "二": 490.0, "三": 390.0, "四": 490.0, "五": 390.0, "六": 490.0}
  18. PISTON_STROKE_MM = 148.0
  19. CONNECTING_ROD_LENGTH_MM = 460.0
  20. PISTON_ROD_DIAMETER_MM = 78.0
  21. CLEARANCE_VOLUME_L_BY_BORE = {490.0: 1.51, 390.0: 0.74, 360.0: 0.62}
  22. # 7/8/9 号机各缸相位角(月度报告/沁水压缩机参数20250102.docx)。
  23. CYLINDER_PHASE_DEGREE = {"一": 0.0, "二": 0.0, "三": 120.0, "四": 240.0, "五": 240.0, "六": 120.0}
  24. CYLINDER_PHASE_OVERRIDE = {"二": 24.0, "三": 146.0, "四": 147.0, "五": 265.0, "六": 265.0}
  25. PHASE_ROTATION_UNITS = frozenset({"7", "8", "9"})
  26. PULSES_PER_REVOLUTION = 8
  27. MIN_SAMPLES = 60
  28. def unit_number(device_code: str) -> str | None:
  29. match = re.search(r"([789])", device_code or "")
  30. return match.group(1) if match else None
  31. def piecewise_angle(trigger: np.ndarray, count: int) -> np.ndarray:
  32. """Crank angle from the 8 keyphase pulses; falls back to uniform sampling."""
  33. high = np.nan_to_num(np.asarray(trigger, dtype=float), nan=-np.inf) >= 30.0
  34. changes = np.diff(np.r_[False, high, False].astype(np.int8))
  35. offsets = [int(offset) for offset in np.flatnonzero(changes == 1)]
  36. if len(offsets) < PULSES_PER_REVOLUTION or offsets[0] != 0:
  37. return np.linspace(0.0, 360.0, count, endpoint=False)
  38. offsets = offsets[:PULSES_PER_REVOLUTION]
  39. bounds = offsets + [count]
  40. angle = np.full(count, np.nan, dtype=float)
  41. for pulse in range(PULSES_PER_REVOLUTION):
  42. start, end = bounds[pulse], min(bounds[pulse + 1], count)
  43. if end <= start:
  44. continue
  45. angle[start:end] = np.linspace(
  46. pulse * 45.0, (pulse + 1) * 45.0, end - start, endpoint=False,
  47. )
  48. if np.isnan(angle).any():
  49. good = np.flatnonzero(~np.isnan(angle))
  50. if not len(good):
  51. return np.linspace(0.0, 360.0, count, endpoint=False)
  52. angle = np.interp(np.arange(count), good, angle[good])
  53. return angle
  54. def keyphase_angle(keyphase_info) -> float | None:
  55. if not keyphase_info:
  56. return None
  57. parts = str(keyphase_info).strip().split("-")
  58. try:
  59. return float(parts[-1])
  60. except (TypeError, ValueError):
  61. return None
  62. def phase_degrees(unit: str, cylinder: str, keyphase_info=None) -> float:
  63. if unit not in PHASE_ROTATION_UNITS:
  64. return 0.0
  65. if cylinder in CYLINDER_PHASE_OVERRIDE:
  66. return CYLINDER_PHASE_OVERRIDE[cylinder]
  67. angle = keyphase_angle(keyphase_info)
  68. if angle is not None:
  69. return angle
  70. return CYLINDER_PHASE_DEGREE[cylinder]
  71. def volume_from_angle(
  72. cylinder: str,
  73. side: str,
  74. unit: str,
  75. angle_degree: np.ndarray,
  76. ) -> np.ndarray:
  77. bore = CYLINDER_BORE_MM[cylinder]
  78. clearance = CLEARANCE_VOLUME_L_BY_BORE[bore]
  79. crank_radius = PISTON_STROKE_MM / 2.0
  80. rod_length = CONNECTING_ROD_LENGTH_MM
  81. rod_diameter = (
  82. PISTON_ROD_DIAMETER_MM
  83. if side == "轴侧" and unit in PHASE_ROTATION_UNITS
  84. else 0.0
  85. )
  86. theta = np.deg2rad(np.asarray(angle_degree, dtype=float))
  87. travel = (
  88. crank_radius * (1.0 - np.cos(theta))
  89. + rod_length
  90. - np.sqrt(rod_length**2 - (crank_radius * np.sin(theta)) ** 2)
  91. )
  92. piston_area = np.pi * ((bore / 2.0) ** 2 - (rod_diameter / 2.0) ** 2)
  93. return clearance + piston_area * travel / 1_000_000.0
  94. def _side(unit: str, cylinder: str, side: str) -> dict[str, Any] | None:
  95. point = f"{unit}号机组{cylinder}缸压力{side}"
  96. connection = connect()
  97. try:
  98. with connection.cursor() as cursor:
  99. cursor.execute(
  100. """
  101. SELECT id, sample_time, rpm, keyphase_info FROM wave_file
  102. WHERE point_name = %s AND measurement_type = '压力' AND rpm > 0
  103. ORDER BY sample_time DESC
  104. LIMIT 1
  105. """,
  106. (point,),
  107. )
  108. row = cursor.fetchone()
  109. if not row:
  110. return None
  111. file_id = int(row["id"])
  112. cursor.execute(
  113. "SELECT signal_value, second_value FROM wave_sample_one "
  114. "WHERE wave_file_id = %s ORDER BY sample_index",
  115. (file_id,),
  116. )
  117. samples = [
  118. (float(item["signal_value"]), item["second_value"])
  119. for item in cursor.fetchall()
  120. if item["signal_value"] is not None
  121. ]
  122. finally:
  123. connection.close()
  124. if len(samples) < MIN_SAMPLES:
  125. return None
  126. pressure = np.asarray([item[0] for item in samples], dtype=float)
  127. trigger = np.asarray(
  128. [float(item[1]) if item[1] is not None else np.nan for item in samples],
  129. dtype=float,
  130. )
  131. angle = piecewise_angle(trigger, len(pressure))
  132. phase = phase_degrees(unit, cylinder, row.get("keyphase_info"))
  133. if phase:
  134. angle = np.mod(angle - phase, 360.0)
  135. volume = volume_from_angle(cylinder, side, unit, angle)
  136. sample_time = row["sample_time"]
  137. return {
  138. "side": side,
  139. "point_name": point,
  140. "wave_file_id": file_id,
  141. "sample_time": sample_time.strftime("%Y-%m-%d %H:%M:%S") if sample_time else None,
  142. "rpm": float(row["rpm"]) if row["rpm"] is not None else None,
  143. "pressure": [round(float(value), 4) for value in pressure.tolist()],
  144. "volume": [round(float(value), 5) for value in volume.tolist()],
  145. "angle": [round(float(value), 2) for value in angle.tolist()],
  146. }
  147. def query_indicator(device_code: str, cylinder: str) -> dict[str, Any]:
  148. unit = unit_number(device_code)
  149. if unit is None:
  150. raise ValueError(f"不支持的机组:{device_code}")
  151. if cylinder not in CYLINDERS:
  152. raise ValueError(f"不支持的缸号:{cylinder}")
  153. sides = [item for item in (_side(unit, cylinder, side) for side in SIDES) if item]
  154. return {"device_code": device_code, "unit": unit, "cylinder": cylinder, "sides": sides}
  155. __all__ = ["CYLINDERS", "SIDES", "query_indicator"]