indicator_service.py 6.1 KB

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