# -*- 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)