From f91e12196537c6c1e64353160e8885445e3e222b Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Thu, 24 Sep 2026 22:47:18 +0200 Subject: [PATCH] =?UTF-8?q?lead=20capture=20by=20range:=20we=20under-lead?= =?UTF-8?q?=20(0.40->0.135)=20=E2=80=94=20a=20RANGE=20effect,=20not=20a=20?= =?UTF-8?q?power=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measure, for every shot ModularBot fires at the real DrussGT, the lead we actually applied vs the lead the enemy's motion required, from the recorded live battles (/tmp/tfil_ab2, 70 battles / 490 rounds / 54926 shots, plus a 35-battle powtest replication of a different binary). - requiredLead from an AIM-INDEPENDENT interception solve (bullet speed vs enemy truth), appliedLead from the server-recorded bullet bearing. - capture = applied/required, guarded at 2px lateral lead (1.6% excluded); headline metric is the robust proportional slope. - validation: hits 11.6px mean miss / 80.8% inside 18px, misses 134px, 11.6x separation; 496/496 death + 70/70 owner attributions correct. Direct answer: capture falls with RANGE (capSlp 0.401 -> 0.135, and |err|/tolerance 1.27 -> 7.54) but is FLAT across fired POWER within a band (450+, enemy alive: 0.154 / 0.127 / 0.127). The sub-0.5 long-range shots (1.18% hit) are finishKill endgame shots at a near-dead DrussGT, not a lead-capture failure. A naive linear predictor captures 0.29-0.60; we reach 46-67% of that, so the under-lead is real but capture=1.0 is unattainable against a dodger (oracle required lead). --- .../tests/analyze_lead_capture_by_range.py | 583 ++++++ .../fixtures/lead_capture_by_range_output.txt | 213 +++ .../lead_capture_by_range_results.json | 1692 +++++++++++++++++ docs/lead_capture_by_range.md | 307 +++ 4 files changed, 2795 insertions(+) create mode 100644 common_libs/tests/analyze_lead_capture_by_range.py create mode 100644 common_libs/tests/fixtures/lead_capture_by_range_output.txt create mode 100644 common_libs/tests/fixtures/lead_capture_by_range_results.json create mode 100644 docs/lead_capture_by_range.md diff --git a/common_libs/tests/analyze_lead_capture_by_range.py b/common_libs/tests/analyze_lead_capture_by_range.py new file mode 100644 index 0000000..1a1a216 --- /dev/null +++ b/common_libs/tests/analyze_lead_capture_by_range.py @@ -0,0 +1,583 @@ +#!/usr/bin/env python3 +"""MEASURE LEAD CAPTURE BY RANGE: how much of the required lead do we apply? + +Reads the recent LIVE recorded battles vs the real DrussGT +(``/tmp/tfil_ab2/out//run.jsonl`` per-tick worldstate + +``.events.jsonl`` fire/hit sidecar + ``.jsonl.rounds.json`` tick index, +produced by ``tools/robocode_shim/run_bridge_battle.sh`` / ``tools/ab/ab_run.sh``) +and, for every shot WE fire, compares the lead we actually put into the bullet +against the lead the enemy's motion required -- split by RANGE band and then by +fired POWER inside each band. + +WHICH SIDE IS US +---------------- +``TrBattleCapture`` writes the movement subject (the bot whose name matches +``--subject``, here DrussGT) as ``e*`` and the adversary (ModularBot, our bot) +as ``s*``. So our worldstate track is ``sx,sy,se`` and DrussGT's is +``ex,ey,ee``. Owner ids in the event sidecar are NOT stable across runs, so +every run is attributed independently: a fire event's (x,y) is the firing +tank's centre and its energy drops by exactly the fired power one tick later. +Attribution is cross-checked against the server ``death`` events (496/496). + +THE DECOMPOSITION (all angles in deg, all relative to the LOS at the FIRE tick) +------------------------------------------------------------------------------ + O = the firing tank's centre at the fire tick (the bullet line origin) + v = 20 - 3p px/tick + t* = the AIM-INDEPENDENT interception tick: the first tick k with + |E(t0+k) - O| <= v*k. It depends only on the enemy's recorded + truth and the bullet speed, never on our aim. + required = bearing(O -> E(t*)) - LOS (the lead a perfect gun would need) + applied = bullet bearing (server-recorded, perfect truth) - LOS + leadErr = applied - required (wrapped) + capture = applied / required (guarded: only where |required| >= 2px at range) + miss_px = |E(t*) - (O + v*t* * u)| (bullet line vs enemy at t*) + +CAPTURE < 1 IS NOT BY ITSELF A BUG +---------------------------------- +``required`` uses the enemy's ACTUAL future dodge (perfect information), which +no real gun can know. So the script also computes a NAIVE LINEAR PREDICTOR +control -- the same bullet speed, aimed at the intercept of a straight-line +continuation of the enemy's last LIN_M ticks of velocity. That control is the +ceiling a trivial predictive gun reaches against this dodger; our capture is +read against it, not only against 1.0. + +MEASURED = every number printed. INFERRED = the causal reading in the doc. +""" +from __future__ import annotations + +import argparse +import collections +import json +import math +import os +import statistics + +US_SIDE = "s" # adversary = ModularBot = us +BOT_RADIUS = 18.0 +SPEED_A, SPEED_B = 20.0, 3.0 # bullet speed = 20 - 3*p px/tick +MAX_FLIGHT = 220 +MIN_REQ_PX = 2.0 # capture defined only above this lateral lead +LIN_M = 4 # ticks of velocity for the naive-predictor control + +BANDS = [(0, 100), (100, 200), (200, 300), (300, 450), (450, 1e9)] +BAND_LABELS = ["0-100", "100-200", "200-300", "300-450", "450+"] +PWR_EDGES = [0.0, 0.5, 0.75, 1.0, 1.5, 100.0] +PWR_LABELS = ["<0.50", "0.50-0.75", "0.75-1.00", "1.00-1.50", ">=1.50"] + + +def wrap180(x): + """Signed angular difference in (-180, 180].""" + x = (x + 180.0) % 360.0 - 180.0 + return x + 360.0 if x <= -180.0 else x + + +def band_of(r): + for (lo, hi), lab in zip(BANDS, BAND_LABELS): + if lo <= r < hi: + return lab + return BAND_LABELS[-1] + + +def pbin(p): + for i in range(len(PWR_EDGES) - 1): + if PWR_EDGES[i] <= p < PWR_EDGES[i + 1]: + return i + return None + + +def mean(xs): + return statistics.fmean(xs) if xs else float("nan") + + +def median(xs): + return statistics.median(xs) if xs else float("nan") + + +def stdev(xs): + return statistics.pstdev(xs) if len(xs) > 1 else float("nan") + + +def _rows(path): + out = [] + with open(path) as f: + for line in f: + line = line.strip() + if not line: + continue + o = json.loads(line) + if "tick" in o: + out.append(o) + return out + + +class Run: + """One live battle: capture rows, event sidecar, per-round tick index.""" + + def __init__(self, cap_path, events_path, rounds_path): + self.cap_path = cap_path + self.rows = _rows(cap_path) + self.by_tick = {r["tick"]: r for r in self.rows} + self.events = [json.loads(l) for l in open(events_path) if l.strip()] + self.rounds = json.load(open(rounds_path))["rounds"] + self.start = {r["round"]: r["startTick"] for r in self.rounds} + self.count = {r["round"]: r["count"] for r in self.rounds} + self.owner_side = self._resolve_owner_side() + self.resolution = {} + for ev in self.events: + if ev.get("type") in ("hit", "hitwall", "hitbullet"): + self.resolution[(ev["round"], ev["owner"], ev["bullet"])] = ev["type"] + + # -- attribution ------------------------------------------------------ + def _match(self, t, side, ev): + r, r1 = self.by_tick.get(t), self.by_tick.get(t + 1) + if r is None or r1 is None: + return False + if abs(r[side + "x"] - ev["x"]) > 0.02 or abs(r[side + "y"] - ev["y"]) > 0.02: + return False + ef = "ee" if side == "e" else "se" + return abs((r[ef] - r1[ef]) - ev["power"]) < 0.02 + + def _resolve_owner_side(self): + votes = collections.defaultdict(collections.Counter) + for ev in self.events: + if ev.get("type") != "fire": + continue + guess = self.start.get(ev["round"], 0) + ev["tick"] + for t in range(guess - 8, guess + 9): + for side in ("e", "s"): + if self._match(t, side, ev): + votes[ev["owner"]][side] += 1 + return {o: c.most_common(1)[0][0] for o, c in votes.items() if c} + + def align(self, ev): + guess = self.start.get(ev["round"], 0) + ev["tick"] + side = self.owner_side.get(ev["owner"]) + best = None + for t in range(guess - 8, guess + 9): + for sd in (("e", "s") if side is None else (side,)): + if self._match(t, sd, ev): + d = abs(t - guess) + if best is None or d < best[0]: + best = (d, t, sd) + return None if best is None else best[1] + + def n_position_matches(self, ev): + n = 0 + guess = self.start.get(ev["round"], 0) + ev["tick"] + for t in range(guess - 8, guess + 9): + for sd in ("e", "s"): + r = self.by_tick.get(t) + if r is None: + continue + if abs(r[sd + "x"] - ev["x"]) <= 0.02 and abs(r[sd + "y"] - ev["y"]) <= 0.02: + n += 1 + return n + + def death_side_ok(self): + """Every death must land on a side whose energy has collapsed to 0.""" + out = [] + for ev in self.events: + if ev.get("type") != "death": + continue + side = self.owner_side.get(ev["victim"]) + g = self.start.get(ev["round"], 0) + ev["tick"] + ekey = "ee" if side == "e" else "se" + ok = False + for t in range(g - 8, g + 10): + r = self.by_tick.get(t) + if r is not None and r[ekey] <= 1e-6: + ok = True + break + out.append(ok) + return out + + # -- per-shot extraction --------------------------------------------- + def shots(self): + for ev in self.events: + if ev.get("type") != "fire": + continue + if self.owner_side.get(ev["owner"]) != US_SIDE: + continue + t0 = self.align(ev) + if t0 is None: + continue + s = self._shot(ev, t0) + if s is not None: + yield s + + def _shot(self, ev, t0): + r0 = self.by_tick[t0] + p = ev["power"] + v = SPEED_A - SPEED_B * p + th = math.radians(ev["dir"]) + ux, uy = math.cos(th), math.sin(th) + x0, y0 = ev["x"], ev["y"] # tank centre == bullet line origin + rnd = ev["round"] + end = self.start[rnd] + self.count[rnd] + rng = math.hypot(r0["ex"] - x0, r0["ey"] - y0) + + # aim-independent interception tick (oracle) + tstar = None + for k in range(1, MAX_FLIGHT + 1): + if t0 + k >= end: + break + r = self.by_tick.get(t0 + k) + if r is None: + break + if math.hypot(r["ex"] - x0, r["ey"] - y0) <= v * k: + tstar = (k, r) + break + if tstar is None: + return None + kstar, ea = tstar + + # miss distance: perpendicular of the enemy at the tick our (actual) bullet + # reaches its along-track plane, relative to our bullet's real line. + # Same definition as common_libs/tests/analyze_drussgt_dodge_vs_power.py. + perp_arr = None + for k in range(1, MAX_FLIGHT + 1): + if t0 + k >= end: + break + r = self.by_tick.get(t0 + k) + if r is None: + break + dx, dy = r["ex"] - x0, r["ey"] - y0 + al = dx * ux + dy * uy + if al > 0 and v * k >= al: + perp_arr = dx * uy - dy * ux + break + miss_px = abs(perp_arr) if perp_arr is not None else float("nan") + + los = math.degrees(math.atan2(r0["ey"] - y0, r0["ex"] - x0)) + required = wrap180(math.degrees(math.atan2(ea["ey"] - y0, ea["ex"] - x0)) - los) + applied = wrap180(ev["dir"] - los) + leaderr = wrap180(applied - required) + + # naive linear-predictor control: continue the last LIN_M ticks of velocity + lin = None + rp = self.by_tick.get(t0 - LIN_M) + if rp is not None: + vx = (r0["ex"] - rp["ex"]) / LIN_M + vy = (r0["ey"] - rp["ey"]) / LIN_M + kk = rng / v + for _ in range(40): + kk = math.hypot(r0["ex"] + vx * kk - x0, r0["ey"] + vy * kk - y0) / v + lin = wrap180(math.degrees(math.atan2(r0["ey"] + vy * kk - y0, + r0["ex"] + vx * kk - x0)) - los) + + req_px = abs(math.radians(required)) * rng + capture = (applied / required) if (req_px >= MIN_REQ_PX and abs(required) > 1e-9) else None + capture_lin = (lin / required) if (lin is not None and req_px >= MIN_REQ_PX + and abs(required) > 1e-9) else None + + kind = self.resolution.get((rnd, ev["owner"], ev["bullet"])) + return dict( + run=self.cap_path, rnd=rnd, tick=t0, power=p, speed=v, + range=rng, band=band_of(rng), pbin=pbin(p), flight=kstar, + required=required, applied=applied, leaderr=leaderr, lin=lin, + capture=capture, capture_lin=capture_lin, req_px=req_px, + miss_px=miss_px, + tolerance_deg=math.degrees(math.atan(BOT_RADIUS / rng)), + hit=1.0 if kind == "hit" else 0.0, + resolution=kind, npos=self.n_position_matches(ev), + our_energy=r0["se"], enemy_energy=r0["ee"], rel_tick=t0 - self.start[rnd], + ) + + +def discover_tfil(root): + runs = [] + for sub in sorted(os.listdir(root)): + d = os.path.join(root, sub) + if not os.path.isdir(d): + continue + for fn in sorted(os.listdir(d)): + if not fn.endswith(".jsonl") or fn.endswith(".events.jsonl"): + continue + cap = os.path.join(d, fn) + ev, rj = cap[:-6] + ".events.jsonl", cap + ".rounds.json" + if os.path.exists(ev) and os.path.exists(rj): + runs.append(Run(cap, ev, rj)) + return runs + + +def discover_powtest(root): + """powtest layout: cap__r.jsonl + events__r.json + .rounds.json.""" + runs = [] + if not os.path.isdir(root): + return runs + for fn in sorted(os.listdir(root)): + if not (fn.startswith("cap_") and fn.endswith(".jsonl")): + continue + cap = os.path.join(root, fn) + stem = fn[4:].replace(".jsonl", ".json") + ev = os.path.join(root, "events_" + stem) + rj = cap + ".rounds.json" + if os.path.exists(ev) and os.path.exists(rj): + runs.append(Run(cap, ev, rj)) + return runs + + +# --------------------------------------------------------------- statistics +def slope(xs, ys): + d = sum(x * x for x in xs) + return sum(x * y for x, y in zip(xs, ys)) / d if d > 0 else float("nan") + + +def cell(shots): + caps = [s["capture"] for s in shots if s["capture"] is not None] + lins = [s["capture_lin"] for s in shots if s["capture_lin"] is not None] + req = [s["required"] for s in shots] + resolved = [s for s in shots if s["resolution"] in ("hit", "hitwall", "hitbullet")] + return dict( + n=len(shots), nc=len(caps), excl=len(shots) - len(caps), + unresolved=len(shots) - len(resolved), + range=mean([s["range"] for s in shots]), + req=mean(req), + app=mean([s["applied"] for s in shots]), + cap=mean(caps), cap_med=median(caps), cap_slope=slope(req, [s["applied"] for s in shots]), + cap_lin=mean(lins), cap_lin_slope=slope(req, [s["lin"] for s in shots if s["lin"] is not None]), + ae=mean([abs(s["leaderr"]) for s in shots]), + ae_sd=stdev([abs(s["leaderr"]) for s in shots]), + ae_px=mean([abs(math.radians(s["leaderr"])) * s["range"] for s in shots]), + miss=mean([s["miss_px"] for s in shots]), + med_miss=median([s["miss_px"] for s in shots]), + tol=mean([s["tolerance_deg"] for s in shots]), + hit=mean([s["hit"] for s in resolved]), + enE=mean([s["enemy_energy"] for s in shots]), + flight=mean([s["flight"] for s in shots]), + ) + + +def table(cells, title): + print(f"\n{title}") + hdr = (f"{'cell':<24} {'n':>6} {'excl':>5} {'range':>5} {'flt':>4} {'reqL':>7} {'appL':>7} " + f"{'capt':>6} {'capSlp':>7} {'capLin':>7} {'|err|':>6} {'+/-':>5} {'|err|px':>7} " + f"{'tol':>5} {'hit%':>6}") + print(hdr) + print("-" * len(hdr)) + for name, sh in cells: + if not sh: + continue + c = cell(sh) + print(f"{name:<24} {c['n']:>6} {c['excl']:>5} {c['range']:>5.0f} {c['flight']:>4.1f} " + f"{c['req']:>7.2f} {c['app']:>7.2f} {c['cap']:>6.2f} {c['cap_slope']:>7.3f} " + f"{c['cap_lin_slope']:>7.3f} {c['ae']:>6.2f} {c['ae_sd']:>5.2f} {c['ae_px']:>7.1f} " + f"{c['tol']:>5.2f} {100*c['hit']:>5.2f}") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--tfil", default="/tmp/tfil_ab2/out") + ap.add_argument("--powtest", default="/tmp/powtest") + ap.add_argument("--json", default=None) + args = ap.parse_args() + + runs = discover_tfil(args.tfil) + print("=" * 132) + print("LEAD CAPTURE BY RANGE -- live ModularBot vs real DrussGT") + print("=" * 132) + print(f"corpus : {args.tfil}") + print(f"battles (runs) : {len(runs)}") + if not runs: + raise SystemExit("no runs found") + + amb = 0 + death_ok = death_n = 0 + pow_all = collections.defaultdict(collections.Counter) + sideowners = collections.Counter() + allshots = [] + for rn in runs: + for o, sd in rn.owner_side.items(): + sideowners[(sd, o)] += 1 + amb += sum(1 for ev in rn.events if ev.get("type") == "fire" + and rn.n_position_matches(ev) > 1) + for ev in rn.events: + if ev.get("type") == "fire": + pow_all[rn.owner_side.get(ev["owner"], "?")][round(ev["power"], 2)] += 1 + ds = rn.death_side_ok() + death_n += len(ds) + death_ok += sum(ds) + allshots.extend(rn.shots()) + + sides = collections.Counter(tuple(sorted(rn.owner_side.values())) for rn in runs) + print(f"per-run owner maps: {dict(sides)} (expect all ('e','s'))") + print(f"runs with a broken owner map : {sum(1 for rn in runs if sorted(rn.owner_side.values()) != ['e','s'])}") + print(f"fire events w/ ambiguous position : {amb}") + print(f"death events side-validated : {death_ok}/{death_n}") + for sd in ("s", "e"): + tot = sum(pow_all[sd].values()) + top = pow_all[sd].most_common(6) + print(f" side {sd} = {'US (ModularBot)' if sd == US_SIDE else 'DrussGT'} " + f"n={tot:>6} top powers: {top}") + + hits = [s for s in allshots if s["hit"] == 1.0] + misses = [s for s in allshots if s["resolution"] in ("hitwall", "hitbullet") + and s["hit"] == 0.0] + unresolved = [s for s in allshots if s["resolution"] is None] + print("\n" + "=" * 132) + print("SAMPLE") + print("=" * 132) + print(f"our shots (aligned, with an interception) : {len(allshots)}") + print(f" server hit / hitwall / hitbullet / other : " + f"{sum(1 for s in allshots if s['resolution']=='hit')} / " + f"{sum(1 for s in allshots if s['resolution']=='hitwall')} / " + f"{sum(1 for s in allshots if s['resolution']=='hitbullet')} / " + f"{len(unresolved)}") + print(f"capture excluded (|required| < {MIN_REQ_PX} px lateral) : " + f"{sum(1 for s in allshots if s['capture'] is None)}") + + print("\n" + "=" * 132) + print("GEOMETRY VALIDATION (hits must separate from misses)") + print("=" * 132) + hdr = f"{'':<16}{'n':>7} {'|leadErr|deg':>13} {'|leadErr|px':>12} {'misPx mean':>11} {'misPx med':>10} {'<18px':>7}" + print(hdr) + for lab, grp in (("HITS", hits), ("MISSES", misses)): + if not grp: + continue + ae = [abs(s["leaderr"]) for s in grp] + apx = [abs(math.radians(s["leaderr"])) * s["range"] for s in grp] + mp = [s["miss_px"] for s in grp] + print(f"{lab:<16}{len(grp):>7} {mean(ae):>13.3f} {mean(apx):>12.1f} " + f"{mean(mp):>11.1f} {median(mp):>10.1f} {100*sum(1 for x in mp if x<18)/len(mp):>6.1f}%") + if hits and misses: + sep = mean([s["miss_px"] for s in misses]) / max(1e-9, mean([s["miss_px"] for s in hits])) + print(f"\nmiss separation ratio (misses/hits) = {sep:.2f}x " + f"-> {'OK (bearing recovery validated)' if sep > 2 else 'WEAK - bearing recovery suspect'}") + print(f"overall measured hit rate = {100*mean([s['hit'] for s in allshots]):.2f}%") + + cells = [(lab, [s for s in allshots if s["band"] == lab]) for lab in BAND_LABELS] + print("\n" + "=" * 132) + print("MAIN TABLE -- all our shots, by RANGE band") + print("=" * 132) + print("reqL/appL = mean signed lead vs LOS (deg). capt = mean ratio, capSlp = robust proportional") + print("capture, capLin = the same slope for a naive linear predictor (control).") + table(cells, "by range band") + + print("\n" + "=" * 132) + print("POWER WITHIN RANGE BAND -- this is what separates 'range' from 'power'") + print("=" * 132) + for lab in BAND_LABELS: + sub = [s for s in allshots if s["band"] == lab] + if not sub: + continue + pcs = [(f"{lab} {PWR_LABELS[i]}", [s for s in sub if s["pbin"] == i]) + for i in range(len(PWR_LABELS))] + table(pcs, f"range {lab}") + + # ---- endgame confound and its removal -------------------------------- + print("\n" + "=" * 132) + print("CONFOUND CHECK -- the sub-0.5-power shots at long range are the ENDGAME finish") + print("=" * 132) + print("hit% of our 450+ shots, by the ENEMY's remaining energy at the fire tick:") + sub = [s for s in allshots if s["band"] == "450+"] + for lo, hi in ((0, 2), (2, 5), (5, 10), (10, 20), (20, 40), (40, 150)): + g = [s for s in sub if lo <= s["enemy_energy"] < hi] + if g: + c = cell(g) + print(f" enemy energy [{lo:>3},{hi:>3}) n={c['n']:>6} meanP={mean([x['power'] for x in g]):>5.2f} " + f"capSlp={c['cap_slope']:>6.3f} hit%={100*c['hit']:>5.2f}") + print("\nsame table, EXCLUDING shots fired while the enemy still has >= 5 energy") + print("(this removes the near-dead-target endgame and leaves the clean power comparison):") + for lab in BAND_LABELS: + sub = [s for s in allshots if s["band"] == lab and s["enemy_energy"] >= 5.0] + if not sub: + continue + pcs = [(f"{lab} {PWR_LABELS[i]}", [s for s in sub if s["pbin"] == i]) + for i in range(len(PWR_LABELS))] + table(pcs, f"range {lab} (enemy energy >= 5)") + + # ---- response curve --------------------------------------------------- + print("\n" + "=" * 132) + print("RESPONSE CURVE -- do we move the aim in proportion to the required lead?") + print("=" * 132) + for lab in ("200-300", "300-450", "450+"): + g = sorted([s for s in allshots if s["band"] == lab], key=lambda s: s["required"]) + if len(g) < 100: + continue + nb = 8 + print(f"\nrange {lab} (deciles of required lead -> mean applied lead):") + for i in range(nb): + q = g[i * len(g) // nb:(i + 1) * len(g) // nb] + c = cell(q) + print(f" required [{q[0]['required']:>7.2f},{q[-1]['required']:>7.2f}] " + f"meanReq={c['req']:>7.2f} meanApp={c['app']:>7.2f} " + f"hit%={100*c['hit']:>5.2f} n={c['n']:>5}") + + print("\n" + "=" * 132) + print("DIRECT ANSWER -- does capture degrade with range / power?") + print("=" * 132) + print(f"{'range band':<12} {'n':>6} {'capSlp':>7} {'capLin':>7} {'us/lin':>7} " + f"{'medcap':>7} {'|req|':>6} {'|app|':>6} {'|err|px':>8} {'tol':>5} {'|err|/tol':>9} {'hit%':>6}") + for lab, sh in cells: + if not sh: + continue + c = cell(sh) + ratio = c["ae_px"] / (c["tol"] * math.pi / 180.0 * c["range"]) + areq = mean([abs(s["required"]) for s in sh]) + aapp = mean([abs(s["applied"]) for s in sh]) + print(f"{lab:<12} {c['n']:>6} {c['cap_slope']:>7.3f} {c['cap_lin_slope']:>7.3f} " + f"{c['cap_slope']/c['cap_lin_slope'] if c['cap_lin_slope'] else float('nan'):>7.2f} " + f"{c['cap_med']:>7.3f} {areq:>6.2f} {aapp:>6.2f} {c['ae_px']:>8.1f} {c['tol']:>5.2f} " + f"{ratio:>9.2f} {100*c['hit']:>5.2f}") + print("\n(capSlp = our proportional lead capture; capLin = naive linear predictor control;") + print(" us/lin = our capture / the trivial-predictor ceiling; |req|/|app| = mean ABSOLUTE") + print(" lead magnitude in deg; |err|/tol = mean lead error px / 18px)") + print("\nPOWER WITHIN 450+ BAND, enemy energy >= 5 (removes the endgame confound):") + sub = [s for s in allshots if s["band"] == "450+" and s["enemy_energy"] >= 5.0] + for i in range(len(PWR_LABELS)): + g = [s for s in sub if s["pbin"] == i] + if not g: + continue + c = cell(g) + print(f" power {PWR_LABELS[i]:<11} n={c['n']:>6} meanP={mean([s['power'] for s in g]):>5.2f} " + f"meanRange={c['range']:>4.0f} capSlp={c['cap_slope']:>6.3f} " + f"capLin={c['cap_lin_slope']:>6.3f} |err|px={c['ae_px']:>6.1f} hit%={100*c['hit']:>5.2f}") + + if args.powtest: + pruns = discover_powtest(args.powtest) + if pruns: + pshots = [s for rn in pruns for s in rn.shots()] + print("\n" + "=" * 132) + print(f"ROBUSTNESS CROSS-CHECK -- powtest corpus ({len(pruns)} runs, " + f"{len(pshots)} shots, DIFFERENT binary)") + print("=" * 132) + pc = [(lab, [s for s in pshots if s["band"] == lab]) for lab in BAND_LABELS] + table(pc, "by range band (powtest)") + + if args.json: + blob = dict( + corpus=args.tfil, runs=len(runs), shots=len(allshots), + hits=len(hits), misses=len(misses), + excl=sum(1 for s in allshots if s["capture"] is None), + death_ok=death_ok, death_n=death_n, amb=amb, + overall_hit=mean([s["hit"] for s in allshots]), + hit_ae=mean([abs(s["leaderr"]) for s in hits]), + miss_ae=mean([abs(s["leaderr"]) for s in misses]), + hit_miss_px=mean([s["miss_px"] for s in hits]), + miss_miss_px=mean([s["miss_px"] for s in misses]), + pwr_hist={sd: dict(pow_all[sd]) for sd in pow_all}, + bands={lab: cell([s for s in allshots if s["band"] == lab]) for lab in BAND_LABELS}, + bands_enemy_alive={lab: cell([s for s in allshots if s["band"] == lab + and s["enemy_energy"] >= 5.0]) + for lab in BAND_LABELS}, + band_power_alive={lab: {PWR_LABELS[i]: cell([s for s in allshots + if s["band"] == lab + and s["pbin"] == i + and s["enemy_energy"] >= 5.0]) + for i in range(len(PWR_LABELS))} + for lab in BAND_LABELS}, + hit_by_enemy_energy={f"{lo}-{hi}": cell([s for s in allshots + if s["band"] == "450+" + and lo <= s["enemy_energy"] < hi]) + for lo, hi in ((0, 2), (2, 5), (5, 10), (10, 20), (20, 40), (40, 150))}, + band_power={lab: {PWR_LABELS[i]: cell([s for s in allshots + if s["band"] == lab and s["pbin"] == i]) + for i in range(len(PWR_LABELS))} for lab in BAND_LABELS}, + ) + with open(args.json, "w") as f: + json.dump(blob, f, indent=2) + print(f"\n[json] wrote {args.json}") + + +if __name__ == "__main__": + main() diff --git a/common_libs/tests/fixtures/lead_capture_by_range_output.txt b/common_libs/tests/fixtures/lead_capture_by_range_output.txt new file mode 100644 index 0000000..3e54379 --- /dev/null +++ b/common_libs/tests/fixtures/lead_capture_by_range_output.txt @@ -0,0 +1,213 @@ +==================================================================================================================================== +LEAD CAPTURE BY RANGE -- live ModularBot vs real DrussGT +==================================================================================================================================== +corpus : /tmp/tfil_ab2/out +battles (runs) : 70 +per-run owner maps: {('e', 's'): 70} (expect all ('e','s')) +runs with a broken owner map : 0 +fire events w/ ambiguous position : 12112 +death events side-validated : 496/496 + side s = US (ModularBot) n= 56242 top powers: [(1.0, 32044), (0.5, 14556), (0.1, 807), (0.51, 225), (0.55, 208), (0.53, 199)] + side e = DrussGT n= 67065 top powers: [(0.15, 27209), (0.95, 22789), (0.45, 5274), (0.25, 3071), (0.65, 2862), (0.35, 2340)] + +==================================================================================================================================== +SAMPLE +==================================================================================================================================== +our shots (aligned, with an interception) : 54926 + server hit / hitwall / hitbullet / other : 5480 / 47101 / 1203 / 1142 +capture excluded (|required| < 2.0 px lateral) : 887 + +==================================================================================================================================== +GEOMETRY VALIDATION (hits must separate from misses) +==================================================================================================================================== + n |leadErr|deg |leadErr|px misPx mean misPx med <18px +HITS 5480 1.478 11.4 11.6 10.8 80.8% +MISSES 48304 16.724 141.3 134.1 123.7 0.8% + +miss separation ratio (misses/hits) = 11.59x -> OK (bearing recovery validated) +overall measured hit rate = 9.98% + +==================================================================================================================================== +MAIN TABLE -- all our shots, by RANGE band +==================================================================================================================================== +reqL/appL = mean signed lead vs LOS (deg). capt = mean ratio, capSlp = robust proportional +capture, capLin = the same slope for a naive linear predictor (control). + +by range band +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +0-100 27 1 81 10.3 1.51 -0.23 0.72 0.401 0.597 16.76 11.13 23.2 13.01 44.00 +100-200 274 14 162 14.7 -0.95 2.23 0.07 0.391 0.600 17.74 13.38 50.1 6.57 25.28 +200-300 990 30 261 17.9 -1.67 0.37 0.04 0.246 0.428 16.65 11.97 75.8 3.99 16.04 +300-450 17178 259 405 24.5 -0.56 0.40 0.01 0.198 0.404 15.98 11.27 112.9 2.56 11.79 +450+ 36457 583 535 31.0 0.19 -0.44 0.15 0.135 0.291 14.71 10.36 137.0 1.95 9.14 + +==================================================================================================================================== +POWER WITHIN RANGE BAND -- this is what separates 'range' from 'power' +==================================================================================================================================== + +range 0-100 +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +0-100 0.50-0.75 6 1 83 6.5 -4.82 -1.09 0.41 0.448 0.669 12.35 3.75 17.7 12.61 66.67 +0-100 1.00-1.50 2 0 77 7.5 -28.19 -7.90 0.27 0.292 0.730 20.29 10.29 27.8 13.17 50.00 +0-100 >=1.50 19 0 80 11.8 6.63 0.85 0.84 0.405 0.574 17.78 12.32 24.4 13.12 35.29 + +range 100-200 +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +100-200 <0.50 5 0 161 10.4 4.77 -1.52 -0.08 0.436 0.495 14.84 8.18 41.5 6.48 20.00 +100-200 0.50-0.75 53 12 166 10.8 1.78 5.83 0.04 0.481 0.567 14.31 8.72 41.2 6.38 30.77 +100-200 0.75-1.00 5 0 177 11.2 0.60 -4.71 -1.53 0.527 0.394 11.72 7.59 35.6 5.89 40.00 +100-200 1.00-1.50 23 0 168 12.5 -0.21 6.11 -0.04 0.442 0.616 14.50 11.98 46.5 6.47 42.86 +100-200 >=1.50 188 2 159 16.3 -2.00 1.02 0.13 0.375 0.606 19.34 14.50 53.7 6.65 21.43 + +range 200-300 +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +200-300 <0.50 6 0 257 15.7 -0.40 -3.70 0.06 0.512 0.839 10.39 8.54 50.9 4.07 50.00 +200-300 0.50-0.75 130 12 267 16.1 -1.54 1.25 -0.40 0.202 0.415 15.92 10.93 74.3 3.90 18.55 +200-300 0.75-1.00 21 4 263 17.1 -3.60 3.14 0.34 0.135 -0.015 17.32 10.06 79.1 3.96 11.11 +200-300 1.00-1.50 780 13 261 17.8 -1.29 0.29 0.08 0.287 0.462 16.04 11.33 73.0 3.99 15.92 +200-300 >=1.50 53 1 248 25.3 -6.97 -1.28 0.29 0.063 0.291 27.83 17.66 122.1 4.20 8.70 + +range 300-450 +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +300-450 <0.50 333 63 409 22.0 -0.93 0.92 -0.67 0.094 0.493 16.30 9.09 116.4 2.54 3.07 +300-450 0.50-0.75 5268 82 411 23.4 0.12 0.75 0.25 0.216 0.431 15.16 10.68 108.9 2.52 11.65 +300-450 0.75-1.00 880 12 408 24.7 -0.72 -0.36 -0.19 0.203 0.458 16.00 11.28 113.6 2.55 13.14 +300-450 1.00-1.50 10696 102 402 25.0 -0.86 0.27 -0.07 0.191 0.386 16.38 11.59 114.8 2.59 12.02 +300-450 >=1.50 1 0 312 22.0 -7.39 22.61 -3.06 -3.062 -3.217 30.00 nan 163.1 3.31 0.00 + +range 450+ +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +450+ <0.50 950 120 537 28.2 0.80 0.67 0.02 0.106 0.302 14.61 8.72 136.8 1.94 1.18 +450+ 0.50-0.75 12957 205 536 29.6 0.32 -0.12 0.13 0.151 0.314 14.04 9.96 131.1 1.94 10.20 +450+ 0.75-1.00 2424 28 540 31.6 0.52 -0.41 0.01 0.132 0.284 14.92 10.42 140.6 1.93 9.50 +450+ 1.00-1.50 20126 230 534 32.0 0.05 -0.71 0.18 0.126 0.279 15.11 10.66 140.4 1.95 8.80 + +==================================================================================================================================== +CONFOUND CHECK -- the sub-0.5-power shots at long range are the ENDGAME finish +==================================================================================================================================== +hit% of our 450+ shots, by the ENEMY's remaining energy at the fire tick: + enemy energy [ 0, 2) n= 1017 meanP= 0.22 capSlp= 0.111 hit%= 1.10 + enemy energy [ 2, 5) n= 1076 meanP= 0.54 capSlp= 0.128 hit%= 8.55 + enemy energy [ 5, 10) n= 2614 meanP= 0.59 capSlp= 0.135 hit%= 9.69 + enemy energy [ 10, 20) n= 6758 meanP= 0.63 capSlp= 0.146 hit%= 9.85 + enemy energy [ 20, 40) n= 10733 meanP= 0.83 capSlp= 0.147 hit%= 9.43 + enemy energy [ 40,150) n= 14259 meanP= 0.97 capSlp= 0.122 hit%= 9.12 + +same table, EXCLUDING shots fired while the enemy still has >= 5 energy +(this removes the near-dead-target endgame and leaves the clean power comparison): + +range 0-100 (enemy energy >= 5) +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +0-100 0.50-0.75 5 0 80 6.6 -5.78 -4.34 0.41 0.448 0.669 11.80 3.88 16.2 13.00 80.00 +0-100 1.00-1.50 2 0 77 7.5 -28.19 -7.90 0.27 0.292 0.730 20.29 10.29 27.8 13.17 50.00 +0-100 >=1.50 16 0 81 11.3 5.37 0.41 0.98 0.503 0.605 14.99 11.43 20.8 13.05 40.00 + +range 100-200 (enemy energy >= 5) +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +100-200 0.50-0.75 42 2 168 11.2 2.51 2.50 0.03 0.481 0.573 12.93 8.67 37.8 6.33 35.71 +100-200 0.75-1.00 5 0 177 11.2 0.60 -4.71 -1.53 0.527 0.394 11.72 7.59 35.6 5.89 40.00 +100-200 1.00-1.50 23 0 168 12.5 -0.21 6.11 -0.04 0.442 0.616 14.50 11.98 46.5 6.47 42.86 +100-200 >=1.50 175 2 161 16.3 -2.60 1.10 0.05 0.384 0.590 19.19 14.48 54.1 6.57 22.35 + +range 200-300 (enemy energy >= 5) +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +200-300 <0.50 6 0 257 15.7 -0.40 -3.70 0.06 0.512 0.839 10.39 8.54 50.9 4.07 50.00 +200-300 0.50-0.75 119 2 267 16.2 -1.88 -0.63 -0.41 0.192 0.412 15.49 11.23 72.5 3.89 20.35 +200-300 0.75-1.00 17 0 265 17.5 -4.44 -0.78 0.34 0.135 -0.015 16.75 10.98 77.4 3.93 14.29 +200-300 1.00-1.50 776 9 261 17.8 -1.30 0.17 0.08 0.287 0.462 16.00 11.34 72.8 3.98 16.01 +200-300 >=1.50 40 0 249 25.5 -3.41 -0.27 0.03 0.061 0.290 26.76 18.77 118.8 4.19 11.11 + +range 300-450 (enemy energy >= 5) +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +300-450 <0.50 4 0 381 21.5 2.94 -0.28 -1.41 1.202 1.280 17.26 10.28 109.0 2.76 25.00 +300-450 0.50-0.75 4825 54 412 23.5 0.14 0.58 0.24 0.220 0.430 15.08 10.64 108.4 2.52 11.82 +300-450 0.75-1.00 860 9 408 24.8 -0.72 -0.53 -0.19 0.205 0.453 15.94 11.27 113.3 2.54 13.32 +300-450 1.00-1.50 10681 99 402 25.0 -0.87 0.26 -0.07 0.191 0.385 16.38 11.60 114.8 2.59 12.03 +300-450 >=1.50 1 0 312 22.0 -7.39 22.61 -3.06 -3.062 -3.217 30.00 nan 163.1 3.31 0.00 + +range 450+ (enemy energy >= 5) +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +450+ <0.50 4 0 519 26.8 -1.51 -11.72 -5.43 -3.021 -1.809 26.98 6.38 247.2 2.00 0.00 +450+ 0.50-0.75 11882 137 535 29.5 0.31 -0.21 0.16 0.154 0.312 13.97 9.96 130.3 1.95 10.39 +450+ 0.75-1.00 2377 28 540 31.6 0.47 -0.45 0.01 0.127 0.275 14.94 10.41 140.7 1.93 9.60 +450+ 1.00-1.50 20101 230 534 32.0 0.04 -0.71 0.18 0.127 0.279 15.11 10.66 140.4 1.95 8.80 + +==================================================================================================================================== +RESPONSE CURVE -- do we move the aim in proportion to the required lead? +==================================================================================================================================== + +range 200-300 (deciles of required lead -> mean applied lead): + required [ -46.11, -23.72] meanReq= -27.79 meanApp= -9.18 hit%=19.67 n= 123 + required [ -23.71, -16.96] meanReq= -19.97 meanApp= -1.30 hit%=14.53 n= 124 + required [ -16.83, -11.27] meanReq= -13.99 meanApp= -0.85 hit%=14.29 n= 124 + required [ -11.23, -2.84] meanReq= -7.43 meanApp= 0.34 hit%=14.41 n= 124 + required [ -2.76, 5.10] meanReq= 1.01 meanApp= 1.67 hit%= 9.24 n= 123 + required [ 5.11, 13.22] meanReq= 9.12 meanApp= -3.36 hit%=14.88 n= 124 + required [ 13.23, 23.92] meanReq= 18.32 meanApp= 3.04 hit%=11.76 n= 124 + required [ 23.94, 45.18] meanReq= 27.20 meanApp= 12.55 hit%=29.41 n= 124 + +range 300-450 (deciles of required lead -> mean applied lead): + required [ -28.84, -19.87] meanReq= -24.21 meanApp= -6.04 hit%=13.39 n= 2147 + required [ -19.87, -13.44] meanReq= -16.43 meanApp= -0.20 hit%= 9.09 n= 2147 + required [ -13.44, -7.81] meanReq= -10.63 meanApp= -0.19 hit%=10.22 n= 2147 + required [ -7.80, -1.44] meanReq= -4.66 meanApp= -0.12 hit%=11.82 n= 2148 + required [ -1.44, 5.26] meanReq= 1.83 meanApp= 0.34 hit%= 9.96 n= 2147 + required [ 5.26, 12.33] meanReq= 8.84 meanApp= 0.39 hit%= 9.00 n= 2147 + required [ 12.33, 20.39] meanReq= 16.05 meanApp= 0.44 hit%= 9.60 n= 2147 + required [ 20.39, 28.98] meanReq= 24.76 meanApp= 8.54 hit%=21.20 n= 2148 + +range 450+ (deciles of required lead -> mean applied lead): + required [ -28.60, -17.03] meanReq= -21.60 meanApp= -4.09 hit%= 9.27 n= 4557 + required [ -17.03, -10.56] meanReq= -13.70 meanApp= -1.51 hit%= 7.92 n= 4557 + required [ -10.56, -5.13] meanReq= -7.79 meanApp= -1.00 hit%=10.29 n= 4557 + required [ -5.13, 0.00] meanReq= -2.51 meanApp= -0.55 hit%=10.08 n= 4557 + required [ 0.00, 5.27] meanReq= 2.50 meanApp= 0.36 hit%= 9.95 n= 4557 + required [ 5.27, 11.06] meanReq= 8.12 meanApp= -0.16 hit%= 8.75 n= 4557 + required [ 11.06, 17.66] meanReq= 14.26 meanApp= 0.14 hit%= 7.22 n= 4557 + required [ 17.66, 28.54] meanReq= 22.26 meanApp= 3.26 hit%= 9.69 n= 4558 + +==================================================================================================================================== +DIRECT ANSWER -- does capture degrade with range / power? +==================================================================================================================================== +range band n capSlp capLin us/lin medcap |req| |app| |err|px tol |err|/tol hit% +0-100 27 0.401 0.597 0.67 0.450 24.77 13.38 23.2 13.01 1.27 44.00 +100-200 274 0.391 0.600 0.65 0.442 20.21 16.95 50.1 6.57 2.70 25.28 +200-300 990 0.246 0.428 0.58 0.238 15.71 13.84 75.8 3.99 4.17 16.04 +300-450 17178 0.198 0.404 0.49 0.174 13.46 13.09 112.9 2.56 6.23 11.79 +450+ 36457 0.135 0.291 0.46 0.120 11.59 11.43 137.0 1.95 7.54 9.14 + +(capSlp = our proportional lead capture; capLin = naive linear predictor control; + us/lin = our capture / the trivial-predictor ceiling; |req|/|app| = mean ABSOLUTE + lead magnitude in deg; |err|/tol = mean lead error px / 18px) + +POWER WITHIN 450+ BAND, enemy energy >= 5 (removes the endgame confound): + power <0.50 n= 4 meanP= 0.10 meanRange= 519 capSlp=-3.021 capLin=-1.809 |err|px= 247.2 hit%= 0.00 + power 0.50-0.75 n= 11882 meanP= 0.53 meanRange= 535 capSlp= 0.154 capLin= 0.312 |err|px= 130.3 hit%=10.39 + power 0.75-1.00 n= 2377 meanP= 0.87 meanRange= 540 capSlp= 0.127 capLin= 0.275 |err|px= 140.7 hit%= 9.60 + power 1.00-1.50 n= 20101 meanP= 1.00 meanRange= 534 capSlp= 0.127 capLin= 0.279 |err|px= 140.4 hit%= 8.80 + +==================================================================================================================================== +ROBUSTNESS CROSS-CHECK -- powtest corpus (35 runs, 24277 shots, DIFFERENT binary) +==================================================================================================================================== + +by range band (powtest) +cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit% +------------------------------------------------------------------------------------------------------------------------- +0-100 6 0 91 11.2 -6.18 -0.00 0.60 0.529 0.496 14.10 10.10 20.8 11.38 60.00 +100-200 83 0 170 15.8 -3.28 -2.27 0.89 0.354 0.470 19.49 14.92 58.1 6.16 23.46 +200-300 442 3 260 17.6 -2.70 0.80 -0.02 0.283 0.380 16.47 11.92 74.2 4.01 16.55 +300-450 9843 102 405 24.9 -1.13 0.09 0.05 0.219 0.417 16.12 11.52 114.1 2.56 11.90 +450+ 13903 143 525 30.9 0.12 -0.88 0.18 0.136 0.301 15.14 10.80 138.6 1.98 9.24 + +[json] wrote common_libs/tests/fixtures/lead_capture_by_range_results.json diff --git a/common_libs/tests/fixtures/lead_capture_by_range_results.json b/common_libs/tests/fixtures/lead_capture_by_range_results.json new file mode 100644 index 0000000..836c28f --- /dev/null +++ b/common_libs/tests/fixtures/lead_capture_by_range_results.json @@ -0,0 +1,1692 @@ +{ + "corpus": "/tmp/tfil_ab2/out", + "runs": 70, + "shots": 54926, + "hits": 5480, + "misses": 48304, + "excl": 887, + "death_ok": 496, + "death_n": 496, + "amb": 12112, + "overall_hit": 0.09977060044423407, + "hit_ae": 1.4775872516467854, + "miss_ae": 16.72361506357613, + "hit_miss_px": 11.576939690898541, + "miss_miss_px": 134.13466508949332, + "pwr_hist": { + "s": { + "1.0": 32044, + "0.97": 179, + "0.93": 173, + "0.94": 111, + "0.9": 176, + "0.87": 164, + "0.8": 144, + "0.75": 123, + "0.71": 154, + "0.68": 152, + "0.66": 172, + "0.63": 172, + "0.6": 195, + "0.58": 194, + "0.55": 208, + "0.53": 199, + "0.51": 225, + "0.5": 14556, + "3.0": 114, + "0.99": 93, + "0.1": 807, + "0.96": 102, + "0.92": 111, + "0.88": 140, + "0.84": 174, + "0.7": 164, + "0.76": 180, + "0.73": 158, + "0.67": 165, + "0.64": 173, + "0.61": 166, + "0.59": 196, + "0.56": 172, + "0.54": 191, + "0.52": 184, + "0.81": 132, + "0.77": 157, + "0.74": 150, + "0.65": 174, + "0.57": 184, + "0.89": 121, + "0.85": 105, + "0.82": 146, + "0.78": 120, + "0.72": 152, + "0.69": 167, + "0.79": 176, + "0.46": 33, + "0.42": 33, + "0.38": 26, + "0.31": 25, + "0.95": 109, + "0.91": 132, + "2.0": 159, + "0.86": 138, + "0.62": 195, + "0.49": 16, + "0.45": 15, + "0.41": 19, + "0.34": 20, + "0.26": 20, + "0.23": 26, + "0.19": 27, + "0.15": 28, + "0.11": 23, + "0.98": 103, + "0.83": 137, + "0.47": 22, + "0.43": 24, + "0.36": 18, + "0.32": 20, + "0.28": 22, + "0.21": 20, + "0.17": 20, + "0.13": 13, + "0.3": 17, + "0.48": 9, + "0.33": 16, + "0.12": 30, + "0.39": 36, + "0.37": 14, + "1.5": 30, + "0.14": 19, + "0.29": 15, + "0.25": 18, + "0.24": 24, + "0.16": 29, + "0.2": 25, + "0.35": 21, + "0.27": 12, + "1.35": 1, + "0.22": 11, + "0.18": 8, + "0.4": 18, + "2.37": 1, + "1.9": 1, + "0.44": 14, + "2.93": 1, + "1.34": 1, + "1.73": 1, + "1.22": 1, + "2.2": 1, + "1.45": 1, + "2.77": 1, + "1.88": 1, + "2.21": 1, + "1.78": 1, + "1.71": 1, + "1.64": 1, + "1.46": 1, + "1.4": 1, + "1.04": 1 + }, + "e": { + "1.95": 924, + "0.95": 22789, + "0.85": 2153, + "0.65": 2862, + "0.45": 5274, + "0.35": 2340, + "0.25": 3071, + "0.15": 27209, + "2.95": 188, + "1.15": 255 + } + }, + "bands": { + "0-100": { + "n": 27, + "nc": 26, + "excl": 1, + "unresolved": 2, + "range": 80.62957895986038, + "req": 1.5088175460921163, + "app": -0.22936315724914977, + "cap": 0.7159727547341441, + "cap_med": 0.450273802104334, + "cap_slope": 0.4010119579147417, + "cap_lin": 0.4303496861511536, + "cap_lin_slope": 0.5965655402337754, + "ae": 16.759342946670685, + "ae_sd": 11.12737324607712, + "ae_px": 23.198252074786804, + "miss": 33.95313563994795, + "med_miss": 25.960664839402327, + "tol": 13.00725328006312, + "hit": 0.44, + "enE": 51.36277777777778, + "flight": 10.333333333333334 + }, + "100-200": { + "n": 274, + "nc": 260, + "excl": 14, + "unresolved": 9, + "range": 161.6810124315989, + "req": -0.9463786259448971, + "app": 2.2280941817000484, + "cap": 0.06787018247426788, + "cap_med": 0.44188741910857643, + "cap_slope": 0.3914529204182772, + "cap_lin": 0.29394952113939626, + "cap_lin_slope": 0.5997804990629774, + "ae": 17.74223415497345, + "ae_sd": 13.381771777209659, + "ae_px": 50.11208169084824, + "miss": 54.03239615809763, + "med_miss": 46.34263185802082, + "tol": 6.567575229448967, + "hit": 0.2528301886792453, + "enE": 60.64058832116788, + "flight": 14.693430656934307 + }, + "200-300": { + "n": 990, + "nc": 960, + "excl": 30, + "unresolved": 36, + "range": 261.2683228913605, + "req": -1.6691517911978775, + "app": 0.3709219387889482, + "cap": 0.03758761806317558, + "cap_med": 0.23797467514526088, + "cap_slope": 0.24633381549620795, + "cap_lin": 0.06841256540473495, + "cap_lin_slope": 0.4283232929292067, + "ae": 16.647405770897056, + "ae_sd": 11.974143807216727, + "ae_px": 75.77806790600445, + "miss": 76.7500906068486, + "med_miss": 71.46354459009171, + "tol": 3.986128078960045, + "hit": 0.16037735849056603, + "enE": 68.84954474747475, + "flight": 17.93232323232323 + }, + "300-450": { + "n": 17178, + "nc": 16919, + "excl": 259, + "unresolved": 357, + "range": 405.25183138333733, + "req": -0.5558522871343045, + "app": 0.3955771237987067, + "cap": 0.013232806221349697, + "cap_med": 0.174094823474948, + "cap_slope": 0.19751146420716706, + "cap_lin": 0.029934602921301325, + "cap_lin_slope": 0.40358214764819217, + "ae": 15.984531875373005, + "ae_sd": 11.272996789574869, + "ae_px": 112.93680883902307, + "miss": 108.71757171787033, + "med_miss": 100.40631101329052, + "tol": 2.5644225601136292, + "hit": 0.11788835384341002, + "enE": 46.36762741879148, + "flight": 24.47735475608336 + }, + "450+": { + "n": 36457, + "nc": 35874, + "excl": 583, + "unresolved": 738, + "range": 535.3698276977293, + "req": 0.19296819738147536, + "app": -0.4442719716972844, + "cap": 0.1490144639897323, + "cap_med": 0.12039487657594711, + "cap_slope": 0.13454322305047514, + "cap_lin": 0.19259816266663074, + "cap_lin_slope": 0.29116740544364095, + "ae": 14.705404216620314, + "ae_sd": 10.364948981066206, + "ae_px": 137.04412483122658, + "miss": 129.28545556027856, + "med_miss": 118.20210621167871, + "tol": 1.9457976763086455, + "hit": 0.09143593045717965, + "enE": 38.068431033820666, + "flight": 30.97602655182818 + } + }, + "bands_enemy_alive": { + "0-100": { + "n": 23, + "nc": 23, + "excl": 0, + "unresolved": 1, + "range": 80.40544717620176, + "req": 0.02876036907216209, + "app": -1.3454122259538754, + "cap": 0.797727690623683, + "cap_med": 0.5551413845937901, + "cap_slope": 0.4743936246373541, + "cap_lin": 0.4240826081284313, + "cap_lin_slope": 0.6263392263507851, + "ae": 14.75426668034771, + "ae_sd": 10.393973895948612, + "ae_px": 20.407896517036562, + "miss": 30.060964868450508, + "med_miss": 24.56771769411681, + "tol": 13.050007414901632, + "hit": 0.5, + "enE": 60.17804347826087, + "flight": 9.956521739130435 + }, + "100-200": { + "n": 245, + "nc": 241, + "excl": 4, + "unresolved": 7, + "range": 163.1282385629495, + "req": -1.434319785592103, + "app": 1.6903950379557469, + "cap": 0.005773222872200697, + "cap_med": 0.42962733023711785, + "cap_slope": 0.3994400362001393, + "cap_lin": 0.23489687583876923, + "cap_lin_slope": 0.5881513967328396, + "ae": 17.52403322402478, + "ae_sd": 13.581122163170143, + "ae_px": 50.19144777125449, + "miss": 54.01625711394948, + "med_miss": 45.457887674411, + "tol": 6.502641541251235, + "hit": 0.2689075630252101, + "enE": 67.72614489795919, + "flight": 14.93061224489796 + }, + "200-300": { + "n": 958, + "nc": 947, + "excl": 11, + "unresolved": 33, + "range": 261.59542925399677, + "req": -1.5070244450623984, + "app": 0.011535985756713392, + "cap": 0.02301330696518661, + "cap_med": 0.2376199771742153, + "cap_slope": 0.2526145126328312, + "cap_lin": 0.061519978748213786, + "cap_lin_slope": 0.4336668361746349, + "ae": 16.361796623947544, + "ae_sd": 11.919826032701899, + "ae_px": 74.65085225676557, + "miss": 75.72038266838976, + "med_miss": 69.50684831400375, + "tol": 3.980977210734752, + "hit": 0.1654054054054054, + "enE": 71.13685323590815, + "flight": 17.897703549060545 + }, + "300-450": { + "n": 16371, + "nc": 16209, + "excl": 162, + "unresolved": 338, + "range": 405.1138202009172, + "req": -0.5616132539414026, + "app": 0.31362160408922013, + "cap": 0.013612119402836326, + "cap_med": 0.17624165608702905, + "cap_slope": 0.19975078192860996, + "cap_lin": 0.02542101538082886, + "cap_lin_slope": 0.40088573948312733, + "ae": 15.972594664596752, + "ae_sd": 11.320527920847866, + "ae_px": 112.80886270652677, + "miss": 108.60763805072474, + "med_miss": 100.03029506440211, + "tol": 2.565464415464722, + "hit": 0.12037672300879436, + "enE": 48.54231933907519, + "flight": 24.56239692138538 + }, + "450+": { + "n": 34364, + "nc": 33969, + "excl": 395, + "unresolved": 687, + "range": 535.0530932955318, + "req": 0.16309612151009867, + "app": -0.5213836293286923, + "cap": 0.16092346670108204, + "cap_med": 0.12204968702717578, + "cap_slope": 0.13517495559870765, + "cap_lin": 0.2056368079548827, + "cap_lin_slope": 0.28905972949581915, + "ae": 14.707057997378662, + "ae_sd": 10.420164245502942, + "ae_px": 136.96759181117696, + "miss": 129.15095756880612, + "med_miss": 117.9342445221643, + "tol": 1.9468503349347313, + "hit": 0.09401074917599549, + "enE": 40.25511039750902, + "flight": 31.08677685950413 + } + }, + "band_power_alive": { + "0-100": { + "<0.50": { + "n": 0, + "nc": 0, + "excl": 0, + "unresolved": 0, + "range": NaN, + "req": NaN, + "app": NaN, + "cap": NaN, + "cap_med": NaN, + "cap_slope": NaN, + "cap_lin": NaN, + "cap_lin_slope": NaN, + "ae": NaN, + "ae_sd": NaN, + "ae_px": NaN, + "miss": NaN, + "med_miss": NaN, + "tol": NaN, + "hit": NaN, + "enE": NaN, + "flight": NaN + }, + "0.50-0.75": { + "n": 5, + "nc": 5, + "excl": 0, + "unresolved": 0, + "range": 80.19973608558801, + "req": -5.784996669425965, + "app": -4.3371024027399585, + "cap": 0.4143058565432726, + "cap_med": 0.41489614858222323, + "cap_slope": 0.4483562625057536, + "cap_lin": 0.6285198899870553, + "cap_lin_slope": 0.6688894092993852, + "ae": 11.799439831920887, + "ae_sd": 3.8830863775954816, + "ae_px": 16.228879071158083, + "miss": 23.77793976567463, + "med_miss": 20.348925456286224, + "tol": 12.995881050964195, + "hit": 0.8, + "enE": 20.119, + "flight": 6.6 + }, + "0.75-1.00": { + "n": 0, + "nc": 0, + "excl": 0, + "unresolved": 0, + "range": NaN, + "req": NaN, + "app": NaN, + "cap": NaN, + "cap_med": NaN, + "cap_slope": NaN, + "cap_lin": NaN, + "cap_lin_slope": NaN, + "ae": NaN, + "ae_sd": NaN, + "ae_px": NaN, + "miss": NaN, + "med_miss": NaN, + "tol": NaN, + "hit": NaN, + "enE": NaN, + "flight": NaN + }, + "1.00-1.50": { + "n": 2, + "nc": 2, + "excl": 0, + "unresolved": 0, + "range": 77.00988992436935, + "req": -28.193929663313682, + "app": -7.902429754411628, + "cap": 0.26873027191101717, + "cap_med": 0.26873027191101717, + "cap_slope": 0.2918257841442612, + "cap_lin": 0.7275025942709974, + "cap_lin_slope": 0.7296879237251552, + "ae": 20.291499908902054, + "ae_sd": 10.288503862156333, + "ae_px": 27.785597168085275, + "miss": 32.96827900563402, + "med_miss": 32.96827900563402, + "tol": 13.17245458639505, + "hit": 0.5, + "enE": 101.9, + "flight": 7.5 + }, + ">=1.50": { + "n": 16, + "nc": 16, + "excl": 0, + "unresolved": 1, + "range": 80.8941765484976, + "req": 5.373395697651057, + "app": 0.4091181453489945, + "cap": 0.9836716911128944, + "cap_med": 0.642834027505462, + "cap_slope": 0.502588692712829, + "cap_lin": 0.3222684592797906, + "cap_lin_slope": 0.6053612593396412, + "ae": 14.985495916911796, + "ae_sd": 11.434233525324435, + "ae_px": 20.7916268874925, + "miss": 31.660995945920032, + "med_miss": 28.299326322923605, + "tol": 13.051616007195404, + "hit": 0.4, + "enE": 67.48125, + "flight": 11.3125 + } + }, + "100-200": { + "<0.50": { + "n": 0, + "nc": 0, + "excl": 0, + "unresolved": 0, + "range": NaN, + "req": NaN, + "app": NaN, + "cap": NaN, + "cap_med": NaN, + "cap_slope": NaN, + "cap_lin": NaN, + "cap_lin_slope": NaN, + "ae": NaN, + "ae_sd": NaN, + "ae_px": NaN, + "miss": NaN, + "med_miss": NaN, + "tol": NaN, + "hit": NaN, + "enE": NaN, + "flight": NaN + }, + "0.50-0.75": { + "n": 42, + "nc": 40, + "excl": 2, + "unresolved": 0, + "range": 167.58501036863132, + "req": 2.5111708400249415, + "app": 2.497325712588694, + "cap": 0.03356403514335639, + "cap_med": 0.49225971578872874, + "cap_slope": 0.4811089909502785, + "cap_lin": -0.007709473209938121, + "cap_lin_slope": 0.5731096759499961, + "ae": 12.933078891615569, + "ae_sd": 8.668578016402694, + "ae_px": 37.780656545235864, + "miss": 40.23681219985416, + "med_miss": 39.82112515350846, + "tol": 6.329156920400118, + "hit": 0.35714285714285715, + "enE": 23.665454761904762, + "flight": 11.19047619047619 + }, + "0.75-1.00": { + "n": 5, + "nc": 5, + "excl": 0, + "unresolved": 0, + "range": 177.29592478173686, + "req": 0.6014833041837335, + "app": -4.714534347074624, + "cap": -1.5341673936620293, + "cap_med": 0.28133834335040797, + "cap_slope": 0.5271841911475986, + "cap_lin": 0.5970426589925376, + "cap_lin_slope": 0.39421741908791397, + "ae": 11.724375935640898, + "ae_sd": 7.592018841909129, + "ae_px": 35.64816918094623, + "miss": 33.86894195555372, + "med_miss": 27.45816896169356, + "tol": 5.890191286041689, + "hit": 0.4, + "enE": 33.730000000000004, + "flight": 11.2 + }, + "1.00-1.50": { + "n": 23, + "nc": 23, + "excl": 0, + "unresolved": 2, + "range": 168.44388841904546, + "req": -0.20849192012527565, + "app": 6.1095658419097, + "cap": -0.03600547106678006, + "cap_med": 0.30654392212035936, + "cap_slope": 0.4424331656485674, + "cap_lin": 0.2275713957248691, + "cap_lin_slope": 0.6163743344551899, + "ae": 14.495298634424183, + "ae_sd": 11.979026713826014, + "ae_px": 46.50990497678963, + "miss": 47.91691117103256, + "med_miss": 36.831541334445475, + "tol": 6.470889912584101, + "hit": 0.42857142857142855, + "enE": 83.10217391304347, + "flight": 12.521739130434783 + }, + ">=1.50": { + "n": 175, + "nc": 173, + "excl": 2, + "unresolved": 5, + "range": 160.95519402796216, + "req": -2.600512143480858, + "app": 1.0989243528107593, + "cap": 0.049408948608741045, + "cap_med": 0.4696587079186667, + "cap_slope": 0.38407364468027616, + "cap_lin": 0.28149809600523834, + "cap_lin_slope": 0.5898944947701622, + "ae": 19.189629018104323, + "ae_sd": 14.48051324982911, + "ae_px": 54.069419821066006, + "miss": 58.70058979321276, + "med_miss": 52.44849444029164, + "tol": 6.565949500172028, + "hit": 0.2235294117647059, + "enE": 77.25117942857142, + "flight": 16.251428571428573 + } + }, + "200-300": { + "<0.50": { + "n": 6, + "nc": 6, + "excl": 0, + "unresolved": 0, + "range": 257.04397811113955, + "req": -0.40095100525506666, + "app": -3.7028176137103137, + "cap": 0.058040951459825164, + "cap_med": 0.48337268667604544, + "cap_slope": 0.5116290259806096, + "cap_lin": 0.31048023902142097, + "cap_lin_slope": 0.839316316100375, + "ae": 10.38646529533778, + "ae_sd": 8.544999776665943, + "ae_px": 50.886699738081205, + "miss": 53.18647211884433, + "med_miss": 47.48534452361258, + "tol": 4.068939518914536, + "hit": 0.5, + "enE": 99.75, + "flight": 15.666666666666666 + }, + "0.50-0.75": { + "n": 119, + "nc": 117, + "excl": 2, + "unresolved": 6, + "range": 267.34607606728326, + "req": -1.8817245506155, + "app": -0.6339406015447784, + "cap": -0.40843774236125036, + "cap_med": 0.07800314920836217, + "cap_slope": 0.19151035279258638, + "cap_lin": -0.763777480453482, + "cap_lin_slope": 0.41232527492392135, + "ae": 15.490593569253745, + "ae_sd": 11.22630713916203, + "ae_px": 72.4841182614778, + "miss": 72.66159505854908, + "med_miss": 66.87461202985347, + "tol": 3.8901512058995573, + "hit": 0.20353982300884957, + "enE": 21.875410084033614, + "flight": 16.218487394957982 + }, + "0.75-1.00": { + "n": 17, + "nc": 17, + "excl": 0, + "unresolved": 3, + "range": 264.74408859474806, + "req": -4.444207029665389, + "app": -0.7796793310339183, + "cap": 0.33850017511498004, + "cap_med": 0.25358397604987454, + "cap_slope": 0.13529178092642039, + "cap_lin": 0.13276808091179357, + "cap_lin_slope": -0.015135370315159949, + "ae": 16.745143603207868, + "ae_sd": 10.980503467014255, + "ae_px": 77.39761508035474, + "miss": 78.65935976704714, + "med_miss": 76.46615491738754, + "tol": 3.9315832447824457, + "hit": 0.14285714285714285, + "enE": 33.00927647058823, + "flight": 17.470588235294116 + }, + "1.00-1.50": { + "n": 776, + "nc": 767, + "excl": 9, + "unresolved": 20, + "range": 261.3382725436281, + "req": -1.29565736597803, + "app": 0.1708437449468118, + "cap": 0.08102617318945239, + "cap_med": 0.24590348278611776, + "cap_slope": 0.28716411408060705, + "cap_lin": 0.15327404538692432, + "cap_lin_slope": 0.4617970960690342, + "ae": 15.997106769894566, + "ae_sd": 11.340777221098255, + "ae_px": 72.8327970476196, + "miss": 74.5932582501372, + "med_miss": 69.02833182352721, + "tol": 3.9847389740831543, + "hit": 0.16005291005291006, + "enE": 81.88235528350515, + "flight": 17.789948453608247 + }, + ">=1.50": { + "n": 40, + "nc": 40, + "excl": 0, + "unresolved": 4, + "range": 248.8206326172311, + "req": -3.4104213827934964, + "app": -0.2653221457531849, + "cap": 0.03327485075686055, + "cap_med": 0.16828837814262615, + "cap_slope": 0.06087555854688955, + "cap_lin": 0.6485063366553971, + "cap_lin_slope": 0.29024007483656283, + "ae": 26.76198611339519, + "ae_sd": 18.767484393654854, + "ae_px": 118.764405627956, + "miss": 108.81751083726786, + "med_miss": 118.85933939342557, + "tol": 4.186004455463209, + "hit": 0.1111111111111111, + "enE": 21.139155, + "flight": 25.5 + } + }, + "300-450": { + "<0.50": { + "n": 4, + "nc": 4, + "excl": 0, + "unresolved": 0, + "range": 381.2780552467385, + "req": 2.9433054560504033, + "app": -0.2774530394729382, + "cap": -1.4100819286658457, + "cap_med": 2.2601150261824343, + "cap_slope": 1.2023151267025114, + "cap_lin": -0.538757091070623, + "cap_lin_slope": 1.2804802236484676, + "ae": 17.257839680495692, + "ae_sd": 10.284256939839132, + "ae_px": 109.02125743610677, + "miss": 115.72365613147448, + "med_miss": 132.09424077030533, + "tol": 2.762361024915263, + "hit": 0.25, + "enE": 100.0, + "flight": 21.5 + }, + "0.50-0.75": { + "n": 4825, + "nc": 4771, + "excl": 54, + "unresolved": 106, + "range": 411.6414905879411, + "req": 0.13964495555649875, + "app": 0.5845498270321021, + "cap": 0.2370947603686914, + "cap_med": 0.2247369626740763, + "cap_slope": 0.21987577361851013, + "cap_lin": 0.2988626561126433, + "cap_lin_slope": 0.4296179788172217, + "ae": 15.08408005375962, + "ae_sd": 10.641970350877521, + "ae_px": 108.4023020261597, + "miss": 104.23970806543392, + "med_miss": 94.85780107456506, + "tol": 2.5203630116764058, + "hit": 0.1182453909726637, + "enE": 20.6741334507772, + "flight": 23.458652849740933 + }, + "0.75-1.00": { + "n": 860, + "nc": 851, + "excl": 9, + "unresolved": 19, + "range": 407.9680501923543, + "req": -0.723890879926475, + "app": -0.5293586702837004, + "cap": -0.19126157458193718, + "cap_med": 0.161953787547946, + "cap_slope": 0.20513550617212375, + "cap_lin": -0.19449998574116673, + "cap_lin_slope": 0.45255618321217217, + "ae": 15.936322544081413, + "ae_sd": 11.268280339839015, + "ae_px": 113.26546635560938, + "miss": 109.0332163175552, + "med_miss": 104.88676326309509, + "tol": 2.543403343021588, + "hit": 0.13317479191438764, + "enE": 29.115581162790697, + "flight": 24.760465116279068 + }, + "1.00-1.50": { + "n": 10681, + "nc": 10582, + "excl": 99, + "unresolved": 213, + "range": 401.9529021406476, + "req": -0.8660049344590963, + "app": 0.2572410435135986, + "cap": -0.06984262514944221, + "cap_med": 0.1559070378956981, + "cap_slope": 0.19111787720731255, + "cap_lin": -0.07965725075049614, + "cap_lin_slope": 0.3851119893994255, + "ae": 16.375095342119533, + "ae_sd": 11.595867145828999, + "ae_px": 114.75941050030012, + "miss": 110.53744952960855, + "med_miss": 101.88289499467055, + "tol": 2.587471560570678, + "hit": 0.12027130301872373, + "enE": 62.671492950098305, + "flight": 25.046437599475706 + }, + ">=1.50": { + "n": 1, + "nc": 1, + "excl": 0, + "unresolved": 0, + "range": 311.57527173081314, + "req": -7.385850964632965, + "app": 22.613047947857467, + "cap": -3.0616713031632647, + "cap_med": -3.0616713031632647, + "cap_slope": -3.0616713031632647, + "cap_lin": -3.2169503338007974, + "cap_lin_slope": -3.2169503338007974, + "ae": 29.998898912490432, + "ae_sd": NaN, + "ae_px": 163.13444305527292, + "miss": 177.09202932397116, + "med_miss": 177.09202932397116, + "tol": 3.3063566806656928, + "hit": 0.0, + "enE": 100.0, + "flight": 22.0 + } + }, + "450+": { + "<0.50": { + "n": 4, + "nc": 4, + "excl": 0, + "unresolved": 0, + "range": 518.9423722155948, + "req": -1.5113467463419212, + "app": -11.717613307891298, + "cap": -5.433423756827285, + "cap_med": -5.891403469892164, + "cap_slope": -3.021178622380739, + "cap_lin": -3.442565925213788, + "cap_lin_slope": -1.8087404061278551, + "ae": 26.976131389959626, + "ae_sd": 6.378984226076712, + "ae_px": 247.18113704489696, + "miss": 228.44009058211805, + "med_miss": 235.7395246246679, + "tol": 1.997421799410438, + "hit": 0.0, + "enE": 100.0, + "flight": 26.75 + }, + "0.50-0.75": { + "n": 11882, + "nc": 11745, + "excl": 137, + "unresolved": 252, + "range": 535.4223554938594, + "req": 0.30573533948222004, + "app": -0.20866247193860857, + "cap": 0.1567713454975538, + "cap_med": 0.1468036502870559, + "cap_slope": 0.15355380598107812, + "cap_lin": 0.3738474204499819, + "cap_lin_slope": 0.31159881248573157, + "ae": 13.967640585588336, + "ae_sd": 9.963208903245107, + "ae_px": 130.31251875741907, + "miss": 123.21660007989458, + "med_miss": 111.78300522963711, + "tol": 1.9450627730119083, + "hit": 0.10386930352536543, + "enE": 19.941731795993938, + "flight": 29.52558491836391 + }, + "0.75-1.00": { + "n": 2377, + "nc": 2349, + "excl": 28, + "unresolved": 65, + "range": 540.3514563874147, + "req": 0.47211564902955655, + "app": -0.45252192529590496, + "cap": 0.008167175795027968, + "cap_med": 0.10211813616002974, + "cap_slope": 0.12734276686901772, + "cap_lin": -0.25776574252614337, + "cap_lin_slope": 0.27508419703774345, + "ae": 14.940704022341318, + "ae_sd": 10.412985102600812, + "ae_px": 140.6773585889178, + "miss": 132.4595546285093, + "med_miss": 121.5167509792519, + "tol": 1.9266822105379862, + "hit": 0.09602076124567474, + "enE": 26.807737694572992, + "flight": 31.55952881783761 + }, + "1.00-1.50": { + "n": 20101, + "nc": 19871, + "excl": 230, + "unresolved": 370, + "range": 534.2114764791736, + "req": 0.042570733052455255, + "app": -0.7121528519484953, + "cap": 0.1825614607510983, + "cap_med": 0.10475882036511841, + "cap_slope": 0.12655067350622493, + "cap_lin": 0.161728185251328, + "cap_lin_slope": 0.27893834453213434, + "ae": 15.114067837286433, + "ae_sd": 10.65793519074787, + "ae_px": 140.44088210584522, + "miss": 132.24783553210543, + "med_miss": 121.48342117237542, + "tol": 1.9502818635452548, + "hit": 0.0879833764127515, + "enE": 53.84095139545296, + "flight": 31.954579374160488 + }, + ">=1.50": { + "n": 0, + "nc": 0, + "excl": 0, + "unresolved": 0, + "range": NaN, + "req": NaN, + "app": NaN, + "cap": NaN, + "cap_med": NaN, + "cap_slope": NaN, + "cap_lin": NaN, + "cap_lin_slope": NaN, + "ae": NaN, + "ae_sd": NaN, + "ae_px": NaN, + "miss": NaN, + "med_miss": NaN, + "tol": NaN, + "hit": NaN, + "enE": NaN, + "flight": NaN + } + } + }, + "hit_by_enemy_energy": { + "0-2": { + "n": 1017, + "nc": 837, + "excl": 180, + "unresolved": 16, + "range": 536.6790175757711, + "req": 0.8228983733943923, + "app": 2.1046228363679904, + "cap": 0.05496831547053957, + "cap_med": 0.06465723713227596, + "cap_slope": 0.11091802101776467, + "cap_lin": 0.08702299874142348, + "cap_lin_slope": 0.29502609292298704, + "ae": 15.082588621600044, + "ae_sd": 8.694376004817034, + "ae_px": 141.108117032008, + "miss": 135.20070560850567, + "med_miss": 127.80275347900675, + "tol": 1.9413469945759219, + "hit": 0.01098901098901099, + "enE": 0.605397738446411, + "flight": 28.257620452310718 + }, + "2-5": { + "n": 1076, + "nc": 1068, + "excl": 8, + "unresolved": 35, + "range": 544.2479084515859, + "req": 0.5515974039231604, + "app": -0.39070414359011174, + "cap": -0.1560607109084644, + "cap_med": 0.11287847155386377, + "cap_slope": 0.12833134350858272, + "cap_lin": -0.1393712470629883, + "cap_lin_slope": 0.3597592351276956, + "ae": 14.296085385910057, + "ae_sd": 10.028546390312622, + "ae_px": 135.64719233381962, + "miss": 127.98999057878416, + "med_miss": 116.6784640911685, + "tol": 1.9163857639437514, + "hit": 0.08549471661863593, + "enE": 3.6417165427509293, + "flight": 30.00836431226766 + }, + "5-10": { + "n": 2614, + "nc": 2582, + "excl": 32, + "unresolved": 64, + "range": 539.3964460767202, + "req": 0.420452492131126, + "app": -0.11682654513339118, + "cap": 0.23880415004479427, + "cap_med": 0.07895647477107856, + "cap_slope": 0.13529890696677987, + "cap_lin": 0.35009704552115384, + "cap_lin_slope": 0.287335425975748, + "ae": 14.273381431331266, + "ae_sd": 10.01464119345035, + "ae_px": 134.3931265186744, + "miss": 127.14952400894086, + "med_miss": 115.73281087242975, + "tol": 1.9312761208608171, + "hit": 0.09686274509803922, + "enE": 7.696370045906657, + "flight": 30.10328997704667 + }, + "10-20": { + "n": 6758, + "nc": 6678, + "excl": 80, + "unresolved": 149, + "range": 535.29744789742, + "req": 0.09547694641249485, + "app": -0.338279002333264, + "cap": 0.2586822086676958, + "cap_med": 0.16384709847731554, + "cap_slope": 0.14620752760008707, + "cap_lin": 0.3862715446853324, + "cap_lin_slope": 0.3041510587594136, + "ae": 14.303532167267253, + "ae_sd": 10.143299433757159, + "ae_px": 133.30574225765417, + "miss": 125.84109224458348, + "med_miss": 113.84858120506927, + "tol": 1.9457373569163061, + "hit": 0.0985020426690876, + "enE": 15.210433796981356, + "flight": 30.017164841669132 + }, + "20-40": { + "n": 10733, + "nc": 10615, + "excl": 118, + "unresolved": 209, + "range": 537.6912202611135, + "req": 0.30571039153015067, + "app": -0.5520730632369039, + "cap": 0.14412986059674784, + "cap_med": 0.13277884477531968, + "cap_slope": 0.1468022058972456, + "cap_lin": 0.17052284365010795, + "cap_lin_slope": 0.3123674241870583, + "ae": 14.571755120767444, + "ae_sd": 10.393431274295809, + "ae_px": 136.3017131827541, + "miss": 128.3094502955935, + "med_miss": 116.80046993090255, + "tol": 1.937490955843962, + "hit": 0.09426073736221968, + "enE": 28.84585194260691, + "flight": 31.16984999534147 + }, + "40-150": { + "n": 14259, + "nc": 14094, + "excl": 165, + "unresolved": 265, + "range": 532.1552821382853, + "req": 0.040616415526581066, + "app": -0.6592294525410345, + "cap": 0.11298415391202256, + "cap_med": 0.10153759307250979, + "cap_slope": 0.12189987384085721, + "cap_lin": 0.12003014028162665, + "cap_lin_slope": 0.2657809009063753, + "ae": 15.079655225670196, + "ae_sd": 10.627881044012037, + "ae_px": 139.6762885554207, + "miss": 131.71998167638557, + "med_miss": 121.40458504263833, + "tol": 1.9572779046674582, + "hit": 0.09118193511504931, + "enE": 66.68165102040815, + "flight": 31.71148046847605 + } + }, + "band_power": { + "0-100": { + "<0.50": { + "n": 0, + "nc": 0, + "excl": 0, + "unresolved": 0, + "range": NaN, + "req": NaN, + "app": NaN, + "cap": NaN, + "cap_med": NaN, + "cap_slope": NaN, + "cap_lin": NaN, + "cap_lin_slope": NaN, + "ae": NaN, + "ae_sd": NaN, + "ae_px": NaN, + "miss": NaN, + "med_miss": NaN, + "tol": NaN, + "hit": NaN, + "enE": NaN, + "flight": NaN + }, + "0.50-0.75": { + "n": 6, + "nc": 5, + "excl": 1, + "unresolved": 0, + "range": 82.73888412188033, + "req": -4.820830557854971, + "app": -1.0945330851342305, + "cap": 0.4143058565432726, + "cap_med": 0.41489614858222323, + "cap_slope": 0.4483562625057536, + "cap_lin": 0.6285198899870553, + "cap_lin_slope": 0.6688894092993852, + "ae": 12.352585443749808, + "ae_sd": 3.7543508341635157, + "ae_px": 17.721031711959466, + "miss": 23.963383018113465, + "med_miss": 22.619762368296932, + "tol": 12.610088384382891, + "hit": 0.6666666666666666, + "enE": 16.765833333333333, + "flight": 6.5 + }, + "0.75-1.00": { + "n": 0, + "nc": 0, + "excl": 0, + "unresolved": 0, + "range": NaN, + "req": NaN, + "app": NaN, + "cap": NaN, + "cap_med": NaN, + "cap_slope": NaN, + "cap_lin": NaN, + "cap_lin_slope": NaN, + "ae": NaN, + "ae_sd": NaN, + "ae_px": NaN, + "miss": NaN, + "med_miss": NaN, + "tol": NaN, + "hit": NaN, + "enE": NaN, + "flight": NaN + }, + "1.00-1.50": { + "n": 2, + "nc": 2, + "excl": 0, + "unresolved": 0, + "range": 77.00988992436935, + "req": -28.193929663313682, + "app": -7.902429754411628, + "cap": 0.26873027191101717, + "cap_med": 0.26873027191101717, + "cap_slope": 0.2918257841442612, + "cap_lin": 0.7275025942709974, + "cap_lin_slope": 0.7296879237251552, + "ae": 20.291499908902054, + "ae_sd": 10.288503862156333, + "ae_px": 27.785597168085275, + "miss": 32.96827900563402, + "med_miss": 32.96827900563402, + "tol": 13.17245458639505, + "hit": 0.5, + "enE": 101.9, + "flight": 7.5 + }, + ">=1.50": { + "n": 19, + "nc": 19, + "excl": 0, + "unresolved": 2, + "range": 80.34450249137946, + "req": 6.634258758854965, + "app": 0.8515396196790314, + "cap": 0.842436936660492, + "cap_med": 0.4856514556264448, + "cap_slope": 0.4052008143494761, + "cap_lin": 0.34692037902382744, + "cap_lin_slope": 0.5736289324846443, + "ae": 17.77914458314766, + "ae_sd": 12.323684902972499, + "ae_px": 24.44502217954297, + "miss": 37.21146348203399, + "med_miss": 36.19598304838448, + "tol": 13.11528416224299, + "hit": 0.35294117647058826, + "enE": 56.968421052631584, + "flight": 11.842105263157896 + } + }, + "100-200": { + "<0.50": { + "n": 5, + "nc": 5, + "excl": 0, + "unresolved": 0, + "range": 160.91815647448593, + "req": 4.771757177253886, + "app": -1.523768337009642, + "cap": -0.0780894179645866, + "cap_med": 0.6780898327843868, + "cap_slope": 0.4360137121086668, + "cap_lin": 0.31346740998902634, + "cap_lin_slope": 0.49519677163013354, + "ae": 14.84263371566978, + "ae_sd": 8.18497354271307, + "ae_px": 41.520278698110836, + "miss": 46.00180636762276, + "med_miss": 33.71639175114203, + "tol": 6.475854711931637, + "hit": 0.2, + "enE": 0.6402, + "flight": 10.4 + }, + "0.50-0.75": { + "n": 53, + "nc": 41, + "excl": 12, + "unresolved": 1, + "range": 165.83398814144545, + "req": 1.779195434333774, + "app": 5.830604393312363, + "cap": 0.04460355090674677, + "cap_med": 0.4861841814423617, + "cap_slope": 0.48115394536848943, + "cap_lin": -0.01097287434422986, + "cap_lin_slope": 0.5667798124185786, + "ae": 14.31123554011399, + "ae_sd": 8.7159020834933, + "ae_px": 41.20286889669227, + "miss": 43.03012253128062, + "med_miss": 42.908672696811024, + "tol": 6.382354716107123, + "hit": 0.3076923076923077, + "enE": 18.808990566037735, + "flight": 10.830188679245284 + }, + "0.75-1.00": { + "n": 5, + "nc": 5, + "excl": 0, + "unresolved": 0, + "range": 177.29592478173686, + "req": 0.6014833041837335, + "app": -4.714534347074624, + "cap": -1.5341673936620293, + "cap_med": 0.28133834335040797, + "cap_slope": 0.5271841911475986, + "cap_lin": 0.5970426589925376, + "cap_lin_slope": 0.39421741908791397, + "ae": 11.724375935640898, + "ae_sd": 7.592018841909129, + "ae_px": 35.64816918094623, + "miss": 33.86894195555372, + "med_miss": 27.45816896169356, + "tol": 5.890191286041689, + "hit": 0.4, + "enE": 33.730000000000004, + "flight": 11.2 + }, + "1.00-1.50": { + "n": 23, + "nc": 23, + "excl": 0, + "unresolved": 2, + "range": 168.44388841904546, + "req": -0.20849192012527565, + "app": 6.1095658419097, + "cap": -0.03600547106678006, + "cap_med": 0.30654392212035936, + "cap_slope": 0.4424331656485674, + "cap_lin": 0.2275713957248691, + "cap_lin_slope": 0.6163743344551899, + "ae": 14.495298634424183, + "ae_sd": 11.979026713826014, + "ae_px": 46.50990497678963, + "miss": 47.91691117103256, + "med_miss": 36.831541334445475, + "tol": 6.470889912584101, + "hit": 0.42857142857142855, + "enE": 83.10217391304347, + "flight": 12.521739130434783 + }, + ">=1.50": { + "n": 188, + "nc": 186, + "excl": 2, + "unresolved": 6, + "range": 159.28785210022517, + "req": -1.998276541345205, + "app": 1.0220599574295546, + "cap": 0.13283285886452714, + "cap_med": 0.44188741910857643, + "cap_slope": 0.3752014708293559, + "cap_lin": 0.360699305902025, + "cap_lin_slope": 0.6056685284467948, + "ae": 19.34388169142752, + "ae_sd": 14.503130275425203, + "ae_px": 53.677597222905746, + "miss": 58.6321135883577, + "med_miss": 53.08803379033722, + "tol": 6.6520750794470125, + "hit": 0.21428571428571427, + "enE": 71.99704095744681, + "flight": 16.25531914893617 + } + }, + "200-300": { + "<0.50": { + "n": 6, + "nc": 6, + "excl": 0, + "unresolved": 0, + "range": 257.04397811113955, + "req": -0.40095100525506666, + "app": -3.7028176137103137, + "cap": 0.058040951459825164, + "cap_med": 0.48337268667604544, + "cap_slope": 0.5116290259806096, + "cap_lin": 0.31048023902142097, + "cap_lin_slope": 0.839316316100375, + "ae": 10.38646529533778, + "ae_sd": 8.544999776665943, + "ae_px": 50.886699738081205, + "miss": 53.18647211884433, + "med_miss": 47.48534452361258, + "tol": 4.068939518914536, + "hit": 0.5, + "enE": 99.75, + "flight": 15.666666666666666 + }, + "0.50-0.75": { + "n": 130, + "nc": 118, + "excl": 12, + "unresolved": 6, + "range": 266.7927676541265, + "req": -1.5359001664826033, + "app": 1.2474587183690138, + "cap": -0.3986853604722985, + "cap_med": 0.09434925064913874, + "cap_slope": 0.20233863017760845, + "cap_lin": -0.7525633306796652, + "cap_lin_slope": 0.4152182829728191, + "ae": 15.917165956111582, + "ae_sd": 10.9315838181991, + "ae_px": 74.28347835621435, + "miss": 74.31158803935683, + "med_miss": 70.16251675969957, + "tol": 3.8991222353980755, + "hit": 0.18548387096774194, + "enE": 20.04326, + "flight": 16.123076923076923 + }, + "0.75-1.00": { + "n": 21, + "nc": 17, + "excl": 4, + "unresolved": 3, + "range": 262.6504329262675, + "req": -3.5976914049672195, + "app": 3.1352541648068515, + "cap": 0.33850017511498004, + "cap_med": 0.25358397604987454, + "cap_slope": 0.13529178092642039, + "cap_lin": 0.13276808091179357, + "cap_lin_slope": -0.015135370315159949, + "ae": 17.322015587764486, + "ae_sd": 10.062138160629464, + "ae_px": 79.14742808570033, + "miss": 79.82200160363949, + "med_miss": 77.64490204031988, + "tol": 3.9594147050420085, + "hit": 0.1111111111111111, + "enE": 26.721795238095236, + "flight": 17.095238095238095 + }, + "1.00-1.50": { + "n": 780, + "nc": 767, + "excl": 13, + "unresolved": 20, + "range": 261.23705455058115, + "req": -1.2890129692294248, + "app": 0.2939974951004185, + "cap": 0.08102617318945239, + "cap_med": 0.24590348278611776, + "cap_slope": 0.28716411408060705, + "cap_lin": 0.15327404538692432, + "cap_lin_slope": 0.4617970960690342, + "ae": 16.039100196843314, + "ae_sd": 11.327308443428587, + "ae_px": 72.98009605067016, + "miss": 74.71615889047352, + "med_miss": 69.50684831400375, + "tol": 3.98633199135602, + "hit": 0.15921052631578947, + "enE": 81.46244576923077, + "flight": 17.774358974358975 + }, + ">=1.50": { + "n": 53, + "nc": 52, + "excl": 1, + "unresolved": 7, + "range": 248.10857278941043, + "req": -6.969922832233339, + "app": -1.2811017347909721, + "cap": 0.2861377375837729, + "cap_med": 0.16828837814262615, + "cap_slope": 0.06341113658487778, + "cap_lin": 0.6307193112951167, + "cap_lin_slope": 0.2906598649294342, + "ae": 27.832469174353083, + "ae_sd": 17.66225138184127, + "ae_px": 122.10460250727586, + "miss": 114.115065183111, + "med_miss": 119.24390971727678, + "tol": 4.197747328143966, + "hit": 0.08695652173913043, + "enE": 16.133209433962264, + "flight": 25.28301886792453 + } + }, + "300-450": { + "<0.50": { + "n": 333, + "nc": 270, + "excl": 63, + "unresolved": 7, + "range": 409.1215230433167, + "req": -0.9317180544272952, + "app": 0.9191448810340326, + "cap": -0.6748596925802552, + "cap_med": -0.035273797274138104, + "cap_slope": 0.09375887838761482, + "cap_lin": -0.4950093102438915, + "cap_lin_slope": 0.4926377913850616, + "ae": 16.29854924896224, + "ae_sd": 9.08552689365683, + "ae_px": 116.42556926165817, + "miss": 112.8439835470441, + "med_miss": 113.56284692460481, + "tol": 2.5361522425561307, + "hit": 0.03067484662576687, + "enE": 1.917082882882883, + "flight": 22.01201201201201 + }, + "0.50-0.75": { + "n": 5268, + "nc": 5186, + "excl": 82, + "unresolved": 117, + "range": 411.31057270180537, + "req": 0.11701902921878686, + "app": 0.7459651659230269, + "cap": 0.25284977132901765, + "cap_med": 0.22804986133072283, + "cap_slope": 0.21569990037014347, + "cap_lin": 0.31371317471392995, + "cap_lin_slope": 0.4309449056225032, + "ae": 15.160114894649077, + "ae_sd": 10.675653216953416, + "ae_px": 108.8616274196598, + "miss": 104.6180282321021, + "med_miss": 95.21722489435666, + "tol": 2.5225025277972097, + "hit": 0.11648223645894001, + "enE": 19.21493422551253, + "flight": 23.43507972665148 + }, + "0.75-1.00": { + "n": 880, + "nc": 868, + "excl": 12, + "unresolved": 20, + "range": 407.71239654330174, + "req": -0.7235970593297041, + "app": -0.35653597446373764, + "cap": -0.1901189396584436, + "cap_med": 0.14802653516213743, + "cap_slope": 0.20305942458057133, + "cap_lin": -0.17423224398645895, + "cap_lin_slope": 0.45799351675795746, + "ae": 15.999439842419108, + "ae_sd": 11.283143153599347, + "ae_px": 113.56848396237788, + "miss": 109.32032724864372, + "med_miss": 105.58637644519364, + "tol": 2.5452749499676743, + "hit": 0.1313953488372093, + "enE": 28.51963125, + "flight": 24.748863636363637 + }, + "1.00-1.50": { + "n": 10696, + "nc": 10594, + "excl": 102, + "unresolved": 213, + "range": 401.9536192134067, + "req": -0.8611138237676317, + "app": 0.2665054882879184, + "cap": -0.06957674511326306, + "cap_med": 0.1559070378956981, + "cap_slope": 0.19112480993013292, + "cap_lin": -0.07856805034557726, + "cap_lin_slope": 0.3856301292389115, + "ae": 16.37826107236354, + "ae_sd": 11.593366201110296, + "ae_px": 114.7786400613211, + "miss": 110.55222959176946, + "med_miss": 101.9616119425486, + "tol": 2.5874551619084527, + "hit": 0.12019460078221883, + "enE": 62.588179338070304, + "flight": 25.045344053851906 + }, + ">=1.50": { + "n": 1, + "nc": 1, + "excl": 0, + "unresolved": 0, + "range": 311.57527173081314, + "req": -7.385850964632965, + "app": 22.613047947857467, + "cap": -3.0616713031632647, + "cap_med": -3.0616713031632647, + "cap_slope": -3.0616713031632647, + "cap_lin": -3.2169503338007974, + "cap_lin_slope": -3.2169503338007974, + "ae": 29.998898912490432, + "ae_sd": NaN, + "ae_px": 163.13444305527292, + "miss": 177.09202932397116, + "med_miss": 177.09202932397116, + "tol": 3.3063566806656928, + "hit": 0.0, + "enE": 100.0, + "flight": 22.0 + } + }, + "450+": { + "<0.50": { + "n": 950, + "nc": 830, + "excl": 120, + "unresolved": 15, + "range": 536.9524717598251, + "req": 0.7972622824171611, + "app": 0.6747601593562429, + "cap": 0.020046922888987805, + "cap_med": 0.0622582285236767, + "cap_slope": 0.10619943331620296, + "cap_lin": 0.06925714125379223, + "cap_lin_slope": 0.30184396048954837, + "ae": 14.612596029558194, + "ae_sd": 8.71895889072, + "ae_px": 136.81545947641567, + "miss": 130.8550947360902, + "med_miss": 122.22699087836725, + "tol": 1.9408837094787184, + "hit": 0.011764705882352941, + "enE": 1.058397052631579, + "flight": 28.16526315789474 + }, + "0.50-0.75": { + "n": 12957, + "nc": 12752, + "excl": 205, + "unresolved": 286, + "range": 536.0654255188824, + "req": 0.315844228831131, + "app": -0.11638440291573471, + "cap": 0.1311838428694459, + "cap_med": 0.14403348211870165, + "cap_slope": 0.15084870875338235, + "cap_lin": 0.3315041482000839, + "cap_lin_slope": 0.31374501378366815, + "ae": 14.037169743983702, + "ae_sd": 9.961766559776152, + "ae_px": 131.11325997702755, + "miss": 123.98160162570966, + "med_miss": 112.8610500134223, + "tol": 1.942983547632113, + "hit": 0.10204403756609581, + "enE": 18.56722494404569, + "flight": 29.550281701011038 + }, + "0.75-1.00": { + "n": 2424, + "nc": 2396, + "excl": 28, + "unresolved": 66, + "range": 540.3809256689192, + "req": 0.5209191820807894, + "app": -0.4114611192929324, + "cap": 0.007941175359463957, + "cap_med": 0.10392317907648327, + "cap_slope": 0.1323793933138594, + "cap_lin": -0.25354576261087836, + "cap_lin_slope": 0.2835984607161007, + "ae": 14.921481433246203, + "ae_sd": 10.421154144010892, + "ae_px": 140.55846150669703, + "miss": 132.35267326508838, + "med_miss": 121.41513739247438, + "tol": 1.9265248423586698, + "hit": 0.09499575911789652, + "enE": 26.368570709570957, + "flight": 31.559818481848186 + }, + "1.00-1.50": { + "n": 20126, + "nc": 19896, + "excl": 230, + "unresolved": 371, + "range": 534.2437582199476, + "req": 0.04583825068624399, + "app": -0.7121370844584659, + "cap": 0.1828117468203331, + "cap_med": 0.10471337596568073, + "cap_slope": 0.12645297144612413, + "cap_lin": 0.162441586732522, + "cap_lin_slope": 0.27897840740852947, + "ae": 15.113965811898085, + "ae_sd": 10.657855592613503, + "ae_px": 140.44990322244126, + "miss": 132.25653488536696, + "med_miss": 121.48553433335513, + "tol": 1.9501625915051577, + "hit": 0.0879777271576816, + "enE": 53.779308556096595, + "flight": 31.95627546457319 + }, + ">=1.50": { + "n": 0, + "nc": 0, + "excl": 0, + "unresolved": 0, + "range": NaN, + "req": NaN, + "app": NaN, + "cap": NaN, + "cap_med": NaN, + "cap_slope": NaN, + "cap_lin": NaN, + "cap_lin_slope": NaN, + "ae": NaN, + "ae_sd": NaN, + "ae_px": NaN, + "miss": NaN, + "med_miss": NaN, + "tol": NaN, + "hit": NaN, + "enE": NaN, + "flight": NaN + } + } + } +} \ No newline at end of file diff --git a/docs/lead_capture_by_range.md b/docs/lead_capture_by_range.md new file mode 100644 index 0000000..159caab --- /dev/null +++ b/docs/lead_capture_by_range.md @@ -0,0 +1,307 @@ +# Lead capture by range: how much of the required lead do we actually apply? + +**Question (the user's hypothesis).** *"DrussGT is dodging more when low power only +because DrussGT is moving faster, more far away from the head-on and our displacement +capacity, so we try to hit him but we miss as we are not capable of reaching the +correct angle."* + +**Answer: half right, and the half that is wrong matters.** We genuinely **do not +apply the required lead, and the shortfall grows with range** — capture falls from +**0.40 at 0–200 px to 0.135 at 450+ px** on 54 926 shots, so at long range we put +**~13 %** of the lead a perfect gun would need into the bullet, and miss by ~137 px +against an 18 px bot. But this is a **RANGE** effect, **not a POWER** effect: inside a +range band, capture is the same at 0.5 power as at 1.0 power. Low-power shots miss +more because *they are long-range shots*, plus a separate endgame confound, **not** +because low power costs us displacement capacity. + +The other half: capture below 1.0 is **partly unavoidable**. `requiredLead` uses the +enemy's *actual* future dodge (perfect information), which no real gun can know. A +trivial straight-line predictor — the same bullet, aimed at the intercept of the +enemy's last 4 ticks of velocity — captures only **0.29–0.60**. We capture **46–67 %** +of that trivial ceiling. So the under-lead is real and fixable, but roughly half of +the distance from us to "perfect lead" is the dodger's intrinsic unpredictability. + +Measured on **70 real live battles / 490 rounds / 54 926 shots** against the real, +unmodified DrussGT (`/tmp/tfil_ab2/out/`), replicated on **35 more battles / 24 277 +shots** of a *different* ModularBot build (`/tmp/powtest/`). All live Tank Royale +battles recorded through `tools/robocode_shim/run_bridge_battle.sh`; no offline +fixture replay. + +--- + +## 1. The decomposition + +For every shot **we (ModularBot)** fire at DrussGT, with power `p`, all angles in +degrees relative to the **line of sight (LOS) at the fire tick**: + +* `O` = the firing tank's centre at the fire tick (= the bullet-line origin; the + server's fire `(x,y)` is the tank centre to 0.02 px, verified); +* `v = 20 − 3p` px/tick, so **low power is a faster bullet**; +* `t*` = the **aim-independent** interception tick: the first tick `k` with + `|E(t₀+k) − O| ≤ v·k`, where `E` is DrussGT's recorded true position. It depends + only on the enemy's truth and the bullet speed, **never on our aim**; +* **`requiredLead`** = `bearing(O → E(t*)) − LOS`; +* **`appliedLead`** = our bullet's server-recorded bearing `− LOS`; +* **`leadError`** = `appliedLead − requiredLead` (wrapped to ±180°); +* **`capture`** = `appliedLead / requiredLead`. + +`capture` is guarded: it is defined only when the required lateral lead is at least +**2 px** at the fire range (`|requiredLead|·range ≥ 2 px`). This excludes **887 of +54 926 shots (1.6 %)**. The mean of the *ratio* is a noisy statistic (small +denominators); the headline capture is therefore the **proportional slope** +`Σ(applied·required)/Σ(required²)` computed cell-by-cell, labelled `capSlp`. The mean +ratio (`capt`) and its median (`medcap`) are printed too and tell the same story. + +Arrival-adjacent quantities use two ticks, both non-circular: + +* `miss_px` = perpendicular distance between DrussGT's true position and our bullet's + real line at the tick the bullet reaches DrussGT's along-track plane — the **same + definition used in `docs/drussgt_dodge_vs_power.md`**, so the validation numbers + match that job exactly; +* `flight` = `t*` (ticks in the air). + +### Attribution (stated explicitly, this has bitten the project before) + +* In the capture rows **`e*` is the SUBJECT = DrussGT** and **`s*` is the adversary = + ModularBot = us**, by construction of + `tools/robocode_shim/src/robocode_shim/TrBattleCapture.java` (`en` = the bot whose + name contains "DrussGT", written `e*`; `sh` = the other, written `s*`). +* The event sidecar's `owner` id is a Tank Royale id and **is not stable across runs**. + It is recovered **per battle** from the fire geometry: the owner's position equals + the fire event's `(x,y)` and its energy drops by exactly `power` on the next capture + row. **70/70 battles** resolve to the two sides `('e','s')`. +* Cross-check: for **496/496 death events** the mapped victim is the bot whose energy + is ~0 at the end of that round. +* Fingerprint check on the way round: our recovered side fires + `{1.00 ×32044, 0.50 ×14556, 0.10 ×807, …}` — the documented ModularBot policy + (spike at `TR_POWER_ENERGY_MIN = 0.5` under 20 energy, spike at the 1.0 range cap + beyond `TR_POWER_FAR_DIST = 200`, linear slope between). DrussGT's recovered side + fires `{0.15 ×27209, 0.95 ×22789, 0.45 ×5274, …}` — a completely different, + distance-quantised curve. **No power-value heuristic is used anywhere** (the old + `{1.0, 1.5, 2.0, 3.0}` assumption is exactly what mis-attributed ModularBot in a + previous job). + +Because the four-decimal positions repeat when a tank is stationary, 12 112 fire +events have more than one *position* match inside the ±8-tick search window; the +energy-drop term of the match disambiguates all of them, and the 496/496 death check +plus the hit geometry below validate the result end to end. + +--- + +## 2. Geometry validation: hits separate from misses + +| | n | mean |leadError| (deg) | mean |leadError| (px) | miss_px mean | miss_px median | fraction < 18 px | +|---|---|---|---|---|---|---| +| **HITS** (server truth) | 5 480 | **1.478** | 11.4 | **11.6** | 10.8 | **80.8 %** | +| **MISSES** (hitwall/hitbullet) | 48 304 | **16.724** | 141.3 | **134.1** | 123.7 | **0.8 %** | + +Miss separation ratio (misses/hits) = **11.59×**. The hits show a mean miss of +**11.6 px** and **80.8 % inside 18 px**, matching the previously recorded live +measurement (11.6 px, 80.6 %) exactly — the bearing recovery is correct. Overall +measured hit rate **9.98 %**. + +--- + +## 3. Main result: by range band + +`capt` = mean of the ratio, `capSlp` = proportional capture (robust), `capLin` = the +same slope for the **naive linear predictor control**, `|req|`/`|app|` = mean *absolute* +lead magnitude in degrees, `tol` = angular tolerance `atan(18/range)`. + +| range | n | range px | flight | |req|° | |app|° | capt | **capSlp** | capLin | |err|° | |err|px | tol° | |err|/tol | hit % | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 0–100 | 27 | 81 | 10.3 | 24.8 | 13.4 | 0.72 | **0.401** | 0.597 | 16.8 | 23.2 | 13.01 | 1.27 | 44.00 | +| 100–200 | 274 | 162 | 14.7 | 20.2 | 17.0 | 0.07 | **0.391** | 0.600 | 17.7 | 50.1 | 6.57 | 2.70 | 25.28 | +| 200–300 | 990 | 261 | 17.9 | 15.7 | 13.8 | 0.04 | **0.246** | 0.428 | 16.7 | 75.8 | 3.99 | 4.17 | 16.04 | +| 300–450 | 17 178 | 405 | 24.5 | 13.5 | 13.1 | 0.01 | **0.198** | 0.404 | 16.0 | 112.9 | 2.56 | 6.23 | 11.79 | +| 450+ | 36 457 | 535 | 31.0 | 11.6 | 11.4 | 0.15 | **0.135** | 0.291 | 14.7 | 137.0 | 1.95 | 7.54 | 9.14 | + +Read this table twice. Two things happen at once and they are different: + +1. **The angular error `|leadError|` is roughly constant (~15–18°) at every range** + (it does not shrink with distance). +2. **The window shrinks**: the 18 px bot spans 13° at 100 px but only 1.95° at 500 px, + because `tol = atan(18/range)`. + +So `|err|/tol` grows from **1.27 to 7.54**: the same angular miss that was survivable +up close becomes fatal at distance. This is the "tighter angular window" half of the +story — but it is *not* the whole story, because `capSlp` genuinely **falls with range** +(0.40 → 0.135). We are not applying a constant fraction of a shrinking lead; we are +applying a *shrinking fraction* of a roughly constant-magnitude lead. + +--- + +## 4. Power does NOT change capture (the 'range vs power' split) + +Within the 450+ band, restricting to shots fired while **DrussGT still has ≥ 5 energy** +(which removes the endgame confound of §5): + +| 450+, enemy alive | n | mean p | range px | **capSlp** | capLin | |err|px | hit % | +|---|---|---|---|---|---|---|---| +| 0.50–0.75 | 11 882 | 0.53 | 535 | **0.154** | 0.312 | 130.3 | 10.39 | +| 0.75–1.00 | 2 377 | 0.87 | 540 | **0.127** | 0.275 | 140.7 | 9.60 | +| 1.00–1.50 | 20 101 | 1.00 | 534 | **0.127** | 0.279 | 140.4 | 8.80 | + +**Flat.** Same result in the 300–450 band (`capSlp` 0.220 / 0.205 / 0.191 for the same +three power bins). At 450+ our policy fires only 0.50 or 1.00 in this regime, so the +comparison is exactly "our fastest long-range bullet" (p = 0.5, v = 18.5, flight 29.5 +ticks) versus "our standard bullet" (p = 1.0, v = 17, flight 32 ticks). The faster +low-power bullet has a **shorter** flight and a **smaller** required lead by +construction, and we capture **the same fraction** of it. Power is not the driver. + +--- + +## 5. The endgame confound (why raw per-power hit rates lie) + +Our power policy fires **sub-0.5 power only to finish a nearly-dead enemy**. At 450+, +**944 of the 950 sub-0.5 shots have the enemy's energy below 2** (mean **1.1**), because +`prFinishKill` cuts the power when the enemy's remaining energy is already tiny. Those +shots hit at **1.18 %**, which looks like a catastrophic low-power failure — it is not: + +| 450+, by enemy energy at the fire tick | n | mean p | capSlp | hit % | +|---|---|---|---|---| +| [0, 2) | 1 017 | 0.22 | 0.111 | **1.10** | +| [2, 5) | 1 076 | 0.54 | 0.128 | 8.55 | +| [5, 10) | 2 614 | 0.59 | 0.135 | 9.69 | +| [10, 20) | 6 758 | 0.63 | 0.146 | 9.85 | +| [20, 40) | 10 733 | 0.83 | 0.147 | 9.43 | +| [40, 150) | 14 259 | 0.97 | 0.122 | 9.12 | + +The hit rate collapses **only when the target is already effectively dead**, and it +collapses for **high-power shots too** (73 shots at p ≥ 0.5 against a < 2-energy +DrussGT hit 0.00 %). Every other enemy-energy stratum sits at 8.5–9.9 %. So the +"low power hits 1 %" reading is a *dead-target* artifact, not a lead-capture failure. +Capture itself is flat across enemy energy (0.111 → 0.147 → 0.122). + +--- + +## 6. Do we move the aim in proportion to the required lead? (response curve) + +At 450+, split the shots into deciles of `requiredLead` and look at the **mean applied +lead** in each: + +| requiredLead decile (mean) | −21.6 | −13.7 | −7.8 | −2.5 | +2.5 | +8.1 | +14.3 | +22.3 | +|---|---|---|---|---|---|---|---|---| +| **mean appliedLead** | −4.09 | −1.51 | −1.00 | −0.55 | +0.36 | −0.16 | +0.14 | +3.26 | +| hit % | 9.27 | 7.92 | 10.29 | 10.08 | 9.95 | 8.75 | 7.22 | 9.69 | + +The required lead swings across **±22°**, and our mean applied lead responds by about +**±4°** — and the hit rate does **not** depend on the size of the required lead at all. +This is the cleanest single number in the report: the bullet is aimed, to first order, +**at the line of sight, not at the interception point**. Consistently, +`mean|appliedLead| ≈ mean|requiredLead|` (11.4° vs 11.6° at 450+) — we have as much +*dispersion* of aim as the target has motion, but almost **no correlation** with it +(`capSlp = 0.135`). + +--- + +## 7. The ceiling: what a trivial predictor would do + +`requiredLead` is an **oracle** (it uses DrussGT's actual future dodge). To separate +"our gun is bad" from "the dodge is unpredictable", the analyzer also computes a +**naive linear-predictor control**: same bullet speed, aimed at the intercept of a +straight-line continuation of DrussGT's last 4 ticks of velocity. Its capture +(`capLin`) and the ratio are in §3: + +| range | our capSlp | naive-linear capLin | **us / linear** | +|---|---|---|---| +| 0–100 | 0.401 | 0.597 | 0.67 | +| 100–200 | 0.391 | 0.600 | 0.65 | +| 200–300 | 0.246 | 0.428 | 0.58 | +| 300–450 | 0.198 | 0.404 | 0.49 | +| 450+ | 0.135 | 0.291 | **0.46** | + +So a *trivial* predictor still only reaches 0.29 at long range — most of the oracle +lead is genuinely unattainable against a strong dodger. But we reach only **46 %** of +that trivial benchmark. The under-lead is therefore real and worth fixing, while the +gap from the trivial benchmark to 1.0 is the dodger's unpredictability and is not. + +Corroboration: the powtest corpus — a **different** ModularBot binary — reproduces the +shape almost exactly (`capSlp` 0.529 / 0.354 / 0.283 / 0.219 / 0.136 by the same range +bands). It is a property of the architecture, not of one build. + +--- + +## 8. Caveats and sample sizes + +* **The oracle caveat.** `requiredLead` uses perfect future information. `capture = 1.0` + is *unattainable* against a bot that dodges; it is a diagnostic, not a target. Compare + our capture to the `capLin` control, not to 1.0. +* **Weak correlation, large dispersion.** At long range our applied lead is essentially + an aim with mean 0 and ~11.5° dispersion that is only weakly proportional to the + required lead (proportional slope 0.135). "We apply 13 % of the lead" + is a proportional-fit statement; per shot, what we actually do is *aim near the LOS + with a wide error*. +* **Stale live world state.** The recorded `dir` is what the **live** ModularBot fired + using its own stale between-scan world state; the capture's positions are perfect + truth. The measured `leadError` therefore folds in the bot's scan staleness. That is + the correct thing to measure (it is the error the bullet actually carries), but it is + not the same as the gun's internal prediction error. +* **Stratification.** Power is not randomised: it is assigned by the range cap, the + energy slope and the finishing rule. Every power comparison above is made **within a + range band** and (in §4) with the endgame removed. The raw cross-range power + distribution is reported in the captured output. +* **Sample sizes.** The short bands are thin (0–100: n = 27; 100–200: n = 274) — treat + their capture as indicative. Everything from 200 px out is large (990 → 36 457). + The 450+ sub-0.5 power cell *with the enemy alive* is n = 4 and should be ignored + (it is printed for completeness; the real sub-0.5 shots are the endgame of §5). +* **Rotation direction.** `requiredLead`/`appliedLead` are signed about the LOS; + `capSlp` additionally assumes a proportional (through-origin) relation, which is the + right first-order model for a lead gun but not exact for large angles. + +--- + +## 9. Verdict + +**MEASURED** + +* Capture falls monotonically with range: **capSlp 0.401 → 0.391 → 0.246 → 0.198 → + 0.135**. At 450+ we apply **~13 %** of the required lead and miss by **~137 px** + against an 18 px bot; `|leadError|/tolerance` grows **1.27 → 7.54**. +* Capture is **flat across fired power within a range band** + (450+, enemy alive: 0.154 / 0.127 / 0.127 for p = 0.53 / 0.87 / 1.00). +* The applied lead barely responds to the required lead: across required-lead deciles + spanning ±22°, the mean applied lead moves by ~±4° and the hit rate is flat. +* A naive linear predictor captures 0.29–0.60; we capture **46–67 %** of it. +* Hits validate the geometry (11.6 px mean, 80.8 % inside 18 px) and separate from + misses by **11.6×**; **496/496** deaths and **70/70** owner maps resolve correctly. +* The sub-0.5-power long-range shots (1.18 % hit) are **endgame shots at a near-dead + DrussGT** (mean enemy energy 1.1), and high-power shots there miss just as much. + +**INFERRED** + +* The gun is, to first order, **a line-of-sight / weak-lead gun against DrussGT**, not + an intercept-point gun: the bullet direction tracks the enemy's current line rather + than where the enemy will be, which is why capture is low and why it degrades with + range while the angular error stays ~constant. +* The user's mechanism — "we are not capable of reaching the correct angle at range" — + is **confirmed for range**, but **not through power**: low power does not reduce our + displacement capacity here (it is a faster bullet and we capture the same fraction). + The low-power/long-range association is a policy artifact (the range cap), so + *fixing the lead at range fixes the low-power miss rate too*. +* Where to look next: the gap between our capture and the trivial-linear control + (0.46×) is the actionable, fixable part. The gap from the trivial control to 1.0 is + the dodger's unpredictability and should not be chased. + +--- + +## 10. Reproducing + +```bash +# corpora (live captures; not in the repo, ~500 MB total) +# /tmp/tfil_ab2/out//run*.jsonl{,.events.jsonl,.rounds.json} +# /tmp/powtest/{cap__r.jsonl,events__r.json,cap_*_r.jsonl.rounds.json} + +python3 common_libs/tests/analyze_lead_capture_by_range.py \ + --tfil /tmp/tfil_ab2/out --powtest /tmp/powtest \ + --json common_libs/tests/fixtures/lead_capture_by_range_results.json \ + | tee common_libs/tests/fixtures/lead_capture_by_range_output.txt +``` + +`common_libs/tests/fixtures/lead_capture_by_range_output.txt` is the verbatim captured +output every table above is taken from; `..._results.json` is the same numbers as JSON. +Runtime ≈ 15 s for both corpora, single-threaded pure Python (no numpy). + +To rebuild a corpus, `tools/robocode_shim/make_botdir.sh /tmp/tr_bots/DrussGT` and then +`TR_EVENTS_OUT=.events.jsonl tools/robocode_shim/run_bridge_battle.sh + 7 .jsonl`.