scan.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. #!/usr/bin/env python3
  2. """Cylinder-temperature scan (4#~9#) with rule-based 轻微/异常/严重异常 events.
  3. Examples
  4. --------
  5. python _tmp_temperature/scan.py --device 6# --start "2026-01-01" --end "2026-03-01"
  6. python _tmp_temperature/scan.py --device 4# 5# 6# 7# 8# 9#
  7. python _tmp_temperature/scan.py --device 5# --include-packing
  8. All outputs stay inside ``_tmp_temperature/out/`` so the existing ``models/``
  9. tree is untouched until the rules are validated.
  10. """
  11. from __future__ import annotations
  12. import argparse
  13. import sys
  14. from datetime import datetime, timedelta
  15. from pathlib import Path
  16. ROOT = Path(__file__).resolve().parents[1]
  17. if str(ROOT) not in sys.path:
  18. sys.path.insert(0, str(ROOT))
  19. from _tmp_temperature import unified as temperature_algorithm
  20. from _tmp_temperature import hf_validation
  21. from _tmp_temperature import data as temperature_data
  22. from _tmp_temperature.config import (
  23. CYLINDERS,
  24. PACKING,
  25. UnitConfig,
  26. build_unit_config,
  27. load_device_batches,
  28. normalize_devices,
  29. )
  30. OUT_DIR = Path(__file__).resolve().parent / "out"
  31. def parse_time(value: str) -> datetime:
  32. for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
  33. try:
  34. return datetime.strptime(value, fmt)
  35. except ValueError:
  36. continue
  37. raise ValueError(f"无法解析时间:{value!r}")
  38. def _auto_range(config: UnitConfig):
  39. from models.common.db import connect
  40. placeholders = ",".join(["%s"] * len(config.batches))
  41. connection = connect()
  42. try:
  43. with connection.cursor() as cursor:
  44. cursor.execute(
  45. f"SELECT MIN(sample_time) AS mn, MAX(sample_time) AS mx "
  46. f"FROM pks_long_sample WHERE import_batch_id IN ({placeholders})",
  47. config.batches,
  48. )
  49. row = cursor.fetchone()
  50. finally:
  51. connection.close()
  52. start = row["mn"].replace(minute=0, second=0, microsecond=0)
  53. end = row["mx"].replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
  54. return start, end
  55. def threshold_overrides(args) -> dict:
  56. mapping = {
  57. "mild_spread": args.mild_spread, "abnormal_spread": args.abnormal_spread, "severe_spread": args.severe_spread,
  58. "mild_peer": args.mild_peer, "abnormal_peer": args.abnormal_peer, "severe_peer": args.severe_peer,
  59. "mild_z": args.mild_z, "abnormal_z": args.abnormal_z, "severe_z": args.severe_z,
  60. "mild_delta": args.mild_delta, "abnormal_delta": args.abnormal_delta, "severe_delta": args.severe_delta,
  61. "confirm_hours": args.confirm_hours,
  62. }
  63. return {key: value for key, value in mapping.items() if value is not None}
  64. def point_meta(config: UnitConfig, include_packing: bool) -> dict[str, dict]:
  65. points = list(CYLINDERS[config.unit])
  66. if include_packing and config.unit in PACKING:
  67. points.extend(PACKING[config.unit])
  68. return {
  69. column: {"stage": stage, "cylinder": cylinder, "label": config.labels.get(column, column)}
  70. for stage, cylinder, column in points
  71. }
  72. def hourly_long_rows(rows: list[dict], config: UnitConfig, include_packing: bool) -> list[dict]:
  73. meta = point_meta(config, include_packing)
  74. output = []
  75. for row in rows:
  76. if not row.get("valid"):
  77. continue
  78. hour = row["hour_start"].strftime("%Y-%m-%d %H:%M:%S")
  79. for column, info in meta.items():
  80. if row.get(f"{column}__avg") is None:
  81. continue
  82. output.append({
  83. "机组": config.name,
  84. "批次": row["import_batch_id"],
  85. "小时": hour,
  86. "点位": column,
  87. "点位描述": info["label"],
  88. "级": info["stage"],
  89. "缸": info["cylinder"],
  90. "运行覆盖率": round(row.get("coverage") or 0.0, 4),
  91. "均值": row.get(f"{column}__avg"),
  92. "最小值": row.get(f"{column}__min"),
  93. "最大值": row.get(f"{column}__max"),
  94. "标准差": row.get(f"{column}__std"),
  95. "原始最小值": row.get(f"{column}__minraw"),
  96. "零值样本数": row.get(f"{column}__zeros"),
  97. "高报警样本数": row.get(f"{column}__high"),
  98. "高高报警样本数": row.get(f"{column}__high_high"),
  99. })
  100. return output
  101. def main() -> None:
  102. parser = argparse.ArgumentParser(description="PKS 缸体温度扫描(4#~9#)")
  103. parser.add_argument("--device", nargs="+", default=["4#", "5#", "6#", "7#", "8#", "9#"])
  104. parser.add_argument("--start")
  105. parser.add_argument("--end")
  106. parser.add_argument("--chunk-days", type=int, default=30)
  107. parser.add_argument("--include-packing", action="store_true", help="5#/6# 同时扫描气缸填料温度")
  108. parser.add_argument("--output-dir", default=str(OUT_DIR))
  109. parser.add_argument("--no-hourly-long", action="store_true")
  110. for name in ("mild_spread", "abnormal_spread", "severe_spread",
  111. "mild_peer", "abnormal_peer", "severe_peer",
  112. "mild_peer_low", "abnormal_peer_low", "severe_peer_low",
  113. "mild_z", "abnormal_z", "severe_z",
  114. "mild_delta", "abnormal_delta", "severe_delta"):
  115. parser.add_argument(f"--{name.replace('_', '-')}", type=float)
  116. parser.add_argument("--confirm-hours", type=int)
  117. parser.add_argument("--min-hours", type=int, default=1, help="最短输出段小时数(默认1,观察段也输出)")
  118. args = parser.parse_args()
  119. selected = normalize_devices(args.device)
  120. overrides = threshold_overrides(args)
  121. all_rows: list[dict] = []
  122. all_qc: list[dict] = []
  123. all_features: list[dict] = []
  124. all_segments: list[dict] = []
  125. all_cycles: list[dict] = []
  126. all_channels: list[dict] = []
  127. all_observations: list[dict] = []
  128. all_hf: list[dict] = []
  129. all_long: list[dict] = []
  130. for device in selected:
  131. unit = int(device[0])
  132. config = build_unit_config(unit, include_packing=args.include_packing)
  133. if args.start or args.end:
  134. start = parse_time(args.start) if args.start else None
  135. end = parse_time(args.end) if args.end else None
  136. if start is None or end is None:
  137. auto_start, auto_end = _auto_range(config)
  138. start = start or auto_start
  139. end = end or auto_end
  140. else:
  141. start, end = _auto_range(config)
  142. print(f"[{device}] 批次 {config.batches} {start} ~ {end} 扫描 {len(config.columns)} 个缸温点 ...", flush=True)
  143. rows = temperature_data.fetch_unit_hourly(config, start, end, args.chunk_days)
  144. qc, features, segments, cycles, channels, observations, hf_segments = temperature_algorithm.run(rows, config, overrides, args.min_hours)
  145. print(
  146. f"[{device}] 有效小时 {sum(1 for row in rows if row.get('valid')):,} "
  147. f"特征 {len(features):,} 段 {len(segments):,} 通道异常 {len(channels):,}",
  148. flush=True,
  149. )
  150. all_rows.extend(rows)
  151. all_qc.extend(qc)
  152. all_features.extend(features)
  153. all_segments.extend(segments)
  154. all_cycles.extend(cycles)
  155. all_channels.extend(channels)
  156. all_observations.extend(observations)
  157. all_hf.extend(hf_segments)
  158. if not args.no_hourly_long:
  159. all_long.extend(hourly_long_rows(rows, config, args.include_packing))
  160. output_dir = Path(args.output_dir)
  161. confirmed_hf, hf_candidates = hf_validation.filter_hf(all_hf)
  162. temperature_algorithm.write_results(output_dir, all_qc, all_features, all_segments, all_cycles, all_channels,
  163. all_observations, confirmed_hf, hf_candidates)
  164. if not args.no_hourly_long:
  165. fields = ["机组", "批次", "小时", "点位", "点位描述", "级", "缸", "运行覆盖率",
  166. "均值", "最小值", "最大值", "标准差", "原始最小值", "零值样本数", "高报警样本数", "高高报警样本数"]
  167. temperature_algorithm.write_csv(output_dir / "pks_temperature_hourly.csv", all_long, fields, {field: field for field in fields})
  168. print(f"温度小时长表:{len(all_long):,} 行 -> {output_dir / 'pks_temperature_hourly.csv'}")
  169. print(f"结果目录:{output_dir}")
  170. for level in ("轻微", "异常", "严重异常"):
  171. print(f" {level}段:{sum(1 for segment in all_segments if segment['温度阶段'] == level):,}")
  172. print(f" 观察:{len(all_observations):,}")
  173. print(f" 高频候选:{len(hf_candidates):,} 高频确认:{len(confirmed_hf):,}")
  174. for path in ("温度小时特征.csv", "温度异常段.csv", "温度演化周期.csv", "温度通道质控.csv"):
  175. print(f" {path}")
  176. if __name__ == "__main__":
  177. main()