|
@@ -0,0 +1,165 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+"""Extract per-cycle indicator-diagram features from the wave database (read-only).
|
|
|
|
|
+
|
|
|
|
|
+Fetches a *small, sampled* set of running cycles (wave_sample_one holds one first
|
|
|
|
|
+cycle per wave_file) for the chosen cylinder points and time range, computes the
|
|
|
|
|
+phase-level features in indicator_features.py, and writes one CSV row per cycle.
|
|
|
|
|
+
|
|
|
|
|
+Sampling keeps the query volume low: at most --per-day files per point per day,
|
|
|
|
|
+picked evenly across that day's running files.
|
|
|
|
|
+"""
|
|
|
|
|
+from __future__ import annotations
|
|
|
|
|
+
|
|
|
|
|
+import argparse
|
|
|
|
|
+import csv
|
|
|
|
|
+import sys
|
|
|
|
|
+from collections import defaultdict
|
|
|
|
|
+from datetime import datetime
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+
|
|
|
|
|
+import numpy as np
|
|
|
|
|
+import pymysql
|
|
|
|
|
+
|
|
|
|
|
+BACKEND = Path(__file__).resolve().parents[1]
|
|
|
|
|
+sys.path.insert(0, str(BACKEND))
|
|
|
|
|
+
|
|
|
|
|
+from app.config import settings # noqa: E402
|
|
|
|
|
+from indicator_features.indicator_features import ( # noqa: E402
|
|
|
|
|
+ CYLINDER_STAGE,
|
|
|
|
|
+ cycle_features,
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+CYLINDERS = ("一", "二", "三", "四", "五", "六")
|
|
|
|
|
+SIDES = ("盖侧", "轴侧")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def connect() -> pymysql.connections.Connection:
|
|
|
|
|
+ return pymysql.connect(
|
|
|
|
|
+ host=settings.db_host, port=settings.db_port, user=settings.db_user,
|
|
|
|
|
+ password=settings.db_password, database=settings.db_name, charset="utf8mb4",
|
|
|
|
|
+ cursorclass=pymysql.cursors.DictCursor, connect_timeout=8, read_timeout=300,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def point_name(unit: str, cylinder: str, side: str) -> str:
|
|
|
|
|
+ return f"{unit}号机组{cylinder}缸压力{side}"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def fetch_file_rows(connection, point: str, start: str, end: str) -> list[dict]:
|
|
|
|
|
+ with connection.cursor() as cursor:
|
|
|
|
|
+ cursor.execute(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT id, sample_time, rpm
|
|
|
|
|
+ FROM wave_file
|
|
|
|
|
+ WHERE point_name = %s AND measurement_type = '压力'
|
|
|
|
|
+ AND rpm > 0 AND sample_time >= %s AND sample_time < %s
|
|
|
|
|
+ ORDER BY sample_time
|
|
|
|
|
+ """,
|
|
|
|
|
+ (point, start, end),
|
|
|
|
|
+ )
|
|
|
|
|
+ return list(cursor.fetchall())
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def sample_per_day(rows: list[dict], per_day: int) -> list[dict]:
|
|
|
|
|
+ if per_day <= 0:
|
|
|
|
|
+ return rows
|
|
|
|
|
+ by_day: dict = defaultdict(list)
|
|
|
|
|
+ for row in rows:
|
|
|
|
|
+ by_day[row["sample_time"].date()].append(row)
|
|
|
|
|
+ picked: list[dict] = []
|
|
|
|
|
+ for day in sorted(by_day):
|
|
|
|
|
+ day_rows = by_day[day]
|
|
|
|
|
+ if len(day_rows) <= per_day:
|
|
|
|
|
+ picked.extend(day_rows)
|
|
|
|
|
+ continue
|
|
|
|
|
+ idx = np.linspace(0, len(day_rows) - 1, per_day).round().astype(int)
|
|
|
|
|
+ picked.extend(day_rows[int(i)] for i in idx)
|
|
|
|
|
+ return picked
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def fetch_cycles(connection, file_ids: list[int], batch: int) -> dict[int, np.ndarray]:
|
|
|
|
|
+ cycles: dict[int, list[tuple[int, float]]] = defaultdict(list)
|
|
|
|
|
+ with connection.cursor() as cursor:
|
|
|
|
|
+ for start in range(0, len(file_ids), batch):
|
|
|
|
|
+ chunk = file_ids[start:start + batch]
|
|
|
|
|
+ placeholders = ", ".join(["%s"] * len(chunk))
|
|
|
|
|
+ cursor.execute(
|
|
|
|
|
+ f"""
|
|
|
|
|
+ SELECT wave_file_id, sample_index, CAST(signal_value AS FLOAT) AS sig
|
|
|
|
|
+ FROM wave_sample_one
|
|
|
|
|
+ WHERE wave_file_id IN ({placeholders})
|
|
|
|
|
+ ORDER BY wave_file_id, sample_index
|
|
|
|
|
+ """,
|
|
|
|
|
+ tuple(chunk),
|
|
|
|
|
+ )
|
|
|
|
|
+ for row in cursor.fetchall():
|
|
|
|
|
+ if row["sig"] is None:
|
|
|
|
|
+ continue
|
|
|
|
|
+ cycles[int(row["wave_file_id"])].append((int(row["sample_index"]), float(row["sig"])))
|
|
|
|
|
+ result = {}
|
|
|
|
|
+ for file_id, rows in cycles.items():
|
|
|
|
|
+ rows.sort()
|
|
|
|
|
+ result[file_id] = np.asarray([value for _, value in rows], dtype=float)
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def main() -> None:
|
|
|
|
|
+ parser = argparse.ArgumentParser(description="只读抽取功图单周期特征(抽样)")
|
|
|
|
|
+ parser.add_argument("--unit", required=True, choices=("7", "8", "9"))
|
|
|
|
|
+ parser.add_argument("--cylinders", nargs="+", default=list(CYLINDERS), choices=CYLINDERS)
|
|
|
|
|
+ parser.add_argument("--sides", nargs="+", default=list(SIDES), choices=SIDES)
|
|
|
|
|
+ parser.add_argument("--start", required=True, help="YYYY-MM-DD")
|
|
|
|
|
+ parser.add_argument("--end", required=True, help="YYYY-MM-DD(不含)")
|
|
|
|
|
+ parser.add_argument("--per-day", type=int, default=3, help="每测点每天最多取几个文件")
|
|
|
|
|
+ parser.add_argument("--batch", type=int, default=200, help="IN 查询批大小")
|
|
|
|
|
+ parser.add_argument("--output", required=True)
|
|
|
|
|
+ args = parser.parse_args()
|
|
|
|
|
+
|
|
|
|
|
+ connection = connect()
|
|
|
|
|
+ rows_out: list[dict] = []
|
|
|
|
|
+ try:
|
|
|
|
|
+ for cylinder in args.cylinders:
|
|
|
|
|
+ for side in args.sides:
|
|
|
|
|
+ point = point_name(args.unit, cylinder, side)
|
|
|
|
|
+ files = fetch_file_rows(connection, point, args.start, args.end)
|
|
|
|
|
+ picked = sample_per_day(files, args.per_day)
|
|
|
|
|
+ print(f"{point}: 运行文件 {len(files)},抽样 {len(picked)}", flush=True)
|
|
|
|
|
+ if not picked:
|
|
|
|
|
+ continue
|
|
|
|
|
+ id_to_time = {int(r["id"]): r for r in picked}
|
|
|
|
|
+ cycles = fetch_cycles(connection, list(id_to_time), args.batch)
|
|
|
|
|
+ for file_id, signal in cycles.items():
|
|
|
|
|
+ feats = cycle_features(signal, cylinder)
|
|
|
|
|
+ if not feats:
|
|
|
|
|
+ continue
|
|
|
|
|
+ meta = id_to_time[file_id]
|
|
|
|
|
+ rows_out.append({
|
|
|
|
|
+ "unit": args.unit,
|
|
|
|
|
+ "cylinder": cylinder,
|
|
|
|
|
+ "stage": CYLINDER_STAGE[cylinder],
|
|
|
|
|
+ "side": side,
|
|
|
|
|
+ "point_name": point,
|
|
|
|
|
+ "wave_file_id": file_id,
|
|
|
|
|
+ "sample_time": str(meta["sample_time"]),
|
|
|
|
|
+ "rpm": float(meta["rpm"] or 0.0),
|
|
|
|
|
+ **feats,
|
|
|
|
|
+ })
|
|
|
|
|
+ finally:
|
|
|
|
|
+ connection.close()
|
|
|
|
|
+
|
|
|
|
|
+ if not rows_out:
|
|
|
|
|
+ print("没有特征行,退出")
|
|
|
|
|
+ return
|
|
|
|
|
+ rows_out.sort(key=lambda r: (r["cylinder"], r["side"], r["sample_time"]))
|
|
|
|
|
+ output = Path(args.output)
|
|
|
|
|
+ output.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+ with output.open("w", encoding="utf-8-sig", newline="") as handle:
|
|
|
|
|
+ writer = csv.DictWriter(handle, fieldnames=list(rows_out[0].keys()))
|
|
|
|
|
+ writer.writeheader()
|
|
|
|
|
+ writer.writerows(rows_out)
|
|
|
|
|
+ print(f"特征行 {len(rows_out):,} -> {output}")
|
|
|
|
|
+ print(f"时间:{datetime.now():%Y-%m-%d %H:%M:%S}")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ main()
|