Spaces:
Sleeping
Sleeping
| # -*- 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) | |