poolcoach / tests /test_synth_eval.py
masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
8.43 kB
# -*- coding: utf-8 -*-
"""Khoá harness eval synthetic (BG26 bước 3): GT mapping dấu trục (đúng
chiều probe a>0 → side-L), khoảng cách góc vòng tròn, null tính riêng không
pha vào accuracy, và baseline_predict chạy trọn trên một cú bẩn tự dựng."""
from __future__ import annotations
import math
import sys
from pathlib import Path
import numpy as np
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts" / "broadcast"))
import eval_baseline_synth as ev # noqa: E402
import gen_synth_shots as gs # noqa: E402
def _shot(**kw):
base = {"label_v0": 2.0, "label_phi": 90.0, "label_a": 0.0,
"label_b": 0.0, "v0_ball": 2.6, "phi_ball": 90.0,
"identifiable": 1, "fps": 30, "upconvert": 0, "scratch": 0,
"shot_idx": 0}
base.update(kw)
return base
def test_gt_mapping_dau_truc():
"""a>0 = đánh mép TRÁI → side-L (probe settle-chord BG26b, khớp docstring
_read_spin); b>0 = follow. Ngưỡng nhỏ B_STUN_MAX/A_SIDE_MIN → stun/
neutral."""
assert ev.gt_axes(_shot(label_b=0.3, label_a=0.3)) == ("follow", "side-L")
assert ev.gt_axes(_shot(label_b=-0.3, label_a=-0.3)) == ("draw", "side-R")
assert ev.gt_axes(_shot(label_b=0.05, label_a=0.05)) == ("stun", None)
assert ev.gt_axes(_shot(label_b=ev.B_STUN_MAX + 1e-6,
label_a=-ev.A_SIDE_MIN - 1e-6)) \
== ("follow", "side-R")
def test_khoang_cach_goc_vong_tron():
assert ev.circ_diff_deg(359.0, 1.0) == pytest.approx(2.0)
assert ev.circ_diff_deg(1.0, 359.0) == pytest.approx(2.0)
assert ev.circ_diff_deg(180.0, 0.0) == pytest.approx(180.0)
assert ev.circ_diff_deg(90.0, 90.0) == 0.0
def test_summarize_null_tinh_rieng():
"""2 cú đọc được (1 đúng 1 sai) + 2 cú null → acc = 1/2 (KHÔNG phải
1/4), null_rate = 1/2 — null không đổ vào sai (BRIEF bước 3.2)."""
recs = []
for i, (pred_side, gt_side) in enumerate([
("side-L", "side-L"), ("side-R", "side-L"),
("", "side-L"), ("", "side-L")]):
recs.append({"shot_idx": i, "label_v0": 2.0, "label_phi": 0.0,
"label_a": 0.3, "label_b": 0.0, "v0_ball": 2.6,
"phi_ball": 0.0, "identifiable": 1, "fps": 30,
"upconvert": 0, "scratch": 0, "gt_vert": "stun",
"gt_side": gt_side, "pred_v0": None,
"pred_v0_raw": None, "pred_phi": None,
"pred_vert": "", "pred_side": pred_side,
"pred_conf": "", "n_collisions": 1, "v0_relerr": None,
"v0_relerr_raw": None, "dphi": None})
s = ev.summarize(recs)
assert s["side_acc"] == pytest.approx(0.5)
assert s["side_null_rate"] == pytest.approx(0.5)
assert s["n_side_scored"] == 2
assert s["v0_null_rate"] == 1.0
def _rec(i, ident=1, gt_side="side-L", gt_vert="follow",
pred_side="side-L", pred_vert="follow", dphi=1.0, v0r=0.05):
return {"shot_idx": i, "identifiable": ident, "gt_side": gt_side,
"gt_vert": gt_vert, "pred_side": pred_side,
"pred_vert": pred_vert, "dphi": dphi, "v0_relerr_raw": v0r}
def test_write_gate_csv_tap_con_va_bar(tmp_path):
"""Bảng gate BG27: tập con side/vert lấy đúng shot_idx baseline đọc
được; bar tuyệt đối chấm trên net; số baseline TÍNH LẠI từ CSV; lệch
số mốc tập con → DỪNG (SystemExit)."""
import csv as _csv
base_csv = tmp_path / "base.csv"
fields = ["shot_idx", "identifiable", "gt_side", "gt_vert",
"pred_side", "pred_vert", "dphi", "v0_relerr_raw"]
base_rows = [
# cú 0: baseline đọc side ĐÚNG + vert SAI
{"shot_idx": 0, "identifiable": 1, "gt_side": "side-L",
"gt_vert": "follow", "pred_side": "side-L", "pred_vert": "stun",
"dphi": 50.0, "v0_relerr_raw": 0.5},
# cú 1: baseline null cả hai trục (không vào tập con)
{"shot_idx": 1, "identifiable": 1, "gt_side": "side-R",
"gt_vert": "draw", "pred_side": "", "pred_vert": "",
"dphi": 80.0, "v0_relerr_raw": 0.7},
# cú 2: non-identifiable — không được vào tập con dù có pred
{"shot_idx": 2, "identifiable": 0, "gt_side": "side-L",
"gt_vert": "stun", "pred_side": "side-L", "pred_vert": "stun",
"dphi": "", "v0_relerr_raw": ""},
]
with open(base_csv, "w", newline="", encoding="utf-8") as f:
w = _csv.DictWriter(f, fieldnames=fields)
w.writeheader()
w.writerows(base_rows)
# net: cú 0 side đúng vert đúng; cú 1 side sai vert đúng; cú 2 bỏ qua
recs = [_rec(0), _rec(1, gt_side="side-R", gt_vert="draw",
pred_side="side-L", pred_vert="draw",
dphi=3.0, v0r=0.15),
_rec(2, ident=0, dphi=None, v0r=None)]
rows = ev.write_gate_csv(recs, base_csv, tmp_path / "gate.csv",
n_side=1, n_vert=1)
g = {r["metric"]: r for r in rows}
# tập con side/vert = đúng 1 cú (shot 0); net đúng cả hai trên cú đó
assert g["side_acc"]["net_on_subset"] == 1.0
assert g["vert_acc"]["net_on_subset"] == 1.0
assert g["vert_acc"]["baseline"] == 0.0 # baseline sai vert cú 0
# bar tuyệt đối: side toàn identifiable = 1/2; vert = 2/2
assert g["side_acc"]["net_abs"] == pytest.approx(0.5)
assert g["vert_acc"]["net_abs"] == pytest.approx(1.0)
assert g["side_acc"]["dat_abs"] == 0
assert g["vert_acc"]["dat_abs"] == 1
# median net trên all: dphi (1+3)/2 = 2 → ĐẠT bar ≤2; baseline 65 → thắng
assert g["dphi_med_deg"]["net_abs"] == pytest.approx(2.0)
assert g["dphi_med_deg"]["dat_abs"] == 1 and \
g["dphi_med_deg"]["dat_beat"] == 1
# lệch số mốc tập con → DỪNG
with pytest.raises(SystemExit):
ev.write_gate_csv(recs, base_csv, tmp_path / "gate2.csv",
n_side=630, n_vert=186)
assert (tmp_path / "gate.csv").exists()
def test_lat_target_matched():
"""Filter lát target-matched (BRIEF 28 bước 1): chạm đầu = min(t_first_bb,
t_first_cush) và cú thuộc lát khi ≥ 0.3s; không chạm (cả hai NaN) thì
KHÔNG thuộc lát — nó không có chạm đầu."""
nan = float("nan")
assert ev.t_first_contact_s(_shot(t_first_bb=0.5, t_first_cush=0.8)) == 0.5
assert math.isnan(ev.t_first_contact_s(_shot(t_first_bb=nan,
t_first_cush=nan)))
# cả hai chạm: lấy MIN — bb 0.25 kéo cú ra khỏi lát dù cush 0.6
assert not ev.is_target_matched(_shot(t_first_bb=0.25, t_first_cush=0.6))
assert ev.is_target_matched(_shot(t_first_bb=0.5, t_first_cush=0.8))
# chỉ một loại chạm cũng tính
assert ev.is_target_matched(_shot(t_first_bb=nan, t_first_cush=0.31))
assert not ev.is_target_matched(_shot(t_first_bb=0.1, t_first_cush=nan))
# biên: đúng 0.3 thuộc lát (>=); không chạm → ngoài lát
assert ev.is_target_matched(_shot(t_first_bb=0.3, t_first_cush=nan))
assert not ev.is_target_matched(_shot(t_first_bb=nan, t_first_cush=nan))
def test_baseline_predict_tren_cu_ban_tu_dung(monkeypatch):
"""Integration nhỏ: cú thẳng 2 m/s hướng +y làm bẩn bằng chính
corrupt_shot → baseline đọc v0_raw ≈ 2, phi ≈ 90, quy đổi v0 = raw/K_V;
không va chạm → spin null cả hai trục."""
monkeypatch.setattr(gs, "GAP_RATE_PER_BALL", 0.0)
rng = np.random.default_rng(42)
t_sim = np.arange(0.0, 1.2, 0.01)
sp = np.maximum(2.0 - 0.35 * t_sim, 0.0) # giảm tốc lăn nhẹ
y = 0.4 + np.cumsum(sp) * 0.01
xy_sim = np.stack([np.full_like(t_sim, 0.6), y], axis=1)[:, None, :]
out = gs.corrupt_shot(rng, t_sim, xy_sim, np.array([np.inf]),
50, False, 0.8, 0.0)
shot = _shot(n_frames=len(out["t"]), n_balls=1)
shot.update({"t": out["t"], "xy": out["xy"], "covered": out["covered"],
"img_diff": out["img_diff"]})
p = ev.baseline_predict(shot)
assert p["v0_raw"] == pytest.approx(2.0, abs=0.25)
assert p["v0"] == pytest.approx(p["v0_raw"] / ev.K_V)
assert p["phi"] == pytest.approx(90.0, abs=5.0)
assert p["vert"] is None and p["side"] is None