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>
This commit is contained in:
2026-09-26 16:50:21 +02:00
parent 7a6237ec20
commit e9302bc9f6
14 changed files with 1520 additions and 39 deletions
+26 -6
View File
@@ -16,7 +16,7 @@
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]
import guns/[head_on, pattern_matcher, tm_horizon, lead_gain, bitbrain_net]
# arm indices (fixed order = fixed output)
const
@@ -43,10 +43,16 @@ const
# 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"]
"PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain",
"BitBrainNet"]
const
## The five sub-unity gain arms, in increasing order, resolved to arm indices.
@@ -96,6 +102,7 @@ type Ctx = object
naive: NaiveLinearGun
tmh: TmHorizonGun
lg: LeadGainGun
bbn: BitbrainNetGun
headon: HeadOnGun
st: WorldState
enemy: seq[EnemyInfo]
@@ -173,6 +180,10 @@ proc runRound(ctx: var Ctx, arms: var seq[ArmAcc], r: int) =
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 =
@@ -186,6 +197,7 @@ proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var S
naive: NaiveLinearGun(lastTick: -1),
tmh: initTmHorizonGun(),
lg: initLeadGainGun(),
bbn: initBitbrainNetGun(),
headon: HeadOnGun(),
st: WorldState(arenaWidth: c.arenaW, arenaHeight: c.arenaH),
enemy: newSeq[EnemyInfo](1))
@@ -213,6 +225,11 @@ 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
@@ -296,9 +313,10 @@ proc main() =
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
echo fmt"3. HeadOn (static LOS) mean|err| = {mHead:.3f} deg vs Pattern {mPat:.3f} / TMHorizon {mTmh:.3f} / LeadGain {mLg:.3f}"
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"
@@ -317,7 +335,7 @@ proc main() =
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"
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:
@@ -325,8 +343,10 @@ proc main() =
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 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)."