| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- """Unit / point configuration for the PKS cylinder-temperature scan.
- Only cylinder-body temperature points are configured here (exhaust temperature,
- and optionally packing temperature). The stage/cylinder mapping is the one
- confirmed by the plant:
- * 4#/5#/6#: two stages; cylinders 1/2/3 -> stage 1, cylinders 4/5/6 -> stage 2.
- The point names use stage-local numbering (``一级1#`` / ``二级1#``).
- * 7#/8#/9#: three stages; cylinders 2/4/6 -> stage 1, 3/5 -> stage 2, 1 -> stage 3.
- Run state:
- * 7#/8#/9# expose speed in ``YSJ_41`` (``>0`` means running).
- * 4#/5#/6# have no speed signal and no run-status table, so a data-derived
- proxy is used: motor/body vibration or first-stage suction pressure.
- """
- from __future__ import annotations
- from dataclasses import dataclass, field
- from models.common.db import connect, number
- # --- local constants (kept here so models/common stays untouched) ---
- EXPECTED_PER_HOUR = 720
- TEMP_BASELINE_HOURS = 168
- TEMP_MIN_HISTORY_HOURS = 24
- TEMP_CONFIRM_HOURS = 3
- TEMP_MERGE_GAP_HOURS = 6
- TEMP_RESTART_GAP_HOURS = 12
- TEMP_MIN_HOUR_COVERAGE = 0.80
- TEMP_SENSOR_MIN = -30.0
- TEMP_SENSOR_MAX = 350.0
- TEMP_MIN_STEADY = 45.0
- TEMP_DROPOUT_REF = 60.0
- # ``(stage, cylinder, column)`` for each unit's cylinder exhaust temperatures.
- CYLINDERS: dict[int, list[tuple[str, str, str]]] = {
- 4: [
- ("一级", "1#", "YSJ_9"),
- ("一级", "2#", "YSJ_10"),
- ("一级", "3#", "YSJ_11"),
- ("二级", "1#", "YSJ_13"),
- ("二级", "2#", "YSJ_14"),
- ("二级", "3#", "YSJ_15"),
- ],
- 5: [
- ("一级", "1#", "YSJ_18"),
- ("一级", "2#", "YSJ_20"),
- ("一级", "3#", "YSJ_22"),
- ("二级", "1#", "YSJ_17"),
- ("二级", "2#", "YSJ_19"),
- ("二级", "3#", "YSJ_21"),
- ],
- 6: [
- ("一级", "1#", "YSJ_18"),
- ("一级", "2#", "YSJ_20"),
- ("一级", "3#", "YSJ_22"),
- ("二级", "1#", "YSJ_17"),
- ("二级", "2#", "YSJ_19"),
- ("二级", "3#", "YSJ_21"),
- ],
- 7: [
- ("一级", "2列", "YSJ_16"),
- ("一级", "4列", "YSJ_17"),
- ("一级", "6列", "YSJ_18"),
- ("二级", "3列", "YSJ_19"),
- ("二级", "5列", "YSJ_20"),
- ("三级", "1列", "YSJ_21"),
- ],
- 8: [
- ("一级", "2列", "YSJ_16"),
- ("一级", "4列", "YSJ_17"),
- ("一级", "6列", "YSJ_18"),
- ("二级", "3列", "YSJ_19"),
- ("二级", "5列", "YSJ_20"),
- ("三级", "1列", "YSJ_21"),
- ],
- 9: [
- ("一级", "2列", "YSJ_16"),
- ("一级", "4列", "YSJ_17"),
- ("一级", "6列", "YSJ_18"),
- ("二级", "3列", "YSJ_19"),
- ("二级", "5列", "YSJ_20"),
- ("三级", "1列", "YSJ_21"),
- ],
- }
- STAGE_ORDER: dict[int, list[str]] = {
- 4: ["一级", "二级"],
- 5: ["一级", "二级"],
- 6: ["一级", "二级"],
- 7: ["一级", "二级", "三级"],
- 8: ["一级", "二级", "三级"],
- 9: ["一级", "二级", "三级"],
- }
- # Hourly run-state specification.
- # rpm -> ``YSJ_41 > 0``
- # proxy -> any of the listed columns above ``threshold``
- RUN_SPEC: dict[int, dict] = {
- # 4# has a cumulative running-hour counter: +1 per running hour, flat when
- # stopped. Body vibration / inlet pressure are the fallback if the counter
- # is missing or resets.
- 4: {
- "type": "cumulative",
- "column": "YSJ_31",
- "sql": "(IFNULL(YSJ_37,0) > 0.5 OR IFNULL(YSJ_38,0) > 0.5 OR IFNULL(YSJ_2,0) > 0.5)",
- },
- # 5#/6# have no speed or cumulative counter. Use 2-of-4:
- # discharge pressure, inlet pressure, body vibration, valve position.
- 5: {
- "type": "multi",
- "sql": "((IFNULL(YSJ_5,0) > 0.5) + (IFNULL(YSJ_3,0) > 0.3) + "
- "((IFNULL(YSJ_10,0) > 0.5) OR (IFNULL(YSJ_11,0) > 0.5) OR "
- "(IFNULL(YSJ_12,0) > 0.5) OR (IFNULL(YSJ_13,0) > 0.5)) + "
- "(IFNULL(YSJ_14,100) < 90)) >= 2",
- },
- 6: {
- "type": "multi",
- "sql": "((IFNULL(YSJ_5,0) > 0.5) + (IFNULL(YSJ_3,0) > 0.3) + "
- "((IFNULL(YSJ_10,0) > 0.5) OR (IFNULL(YSJ_11,0) > 0.5) OR "
- "(IFNULL(YSJ_12,0) > 0.5) OR (IFNULL(YSJ_13,0) > 0.5)) + "
- "(IFNULL(YSJ_14,100) < 90)) >= 2",
- },
- 7: {"type": "rpm", "column": "YSJ_41"},
- 8: {"type": "rpm", "column": "YSJ_41"},
- 9: {"type": "rpm", "column": "YSJ_41"},
- }
- # Optional packing-temperature groups for 5#/6# (cylinders 1..6 -> stage 1/2).
- PACKING: dict[int, list[tuple[str, str, str]]] = {
- 5: [
- ("一级", "1#", "YSJ_39"),
- ("一级", "2#", "YSJ_40"),
- ("一级", "3#", "YSJ_41"),
- ("二级", "4#", "YSJ_42"),
- ("二级", "5#", "YSJ_43"),
- ("二级", "6#", "YSJ_44"),
- ],
- 6: [
- ("一级", "1#", "YSJ_39"),
- ("一级", "2#", "YSJ_40"),
- ("一级", "3#", "YSJ_41"),
- ("二级", "4#", "YSJ_42"),
- ("二级", "5#", "YSJ_43"),
- ("二级", "6#", "YSJ_44"),
- ],
- }
- DEFAULT_THRESHOLDS: dict[str, float] = {
- # Same-stage spread change versus its own recent normal spread.
- "mild_spread": 4.0,
- "abnormal_spread": 8.0,
- "severe_spread": 15.0,
- # Cylinder deviation from stage median, versus its own recent normal offset.
- "mild_peer": 4.0,
- "abnormal_peer": 6.0,
- "severe_peer": 10.0,
- # Low-side peer deviation (a cylinder falling relative to its stage peers).
- "mild_peer_low": 5.0,
- "abnormal_peer_low": 8.0,
- "severe_peer_low": 12.0,
- # Own-baseline robust z (reported; used only for single-cylinder stages).
- "mild_z": 3.0,
- "abnormal_z": 5.0,
- "severe_z": 8.0,
- # Own-baseline absolute rise (degC).
- "mild_delta": 5.0,
- "abnormal_delta": 10.0,
- "severe_delta": 15.0,
- # Short-term same-stage relative change (degC over 1/3/6h).
- "observe_rate1": 3.0, "mild_rate1": 4.0, "abnormal_rate1": 6.0, "severe_rate1": 9.0,
- "observe_rate3": 4.0, "mild_rate3": 5.0, "abnormal_rate3": 7.0, "severe_rate3": 11.0,
- "observe_rate6": 6.0, "mild_rate6": 7.0, "abnormal_rate6": 9.0, "severe_rate6": 13.0,
- # Intra-hour high-frequency fluctuation: std relative to its normal baseline.
- "hf_ratio_mild": 2.5, "hf_ratio_abnormal": 3.5, "hf_ratio_severe": 5.0,
- "hf_z_mild": 6.0, "hf_z_abnormal": 9.0, "hf_z_severe": 12.0,
- "hf_min_std": 0.8, "hf_base_floor": 0.3,
- "hf_persist_hours": 3,
- # 12h relative rise (single-cylinder and stage-level), integrated into events.
- "rise_single_mild": 3.0, "rise_single_abnormal": 4.5, "rise_single_severe": 6.0,
- "rise_stage_mild": 3.0, "rise_stage_abnormal": 5.0, "rise_stage_severe": 7.0,
- "rise_stage_flat_limit": 3.0,
- "rise_persist_hours": 2, "rise_baseline_hours": 6, "rise_min_history_hours": 4,
- # Channel / shutdown.
- "channel_ambient": 45.0, "channel_drop_delta": 8.0,
- "channel_min_drop": 20.0, "channel_zero_min": 5,
- "channel_recover_hours": 3, "channel_frozen_hours": 12,
- "channel_frozen_eps": 0.1, "channel_peer_variation": 3.0,
- # Persistence.
- "observe_hours": 1, "event_hours": 2, "confirm_hours": 3,
- "merge_gap_hours": 6,
- }
- def _number(value):
- return number(value)
- def normalize_devices(values) -> list[str]:
- """Accept 4#~9# / 4 / 4号机 and return ``['4#', ...]``."""
- result = []
- for value in values:
- digits = "".join(ch for ch in str(value) if ch.isdigit())
- if not digits or not 4 <= int(digits) <= 9:
- raise ValueError(f"无法识别的压缩机编号:{value!r}(应为 4#~9#)")
- device = f"{int(digits)}#"
- if device not in result:
- result.append(device)
- return result
- def load_device_batches(device: str) -> list[int]:
- """Resolve a device code to its successful PKS import batches."""
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT id FROM compressor_data_import "
- "WHERE data_type = 'PKS' AND directory_path = %s "
- "AND is_imported = 1 AND execution_status = '成功' ORDER BY id",
- (device,),
- )
- batches = [int(row["id"]) for row in cursor.fetchall()]
- finally:
- connection.close()
- if not batches:
- raise RuntimeError(f"{device} 没有成功的 PKS 导入批次")
- return batches
- def _parse_limits(row: dict) -> tuple[float | None, float | None]:
- high = high_high = None
- for index in range(1, 5):
- kind = str(row.get(f"AlarmType{index}") or "").strip()
- limit = _number(row.get(f"AlarmLimit{index}"))
- if not kind or limit is None:
- continue
- if "HighHigh" in kind:
- high_high = limit if high_high is None else max(high_high, limit)
- elif "High" in kind and high is None:
- high = limit
- if high is None and high_high is not None:
- high = high_high
- return high, high_high
- def load_point_limits(unit: int, columns: list[str]) -> dict[str, dict]:
- """Return ``{column: {description, units, high, high_high}}`` from site_point."""
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT ItemName, ItemDescription, Units, "
- "AlarmType1, AlarmType2, AlarmType3, AlarmType4, "
- "AlarmLimit1, AlarmLimit2, AlarmLimit3, AlarmLimit4 "
- "FROM site_point WHERE ItemName LIKE %s",
- (f"YSJ{unit}\\_%",),
- )
- rows = {row["ItemName"]: row for row in cursor.fetchall()}
- finally:
- connection.close()
- result = {}
- for column in columns:
- index = column.split("_")[1]
- row = rows.get(f"YSJ{unit}_{index}")
- if not row:
- result[column] = {"description": column, "units": "", "high": None, "high_high": None}
- continue
- high, high_high = _parse_limits(row)
- result[column] = {
- "description": row.get("ItemDescription") or column,
- "units": row.get("Units") or "",
- "high": high,
- "high_high": high_high,
- }
- return result
- @dataclass
- class UnitConfig:
- unit: int
- name: str
- batches: list[int]
- run: dict
- stages: dict[str, list[str]] = field(default_factory=dict)
- labels: dict[str, str] = field(default_factory=dict)
- limits: dict[str, dict] = field(default_factory=dict)
- point_meta: dict[str, dict] = field(default_factory=dict)
- thresholds: dict[str, float] = field(default_factory=lambda: dict(DEFAULT_THRESHOLDS))
- @property
- def columns(self) -> list[str]:
- return [column for columns in self.stages.values() for column in columns]
- @property
- def stage_order(self) -> list[str]:
- return list(self.stages)
- def build_unit_config(unit: int, batches: list[int] | None = None, include_packing: bool = False) -> UnitConfig:
- if batches is None:
- batches = load_device_batches(f"{unit}#")
- points = list(CYLINDERS[unit])
- if include_packing and unit in PACKING:
- points.extend(PACKING[unit])
- stages: dict[str, list[str]] = {stage: [] for stage in STAGE_ORDER[unit]}
- labels: dict[str, str] = {}
- point_meta: dict[str, dict] = {}
- exhaust = {c for _, _, c in CYLINDERS[unit]}
- for stage, cylinder, column in points:
- stages[stage].append(column)
- label = f"{stage}{cylinder}缸排气温度" if column in exhaust else f"{stage}{cylinder}缸填料温度"
- labels[column] = label
- point_meta[column] = {"stage": stage, "cylinder": cylinder, "label": label}
- limits = load_point_limits(unit, list(labels))
- return UnitConfig(
- unit=unit,
- name=f"{unit}号机",
- batches=list(batches),
- run=RUN_SPEC[unit],
- stages=stages,
- labels=labels,
- limits=limits,
- point_meta=point_meta,
- )
- __all__ = [
- "CYLINDERS", "STAGE_ORDER", "RUN_SPEC", "PACKING", "DEFAULT_THRESHOLDS",
- "UnitConfig", "build_unit_config", "load_point_limits",
- "normalize_devices", "load_device_batches",
- "EXPECTED_PER_HOUR", "TEMP_BASELINE_HOURS", "TEMP_MIN_HISTORY_HOURS",
- "TEMP_CONFIRM_HOURS", "TEMP_MERGE_GAP_HOURS", "TEMP_RESTART_GAP_HOURS",
- "TEMP_MIN_HOUR_COVERAGE", "TEMP_SENSOR_MIN", "TEMP_SENSOR_MAX",
- "TEMP_MIN_STEADY", "TEMP_DROPOUT_REF",
- ]
|