From ad2a8ef68716deaf81243fe945314b2d27604f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=97=E5=B9=B3?= Date: Thu, 11 Jun 2026 18:31:14 +0800 Subject: [PATCH] fix: address verify findings - add visualize subcommand, curl healthcheck, IoU in JSON, 50KB threshold --- scripts/run_e2e.sh | 6 +++--- tests/test_e2e.py | 2 +- ts_anomaly_td/cli.py | 37 +++++++++++++++++++++++++++++++++++++ ts_anomaly_td/e2e.py | 26 +++++++++++++++++++++++++- ts_anomaly_td/schema.py | 2 +- 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/scripts/run_e2e.sh b/scripts/run_e2e.sh index b4db834..9b6e0f8 100755 --- a/scripts/run_e2e.sh +++ b/scripts/run_e2e.sh @@ -13,7 +13,7 @@ docker compose up -d echo "[2/5] Waiting for TDengine to be healthy..." for i in $(seq 1 60); do - if docker compose exec -T tdengine taos -s "SELECT SERVER_VERSION()" > /dev/null 2>&1; then + if curl -sf http://localhost:6041/rest/sql -d 'select server_version()' > /dev/null 2>&1; then echo "TDengine ready (attempt $i)" break fi @@ -30,8 +30,8 @@ for png in render/*.png; do if [ -f "$png" ]; then sz=$(stat -c%s "$png" 2>/dev/null || stat -f%z "$png" 2>/dev/null || echo 0) echo " $png: ${sz} bytes" - if [ "$sz" -lt 10240 ]; then - echo " WARNING: PNG too small (< 10KB)" + if [ "$sz" -lt 51200 ]; then + echo " WARNING: PNG too small (< 50KB)" fi fi done diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 1c04402..d819747 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -40,7 +40,7 @@ def test_e2e_generates_png_and_json(): pngs = list((project_dir / "render").glob("*.png")) assert len(pngs) >= 1, f"no PNGs found, stdout: {r.stdout[-500:]}" for png in pngs: - assert png.stat().st_size > 10240, f"{png.name} too small: {png.stat().st_size} bytes" + assert png.stat().st_size > 51200, f"{png.name} too small: {png.stat().st_size} bytes" # 验证 JSON 日志 logs = list((project_dir / "logs").glob("e2e_*.json")) diff --git a/ts_anomaly_td/cli.py b/ts_anomaly_td/cli.py index efc858b..20b7143 100644 --- a/ts_anomaly_td/cli.py +++ b/ts_anomaly_td/cli.py @@ -79,6 +79,33 @@ def cmd_e2e(args): return run_e2e(args) +def cmd_visualize(args): + """visualize 子命令:渲染双栏对比图。""" + logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") + + from ts_anomaly_td.io_csv import read_csv + from ts_anomaly_td.visualize import render_contrast + + ts, vals, labels = read_csv(args.gt_csv) + + conn = TDConnection(args.url) + try: + db = args.db or "ts_anomaly" + conn.execute_no_result(f"USE {db}") + + if args.algos: + from ts_anomaly_td.detection import detect_all_algos + results = detect_all_algos(conn, args.stable, algos=args.algos.split(",")) + else: + from ts_anomaly_td.detection import detect_all_algos + results = detect_all_algos(conn, args.stable) + + render_contrast(ts, vals, labels, results, args.output, title=args.stable) + logger.info("visualize: %s", args.output) + finally: + conn.close() + + def cmd_watch(args): """watch 子命令:TMQ 准实时监控。""" logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") @@ -132,6 +159,15 @@ def build_parser() -> argparse.ArgumentParser: p_e2e.add_argument("--log-dir", default="logs", help="JSON 日志目录 (default: logs)") p_e2e.add_argument("--url", default="ws://root:taosdata@localhost:6041", help="TDengine WebSocket URL") + # visualize + p_viz = sub.add_parser("visualize", help="渲染 GT 对比可视化图") + p_viz.add_argument("--gt-csv", required=True, help="GT CSV 文件路径") + p_viz.add_argument("--stable", required=True, help="supertable 名称") + p_viz.add_argument("--db", default="ts_anomaly", help="数据库名") + p_viz.add_argument("--algos", default=None, help="逗号分隔算法列表 (默认全 6 个)") + p_viz.add_argument("--output", required=True, help="PNG 输出路径") + p_viz.add_argument("--url", default="ws://root:taosdata@localhost:6041", help="TDengine WebSocket URL") + # watch (Iter-2) p_watch = sub.add_parser("watch", help="TMQ 准实时异常监控") p_watch.add_argument("--stable", required=True, help="supertable 名称") @@ -151,6 +187,7 @@ def main(): "detect-batch": cmd_detect_batch, "forecast-anomaly": cmd_forecast_anomaly, "e2e": cmd_e2e, + "visualize": cmd_visualize, "watch": cmd_watch, } diff --git a/ts_anomaly_td/e2e.py b/ts_anomaly_td/e2e.py index 95799bf..6a79a3d 100644 --- a/ts_anomaly_td/e2e.py +++ b/ts_anomaly_td/e2e.py @@ -11,7 +11,7 @@ from ts_anomaly_td.schema import setup_schema from ts_anomaly_td.io_csv import read_csv from ts_anomaly_td.detection import detect_all_algos, ALL_ALGOS from ts_anomaly_td.forecast import forecast_anomaly -from ts_anomaly_td.visualize import render_contrast +from ts_anomaly_td.visualize import render_contrast, _compute_iou logger = logging.getLogger(__name__) @@ -94,7 +94,31 @@ def run_e2e(args): "anomaly_count": fc_anom_count, }, "png": str(png_path), + "iou": None, } + + # 计算 GT windows(从 labels 提取) + gt_windows_for_iou = [] + in_anom = False + anom_start = None + for i, l in enumerate(labels): + if l == 1 and not in_anom: + in_anom = True + anom_start = ts[i] + elif l == 0 and in_anom: + in_anom = False + gt_windows_for_iou.append((anom_start, ts[i])) + if in_anom: + gt_windows_for_iou.append((anom_start, ts[-1])) + + # 收集检测 windows + all_det_windows = [] + for r in det_results.values(): + if not r.get("error"): + all_det_windows.extend(r.get("windows", [])) + + iou = _compute_iou(gt_windows_for_iou, all_det_windows) + ds_summary["iou"] = round(iou, 3) for algo, r in det_results.items(): ds_summary["algorithms"][algo] = { "window_count": len(r["windows"]), diff --git a/ts_anomaly_td/schema.py b/ts_anomaly_td/schema.py index 5dce142..c9fc3dd 100644 --- a/ts_anomaly_td/schema.py +++ b/ts_anomaly_td/schema.py @@ -24,7 +24,7 @@ def create_supertable(conn, stable: str) -> None: """ sql = ( f"CREATE STABLE IF NOT EXISTS ds_{stable} " - f"(ts TIMESTAMP, `value` DOUBLE, is_anomaly INT) " + f"(ts TIMESTAMP, `value` DOUBLE, `label` INT) " f"TAGS (series_id INT)" ) conn.execute_no_result(sql)