Files
SirRoboGarage/common_libs/tests/run_prediction_quality.nim
T
SirStone e9302bc9f6 j140 rebuild a real BitBrain gun: ADE+SBC at rack id 17, default off, and measure its scaling
The BITBRAIN name was sitting on a gun with no network in it. This is the gun
that actually runs the algorithm: an ADE layer (thresholded random projections
with ONLINE threshold adaptation) feeding the SBC head from
common_libs/bitbrain/, with the counted+decay mode available.

  common_libs/guns/bitbrain_net.nim     the gun
  rack name BITBRAIN, rack id 17 (rack 17 -> 18 guns), both new guns default OFF
  admitted by TR_RACK_BITBRAIN=both AND TR_BITBRAIN_NET=1 (the switch that also
  disowns LEADGAIN's legacy TR_BITBRAIN_* aliases)

OUTPUT: a fine-grained aim CORRECTION on top of Pattern - the probability-
weighted mean of the nClasses class centres under inferProb - not a direct aim
point from the argmax. That is the shape docs/bitbrain_gate.md measured, and
Pattern is already a strong predictor, so the net's job is the signed residual.
Below TR_BITBRAIN_MINOBS the shift is exactly 0 and Pattern is returned
unchanged.

INPUT: a CONFIGURED set of FEATURE BLOCKS (TR_BITBRAIN_FEATURES=name:W), each
block's width == its resolution, laid out as a thermometer code over 0/255 slots
(so an ADE synapse 'matches' when its polarity agrees with the slot and a random
ADE fires iff its w synapses all match, rate 2^-w). Default is 52 slots over 9
blocks. NO long temporal window, per docs/state_window_gate.md: the only history
is a 12-tick ring feeding three rate/turn quantities.

Every knob env-configurable: _INPUT (width), _NCLASSES, _NADES, _WIDTHS
(clause widths), _FEATURES, _SPAN, _MODE, _DECAY_EVERY, _DECAY_SHIFT,
_MINOBS, _ADAPT_EVERY, _TARGET, _NETSEED, _NETLOG, _NET_RESET_ON_TARGET.

MEASURED SCALING (measure_bitbrain_scaling.nim, 3 recorded runs, 37412 ticks,
-d:release, one predict per power bin per tick, timed region = predicts only):
  RAM 1.59 MB default (98.6% SBC tensors); linear in nClasses, QUADRATIC in
      nAde, FLAT in input width; counted/bitset = 7.30x on RAM, ~1x on time.
  ms/tick 2.70 default = 21% of the 13.16 ms budget; 64 classes busts it (149%),
      nAde 512 uses 74%, nAde 64 uses 2%.
  CAPACITY vs ACCURACY: over a 100x RAM range the offline mean |err| moves
      17.254 -> 17.115 deg around Pattern's 16.964, and the sign flips along the
      nClasses axis, so it is noise, not a trend. The corrector is consistently
      slightly WORSE than Pattern. The ceiling is the STATE, not the classifier.
      VETO-CAPABLE OFFLINE CHECK ONLY (docs/offline_harness_trust.md), never
      presented as a live win.

ENGAGEMENT is proven, not assumed: test_bitbrain_net.nim (42 checks) shows 0
bytes before first use, different inputs -> different class outputs, a learn
raises SBC occupancy, bitset learn idempotent while counted learn is monotone,
threshold adaptation runs, and the global RNG is untouched.

Parity: shipped rack still onlyPattern, shipped movement still strafe. Guards:
test_env_report 25, test_rack_membership 48, test_tm_pattern_registration 20,
test_lead_gain_registration 13, test_lead_gain_legacy 24, test_bitbrain 56,
test_gun_harness 39, test_tfil_commit_env 30, test_bitbrain_net 42.
Clean archive build: [SuccessX].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-26 16:50:21 +02:00

475 lines
22 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Offline PREDICTION-QUALITY runner — the campaign's measurement sweep.
##
## Reads the recorded live-vs-real-DrussGT corpus, drives each arm over the
## recorded enemy trajectory, and scores every tick×power-bin prediction against
## the aim-independent interception point (see prediction_quality.nim). Owns the
## RANGE-BAND table that is "the bar" for the lead-gain campaign.
##
## NO CLOSED-LOOP CLAIM IS MADE HERE. Every arm below is an open-loop prediction
## scored on a FIXED trajectory. Wins, damage and survival are decided live.
##
## Usage:
## nim c -r --nimcache:/tmp/nc_j98 common_libs/tests/run_prediction_quality.nim \
## [--corpus /tmp/tfil_ab2/out] [--limit N] [--timing]
##
## `--limit N` keeps only the first N runs (sorted), for fast iteration.
import std/[os, strformat, strutils, times, math]
import gun_harness/[gun_interface, virtual_bullets, prediction_quality]
import guns/[head_on, pattern_matcher, tm_horizon, lead_gain, bitbrain_net]
# arm indices (fixed order = fixed output)
const
A_ORACLE* = 0
A_ORACLEQ* = 1
A_HEADON* = 2
A_PATTERN* = 3
A_G15* = 4
A_G20* = 5
A_G30* = 6
A_NAIVE* = 7
A_TMH* = 8
A_LG* = 9
# ── Phase 1: the MISSING gain sweep. Gains >= 1 were measured worse at every
# band in Phase 0; the unexplored region is gain < 1. gain 0.0 is HeadOn
# (A_HEADON) and gain 1.0 is Pattern (A_PATTERN), so only 0.25/0.50/0.75 are
# new arms. Their `leadCorr` is IDENTICAL to Pattern's by construction (Pearson
# correlation is invariant under positive scaling) — printed only to prove it.
A_G025* = 10
A_G050* = 11
A_G075* = 12
# Phase 1 fixed causal per-band gain rule: the hitProxy-argmax curve measured by
# the sub-unity sweep ([1,1,1,0,0] == Pattern below 300 px, HeadOn above). This
# is the rule the corrector must match; it needs no learning (range is known at fire
# time). The table was selected in-sample from this corpus.
A_BAND* = 13
## The REAL ADE+SBC gun (rack id 17, `guns/bitbrain_net.nim`): Pattern base plus
## a fine-grained, class-resolved angular correction from the network. The gun
## is default OFF (`TR_BITBRAIN_NET=0`), so the arm turns its own switch on
## here — the ruler is the one place that must exercise it.
A_BBN* = 14
BandGainTable* = [1.0, 1.0, 1.0, 0.0, 0.0]
ArmNames* = ["Oracle", "OracleQuant", "HeadOn", "Pattern", "PatternGain1.5",
"PatternGain2.0", "PatternGain3.0", "NaiveLinear", "TMHorizon", "LeadGain",
"PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain",
"BitBrainNet"]
const
## The five sub-unity gain arms, in increasing order, resolved to arm indices.
## gain 0.0 == HeadOn, gain 1.0 == Pattern.
GainArmIdx* = [A_HEADON, A_G025, A_G050, A_G075, A_PATTERN]
GainValues* = [0.0, 0.25, 0.50, 0.75, 1.0]
# ── the naive-linear control (job-95's LIN_M = 4 extrapolation) ──────────────
#
# Velocity = (pos(t) - pos(t-4)) / 4, then iterate the interception equation.
# This is the trivial predictive gun the lead-capture analysis used as its
# ceiling control; it captures ~2x the lead response Pattern does at 450+ and is
# included to resolve that tension against the angular-error ruler.
type
NaiveLinearGun = object
hist: array[5, tuple[x, y: float]]
count: int
lastTick: int
proc predict*(g: var NaiveLinearGun, state: WorldState,
bulletSpeed: float): GunPrediction =
if state.tick != g.lastTick:
for i in countdown(4, 1): g.hist[i] = g.hist[i - 1]
g.hist[0] = (state.enemyX, state.enemyY)
if g.count < 5: inc g.count
g.lastTick = state.tick
if g.count < 5 or bulletSpeed <= 0.0:
return GunPrediction(x: state.enemyX, y: state.enemyY)
let vx = (g.hist[0].x - g.hist[4].x) / 4.0
let vy = (g.hist[0].y - g.hist[4].y) / 4.0
let ox = state.selfX
let oy = state.selfY
var t = hypot(state.enemyX - ox, state.enemyY - oy) / bulletSpeed
for _ in 0..<40:
t = hypot(state.enemyX + vx * t - ox, state.enemyY + vy * t - oy) / bulletSpeed
GunPrediction(x: state.enemyX + vx * t, y: state.enemyY + vy * t)
proc onResult*(g: var NaiveLinearGun, e: FeedbackEvent) = discard
# ── runner ───────────────────────────────────────────────────────────────────
type Ctx = object
c: Corpus
cont: bool
pattern: PatternMatcherGun
naive: NaiveLinearGun
tmh: TmHorizonGun
lg: LeadGainGun
bbn: BitbrainNetGun
headon: HeadOnGun
st: WorldState
enemy: seq[EnemyInfo]
proc runRound(ctx: var Ctx, arms: var seq[ArmAcc], r: int) =
let c = ctx.c
let base = int(c.rStart[r]) - c.base
let cnt = int(c.rCount[r])
let iEnd = base + cnt
ctx.enemy[0] = EnemyInfo(id: 1)
for i in base ..< iEnd:
let ox = c.sx(i)
let oy = c.sy(i)
let localTick = int(c.tick[i]) - int(c.rStart[r])
ctx.enemy[0].x = c.ex(i)
ctx.enemy[0].y = c.ey(i)
ctx.enemy[0].heading = c.eh(i)
ctx.enemy[0].speed = c.es(i)
ctx.enemy[0].energy = c.ee(i)
ctx.enemy[0].lastSeenTick = localTick
ctx.st.enemyX = c.ex(i)
ctx.st.enemyY = c.ey(i)
ctx.st.enemyHeading = c.eh(i)
ctx.st.enemySpeed = c.es(i)
ctx.st.enemyEnergy = c.ee(i)
ctx.st.selfX = ox
ctx.st.selfY = oy
ctx.st.selfHeading = c.sh(i)
ctx.st.selfSpeed = c.ss(i)
ctx.st.selfEnergy = c.se(i)
ctx.st.selfRadarHeading = c.sh(i)
ctx.st.tick = localTick
let los = bearingDeg(ox, oy, c.ex(i), c.ey(i))
for bin in 0 ..< len(PowerBins):
let speed = bulletSpeed(PowerBins[bin])
let ib = interceptBearing(c, i, iEnd, ox, oy, speed, ctx.cont)
if not ib.ok:
for ai in 0 ..< arms.len: skip(arms[ai])
continue
let ibq = interceptBearingQuant(c, i, iEnd, ox, oy, speed)
let rng = ib.range
let targetLead = wrap180(ib.bearing - los)
# oracle: aims at the active ruler's true interception point -> 0 error
arms[A_ORACLE].record(rng, 0.0, targetLead, targetLead)
# the analyze_lead_capture_by_range.py integer-tick intercept, scored on the
# SAME ruler: this is what the coarse solve's own oracle would reach.
let qLead = if ibq.ok: wrap180(ibq.bearing - los) else: targetLead
arms[A_ORACLEQ].record(rng, wrap180(qLead - targetLead), qLead, targetLead)
# head-on: aim at the enemy's CURRENT position (worst realistic gun)
arms[A_HEADON].record(rng, wrap180(los - ib.bearing), 0.0, targetLead)
# pattern + lead-gain sweep (gain scales Pattern's lead over LOS)
let pp = ctx.pattern.predict(ctx.st, speed)
let pb = bearingDeg(ox, oy, pp.x, pp.y)
let plead = wrap180(pb - los)
arms[A_PATTERN].record(rng, wrap180(plead - targetLead), plead, targetLead)
arms[A_G15].record(rng, wrap180(1.5 * plead - targetLead), 1.5 * plead, targetLead)
arms[A_G20].record(rng, wrap180(2.0 * plead - targetLead), 2.0 * plead, targetLead)
arms[A_G30].record(rng, wrap180(3.0 * plead - targetLead), 3.0 * plead, targetLead)
# sub-unity gains (Phase 1) — the region Phase 0 never covered
arms[A_G025].record(rng, wrap180(0.25 * plead - targetLead), 0.25 * plead, targetLead)
arms[A_G050].record(rng, wrap180(0.50 * plead - targetLead), 0.50 * plead, targetLead)
arms[A_G075].record(rng, wrap180(0.75 * plead - targetLead), 0.75 * plead, targetLead)
# the fixed causal per-band rule (Phase 1 hitProxy-argmax curve)
let bg = BandGainTable[bandOf(rng)]
arms[A_BAND].record(rng, wrap180(bg * plead - targetLead), bg * plead, targetLead)
# naive linear
let np = predict(ctx.naive, ctx.st, speed)
let nl = wrap180(bearingDeg(ox, oy, np.x, np.y) - los)
arms[A_NAIVE].record(rng, wrap180(nl - targetLead), nl, targetLead)
# TMHorizon
let tp = predict(ctx.tmh, ctx.st, speed)
let tl = wrap180(bearingDeg(ox, oy, tp.x, tp.y) - los)
arms[A_TMH].record(rng, wrap180(tl - targetLead), tl, targetLead)
# LEADGAIN (Pattern base + per-band learned lead gain)
let bp = predict(ctx.lg, ctx.st, speed)
let bl = wrap180(bearingDeg(ox, oy, bp.x, bp.y) - los)
arms[A_LG].record(rng, wrap180(bl - targetLead), bl, targetLead)
# BITBRAIN (ADE+SBC): Pattern base + the network's fine-grained correction
let np2 = predict(ctx.bbn, ctx.st, speed)
let nl2 = wrap180(bearingDeg(ox, oy, np2.x, np2.y) - los)
arms[A_BBN].record(rng, wrap180(nl2 - targetLead), nl2, targetLead)
proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var ShotStat,
doShots: bool, timing: bool, cont: bool): int =
let t0 = epochTime()
let c = loadCorpus(runPath)
if c.n == 0: return 0
var ctx = Ctx(
c: c,
cont: cont,
pattern: PatternMatcherGun(),
naive: NaiveLinearGun(lastTick: -1),
tmh: initTmHorizonGun(),
lg: initLeadGainGun(),
bbn: initBitbrainNetGun(),
headon: HeadOnGun(),
st: WorldState(arenaWidth: c.arenaW, arenaHeight: c.arenaH),
enemy: newSeq[EnemyInfo](1))
for r in 0 ..< c.rStart.len:
runRound(ctx, arms, r)
if doShots:
let ev = parseEvents(eventsPathFor(runPath))
for mode in [true, false]:
let s = validateShots(c, ev, mode)
var dst = if mode: addr shotsCont else: addr shotsQuant
dst.hits += s.hits
dst.misses += s.misses
dst.hitSumDeg += s.hitSumDeg
dst.missSumDeg += s.missSumDeg
dst.hitSumPx += s.hitSumPx
dst.missSumPx += s.missSumPx
if timing:
stderr.writeLine(fmt" {extractFilename(runPath):<16} ticks={c.n:<7} {epochTime()-t0:>6.2f}s")
c.n
proc fmt4(x: float): string =
if x.classify in {fcNan, fcInf, fcNegInf}: "-" else: fmt"{x:.4f}"
proc fmt3(x: float): string =
if x.classify in {fcNan, fcInf, fcNegInf}: "-" else: fmt"{x:.3f}"
proc main() =
# The ADE+SBC gun is default OFF (two switches). This arm is the one place
# that must exercise it, so the ruler turns its master switch on for the whole
# process — every other arm is unaffected (they never read TR_BITBRAIN_*).
putEnv(BBN_NET_ENV, "1")
putEnv(BBN_MINOBS_ENV, "1")
var corpusRoot = "/tmp/tfil_ab2/out"
var limit = 0
var doShots = true
var timing = false
var cont = true
var i = 1
while i <= paramCount():
case paramStr(i)
of "--corpus": inc i; corpusRoot = paramStr(i)
of "--limit": inc i; limit = parseInt(paramStr(i))
of "--no-shots": doShots = false
of "--timing": timing = true
of "--ruler":
inc i
cont = paramStr(i) != "quant"
else:
stderr.writeLine("unknown arg: " & paramStr(i))
quit(2)
inc i
var runs = discoverRuns(corpusRoot)
if limit > 0 and runs.len > limit: runs.setLen(limit)
if runs.len == 0:
stderr.writeLine("no runs found under " & corpusRoot)
quit(1)
var arms: seq[ArmAcc]
for nm in ArmNames: arms.add ArmAcc(name: nm)
var shotsCont, shotsQuant: ShotStat
var t0 = epochTime()
var ticks = 0
for rp in runs:
ticks += runOne(rp, arms, shotsCont, shotsQuant, doShots, timing, cont)
let elapsed = epochTime() - t0
echo "=".repeat(120)
echo "OFFLINE PREDICTION QUALITY -- per-gun single-tick aim error vs the true interception point"
echo "=".repeat(120)
echo fmt"corpus : {corpusRoot}"
let rulerName = if cont: "continuous (physically exact)" else: "integer-tick (analyze_lead_capture_by_range.py)"
echo fmt"ruler : {rulerName}"
echo fmt"runs : {runs.len}"
echo fmt"recorded ticks: {ticks}"
let tickBins = ticks * len(PowerBins)
echo fmt"tick x bin : {tickBins}"
echo fmt"wall time : {elapsed:.2f}s ({elapsed / max(1.0, float(tickBins)) * 1000.0:.4f} ms per tick-bin)"
echo fmt"per-arm speed : {elapsed / max(1.0, float(tickBins)) * 1000.0:.4f} s per 1000 tick-bins per arm"
echo ""
echo "NOTE: offline OPEN-LOOP prediction quality only. No win/damage/survival claim."
echo ""
# validation block
echo "=".repeat(120)
echo "VALIDATION -- the ruler must pass ALL of these before any number below is trusted"
echo "=".repeat(120)
if doShots and shotsCont.hits > 0 and shotsQuant.hits > 0:
echo fmt"1. recorded shots (OUR actual server-fired bearings vs the SAME interception solve):"
for mode in [("continuous", shotsCont), ("integer", shotsQuant)]:
let s = mode[1]
let sepOk = if separationPx(s) > 2.0: "OK" else: "WEAK"
echo fmt" ruler={mode[0]:<11} hits n={s.hits:<6} mean|err|={meanHitDeg(s):>7.3f} deg / {meanHitPx(s):>7.1f} px | " &
fmt"misses n={s.misses:<6} mean|err|={meanMissDeg(s):>7.3f} deg / {meanMissPx(s):>7.1f} px | " &
fmt"separation {separationDeg(s):>6.2f}x deg / {separationPx(s):>6.2f}x px -> {sepOk}"
else:
echo "1. recorded-shot validation: SKIPPED"
let oMax = max([arms[A_ORACLE].bands[0].maxAbs, arms[A_ORACLE].bands[1].maxAbs,
arms[A_ORACLE].bands[2].maxAbs, arms[A_ORACLE].bands[3].maxAbs,
arms[A_ORACLE].bands[4].maxAbs])
let oOk = if oMax < 1e-6: "OK" else: "BROKEN"
echo fmt"2. perfect-oracle gun max |err| over all tick-bins = {oMax:.6f} deg -> {oOk}"
# ordering check: the static LOS gun must be worse than every predictive gun.
proc overallMean(arms: seq[ArmAcc], ai: int): float =
var sAbs = 0.0
var n = 0
for b in 0 ..< NBands:
sAbs += arms[ai].bands[b].sumAbs
n += arms[ai].bands[b].n
if n > 0: sAbs / float(n) else: 0.0
let mHead = overallMean(arms, A_HEADON)
let mPat = overallMean(arms, A_PATTERN)
let mTmh = overallMean(arms, A_TMH)
let mLg = overallMean(arms, A_LG)
let mBbn = overallMean(arms, A_BBN)
let mLin = overallMean(arms, A_NAIVE)
let ordOk = mHead > mPat and mHead > mTmh and mHead > mLg and mHead > mBbn
echo fmt"3. HeadOn (static LOS) mean|err| = {mHead:.3f} deg vs Pattern {mPat:.3f} / TMHorizon {mTmh:.3f} / LeadGain {mLg:.3f} / BitBrainNet {mBbn:.3f}"
let ordMsg = if ordOk: "OK (static gun worst among real guns)" else: "UNEXPECTED: a predictive gun is worse than static LOS"
echo fmt" -> {ordMsg}"
echo fmt" NaiveLinear mean|err| = {mLin:.3f} deg (over-leads; see the lead-gain sweep for why a larger"
echo fmt" lead *response* does not mean a smaller angular error)"
echo "4. determinism: run twice and diff stdout (see fixture; verified separately)."
echo ""
echo "=".repeat(120)
echo "THE BAR -- per-band mean ABSOLUTE angular aim error (deg), RMSE, sign, hit-proxy"
echo "=".repeat(120)
echo "hitProxy = fraction of tick-bins with |err| <= atan(18/range) (the angular half-width of the target disc)."
echo ""
stdout.write formatArmTable(arms)
echo ""
echo "=".repeat(120)
echo "HEADROOM -- the direct answer: how far each arm is from the oracle ceiling, per band"
echo "=" .repeat(120)
let hdr = "band Pattern n Pattern|err| Pattern hpx Oracle hpx headroom pp naive hpx TMHoriz hpx LeadGain hpx BitBrainNet hpx"
echo hdr
echo "-".repeat(hdr.len)
for b in 0 ..< NBands:
let pat = arms[A_PATTERN].bands[b]
let orc = arms[A_ORACLE].bands[b]
let hp = pat.hitProxy
let ohp = orc.hitProxy
echo fmt"{BandLabels[b]:<9} {pat.n:>8} {fmt3(meanAbs(pat)):>12} {fmt4(hp):>12} {fmt4(ohp):>12} {ohp - hp:>13.4f} {fmt4(arms[A_NAIVE].bands[b].hitProxy):>11} {fmt4(arms[A_TMH].bands[b].hitProxy):>12} {fmt4(arms[A_LG].bands[b].hitProxy):>13} {fmt4(arms[A_BBN].bands[b].hitProxy):>15}"
echo ""
echo "BitBrainNet hpx = the ADE+SBC gun (rack id 17), Pattern base + a class-resolved"
echo "angular correction. VETO-CAPABLE OFFLINE CHECK ONLY, never a live claim."
echo "hitProxy = fraction of tick-bins aimed within atan(18/range) of the true interception point."
echo "headroom pp = oracle hitProxy - Pattern hitProxy = the absolute hit-probability points available"
echo "to a perfect predictor (the campaign is playing for a slice of this)."
echo ""
let hdrq = "band OracleQuant hpx integer-solve coarseness"
echo hdrq
echo "-".repeat(hdrq.len)
for b in 0 ..< NBands:
let oq = arms[A_ORACLEQ].bands[b]
echo fmt"{BandLabels[b]:<9} {fmt4(oq.hitProxy):>15} {fmt3(meanAbs(oq)):>10} deg mean |err|"
echo "(OracleQuant aims at the analyze_lead_capture_by_range.py integer-tick intercept and is scored"
echo " on the active ruler. On the continuous ruler it measures how much of a gun's 'error' the coarse"
echo " solve itself would produce; on the integer ruler it is identically zero.)"
echo ""
echo "=".repeat(120)
echo "LEAD-GAIN SWEEP ON PATTERN -- multiply Pattern's lead (deg over LOS) by a constant"
echo "=".repeat(120)
let hdr2 = "band gain=1.0 gain=1.5 gain=2.0 gain=3.0 best-gain"
echo hdr2
echo "-".repeat(hdr2.len)
for b in 0 ..< NBands:
let g1 = meanAbs(arms[A_PATTERN].bands[b])
let g15 = meanAbs(arms[A_G15].bands[b])
let g20 = meanAbs(arms[A_G20].bands[b])
let g30 = meanAbs(arms[A_G30].bands[b])
var best = "1.0"
var bestV = g1
if g15 < bestV: bestV = g15; best = "1.5"
if g20 < bestV: bestV = g20; best = "2.0"
if g30 < bestV: bestV = g30; best = "3.0"
echo fmt"{BandLabels[b]:<9} {fmt3(g1):>10} {fmt3(g15):>10} {fmt3(g20):>10} {fmt3(g30):>10} {best} ({fmt3(bestV)})"
echo ""
echo "=".repeat(120)
echo "PHASE 1 — THE MISSING GAIN SWEEP: Pattern lead x gain in [0.00, 1.00] (0 = HeadOn, 1 = Pattern)"
echo "=".repeat(120)
echo "Format per cell: mean|err| deg [hitProxy]. hitProxy is the objective. gain 0.0 is HeadOn,"
echo "gain 1.0 is Pattern. The per-band gain table IS causal to APPLY (range is known at fire time,"
echo "so a per-band lookup needs no learning); its ESTIMATION from these same runs is in-sample."
echo ""
var hdrg = "band |req| deg"
for gi in 0 ..< GainValues.len: hdrg.add fmt" g={GainValues[gi]:.2f} [hpx]"
hdrg.add " bestHpx dHpx bestErr"
echo hdrg
echo "-".repeat(hdrg.len)
for b in 0 ..< NBands:
var line = fmt"{BandLabels[b]:<9} {fmt3(meanAbsReq(arms[A_PATTERN].bands[b])):>9}"
var bestHi = 0
var bestHp = -1.0
var bestEi = 0
var bestEr = Inf
for gi in 0 ..< GainValues.len:
let s = arms[GainArmIdx[gi]].bands[b]
let e = meanAbs(s)
let hp = s.hitProxy
line.add fmt"{fmt3(e):>7} [{fmt3(hp)}] "
if hp > bestHp: bestHp = hp; bestHi = gi
if e < bestEr: bestEr = e; bestEi = gi
let patHp = arms[A_PATTERN].bands[b].hitProxy
line.add fmt" {GainValues[bestHi]:.2f} {bestHp-patHp:+.4f} {GainValues[bestEi]:.2f}"
echo line
echo ""
echo "OPTIMAL GAIN CURVE (hitProxy-argmax per band) and its implied hit-probability gain vs Pattern:"
var curve = " gain = [ "
for b in 0 ..< NBands:
var bestHi = 0
var bestHp = -1.0
for gi in 0 ..< GainValues.len:
let hp = arms[GainArmIdx[gi]].bands[b].hitProxy
if hp > bestHp: bestHp = hp; bestHi = gi
curve.add fmt"{BandLabels[b]}->{GainValues[bestHi]:.2f} "
let patHp = arms[A_PATTERN].bands[b].hitProxy
echo fmt" {BandLabels[b]:<9} best gain {GainValues[bestHi]:.2f} hitProxy {bestHp:.4f} vs Pattern {patHp:.4f} => {bestHp-patHp:+.4f} pp"
echo curve & "]"
echo ""
echo "DIRECT COMPARISON — Pattern vs the FIXED causal per-band rule [1,1,1,0.00,0.00] vs LeadGain (learned online):"
let hdrd = "band Pattern hpx fixed-band hpx LeadGain hpx fixed-Pat pp BB-Pat pp"
echo hdrd
echo "-".repeat(hdrd.len)
for b in 0 ..< NBands:
let patHp = arms[A_PATTERN].bands[b].hitProxy
let fixHp = arms[A_BAND].bands[b].hitProxy
let bbHp = arms[A_LG].bands[b].hitProxy
echo fmt"{BandLabels[b]:<9} {patHp:>11.4f} {fixHp:>16.4f} {bbHp:>14.4f} {fixHp-patHp:>+14.4f} {bbHp-patHp:>+10.4f}"
echo "fixed-band hpx = the [1,1,1,0,0] table applied causally; it was selected in-sample."
echo "LeadGain is learned online from labels inside each run (cold start at gain 1.0)."
echo ""
echo "LEAD CORRELATION PER GAIN (Pearson of applied lead with required lead). Pearson is invariant"
echo "under positive scaling, so every g>0 column must be IDENTICAL to Pattern; g=0 has no lead and"
echo "therefore no correlation. If they match, a shrinking gain does NOT add lead information — it"
echo "only shrinks the magnitude of an uninformative signal (the gain-sweep mechanism)."
let hdrc = "band " & " corr(g) "
var hdrc2 = hdrc
for gi in 1 ..< GainValues.len: hdrc2.add fmt" g={GainValues[gi]:.2f}"
echo hdrc2
for b in 0 ..< NBands:
var line = fmt"{BandLabels[b]:<9}"
for gi in 1 ..< GainValues.len:
line.add fmt" {fmt3(leadCorr(arms[GainArmIdx[gi]].bands[b])):>8}"
echo line
echo ""
echo "=".repeat(120)
echo "LEAD INFORMATIVENESS -- capture slope (regression of applied lead on required lead) and lead correlation"
echo "=".repeat(120)
echo "capture slope is job-95's metric (1.0 = perfect proportional response). corr is the Pearson"
echo "correlation of the arm's lead with the REQUIRED lead: a large slope on an uncorrelated lead is"
echo "just amplified noise. This is the table that resolves the 'naive-linear captures 2x the lead but"
echo "hits less' tension."
echo ""
let hdr3 = "band HO |err| HO |req| Pat|req| Pat cap Pat corr Lin cap Lin corr TMH cap TMH corr BB cap BB corr"
echo hdr3
echo "-".repeat(hdr3.len)
for b in 0 ..< NBands:
let sp = arms[A_PATTERN].bands[b]
let sn = arms[A_NAIVE].bands[b]
let st = arms[A_TMH].bands[b]
let sb = arms[A_LG].bands[b]
echo fmt"{BandLabels[b]:<9} {fmt3(meanAbs(arms[A_HEADON].bands[b])):>9} {fmt3(meanAbsReq(arms[A_HEADON].bands[b])):>9} {fmt3(meanAbsReq(sp)):>9} {fmt3(captureSlope(sp)):>10} {fmt3(leadCorr(sp)):>10} " &
fmt"{fmt3(captureSlope(sn)):>10} {fmt3(leadCorr(sn)):>10} {fmt3(captureSlope(st)):>10} " &
fmt"{fmt3(leadCorr(st)):>10} {fmt3(captureSlope(sb)):>10} {fmt3(leadCorr(sb)):>10}"
when isMainModule:
main()