fix: address verify findings - add visualize subcommand, curl healthcheck, IoU in JSON, 50KB threshold
This commit is contained in:
+3
-3
@@ -13,7 +13,7 @@ docker compose up -d
|
|||||||
|
|
||||||
echo "[2/5] Waiting for TDengine to be healthy..."
|
echo "[2/5] Waiting for TDengine to be healthy..."
|
||||||
for i in $(seq 1 60); do
|
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)"
|
echo "TDengine ready (attempt $i)"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
@@ -30,8 +30,8 @@ for png in render/*.png; do
|
|||||||
if [ -f "$png" ]; then
|
if [ -f "$png" ]; then
|
||||||
sz=$(stat -c%s "$png" 2>/dev/null || stat -f%z "$png" 2>/dev/null || echo 0)
|
sz=$(stat -c%s "$png" 2>/dev/null || stat -f%z "$png" 2>/dev/null || echo 0)
|
||||||
echo " $png: ${sz} bytes"
|
echo " $png: ${sz} bytes"
|
||||||
if [ "$sz" -lt 10240 ]; then
|
if [ "$sz" -lt 51200 ]; then
|
||||||
echo " WARNING: PNG too small (< 10KB)"
|
echo " WARNING: PNG too small (< 50KB)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ def test_e2e_generates_png_and_json():
|
|||||||
pngs = list((project_dir / "render").glob("*.png"))
|
pngs = list((project_dir / "render").glob("*.png"))
|
||||||
assert len(pngs) >= 1, f"no PNGs found, stdout: {r.stdout[-500:]}"
|
assert len(pngs) >= 1, f"no PNGs found, stdout: {r.stdout[-500:]}"
|
||||||
for png in pngs:
|
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 日志
|
# 验证 JSON 日志
|
||||||
logs = list((project_dir / "logs").glob("e2e_*.json"))
|
logs = list((project_dir / "logs").glob("e2e_*.json"))
|
||||||
|
|||||||
@@ -79,6 +79,33 @@ def cmd_e2e(args):
|
|||||||
return run_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):
|
def cmd_watch(args):
|
||||||
"""watch 子命令:TMQ 准实时监控。"""
|
"""watch 子命令:TMQ 准实时监控。"""
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
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("--log-dir", default="logs", help="JSON 日志目录 (default: logs)")
|
||||||
p_e2e.add_argument("--url", default="ws://root:taosdata@localhost:6041", help="TDengine WebSocket URL")
|
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)
|
# watch (Iter-2)
|
||||||
p_watch = sub.add_parser("watch", help="TMQ 准实时异常监控")
|
p_watch = sub.add_parser("watch", help="TMQ 准实时异常监控")
|
||||||
p_watch.add_argument("--stable", required=True, help="supertable 名称")
|
p_watch.add_argument("--stable", required=True, help="supertable 名称")
|
||||||
@@ -151,6 +187,7 @@ def main():
|
|||||||
"detect-batch": cmd_detect_batch,
|
"detect-batch": cmd_detect_batch,
|
||||||
"forecast-anomaly": cmd_forecast_anomaly,
|
"forecast-anomaly": cmd_forecast_anomaly,
|
||||||
"e2e": cmd_e2e,
|
"e2e": cmd_e2e,
|
||||||
|
"visualize": cmd_visualize,
|
||||||
"watch": cmd_watch,
|
"watch": cmd_watch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-1
@@ -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.io_csv import read_csv
|
||||||
from ts_anomaly_td.detection import detect_all_algos, ALL_ALGOS
|
from ts_anomaly_td.detection import detect_all_algos, ALL_ALGOS
|
||||||
from ts_anomaly_td.forecast import forecast_anomaly
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -94,7 +94,31 @@ def run_e2e(args):
|
|||||||
"anomaly_count": fc_anom_count,
|
"anomaly_count": fc_anom_count,
|
||||||
},
|
},
|
||||||
"png": str(png_path),
|
"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():
|
for algo, r in det_results.items():
|
||||||
ds_summary["algorithms"][algo] = {
|
ds_summary["algorithms"][algo] = {
|
||||||
"window_count": len(r["windows"]),
|
"window_count": len(r["windows"]),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def create_supertable(conn, stable: str) -> None:
|
|||||||
"""
|
"""
|
||||||
sql = (
|
sql = (
|
||||||
f"CREATE STABLE IF NOT EXISTS ds_{stable} "
|
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)"
|
f"TAGS (series_id INT)"
|
||||||
)
|
)
|
||||||
conn.execute_no_result(sql)
|
conn.execute_no_result(sql)
|
||||||
|
|||||||
Reference in New Issue
Block a user