indicator_service.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. """压缩机示功图(P-V)。
  2. 读取波库中该测点**最近一次运行**(``rpm>0``)的 ``wave_file`` 首个周期
  3. (``wave_sample_one``),按缸径/行程/连杆/余隙把曲柄转角换算为容积(L),
  4. 返回盖侧 / 轴侧两副压力功图(P-V)。
  5. 首周期本身即一个完整冲程,样本按时间顺序对应曲柄角 0→360°,直接成闭环,
  6. 无需额外相位旋转(与 backend/app/services/data_service.py 的示功图一致)。
  7. """
  8. from __future__ import annotations
  9. import re
  10. from typing import Any
  11. import numpy as np
  12. from db import connect
  13. CYLINDERS = ("一", "二", "三", "四", "五", "六")
  14. SIDES = ("盖侧", "轴侧")
  15. CYLINDER_BORE_MM = {"一": 360.0, "二": 490.0, "三": 390.0, "四": 490.0, "五": 390.0, "六": 490.0}
  16. PISTON_STROKE_MM = 148.0
  17. CONNECTING_ROD_LENGTH_MM = 460.0
  18. CLEARANCE_VOLUME_L_BY_BORE = {490.0: 1.51, 390.0: 0.74, 360.0: 0.62}
  19. MIN_SAMPLES = 60
  20. def unit_number(device_code: str) -> str | None:
  21. match = re.search(r"([789])", device_code or "")
  22. return match.group(1) if match else None
  23. def volume_from_angle(cylinder: str, count: int) -> np.ndarray:
  24. bore = CYLINDER_BORE_MM[cylinder]
  25. clearance = CLEARANCE_VOLUME_L_BY_BORE[bore]
  26. crank_radius = PISTON_STROKE_MM / 2.0
  27. rod = CONNECTING_ROD_LENGTH_MM
  28. theta = np.linspace(0.0, 2.0 * np.pi, count, endpoint=False)
  29. travel = (
  30. crank_radius * (1.0 - np.cos(theta))
  31. + rod
  32. - np.sqrt(rod**2 - (crank_radius * np.sin(theta)) ** 2)
  33. )
  34. piston_area = np.pi * (bore / 2.0) ** 2
  35. return clearance + piston_area * travel / 1_000_000.0
  36. def _side(unit: str, cylinder: str, side: str) -> dict[str, Any] | None:
  37. point = f"{unit}号机组{cylinder}缸压力{side}"
  38. connection = connect()
  39. try:
  40. with connection.cursor() as cursor:
  41. cursor.execute(
  42. """
  43. SELECT id, sample_time, rpm FROM wave_file
  44. WHERE point_name = %s AND measurement_type = '压力' AND rpm > 0
  45. ORDER BY sample_time DESC
  46. LIMIT 1
  47. """,
  48. (point,),
  49. )
  50. row = cursor.fetchone()
  51. if not row:
  52. return None
  53. file_id = int(row["id"])
  54. cursor.execute(
  55. "SELECT signal_value FROM wave_sample_one WHERE wave_file_id = %s ORDER BY sample_index",
  56. (file_id,),
  57. )
  58. values = [
  59. float(item["signal_value"])
  60. for item in cursor.fetchall()
  61. if item["signal_value"] is not None
  62. ]
  63. finally:
  64. connection.close()
  65. if len(values) < MIN_SAMPLES:
  66. return None
  67. pressure = np.asarray(values, dtype=float)
  68. volume = volume_from_angle(cylinder, len(pressure))
  69. angle = np.linspace(0.0, 360.0, len(pressure), endpoint=False)
  70. sample_time = row["sample_time"]
  71. return {
  72. "side": side,
  73. "point_name": point,
  74. "wave_file_id": file_id,
  75. "sample_time": sample_time.strftime("%Y-%m-%d %H:%M:%S") if sample_time else None,
  76. "rpm": float(row["rpm"]) if row["rpm"] is not None else None,
  77. "pressure": [round(float(value), 4) for value in pressure.tolist()],
  78. "volume": [round(float(value), 5) for value in volume.tolist()],
  79. "angle": [round(float(value), 2) for value in angle.tolist()],
  80. }
  81. def query_indicator(device_code: str, cylinder: str) -> dict[str, Any]:
  82. unit = unit_number(device_code)
  83. if unit is None:
  84. raise ValueError(f"不支持的机组:{device_code}")
  85. if cylinder not in CYLINDERS:
  86. raise ValueError(f"不支持的缸号:{cylinder}")
  87. sides = [item for item in (_side(unit, cylinder, side) for side in SIDES) if item]
  88. return {"device_code": device_code, "unit": unit, "cylinder": cylinder, "sides": sides}
  89. __all__ = ["CYLINDERS", "SIDES", "query_indicator"]