poolcoach / tests /test_diag_cushion_offset.py
masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
5.73 kB
# -*- coding: utf-8 -*-
"""Unit test hàm đo sự kiện băng của BG30 (diag_cushion_offset.py).
Kiểm HÀM ĐO trên track tổng hợp — không đụng dữ liệu pilot thật, không cần
cv2/torch (module chẩn đoán thuần numpy). Import kiểu script (repo không cài
package): đẩy scripts/broadcast vào path như mọi launcher.
"""
from __future__ import annotations
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 diag_cushion_offset as dg # noqa: E402
R = dg.R
W, L = dg.W, dg.L
FPS = 30.0
def _bounce_track(rail: str = "x0", apex_m: float = 0.030,
v_mps: float = 0.8, dur_s: float = 2.0):
"""Track tổng hợp: bi chạy VUÔNG GÓC vào một mép, đảo chiều tại apex
đã biết (apex rơi ĐÚNG một mốc frame → không lượng tử hoá), rồi lùi ra.
Trả (t, xy) đủ shape cho find_rail_events."""
t = np.arange(0.0, dur_s, 1 / FPS)
t_hit = dur_s / 2
d = apex_m + v_mps * np.abs(t - t_hit) # khoảng cách tới mép
along = 0.9 # toạ độ dọc mép cố định
if rail == "x0":
xy = np.stack([d, np.full_like(d, along)], axis=1)
elif rail == "xW":
xy = np.stack([W - d, np.full_like(d, along)], axis=1)
elif rail == "y0":
xy = np.stack([np.full_like(d, 0.5), d], axis=1)
else:
xy = np.stack([np.full_like(d, 0.5), L - d], axis=1)
return t, xy
def test_tim_dung_su_kien_va_apex():
t, xy = _bounce_track("x0", apex_m=0.030, v_mps=0.8)
ev = dg.find_rail_events(t, xy, t_from=0.0, dt_med=1 / FPS)
assert len(ev) == 1
e = ev[0]
assert e["rail"] == "x0"
# apex nằm đúng mốc frame → đo đúng tới làm tròn 0.1mm
assert e["apex_mm"] == pytest.approx(30.0, abs=0.11)
assert e["offset_mm"] == pytest.approx(30.0 - R * 1000, abs=0.11)
assert e["v_perp_in_mps"] == pytest.approx(0.8, rel=0.15)
assert e["gap_flag"] == 0
def test_apex_giua_hai_frame_chi_do_thua_khong_do_thieu():
# dịch pha lấy mẫu nửa frame: apex thật rơi GIỮA hai mốc → apex đo
# PHẢI >= apex thật (lượng tử hoá một phía), thừa <= v·dt/2 + epsilon
t = np.arange(0.0, 2.0, 1 / FPS) + 0.5 / FPS
t_hit, v, apex = 1.0, 0.8, 0.030
d = apex + v * np.abs(t - t_hit)
xy = np.stack([d, np.full_like(d, 0.9)], axis=1)
ev = dg.find_rail_events(t, xy, t_from=0.0, dt_med=1 / FPS)
assert len(ev) == 1
du = ev[0]["apex_mm"] - apex * 1000
assert -0.11 <= du <= v * (1 / FPS) / 2 * 1000 + 0.11
def test_khong_bat_su_kien_khi_chay_doc_mep():
# bi chạy SONG SONG mép ở khoảng cách 40mm — không đảo chiều vuông góc
t = np.arange(0.0, 2.0, 1 / FPS)
xy = np.stack([np.full_like(t, 0.040), 0.3 + 0.8 * t], axis=1)
assert dg.find_rail_events(t, xy, 0.0, 1 / FPS) == []
def test_dedup_hai_cuc_tieu_sat_nhau_thanh_mot():
# nhiễu tạo 2 cực tiểu cách 1 frame quanh cùng một cú chạm → 1 sự kiện
t, xy = _bounce_track("y0", apex_m=0.030, v_mps=0.8)
i = int(np.argmin(xy[:, 1]))
xy[i - 1, 1] = xy[i, 1] + 0.0002 # cực tiểu phụ ngay cạnh
ev = dg.find_rail_events(t, xy, 0.0, 1 / FPS)
assert len(ev) == 1
def test_co_bi_tinh_va_co_mieng_lo():
t, xy = _bounce_track("x0", apex_m=0.030, v_mps=0.8)
ev = dg.find_rail_events(t, xy, 0.0, 1 / FPS)
# bi tĩnh ngay cạnh apex → ball_flag; không có → 0
dg.flag_events(ev, statics=[(0.05, 0.92)])
assert ev[0]["ball_flag"] == 1
dg.flag_events(ev, statics=[])
assert ev[0]["ball_flag"] == 0
assert ev[0]["pocket_flag"] == 0 # along=0.9 xa mọi miệng lỗ
# sự kiện sát góc bàn → pocket_flag
t2, xy2 = _bounce_track("x0", apex_m=0.030, v_mps=0.8)
xy2[:, 1] = 0.05 # dọc mép, ngay góc (0,0)
ev2 = dg.find_rail_events(t2, xy2, 0.0, 1 / FPS)
dg.flag_events(ev2, statics=[])
assert ev2 and ev2[0]["pocket_flag"] == 1
def test_to_canonical_xoay_180():
e = {"x_m": 0.1, "y_m": 0.2, "rail": "y0"}
assert dg.to_canonical(e, flip=False) == (0.1, 0.2, "y0")
x, y, rail = dg.to_canonical(e, flip=True)
assert (round(x, 4), round(y, 4), rail) == (W - 0.1, L - 0.2, "yL")
assert dg.to_canonical({"x_m": 0.0, "y_m": 0.0, "rail": "x0"},
True)[2] == "xW"
def test_k_coeff_dau_theo_mat_phang_click():
h = 2.4
assert dg.k_coeff(h, 0.0) > 0 # mặt vải: lệch RA XA camera
assert dg.k_coeff(h, 0.040) < 0 # click trên mặt gỗ > R: đổi dấu
assert dg.k_coeff(h, 0.0) == pytest.approx(R / (h - R))
def test_event_proj_dau_bang_gan_va_xa():
cam = np.array([W / 2, L + 2.5, 2.4]) # camera hệ chuẩn, phía y > L
k0 = dg.k_coeff(2.4, 0.0)
xy = np.array([[0.5, L - 0.05], [0.5, 0.05]])
proj = dg.event_proj_m(cam, xy, ["yL", "y0"], k0)
assert proj[0] > 0 # băng GẦN camera: apex đọc THỪA
assert proj[1] < 0 # băng XA: lệch chui VÀO băng
assert proj[0] == pytest.approx(cam[1] - (L - R), abs=0.01)
def test_fit_session_zc_khoi_phuc_k_da_biet():
h, k_true = 2.41, 0.0095
proj = np.array([2.5, 2.5, -0.6, -0.6, -0.65])
rng = np.random.default_rng(20260813)
offs = k_true * proj + rng.normal(0.0, 0.0005, len(proj))
k_fit, zc = dg.fit_session_zc(h, proj, offs, np.ones(len(proj)))
assert k_fit == pytest.approx(k_true, rel=0.1)
assert zc == pytest.approx((R - k_true * (h - R)) * 1000, abs=1.5)