7a6237ec20
The gun at rack id 16 learned a multiplier for Pattern's lead, separately
per range band. It was called BITBRAIN and shipped a TR_BITBRAIN_* prefix,
which is why the name read as a neural network it no longer contains.
guns/bitbrain_gun.nim -> guns/lead_gain.nim (rack id 16 UNCHANGED)
RackGunNames[16] BITBRAIN -> LEADGAIN
TR_BITBRAIN_* knobs -> TR_LEADGAIN_*
[bb] log line -> [lg]
BACKWARD COMPATIBILITY is mandatory: the live .env carries
TR_RACK_BITBRAIN=both, TR_BITBRAIN_GAINS, TR_BITBRAIN_MEM=decay and
TR_BITBRAIN_LOG=1, and those must keep behaving identically. The new ADE+SBC
gun (next commit) claims the BITBRAIN name and the TR_BITBRAIN_* prefix, so
the namespace is disambiguated by ONE deterministic switch, TR_BITBRAIN_NET
(default 0):
TR_BITBRAIN_NET unset/0 -> LEGACY: the 14 frozen legacy suffixes are aliases
for TR_LEADGAIN_*, and TR_RACK_BITBRAIN still
selects rack id 16. One [depr] line on stderr
names the new spelling of each honoured knob.
TR_BITBRAIN_NET = 1 -> the TR_BITBRAIN_* names belong to the new gun.
The legacy suffix set and the new gun's knob set are DISJOINT, so no name is
ever claimed twice; the new name always wins over its alias.
Parity: shipped rack is still onlyPattern, shipped movement is still strafe.
Guards unchanged: test_env_report 25, test_rack_membership 48,
test_tm_pattern_registration 20, test_lead_gain_registration 13 (was
test_bitbrain_registration), test_bitbrain 56, test_gun_harness 39,
test_tfil_commit_env 30. New: test_lead_gain_legacy 24.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
455 lines
20 KiB
Nim
455 lines
20 KiB
Nim
## 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]
|
||
|
||
# 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
|
||
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"]
|
||
|
||
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
|
||
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)
|
||
|
||
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(),
|
||
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() =
|
||
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 mLin = overallMean(arms, A_NAIVE)
|
||
let ordOk = mHead > mPat and mHead > mTmh and mHead > mLg
|
||
echo fmt"3. HeadOn (static LOS) mean|err| = {mHead:.3f} deg vs Pattern {mPat:.3f} / TMHorizon {mTmh:.3f} / LeadGain {mLg:.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"
|
||
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}"
|
||
echo ""
|
||
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()
|