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()