config.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. """Unit / point configuration for the PKS cylinder-temperature scan.
  2. Only cylinder-body temperature points are configured here (exhaust temperature,
  3. and optionally packing temperature). The stage/cylinder mapping is the one
  4. confirmed by the plant:
  5. * 4#/5#/6#: two stages; cylinders 1/2/3 -> stage 1, cylinders 4/5/6 -> stage 2.
  6. The point names use stage-local numbering (``一级1#`` / ``二级1#``).
  7. * 7#/8#/9#: three stages; cylinders 2/4/6 -> stage 1, 3/5 -> stage 2, 1 -> stage 3.
  8. Run state:
  9. * 7#/8#/9# expose speed in ``YSJ_41`` (``>0`` means running).
  10. * 4#/5#/6# have no speed signal and no run-status table, so a data-derived
  11. proxy is used: motor/body vibration or first-stage suction pressure.
  12. """
  13. from __future__ import annotations
  14. from dataclasses import dataclass, field
  15. from models.common.db import connect, number
  16. # --- local constants (kept here so models/common stays untouched) ---
  17. EXPECTED_PER_HOUR = 720
  18. TEMP_BASELINE_HOURS = 168
  19. TEMP_MIN_HISTORY_HOURS = 24
  20. TEMP_CONFIRM_HOURS = 3
  21. TEMP_MERGE_GAP_HOURS = 6
  22. TEMP_RESTART_GAP_HOURS = 12
  23. TEMP_MIN_HOUR_COVERAGE = 0.80
  24. TEMP_SENSOR_MIN = -30.0
  25. TEMP_SENSOR_MAX = 350.0
  26. TEMP_MIN_STEADY = 45.0
  27. TEMP_DROPOUT_REF = 60.0
  28. # ``(stage, cylinder, column)`` for each unit's cylinder exhaust temperatures.
  29. CYLINDERS: dict[int, list[tuple[str, str, str]]] = {
  30. 4: [
  31. ("一级", "1#", "YSJ_9"),
  32. ("一级", "2#", "YSJ_10"),
  33. ("一级", "3#", "YSJ_11"),
  34. ("二级", "1#", "YSJ_13"),
  35. ("二级", "2#", "YSJ_14"),
  36. ("二级", "3#", "YSJ_15"),
  37. ],
  38. 5: [
  39. ("一级", "1#", "YSJ_18"),
  40. ("一级", "2#", "YSJ_20"),
  41. ("一级", "3#", "YSJ_22"),
  42. ("二级", "1#", "YSJ_17"),
  43. ("二级", "2#", "YSJ_19"),
  44. ("二级", "3#", "YSJ_21"),
  45. ],
  46. 6: [
  47. ("一级", "1#", "YSJ_18"),
  48. ("一级", "2#", "YSJ_20"),
  49. ("一级", "3#", "YSJ_22"),
  50. ("二级", "1#", "YSJ_17"),
  51. ("二级", "2#", "YSJ_19"),
  52. ("二级", "3#", "YSJ_21"),
  53. ],
  54. 7: [
  55. ("一级", "2列", "YSJ_16"),
  56. ("一级", "4列", "YSJ_17"),
  57. ("一级", "6列", "YSJ_18"),
  58. ("二级", "3列", "YSJ_19"),
  59. ("二级", "5列", "YSJ_20"),
  60. ("三级", "1列", "YSJ_21"),
  61. ],
  62. 8: [
  63. ("一级", "2列", "YSJ_16"),
  64. ("一级", "4列", "YSJ_17"),
  65. ("一级", "6列", "YSJ_18"),
  66. ("二级", "3列", "YSJ_19"),
  67. ("二级", "5列", "YSJ_20"),
  68. ("三级", "1列", "YSJ_21"),
  69. ],
  70. 9: [
  71. ("一级", "2列", "YSJ_16"),
  72. ("一级", "4列", "YSJ_17"),
  73. ("一级", "6列", "YSJ_18"),
  74. ("二级", "3列", "YSJ_19"),
  75. ("二级", "5列", "YSJ_20"),
  76. ("三级", "1列", "YSJ_21"),
  77. ],
  78. }
  79. STAGE_ORDER: dict[int, list[str]] = {
  80. 4: ["一级", "二级"],
  81. 5: ["一级", "二级"],
  82. 6: ["一级", "二级"],
  83. 7: ["一级", "二级", "三级"],
  84. 8: ["一级", "二级", "三级"],
  85. 9: ["一级", "二级", "三级"],
  86. }
  87. # Hourly run-state specification.
  88. # rpm -> ``YSJ_41 > 0``
  89. # proxy -> any of the listed columns above ``threshold``
  90. RUN_SPEC: dict[int, dict] = {
  91. # 4# has a cumulative running-hour counter: +1 per running hour, flat when
  92. # stopped. Body vibration / inlet pressure are the fallback if the counter
  93. # is missing or resets.
  94. 4: {
  95. "type": "cumulative",
  96. "column": "YSJ_31",
  97. "sql": "(IFNULL(YSJ_37,0) > 0.5 OR IFNULL(YSJ_38,0) > 0.5 OR IFNULL(YSJ_2,0) > 0.5)",
  98. },
  99. # 5#/6# have no speed or cumulative counter. Use 2-of-4:
  100. # discharge pressure, inlet pressure, body vibration, valve position.
  101. 5: {
  102. "type": "multi",
  103. "sql": "((IFNULL(YSJ_5,0) > 0.5) + (IFNULL(YSJ_3,0) > 0.3) + "
  104. "((IFNULL(YSJ_10,0) > 0.5) OR (IFNULL(YSJ_11,0) > 0.5) OR "
  105. "(IFNULL(YSJ_12,0) > 0.5) OR (IFNULL(YSJ_13,0) > 0.5)) + "
  106. "(IFNULL(YSJ_14,100) < 90)) >= 2",
  107. },
  108. 6: {
  109. "type": "multi",
  110. "sql": "((IFNULL(YSJ_5,0) > 0.5) + (IFNULL(YSJ_3,0) > 0.3) + "
  111. "((IFNULL(YSJ_10,0) > 0.5) OR (IFNULL(YSJ_11,0) > 0.5) OR "
  112. "(IFNULL(YSJ_12,0) > 0.5) OR (IFNULL(YSJ_13,0) > 0.5)) + "
  113. "(IFNULL(YSJ_14,100) < 90)) >= 2",
  114. },
  115. 7: {"type": "rpm", "column": "YSJ_41"},
  116. 8: {"type": "rpm", "column": "YSJ_41"},
  117. 9: {"type": "rpm", "column": "YSJ_41"},
  118. }
  119. # Optional packing-temperature groups for 5#/6# (cylinders 1..6 -> stage 1/2).
  120. PACKING: dict[int, list[tuple[str, str, str]]] = {
  121. 5: [
  122. ("一级", "1#", "YSJ_39"),
  123. ("一级", "2#", "YSJ_40"),
  124. ("一级", "3#", "YSJ_41"),
  125. ("二级", "4#", "YSJ_42"),
  126. ("二级", "5#", "YSJ_43"),
  127. ("二级", "6#", "YSJ_44"),
  128. ],
  129. 6: [
  130. ("一级", "1#", "YSJ_39"),
  131. ("一级", "2#", "YSJ_40"),
  132. ("一级", "3#", "YSJ_41"),
  133. ("二级", "4#", "YSJ_42"),
  134. ("二级", "5#", "YSJ_43"),
  135. ("二级", "6#", "YSJ_44"),
  136. ],
  137. }
  138. DEFAULT_THRESHOLDS: dict[str, float] = {
  139. # Same-stage spread change versus its own recent normal spread.
  140. "mild_spread": 4.0,
  141. "abnormal_spread": 8.0,
  142. "severe_spread": 15.0,
  143. # Cylinder deviation from stage median, versus its own recent normal offset.
  144. "mild_peer": 4.0,
  145. "abnormal_peer": 6.0,
  146. "severe_peer": 10.0,
  147. # Low-side peer deviation (a cylinder falling relative to its stage peers).
  148. "mild_peer_low": 5.0,
  149. "abnormal_peer_low": 8.0,
  150. "severe_peer_low": 12.0,
  151. # Own-baseline robust z (reported; used only for single-cylinder stages).
  152. "mild_z": 3.0,
  153. "abnormal_z": 5.0,
  154. "severe_z": 8.0,
  155. # Own-baseline absolute rise (degC).
  156. "mild_delta": 5.0,
  157. "abnormal_delta": 10.0,
  158. "severe_delta": 15.0,
  159. # Short-term same-stage relative change (degC over 1/3/6h).
  160. "observe_rate1": 3.0, "mild_rate1": 4.0, "abnormal_rate1": 6.0, "severe_rate1": 9.0,
  161. "observe_rate3": 4.0, "mild_rate3": 5.0, "abnormal_rate3": 7.0, "severe_rate3": 11.0,
  162. "observe_rate6": 6.0, "mild_rate6": 7.0, "abnormal_rate6": 9.0, "severe_rate6": 13.0,
  163. # Intra-hour high-frequency fluctuation: std relative to its normal baseline.
  164. "hf_ratio_mild": 2.5, "hf_ratio_abnormal": 3.5, "hf_ratio_severe": 5.0,
  165. "hf_z_mild": 6.0, "hf_z_abnormal": 9.0, "hf_z_severe": 12.0,
  166. "hf_min_std": 0.8, "hf_base_floor": 0.3,
  167. "hf_persist_hours": 3,
  168. # 12h relative rise (single-cylinder and stage-level), integrated into events.
  169. "rise_single_mild": 3.0, "rise_single_abnormal": 4.5, "rise_single_severe": 6.0,
  170. "rise_stage_mild": 3.0, "rise_stage_abnormal": 5.0, "rise_stage_severe": 7.0,
  171. "rise_stage_flat_limit": 3.0,
  172. "rise_persist_hours": 2, "rise_baseline_hours": 6, "rise_min_history_hours": 4,
  173. # Channel / shutdown.
  174. "channel_ambient": 45.0, "channel_drop_delta": 8.0,
  175. "channel_min_drop": 20.0, "channel_zero_min": 5,
  176. "channel_recover_hours": 3, "channel_frozen_hours": 12,
  177. "channel_frozen_eps": 0.1, "channel_peer_variation": 3.0,
  178. # Persistence.
  179. "observe_hours": 1, "event_hours": 2, "confirm_hours": 3,
  180. "merge_gap_hours": 6,
  181. }
  182. def _number(value):
  183. return number(value)
  184. def normalize_devices(values) -> list[str]:
  185. """Accept 4#~9# / 4 / 4号机 and return ``['4#', ...]``."""
  186. result = []
  187. for value in values:
  188. digits = "".join(ch for ch in str(value) if ch.isdigit())
  189. if not digits or not 4 <= int(digits) <= 9:
  190. raise ValueError(f"无法识别的压缩机编号:{value!r}(应为 4#~9#)")
  191. device = f"{int(digits)}#"
  192. if device not in result:
  193. result.append(device)
  194. return result
  195. def load_device_batches(device: str) -> list[int]:
  196. """Resolve a device code to its successful PKS import batches."""
  197. connection = connect()
  198. try:
  199. with connection.cursor() as cursor:
  200. cursor.execute(
  201. "SELECT id FROM compressor_data_import "
  202. "WHERE data_type = 'PKS' AND directory_path = %s "
  203. "AND is_imported = 1 AND execution_status = '成功' ORDER BY id",
  204. (device,),
  205. )
  206. batches = [int(row["id"]) for row in cursor.fetchall()]
  207. finally:
  208. connection.close()
  209. if not batches:
  210. raise RuntimeError(f"{device} 没有成功的 PKS 导入批次")
  211. return batches
  212. def _parse_limits(row: dict) -> tuple[float | None, float | None]:
  213. high = high_high = None
  214. for index in range(1, 5):
  215. kind = str(row.get(f"AlarmType{index}") or "").strip()
  216. limit = _number(row.get(f"AlarmLimit{index}"))
  217. if not kind or limit is None:
  218. continue
  219. if "HighHigh" in kind:
  220. high_high = limit if high_high is None else max(high_high, limit)
  221. elif "High" in kind and high is None:
  222. high = limit
  223. if high is None and high_high is not None:
  224. high = high_high
  225. return high, high_high
  226. def load_point_limits(unit: int, columns: list[str]) -> dict[str, dict]:
  227. """Return ``{column: {description, units, high, high_high}}`` from site_point."""
  228. connection = connect()
  229. try:
  230. with connection.cursor() as cursor:
  231. cursor.execute(
  232. "SELECT ItemName, ItemDescription, Units, "
  233. "AlarmType1, AlarmType2, AlarmType3, AlarmType4, "
  234. "AlarmLimit1, AlarmLimit2, AlarmLimit3, AlarmLimit4 "
  235. "FROM site_point WHERE ItemName LIKE %s",
  236. (f"YSJ{unit}\\_%",),
  237. )
  238. rows = {row["ItemName"]: row for row in cursor.fetchall()}
  239. finally:
  240. connection.close()
  241. result = {}
  242. for column in columns:
  243. index = column.split("_")[1]
  244. row = rows.get(f"YSJ{unit}_{index}")
  245. if not row:
  246. result[column] = {"description": column, "units": "", "high": None, "high_high": None}
  247. continue
  248. high, high_high = _parse_limits(row)
  249. result[column] = {
  250. "description": row.get("ItemDescription") or column,
  251. "units": row.get("Units") or "",
  252. "high": high,
  253. "high_high": high_high,
  254. }
  255. return result
  256. @dataclass
  257. class UnitConfig:
  258. unit: int
  259. name: str
  260. batches: list[int]
  261. run: dict
  262. stages: dict[str, list[str]] = field(default_factory=dict)
  263. labels: dict[str, str] = field(default_factory=dict)
  264. limits: dict[str, dict] = field(default_factory=dict)
  265. point_meta: dict[str, dict] = field(default_factory=dict)
  266. thresholds: dict[str, float] = field(default_factory=lambda: dict(DEFAULT_THRESHOLDS))
  267. @property
  268. def columns(self) -> list[str]:
  269. return [column for columns in self.stages.values() for column in columns]
  270. @property
  271. def stage_order(self) -> list[str]:
  272. return list(self.stages)
  273. def build_unit_config(unit: int, batches: list[int] | None = None, include_packing: bool = False) -> UnitConfig:
  274. if batches is None:
  275. batches = load_device_batches(f"{unit}#")
  276. points = list(CYLINDERS[unit])
  277. if include_packing and unit in PACKING:
  278. points.extend(PACKING[unit])
  279. stages: dict[str, list[str]] = {stage: [] for stage in STAGE_ORDER[unit]}
  280. labels: dict[str, str] = {}
  281. point_meta: dict[str, dict] = {}
  282. exhaust = {c for _, _, c in CYLINDERS[unit]}
  283. for stage, cylinder, column in points:
  284. stages[stage].append(column)
  285. label = f"{stage}{cylinder}缸排气温度" if column in exhaust else f"{stage}{cylinder}缸填料温度"
  286. labels[column] = label
  287. point_meta[column] = {"stage": stage, "cylinder": cylinder, "label": label}
  288. limits = load_point_limits(unit, list(labels))
  289. return UnitConfig(
  290. unit=unit,
  291. name=f"{unit}号机",
  292. batches=list(batches),
  293. run=RUN_SPEC[unit],
  294. stages=stages,
  295. labels=labels,
  296. limits=limits,
  297. point_meta=point_meta,
  298. )
  299. __all__ = [
  300. "CYLINDERS", "STAGE_ORDER", "RUN_SPEC", "PACKING", "DEFAULT_THRESHOLDS",
  301. "UnitConfig", "build_unit_config", "load_point_limits",
  302. "normalize_devices", "load_device_batches",
  303. "EXPECTED_PER_HOUR", "TEMP_BASELINE_HOURS", "TEMP_MIN_HISTORY_HOURS",
  304. "TEMP_CONFIRM_HOURS", "TEMP_MERGE_GAP_HOURS", "TEMP_RESTART_GAP_HOURS",
  305. "TEMP_MIN_HOUR_COVERAGE", "TEMP_SENSOR_MIN", "TEMP_SENSOR_MAX",
  306. "TEMP_MIN_STEADY", "TEMP_DROPOUT_REF",
  307. ]