indicator_service.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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. UNIT_PHASE_OVERRIDE = {
  25. "7": {
  26. "一": 141.0, "二": 160.0, "三": 280.0,
  27. "四": 280.0, "五": 40.0, "六": 40.0,
  28. },
  29. "8": {
  30. "一": 8.0, "二": 23.0, "三": 138.0,
  31. "四": 145.0, "五": 262.0, "六": 262.0,
  32. },
  33. "9": {
  34. "一": 16.0, "二": 27.0, "三": 153.0,
  35. "四": 153.0, "五": 269.0, "六": 269.0,
  36. },
  37. }
  38. PHASE_ROTATION_UNITS = frozenset({"7", "8", "9"})
  39. PULSES_PER_REVOLUTION = 8
  40. MIN_SAMPLES = 60
  41. def unit_number(device_code: str) -> str | None:
  42. match = re.search(r"([789])", device_code or "")
  43. return match.group(1) if match else None
  44. def piecewise_angle(trigger: np.ndarray, count: int) -> np.ndarray:
  45. """Crank angle from the 8 keyphase pulses; falls back to uniform sampling."""
  46. high = np.nan_to_num(np.asarray(trigger, dtype=float), nan=-np.inf) >= 30.0
  47. changes = np.diff(np.r_[False, high, False].astype(np.int8))
  48. offsets = [int(offset) for offset in np.flatnonzero(changes == 1)]
  49. if len(offsets) < PULSES_PER_REVOLUTION or offsets[0] != 0:
  50. return np.linspace(0.0, 360.0, count, endpoint=False)
  51. offsets = offsets[:PULSES_PER_REVOLUTION]
  52. bounds = offsets + [count]
  53. angle = np.full(count, np.nan, dtype=float)
  54. for pulse in range(PULSES_PER_REVOLUTION):
  55. start, end = bounds[pulse], min(bounds[pulse + 1], count)
  56. if end <= start:
  57. continue
  58. angle[start:end] = np.linspace(
  59. pulse * 45.0, (pulse + 1) * 45.0, end - start, endpoint=False,
  60. )
  61. if np.isnan(angle).any():
  62. good = np.flatnonzero(~np.isnan(angle))
  63. if not len(good):
  64. return np.linspace(0.0, 360.0, count, endpoint=False)
  65. angle = np.interp(np.arange(count), good, angle[good])
  66. return angle
  67. def keyphase_angle(keyphase_info) -> float | None:
  68. if not keyphase_info:
  69. return None
  70. parts = str(keyphase_info).strip().split("-")
  71. try:
  72. return float(parts[-1])
  73. except (TypeError, ValueError):
  74. return None
  75. def phase_degrees(unit: str, cylinder: str, keyphase_info=None) -> float:
  76. if unit not in PHASE_ROTATION_UNITS:
  77. return 0.0
  78. unit_override = UNIT_PHASE_OVERRIDE.get(unit)
  79. if unit_override and cylinder in unit_override:
  80. return unit_override[cylinder]
  81. angle = keyphase_angle(keyphase_info)
  82. if angle is not None:
  83. return angle
  84. return CYLINDER_PHASE_DEGREE[cylinder]
  85. def volume_from_angle(
  86. cylinder: str,
  87. side: str,
  88. unit: str,
  89. angle_degree: np.ndarray,
  90. ) -> np.ndarray:
  91. bore = CYLINDER_BORE_MM[cylinder]
  92. clearance = CLEARANCE_VOLUME_L_BY_BORE[bore]
  93. crank_radius = PISTON_STROKE_MM / 2.0
  94. rod_length = CONNECTING_ROD_LENGTH_MM
  95. rod_diameter = (
  96. PISTON_ROD_DIAMETER_MM
  97. if side == "轴侧" and unit in PHASE_ROTATION_UNITS
  98. else 0.0
  99. )
  100. theta = np.deg2rad(np.asarray(angle_degree, dtype=float))
  101. travel = (
  102. crank_radius * (1.0 - np.cos(theta))
  103. + rod_length
  104. - np.sqrt(rod_length**2 - (crank_radius * np.sin(theta)) ** 2)
  105. )
  106. piston_area = np.pi * ((bore / 2.0) ** 2 - (rod_diameter / 2.0) ** 2)
  107. return clearance + piston_area * travel / 1_000_000.0
  108. def _side(unit: str, cylinder: str, side: str) -> dict[str, Any] | None:
  109. point = f"{unit}号机组{cylinder}缸压力{side}"
  110. connection = connect()
  111. try:
  112. with connection.cursor() as cursor:
  113. cursor.execute(
  114. """
  115. SELECT id, sample_time, rpm, keyphase_info FROM wave_file
  116. WHERE point_name = %s AND measurement_type = '压力' AND rpm > 0
  117. ORDER BY sample_time DESC
  118. LIMIT 1
  119. """,
  120. (point,),
  121. )
  122. row = cursor.fetchone()
  123. if not row:
  124. return None
  125. file_id = int(row["id"])
  126. cursor.execute(
  127. "SELECT signal_value, second_value FROM wave_sample_one "
  128. "WHERE wave_file_id = %s ORDER BY sample_index",
  129. (file_id,),
  130. )
  131. samples = [
  132. (float(item["signal_value"]), item["second_value"])
  133. for item in cursor.fetchall()
  134. if item["signal_value"] is not None
  135. ]
  136. finally:
  137. connection.close()
  138. if len(samples) < MIN_SAMPLES:
  139. return None
  140. pressure = np.asarray([item[0] for item in samples], dtype=float)
  141. trigger = np.asarray(
  142. [float(item[1]) if item[1] is not None else np.nan for item in samples],
  143. dtype=float,
  144. )
  145. angle = piecewise_angle(trigger, len(pressure))
  146. phase = phase_degrees(unit, cylinder, row.get("keyphase_info"))
  147. if phase:
  148. angle = np.mod(angle - phase, 360.0)
  149. volume = volume_from_angle(cylinder, side, unit, angle)
  150. sample_time = row["sample_time"]
  151. return {
  152. "side": side,
  153. "point_name": point,
  154. "wave_file_id": file_id,
  155. "sample_time": sample_time.strftime("%Y-%m-%d %H:%M:%S") if sample_time else None,
  156. "rpm": float(row["rpm"]) if row["rpm"] is not None else None,
  157. "pressure": [round(float(value), 4) for value in pressure.tolist()],
  158. "volume": [round(float(value), 5) for value in volume.tolist()],
  159. "angle": [round(float(value), 2) for value in angle.tolist()],
  160. }
  161. def query_indicator(device_code: str, cylinder: str) -> dict[str, Any]:
  162. unit = unit_number(device_code)
  163. if unit is None:
  164. raise ValueError(f"不支持的机组:{device_code}")
  165. if cylinder not in CYLINDERS:
  166. raise ValueError(f"不支持的缸号:{cylinder}")
  167. sides = [item for item in (_side(unit, cylinder, side) for side in SIDES) if item]
  168. return {"device_code": device_code, "unit": unit, "cylinder": cylinder, "sides": sides}
  169. __all__ = ["CYLINDERS", "SIDES", "query_indicator"]