validate_samples.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. """Sample validation for the generated per-cycle pretraining samples.
  2. Cross-checks a sampled subset of the generated .npy files against the database:
  3. 1. 周期数核对: npy 文件数与 manifest 中的 cycle_count 一致。
  4. 2. 顺序核对: 每个 npy 内的信号列与 DB 中 [start, end) 区间的原始行完全一致,
  5. 且 sample_index 逐行递增(时间顺序未打乱)。
  6. 3. 边界核对: 周期起点/终点处的 second_value >= 30(键相触发),
  7. 第 3、4 列全为 1,信号列为有限值。
  8. 4. 命名核对: 文件名中的 sample_index 起止与 DB 实测起止一致。
  9. Usage: python3 validate_samples.py [--all] [--max-files N]
  10. """
  11. import argparse
  12. import csv
  13. import re
  14. import sys
  15. from pathlib import Path
  16. import numpy as np
  17. from tqdm import tqdm
  18. ROOT = Path(__file__).resolve().parents[1]
  19. sys.path.insert(0, str(ROOT))
  20. from app.algorithms.cycles import detect_cycles # noqa: E402
  21. from app.db import get_connection # noqa: E402
  22. HERE = Path(__file__).resolve().parent
  23. OUTPUT_DIR = HERE / "inputdatas"
  24. MANIFEST_PATH = HERE / "manifest.csv"
  25. NAME_RE = re.compile(r"^(\d+)_(\d+)_(\d+)_(\d+)\.npy$")
  26. def load_manifest() -> list[dict[str, str]]:
  27. with MANIFEST_PATH.open(encoding="utf-8-sig") as handle:
  28. return list(csv.DictReader(handle))
  29. def list_npy_files(folder: Path, file_id: int | None = None) -> list[Path]:
  30. pattern = f"{file_id}_*.npy" if file_id is not None else "*.npy"
  31. return sorted(folder.glob(pattern))
  32. def read_wave_samples(connection, file_id: int) -> np.ndarray:
  33. with connection.cursor() as cursor:
  34. cursor.execute(
  35. """
  36. SELECT sample_index, signal_value, second_value
  37. FROM wave_sample
  38. WHERE wave_file_id = %s
  39. ORDER BY wave_file_id, sample_index
  40. """,
  41. (file_id,),
  42. )
  43. rows = cursor.fetchall()
  44. return np.asarray(
  45. [
  46. (
  47. float(row["sample_index"]),
  48. float(row["signal_value"]),
  49. float(row["second_value"]) if row["second_value"] is not None else np.nan,
  50. )
  51. for row in rows
  52. ],
  53. dtype=float,
  54. )
  55. def validate_file(connection, manifest_row: dict[str, str]) -> list[str]:
  56. problems: list[str] = []
  57. file_id = int(manifest_row["wave_file_id"])
  58. folder = OUTPUT_DIR / manifest_row["group"]
  59. files = sorted(folder.glob(f"{file_id}_*.npy"))
  60. expected_count = int(manifest_row["cycle_count"] or 0)
  61. if len(files) != expected_count:
  62. problems.append(
  63. f"周期数不符: manifest={expected_count}, 实际 npy 文件={len(files)}"
  64. )
  65. return problems
  66. samples = read_wave_samples(connection, file_id)
  67. cycles, _ = detect_cycles(samples)
  68. if len(cycles) != expected_count:
  69. problems.append(f"重算周期数不符: detect_cycles={len(cycles)}, manifest={expected_count}")
  70. by_number: dict[int, Path] = {}
  71. parsed: list[tuple[int, int, int]] = []
  72. for path in files:
  73. match = NAME_RE.match(path.name)
  74. if not match:
  75. problems.append(f"文件名不符合规范: {path.name}")
  76. continue
  77. file_id_name, cycle_number, start_name, end_name = (int(part) for part in match.groups())
  78. if file_id_name != file_id:
  79. problems.append(f"文件名 id 不符: {path.name}")
  80. by_number[cycle_number] = path
  81. parsed.append((cycle_number, start_name, end_name))
  82. for cycle in cycles:
  83. start_sample_index, end_exclusive = (
  84. int(samples[cycle.start_offset, 0]),
  85. int(samples[cycle.end_offset, 0]) if cycle.end_offset < len(samples) else int(samples[-1, 0]) + 1,
  86. )
  87. path = by_number.get(cycle.number)
  88. if path is None:
  89. problems.append(f"周期 {cycle.number} 缺少 npy 文件")
  90. continue
  91. expected_name = f"{file_id}_{cycle.number}_{start_sample_index}_{end_exclusive}.npy"
  92. if path.name != expected_name:
  93. problems.append(
  94. f"周期 {cycle.number} 文件名不符: 应为 {expected_name}, 实际 {path.name}"
  95. )
  96. matrix = np.load(path)
  97. if matrix.ndim != 2 or matrix.shape[1] != 4:
  98. problems.append(f"周期 {cycle.number} 矩阵形状异常: {matrix.shape}")
  99. continue
  100. if not np.all(np.isfinite(matrix[:, :2])):
  101. problems.append(f"周期 {cycle.number} 信号列存在非有限值")
  102. signal = samples[cycle.start_offset : cycle.end_offset, 1]
  103. second = samples[cycle.start_offset : cycle.end_offset, 2]
  104. if matrix.shape[0] != len(signal):
  105. problems.append(
  106. f"周期 {cycle.number} 行数不符: npy={matrix.shape[0]}, DB={len(signal)}"
  107. )
  108. if not np.array_equal(matrix[:, 0], signal):
  109. problems.append(f"周期 {cycle.number} signal_value 与 DB 不一致")
  110. if not np.array_equal(matrix[:, 1], second):
  111. problems.append(f"周期 {cycle.number} second_value 与 DB 不一致")
  112. if not np.all(matrix[:, 2] == 1) or not np.all(matrix[:, 3] == 1):
  113. problems.append(f"周期 {cycle.number} 扩展列不为 1")
  114. indices = samples[cycle.start_offset : cycle.end_offset, 0]
  115. if len(indices) > 1 and np.any(np.diff(indices) <= 0):
  116. problems.append(f"周期 {cycle.number} sample_index 未严格递增")
  117. boundary_start = samples[cycle.start_offset, 2]
  118. boundary_end = samples[cycle.end_offset, 2]
  119. if boundary_start < 30 or boundary_end < 30:
  120. problems.append(
  121. f"周期 {cycle.number} 零标记边界未达触发阈值 30 (start={boundary_start:.3f}, end={boundary_end:.3f})"
  122. )
  123. return problems
  124. def main() -> int:
  125. parser = argparse.ArgumentParser(description="抽样验证预训练样本数据")
  126. parser.add_argument("--all", action="store_true", help="验证全部文件而非抽样")
  127. parser.add_argument("--max-files", type=int, default=20, help="抽样文件数上限(默认 20)")
  128. args = parser.parse_args()
  129. manifest = [row for row in load_manifest() if not row.get("error")]
  130. if not manifest:
  131. print("manifest 中没有成功记录,请先运行 generate_samples.py")
  132. return 1
  133. if args.all:
  134. sampled = manifest
  135. else:
  136. step = max(1, len(manifest) // args.max_files)
  137. sampled = manifest[::step][: args.max_files]
  138. if len(sampled) < len(manifest):
  139. sampled.append(manifest[-1])
  140. print(f"抽样验证 {len(sampled)}/{len(manifest)} 个文件: " + ", ".join(item["wave_file_id"] for item in sampled))
  141. problems_by_id: dict[str, list[str]] = {}
  142. connection = get_connection()
  143. try:
  144. for row in tqdm(sampled, desc="验证样本", unit="文件"):
  145. problems = validate_file(connection, row)
  146. if problems:
  147. problems_by_id[row["wave_file_id"]] = problems
  148. print(f" id={row['wave_file_id']} ({row['group']}): {len(problems)} 个问题")
  149. for problem in problems[:8]:
  150. print(f" - {problem}")
  151. finally:
  152. connection.close()
  153. checked = sum(1 for item in sampled if item["wave_file_id"] not in problems_by_id)
  154. print(
  155. f"\n结果: {checked}/{len(sampled)} 个文件完全通过"
  156. if not problems_by_id
  157. else f"\n结果: {checked}/{len(sampled)} 通过, {len(problems_by_id)} 个文件存在问题"
  158. )
  159. return 1 if problems_by_id else 0
  160. if __name__ == "__main__":
  161. sys.exit(main())