Spaces:
Sleeping
Sleeping
File size: 5,737 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 | """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()
|