Spaces:
Sleeping
Sleeping
File size: 8,797 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 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | """G6.1 + G6.2 — chứng minh RENDER LƯỜI đúng là lười, và không đổi quỹ đạo.
31/07/2026: app chỉ còn MỘT đường (zone V2) — bản tham chiếu gọi thẳng
``recommend_v2`` với ``n_render`` đủ mọi cú; không còn vòng lặp engine.
Chạy (venv, từ gốc repo):
python scripts/check_lazy_render.py
python scripts/check_lazy_render.py --boards 1
Vì sao ở `scripts/` chứ không ở `tests/`: `conftest.py` cố ý nhét stub pooltool
RỖNG, nên test app không chạm được tầng vật lý — mà cả hai câu hỏi dưới đây đều
là câu hỏi về sim thật. Cùng lý do `check_api_drift.py` tồn tại ngoài `tests/`.
HAI PHÉP KIỂM:
G6.1 `/api/recommend` trả ĐÚNG MỘT cú kèm `trajectories`, và đó là cú rank 1.
Ba cú thay thế có đủ tham số nhưng `trajectories` rỗng.
G6.2 `/api/trajectory` trả quỹ đạo KHỚP quỹ đạo mà `n_render = 1 +
alternatives` sẽ trả cho CÙNG cú đó. So từng điểm, không dung sai: sim
tất định thì hoặc khớp tuyệt đối, hoặc hai đường đã khác nhau ở đâu đó.
Tham số cú gửi đi là tham số **lấy từ JSON của `/api/recommend`** — tức
đúng thứ FE có trong tay, không phải float gốc trong bộ nhớ server. Đó
là chỗ phép kiểm này có răng: bản trước 30/07 làm tròn `phi` 2 chữ số
và `v0` 3 chữ số, nên `v0` lưới 0.8888888888888888 xuống JSON thành
0.889 và lần sim lại chạy ở một ô KHÁC. Không có phép kiểm này thì sai
lệch đó chỉ hiện ra dưới dạng "cú thay thế vẽ hơi lệch", không ai bắt.
G6.2b `balls_final` của `/api/trajectory` khớp `outcome.balls_final` mà
`/api/recommend` đã trả cho cú đó. Vế này KHÁC G6.2 và cũng cần: FE
animate theo quỹ đạo lười rồi `applyFinal()` áp `outcome.balls_final`
(từ sim của search, không phải sim render). Hai số lệch nhau thì bi
NHẢY một đoạn ở khung hình cuối.
Thế bàn CỐ ĐỊNH, không random — cùng lý do `check_api_drift.BOARDS`: một thế
bàn ngẫu nhiên làm "chạy lại thấy xanh" không nói lên điều gì.
Lệch → DỪNG, ghi HANDOFF (BRIEF 30/07, mục "Nếu bí").
"""
from __future__ import annotations
import argparse
import io
import os
import sys
import time
from pathlib import Path
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8",
errors="replace", line_buffering=True)
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
sys.path.insert(0, str(ROOT))
BOARDS = [
("6 bi", {"cue": [0.50, 0.42], "1": [0.28, 0.95], "2": [0.66, 1.22],
"3": [0.80, 0.60], "5": [0.36, 1.55], "9": [0.55, 1.70]}),
("4 bi", {"cue": [0.45, 0.30], "1": [0.32, 1.09], "2": [0.74, 0.95],
"9": [0.50, 1.29]}),
]
ALTERNATIVES = 3
def _max_diff(a, b):
"""Sai khác lớn nhất giữa hai polyline cùng độ dài (mét)."""
return max(max(abs(p[0] - q[0]), abs(p[1] - q[1])) for p, q in zip(a, b))
def cmp_traj(tag, got, ref) -> bool:
"""So hai dict {ball_id: [[x, y], ...]}. In nguyên chỗ lệch nếu có."""
if set(got) != set(ref):
print(f" [LỆCH] {tag}: tập bi khác nhau — "
f"api={sorted(got)} ref={sorted(ref)}")
return False
bad = []
for bid in sorted(ref):
g, r = got[bid], ref[bid]
if len(g) != len(r):
bad.append(f"bi {bid}: {len(g)} điểm vs {len(r)}")
elif g != r:
bad.append(f"bi {bid}: lệch tối đa {_max_diff(g, r):.3e} m")
if bad:
print(f" [LỆCH] {tag}: " + " | ".join(bad))
return False
npt = sum(len(v) for v in ref.values())
print(f" [OK] {tag}: {len(ref)} bi, {npt} điểm — khớp TỪNG ĐIỂM")
return True
def cmp_final(tag, got, ref) -> bool:
"""So hai dict balls_final dạng JSON ({'x':..,'y':..} | None)."""
if set(got) != set(ref):
print(f" [LỆCH] {tag}: tập bi khác nhau")
return False
bad = [bid for bid in ref if got[bid] != ref[bid]]
if bad:
for bid in bad:
print(f" [LỆCH] {tag}: bi {bid} traj={got[bid]} recommend={ref[bid]}")
return False
print(f" [OK] {tag}: {len(ref)} bi khớp")
return True
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--boards", type=int, default=len(BOARDS))
args = ap.parse_args()
import numpy as np
from fastapi.testclient import TestClient
from app import main as app_main
print("[lazy] khởi động app (JIT Numba ~40s lần đầu)...", flush=True)
with TestClient(app_main.app) as client:
t0 = time.time()
while True:
h = client.get("/api/health").json()
if h["status"] == "error":
print(f"[lazy] app lỗi khởi động: {h['error']}")
return 2
if h["status"] == "ok":
break
if time.time() - t0 > 300:
print("[lazy] quá 300s vẫn chưa 'ok' — dừng")
return 2
time.sleep(2)
print(f"[lazy] app sẵn sàng sau {time.time() - t0:.1f}s", flush=True)
from poolcoach_rl.envs import PositionPlayEnv
from poolcoach_rl.recommend import recommend_v2
# env RIÊNG, không dùng lại `state.env_h` — cùng lý do check_api_drift:
# bằng chứng phải là "một script bất kỳ dựng env của nó", không phải
# "cùng một object đi qua hai lối vào".
env_h = PositionPlayEnv()
ok = True
for name, balls in BOARDS[:args.boards]:
print(f"\n[{name}] {len(balls) - 1} bi mục tiêu")
body = {"balls": {b: {"x": xy[0], "y": xy[1]}
for b, xy in balls.items()},
"alternatives": ALTERNATIVES}
r = client.post("/api/recommend", json=body)
if r.status_code != 200:
print(f" API {r.status_code}: {r.json().get('detail')}")
ok = False
continue
data = r.json()
shots = data["shots"]
if not shots:
print(" hết đường trên thế bàn này — bỏ qua "
f"(message: {data.get('message')!r})")
continue
# ---- G6.1
have = [i for i, s in enumerate(shots) if s["trajectories"]]
g61 = have == [0]
print(f" G6.1 render lười: {len(shots)} cú, có quỹ đạo ở "
f"index {have}"
+ (" — ĐẠT" if g61 else " — ĐỎ, phải là ĐÚNG [0]"))
ok &= g61
# ---- tham chiếu: bản n_render = MỌI cú, gọi thẳng thư viện
ref = recommend_v2(
{b: np.array(xy) for b, xy in balls.items()},
env_h=env_h, alternatives=ALTERNATIVES, n_render=len(shots))
if len(ref.shots) != len(shots):
print(f" [LỆCH] số cú khác nhau: api {len(shots)} vs "
f"lib {len(ref.shots)} — DRIFT, dừng thế bàn này")
ok = False
continue
# ---- G6.2 + G6.2b, cho MỌI cú (kể cả rank 1: nó phải khớp
# chính nó, và đó là đối chứng cho ba cú kia)
for i, s in enumerate(shots):
tr = client.post("/api/trajectory", json={
"balls": body["balls"], "phi": s["phi"], "v0": s["v0"],
"side": s["side"], "vert": s["vert"]})
if tr.status_code != 200:
print(f" /api/trajectory cú {i}: {tr.status_code} "
f"{tr.json().get('detail')}")
ok = False
continue
td = tr.json()
ok &= cmp_traj(f"G6.2 cú {i + 1} (phi={s['phi']:.6f} "
f"v0={s['v0']:.6f})",
td["trajectories"],
ref.shots[i].trajectories or {})
ok &= cmp_final(f"G6.2b cú {i + 1} balls_final",
td["balls_final"],
s["outcome"]["balls_final"])
print("\n[lazy] G6 " + ("XANH — render lười không đổi một điểm quỹ đạo nào"
if ok else "ĐỎ — DỪNG và ghi HANDOFF"))
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())
|