Spaces:
Sleeping
Sleeping
| """Benchmark tốc độ sim N bi — việc ĐẦU TIÊN của FullRack phase A (§4.4). | |
| Rủi ro chính của full rack: sim 10 bi nhiều event hơn hẳn 3 bi. Đo ms/cú | |
| trên máy thật (sandbox không có pooltool) cho 3 kịch bản: | |
| 1. ref_3bi — cue+1+2 random (mốc so sánh với app hiện tại) | |
| 2. midgame_10 — 10 bi rải random, đánh vào bi 1 (kịch bản search chính) | |
| 3. break_rack — cue đánh vỡ rack 9 bi nguyên (worst case nhiều event) | |
| In kèm ước lượng thời gian search oracle/cú (grid 250 cell × số lỗ khả thi, | |
| giả định 3 lỗ = 750 sim) — ngưỡng chấp nhận theo design: < ~5s/cú. | |
| Chạy: python scripts/benchmark_fullrack.py --shots 200 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import math | |
| import os | |
| import statistics | |
| import sys | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| sys.stderr.reconfigure(encoding="utf-8") | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT / "src")) | |
| SEARCH_SIMS = 750 # 3 lỗ khả thi × 250 cell (10 V0 × 5 side × 5 vert) | |
| def _place_random(rng, w, l, n, ball_r, min_gap=1.1): | |
| """n vị trí không chồng nhau (tâm cách nhau ≥ 2R×min_gap, né sát mép).""" | |
| pts = [] | |
| while len(pts) < n: | |
| p = np.array([rng.uniform(2 * ball_r, w - 2 * ball_r), | |
| rng.uniform(2 * ball_r, l - 2 * ball_r)]) | |
| if all(np.linalg.norm(p - q) >= 2 * ball_r * min_gap for q in pts): | |
| pts.append(p) | |
| return pts | |
| def _aim_at(balls, target, rng, noise_deg): | |
| d = balls[target] - balls["cue"] | |
| return (math.degrees(math.atan2(d[1], d[0])) | |
| + rng.uniform(-noise_deg, noise_deg)) % 360.0 | |
| def _bench(env_h, name, gen_case, n_shots, rng): | |
| from poolcoach_rl.recommend.simulate import simulate_shot_multi | |
| times, fails, contacts = [], 0, 0 | |
| for _ in range(n_shots): | |
| balls, phi, v0, a, b = gen_case(rng) | |
| t0 = time.perf_counter() | |
| r = simulate_shot_multi(env_h, balls, phi, v0, a, b) | |
| times.append((time.perf_counter() - t0) * 1000.0) | |
| if r is None: | |
| fails += 1 | |
| elif r["first_contact"] is not None: | |
| contacts += 1 | |
| med = statistics.median(times) | |
| p90 = statistics.quantiles(times, n=10)[-1] | |
| sps = 1000.0 / med | |
| ncpu = os.cpu_count() or 1 | |
| est1 = SEARCH_SIMS * med / 1000.0 | |
| print(f"=== {name} ({n_shots} cú) ===") | |
| print(f" mean {statistics.mean(times):7.1f} ms | median {med:7.1f}" | |
| f" | p90 {p90:7.1f} | max {max(times):7.1f}" | |
| f" -> {sps:6.1f} sim/s (1 core)") | |
| print(f" contact {contacts}/{n_shots}, sim fail {fails}") | |
| print(f" search ~{SEARCH_SIMS} sim: {est1:6.1f} s (1 core)" | |
| f" | ~{est1 / ncpu:5.1f} s ({ncpu} core lý thuyết)") | |
| print() | |
| return med | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--shots", type=int, default=200, help="số cú/kịch bản") | |
| ap.add_argument("--seed", type=int, default=42) | |
| args = ap.parse_args() | |
| import pooltool as pt | |
| from poolcoach_rl.envs import PositionPlayEnv | |
| from poolcoach_rl.envs.position_env import BALL_R | |
| from poolcoach_rl.recommend.core import warmup | |
| env_h = PositionPlayEnv() | |
| w, l = env_h.w, env_h.l | |
| rng = np.random.default_rng(args.seed) | |
| print("JIT warmup (~40s lần đầu)...", flush=True) | |
| print(f" xong sau {warmup(env_h):.1f}s\n") | |
| # Rack 9 bi chuẩn từ pooltool; nếu get_rack không kèm cue thì đặt | |
| # cue phía đối diện rack (chỉ cần hợp lý cho benchmark). | |
| rack = pt.get_rack(pt.GameType.NINEBALL, env_h.table) | |
| rack_xy = {bid: np.asarray(bb.state.rvw[0][:2], dtype=np.float64) | |
| for bid, bb in rack.items()} | |
| if "cue" not in rack_xy: | |
| cue_y = l - rack_xy["1"][1] | |
| rack_xy["cue"] = np.array( | |
| [w / 2, min(max(cue_y, 2 * BALL_R), l - 2 * BALL_R)]) | |
| n_rack = len(rack_xy) | |
| print(f"rack: {n_rack} bi ({', '.join(sorted(rack_xy))})\n") | |
| ids10 = ["cue"] + [str(i) for i in range(1, 10)] | |
| def case_ref3(rng): | |
| balls = dict(zip(["cue", "1", "2"], | |
| _place_random(rng, w, l, 3, BALL_R))) | |
| return (balls, _aim_at(balls, "1", rng, 30.0), | |
| rng.uniform(0.5, 4.0), | |
| rng.uniform(-0.4, 0.4), rng.uniform(-0.4, 0.4)) | |
| def case_mid10(rng): | |
| balls = dict(zip(ids10, _place_random(rng, w, l, 10, BALL_R))) | |
| return (balls, _aim_at(balls, "1", rng, 30.0), | |
| rng.uniform(0.5, 4.0), | |
| rng.uniform(-0.4, 0.4), rng.uniform(-0.4, 0.4)) | |
| def case_break(rng): | |
| balls = {k: v.copy() for k, v in rack_xy.items()} | |
| return (balls, _aim_at(balls, "1", rng, 5.0), | |
| rng.uniform(2.5, 4.0), | |
| rng.uniform(-0.2, 0.2), rng.uniform(-0.2, 0.2)) | |
| m3 = _bench(env_h, "ref_3bi (mốc app hiện tại)", case_ref3, | |
| args.shots, rng) | |
| m10 = _bench(env_h, "midgame_10bi (kịch bản search chính)", case_mid10, | |
| args.shots, rng) | |
| mbr = _bench(env_h, f"break_rack ({n_rack} bi, worst case)", case_break, | |
| max(args.shots // 4, 20), rng) | |
| print("=== KẾT LUẬN ===") | |
| print(f" 10 bi chậm hơn 3 bi: ×{m10 / m3:.1f} (midgame), " | |
| f"×{mbr / m3:.1f} (break)") | |
| verdict = ("ĐẠT — search midgame 1 core đã < 5s/cú" | |
| if SEARCH_SIMS * m10 / 1000.0 < 5.0 else | |
| "CHƯA ĐẠT ngưỡng 5s/cú 1 core → cần ProcessPool trong BE " | |
| "(mitigation 1, §4.4) hoặc coarse-to-fine (mitigation 2)") | |
| print(f" {verdict}") | |
| if __name__ == "__main__": | |
| main() | |