predict_re.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. """Re-run TSPulse detection for files currently marked 无周期 (tspluse_status = -1).
  2. The original full re-run used a fixed 3000-sample prefix per file. Files that
  3. start mid-revolution have too few trigger pulses inside that prefix for the
  4. zero-marker segmentation to find a complete cycle, so they were mislabelled -1
  5. even though the client and the training generator (which read the whole file)
  6. detect them normally.
  7. This script re-processes ONLY the rows still holding ``tspluse_status = -1``,
  8. using predict.py's adaptive multi-stage window but starting at the 7000-sample
  9. stage (these files already failed the smaller windows). Files that still yield
  10. no cycle stay -1; the rest are written their corrected status (0 or the
  11. proportional anomaly value).
  12. The query filters on ``tspluse_status = -1``, so the ``(sample_time, id)``
  13. watermark is naturally idempotent: a file already fixed is no longer selected
  14. on resume. Run it once after the full re-run finishes; no column reset needed.
  15. Usage:
  16. conda activate tspulse
  17. python predict_re.py [--start-time ""] [--start-id 0] [--batch 100]
  18. [--limit 0] [--report-every 100] [--write-batch 100]
  19. [--dry-run]
  20. """
  21. import argparse
  22. import sys
  23. import numpy as np
  24. import torch
  25. import predict
  26. from predict import (
  27. MEASUREMENT_TYPE,
  28. STAGE_LIMITS,
  29. compute_status,
  30. encode_batch,
  31. flush_status,
  32. load_baseline,
  33. load_model,
  34. resolve_batch_cycles,
  35. )
  36. def fetch_batch(
  37. connection,
  38. watermark_time: object | None,
  39. watermark_id: int,
  40. batch: int,
  41. ) -> list[dict]:
  42. with connection.cursor() as cursor:
  43. if watermark_time is None:
  44. cursor.execute(
  45. """
  46. SELECT id, point_name, measurement_type, sample_time, sample_count
  47. FROM wave_file
  48. WHERE rpm > 0 AND measurement_type = %s AND tspluse_status = -1
  49. ORDER BY sample_time ASC, id ASC
  50. LIMIT %s
  51. """,
  52. (MEASUREMENT_TYPE, batch),
  53. )
  54. else:
  55. cursor.execute(
  56. """
  57. SELECT id, point_name, measurement_type, sample_time, sample_count
  58. FROM wave_file
  59. WHERE rpm > 0 AND measurement_type = %s AND tspluse_status = -1
  60. AND (sample_time > %s OR (sample_time = %s AND id > %s))
  61. ORDER BY sample_time ASC, id ASC
  62. LIMIT %s
  63. """,
  64. (MEASUREMENT_TYPE, watermark_time, watermark_time, watermark_id, batch),
  65. )
  66. return cursor.fetchall()
  67. def main() -> int:
  68. parser = argparse.ArgumentParser(description="重新处理 tspluse_status = -1 的文件")
  69. parser.add_argument("--start-time", type=str, default="",
  70. help="起始水位时间(含),留空从头开始")
  71. parser.add_argument("--start-id", type=int, default=0,
  72. help="起始水位 id(含)")
  73. parser.add_argument("--batch", type=int, default=100, help="每轮读取的文件数")
  74. parser.add_argument("--read-workers", type=int, default=4, help="并行读样本的连接数")
  75. parser.add_argument("--limit", type=int, default=0, help="最多处理文件数,0 表示不限")
  76. parser.add_argument("--report-every", type=int, default=100, help="每 N 个文件打印一次汇总")
  77. parser.add_argument("--write-batch", type=int, default=100,
  78. help="回写攒满 N 条才批量写一次")
  79. parser.add_argument("--dry-run", action="store_true", help="只预测不回写数据库")
  80. args = parser.parse_args()
  81. device = predict.get_device()
  82. print(f"设备: {predict.describe(device)}")
  83. model, mean, std = load_model(device)
  84. part_index, centroids, radii = load_baseline()
  85. print(f"基准表: {len(part_index)} 个部位, 重新处理 tspluse_status = -1 的文件, "
  86. f"自适应窗口: {STAGE_LIMITS[1:]} (跳过 {STAGE_LIMITS[0]})")
  87. start_time = args.start_time.strip()
  88. watermark_time: object | None = start_time or None
  89. watermark_id = args.start_id
  90. total = 0
  91. stats = {"正常": 0, "异常": 0, "无周期": 0}
  92. skipped = {"无基准": 0, "坏数据": 0, "周期过长": 0, "无样本": 0}
  93. errors = 0
  94. last_reported = 0
  95. write_buffer: list[tuple[int, int]] = []
  96. read_workers = max(1, min(args.read_workers, args.batch))
  97. reader_connections = [predict.get_connection() for _ in range(read_workers)]
  98. def flush(connection) -> None:
  99. if args.dry_run:
  100. write_buffer.clear()
  101. else:
  102. flush_status(connection, write_buffer)
  103. def summary_text() -> str:
  104. return (
  105. f"已处理 {total}, 正常 {stats['正常']}, 异常 {stats['异常']}, "
  106. f"无周期 {stats['无周期']}, 跳过 {sum(skipped.values())}({dict(skipped)}), "
  107. f"错误 {errors}, 水位 sample_time={watermark_time} id={watermark_id}"
  108. )
  109. print(f"水位起点: sample_time={start_time or '从头'}, id={args.start_id}, "
  110. f"批大小: {args.batch}, 读并行: {read_workers} 连接"
  111. + (", 干跑(不回写)" if args.dry_run else ""))
  112. try:
  113. while True:
  114. connection = predict.get_connection()
  115. try:
  116. rows = fetch_batch(connection, watermark_time, watermark_id, args.batch)
  117. if not rows:
  118. flush(connection)
  119. connection.close()
  120. print(f"无待处理的 -1 文件,处理完成 [{summary_text()}]")
  121. return 0
  122. file_ids = [int(row["id"]) for row in rows]
  123. part_of = {int(row["id"]): f"{row['point_name']}_{row['measurement_type']}" for row in rows}
  124. resolved, batch_errors = resolve_batch_cycles(
  125. rows, part_of, part_index, reader_connections, read_workers,
  126. first_stage=1,
  127. )
  128. errors += batch_errors
  129. candidates: list[tuple[int, str, np.ndarray]] = []
  130. for row in rows:
  131. file_id = int(row["id"])
  132. total += 1
  133. status, matrix = resolved[file_id]
  134. if status == "ok":
  135. candidates.append((file_id, part_of[file_id], matrix))
  136. elif status == "无周期":
  137. stats["无周期"] += 1
  138. write_buffer.append((file_id, -1))
  139. else:
  140. if status in skipped:
  141. skipped[status] += 1
  142. write_buffer.append((file_id, 0))
  143. if candidates:
  144. fingerprints = encode_batch(
  145. model, mean, std, device, [item[2] for item in candidates]
  146. )
  147. for (file_id, part, _matrix), fingerprint in zip(candidates, fingerprints):
  148. index = part_index[part]
  149. distance = float(np.linalg.norm(fingerprint - centroids[index]))
  150. radius = float(radii[index])
  151. status = compute_status(distance, radius)
  152. stats["异常" if status > 0 else "正常"] += 1
  153. write_buffer.append((file_id, status))
  154. if len(write_buffer) >= args.write_batch:
  155. flush(connection)
  156. if total // args.report_every > last_reported:
  157. last_reported = total // args.report_every
  158. print(f" 汇总: {summary_text()}", flush=True)
  159. if args.limit and total >= args.limit:
  160. flush(connection)
  161. watermark_time = rows[-1]["sample_time"]
  162. watermark_id = int(rows[-1]["id"])
  163. connection.close()
  164. print(f"已达 --limit={args.limit},退出")
  165. print(f" 汇总: {summary_text()}")
  166. return 0
  167. flush(connection)
  168. watermark_time = rows[-1]["sample_time"]
  169. watermark_id = int(rows[-1]["id"])
  170. finally:
  171. try:
  172. connection.close()
  173. except Exception:
  174. pass
  175. except KeyboardInterrupt:
  176. if write_buffer and not args.dry_run:
  177. try:
  178. with predict.get_connection() as connection:
  179. flush_status(connection, write_buffer)
  180. except Exception:
  181. pass
  182. for connection in reader_connections:
  183. try:
  184. connection.close()
  185. except Exception:
  186. pass
  187. print(f"\n已中断。{summary_text()}")
  188. if watermark_time is not None:
  189. print(f"续跑命令: python predict_re.py --start-time \"{watermark_time}\" --start-id {watermark_id}")
  190. return 0
  191. for connection in reader_connections:
  192. try:
  193. connection.close()
  194. except Exception:
  195. pass
  196. return 0
  197. if __name__ == "__main__":
  198. sys.exit(main())