Spaces:
Sleeping
Sleeping
File size: 8,429 Bytes
78738de | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | # -*- 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
|