data.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. """数据读取:首周期波形 + 粗采样 + 温度参考。
  2. 振动特征只使用 wave_file / wave_sample_one 的首周期;
  3. 温度 CSV 只在事后验证时读取,不参与特征和标签。
  4. """
  5. from __future__ import annotations
  6. import re
  7. from datetime import datetime, timedelta
  8. from pathlib import Path
  9. import numpy as np
  10. import pandas as pd
  11. from models.common.db import connect
  12. from . import config
  13. def fetch_metadata(
  14. cylinders: list[str] | None = None,
  15. start: datetime | None = None,
  16. end: datetime | None = None,
  17. ) -> list[dict]:
  18. cylinders = cylinders or list(config.CYLINDERS)
  19. points = [config.CYLINDERS[name]["point"] for name in cylinders]
  20. placeholders = ", ".join(["%s"] * len(points))
  21. clauses = [f"point_name IN ({placeholders})", "rpm > 0"]
  22. params: list = list(points)
  23. if start is not None:
  24. clauses.append("sample_time >= %s")
  25. params.append(start)
  26. if end is not None:
  27. clauses.append("sample_time <= %s")
  28. params.append(end)
  29. sql = f"""
  30. SELECT id, device_part, point_name, sample_time, rpm, sample_count,
  31. sample_frequency_hz, cycle_start, cycle_end
  32. FROM wave_file
  33. WHERE {' AND '.join(clauses)}
  34. ORDER BY point_name, sample_time
  35. """
  36. connection = connect()
  37. try:
  38. with connection.cursor() as cursor:
  39. cursor.execute(sql, params)
  40. return cursor.fetchall()
  41. finally:
  42. connection.close()
  43. def coarse_metadata(rows: list[dict], minutes: int) -> list[dict]:
  44. """每个缸每个粗时间桶保留一个文件,先做轻量筛查。"""
  45. if not rows:
  46. return []
  47. frame = pd.DataFrame(rows)
  48. frame["sample_time"] = pd.to_datetime(frame["sample_time"])
  49. frame["bucket"] = frame["sample_time"].dt.floor(f"{minutes}min")
  50. frame = (
  51. frame.sort_values(["point_name", "sample_time"])
  52. .drop_duplicates(["point_name", "bucket"], keep="first")
  53. .drop(columns=["bucket"])
  54. )
  55. return frame.to_dict("records")
  56. def fetch_samples(file_ids: list[int], chunk_size: int = 200) -> dict[int, np.ndarray]:
  57. """按块读取首周期波形。返回 id -> signal_value 数组。"""
  58. result: dict[int, np.ndarray] = {}
  59. if not file_ids:
  60. return result
  61. connection = connect()
  62. try:
  63. with connection.cursor() as cursor:
  64. for offset in range(0, len(file_ids), chunk_size):
  65. chunk = file_ids[offset:offset + chunk_size]
  66. placeholders = ", ".join(["%s"] * len(chunk))
  67. cursor.execute(
  68. f"""
  69. SELECT wave_file_id, signal_value
  70. FROM wave_sample_one
  71. WHERE wave_file_id IN ({placeholders})
  72. ORDER BY wave_file_id, sample_index
  73. """,
  74. chunk,
  75. )
  76. current_id: int | None = None
  77. values: list[float] = []
  78. for row in cursor.fetchall():
  79. file_id = int(row["wave_file_id"])
  80. if current_id is None:
  81. current_id = file_id
  82. if file_id != current_id:
  83. result[current_id] = np.asarray(values, dtype=float)
  84. values = []
  85. current_id = file_id
  86. values.append(float(row["signal_value"]))
  87. if current_id is not None:
  88. result[current_id] = np.asarray(values, dtype=float)
  89. finally:
  90. connection.close()
  91. return result
  92. def load_temperature_reference() -> pd.DataFrame:
  93. """读取温度异常段,仅用于最后对照。"""
  94. path = config.TEMPERATURE_LABELS
  95. if not path.exists():
  96. return pd.DataFrame()
  97. frame = pd.read_csv(path, encoding="utf-8-sig")
  98. if frame.empty:
  99. return frame
  100. frame = frame[frame["机组"].astype(str).str.strip() == "5号机"].copy()
  101. frame["cylinder"] = frame["阶段部位"].map(_temperature_cylinder)
  102. frame["start_time"] = pd.to_datetime(frame["开始小时"], errors="coerce")
  103. frame["end_time"] = pd.to_datetime(frame["结束小时"], errors="coerce")
  104. return frame
  105. def _temperature_cylinder(label: str) -> str | None:
  106. text = str(label or "").strip()
  107. match = re.match(r"^(一级|二级)(\d)#缸$", text)
  108. if not match:
  109. return None
  110. return config.STAGE_CYLINDER_TO_GLOBAL.get((match.group(1), f"{match.group(2)}#"))
  111. def merge_intervals(intervals: list[tuple[datetime, datetime]], gap: timedelta = timedelta(0)) -> list[tuple[datetime, datetime]]:
  112. if not intervals:
  113. return []
  114. intervals = sorted(intervals)
  115. merged = [intervals[0]]
  116. for start, end in intervals[1:]:
  117. prev_start, prev_end = merged[-1]
  118. if start <= prev_end + gap:
  119. merged[-1] = (prev_start, max(prev_end, end))
  120. else:
  121. merged.append((start, end))
  122. return merged
  123. def refine_windows(candidate_labels: pd.DataFrame, events: list[dict]) -> dict[str, list[tuple[datetime, datetime]]]:
  124. """二次评估窗口:事件窗口 + 粗筛候选窗口。"""
  125. windows: dict[str, list[tuple[datetime, datetime]]] = {name: [] for name in config.CYLINDERS}
  126. for event in events:
  127. event_time = pd.Timestamp(event["date"]).to_pydatetime()
  128. cylinder = event["cylinder"]
  129. windows[cylinder].append((
  130. event_time - timedelta(days=10),
  131. event_time + timedelta(days=3),
  132. ))
  133. if not candidate_labels.empty:
  134. for row in candidate_labels.itertuples(index=False):
  135. cylinder = getattr(row, "cylinder", None)
  136. if cylinder not in windows:
  137. continue
  138. start = pd.Timestamp(getattr(row, "start_time")).to_pydatetime() - timedelta(days=3)
  139. end = pd.Timestamp(getattr(row, "end_time")).to_pydatetime() + timedelta(days=1)
  140. windows[cylinder].append((start, end))
  141. # 二次评估需要同级对照缸来计算跨缸残差。
  142. expanded = {name: list(items) for name, items in windows.items()}
  143. for cylinder, items in windows.items():
  144. stage = config.CYLINDERS[cylinder]["stage"]
  145. for peer in config.STAGE_PEERS[stage]:
  146. if peer != cylinder:
  147. expanded[peer].extend(items)
  148. return {
  149. cylinder: merge_intervals(items, gap=timedelta(hours=6))
  150. for cylinder, items in expanded.items()
  151. if items
  152. }