Files
SirRoboGarage/common_libs/tests/test_lead_gain_legacy.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

176 lines
7.4 KiB
Nim

## Backward-compatibility guard for the LEADGAIN rename (gun id 16).
##
## The owner's live `.env` predates the rename and carries `TR_RACK_BITBRAIN`,
## `TR_BITBRAIN_GAINS`, `TR_BITBRAIN_MEM`, `TR_BITBRAIN_LOG`. Those names used to
## address the corrector that is now called LEADGAIN, and they MUST keep working,
## because the `TR_BITBRAIN_*` prefix now belongs to the new ADE+SBC gun
## (`guns/bitbrain_net.nim`, rack id 17). This test pins the disambiguation:
##
## * `TR_BITBRAIN_NET` unset/0 -> LEGACY MODE: every `TR_BITBRAIN_<X>` in the
## frozen `LegacyKnobEnvNames` set is an alias for `TR_LEADGAIN_<X>`, and
## `TR_RACK_BITBRAIN` still selects rack id 16.
## * `TR_BITBRAIN_NET=1` -> NEW-NETWORK MODE: the `TR_BITBRAIN_*` names
## belong to the new gun; this gun reads only `TR_LEADGAIN_<X>`.
##
## The two name sets are DISJOINT, so the mapping is total and deterministic.
## No Java, no battle, no network build.
##
## Run: nim c -r common_libs/tests/test_lead_gain_legacy.nim
import std/[os, strutils]
import gun_harness/virtual_bullets
import gun_harness/selector
import guns/lead_gain
const LeadGainId = 16
var failures = 0
proc check(name: string, ok: bool) =
if ok: echo "PASS: ", name
else: echo "FAIL: ", name; inc failures
proc clearEnv() =
for suffix in LegacyKnobEnvNames:
delEnv(LegacyPrefix & suffix)
delEnv(NewPrefix & suffix)
delEnv(LegacyRackEnvName)
delEnv("TR_RACK_" & RackGunNames[LeadGainId])
delEnv(LG_NET_SWITCH_ENV)
# ── the alias table itself ───────────────────────────────────────────────────
proc testAliasTableIsDisjoint() =
## Every legacy name must be a `TR_BITBRAIN_<X>` whose `X` is in the frozen
## set, and the new name must be the `TR_LEADGAIN_<X>` of the same suffix.
var ok = LegacyKnobEnvNames.len == 14
var suffixes: seq[string]
for s in LegacyKnobEnvNames:
if s.len == 0 or s in suffixes: ok = false
suffixes.add s
check "table: 14 unique legacy suffixes", ok
check "table: the legacy prefix is TR_BITBRAIN_", LegacyPrefix == "TR_BITBRAIN_"
check "table: the new prefix is TR_LEADGAIN_", NewPrefix == "TR_LEADGAIN_"
# The corrector's own live knobs must all be in the alias set, otherwise the
# owner's `.env` silently stops working.
for knob in [LG_MEM_ENV, LG_GAINS_ENV, LG_LOG_ENV, LG_MIN_OBS_ENV, LG_DECAY_ENV,
LG_DECAY_FRAC_ENV, LG_RESET_ON_TARGET_ENV, LG_N_ENV, LG_NADE_ENV,
LG_RANGE_ENV, LG_WARMUP_ENV, LG_ADAPT_ENV, LG_CALIB_ENV, LG_SEED_ENV]:
let suffix = knob[NewPrefix.len .. ^1]
if suffix notin LegacyKnobEnvNames: ok = false
check "table: every corrector knob has a legacy alias", ok
# ── the switch ───────────────────────────────────────────────────────────────
proc testSwitch() =
clearEnv()
check "switch: unset => legacy mode (new ADE+SBC namespace disowned)",
not netSwitchOn()
putEnv(LG_NET_SWITCH_ENV, "0")
check "switch: 0 => legacy mode", not netSwitchOn()
putEnv(LG_NET_SWITCH_ENV, "1")
check "switch: 1 => new-network mode", netSwitchOn()
putEnv(LG_NET_SWITCH_ENV, "yes")
check "switch: yes => new-network mode", netSwitchOn()
putEnv(LG_NET_SWITCH_ENV, "banana")
check "switch: an unknown value is NOT new-network mode (fails safe)",
not netSwitchOn()
clearEnv()
# ── knob aliasing ────────────────────────────────────────────────────────────
proc testKnobAlias() =
clearEnv()
# the owner's exact .env
putEnv("TR_BITBRAIN_GAINS", "1.0,0.5,0.25")
putEnv("TR_BITBRAIN_MEM", "decay")
putEnv("TR_BITBRAIN_LOG", "1")
var g = initLeadGainGun()
check "alias: TR_BITBRAIN_GAINS feeds the candidate set",
g.cands == @[0.25, 0.5, 1.0]
check "alias: TR_BITBRAIN_MEM=decay is honoured", g.memMode == lgDecay
check "alias: TR_BITBRAIN_LOG=1 turns the log on", g.logEnabled
# the new name always wins over the legacy alias
putEnv("TR_LEADGAIN_GAINS", "0.5,0.75")
var g2 = initLeadGainGun()
check "alias: the NEW name wins when both are set", g2.cands == @[0.5, 0.75]
# the switch disowns the whole legacy namespace
putEnv(LG_NET_SWITCH_ENV, "1")
delEnv("TR_LEADGAIN_GAINS")
var g3 = initLeadGainGun()
check "alias: TR_BITBRAIN_NET=1 makes the legacy names inert for this gun",
g3.memMode == lgPerRound and not g3.logEnabled and
g3.cands == @[0.0, 0.25, 0.5, 0.75, 1.0]
# ... and then the NEW names still work
putEnv("TR_LEADGAIN_MEM", "retained")
var g4 = initLeadGainGun()
check "alias: TR_LEADGAIN_* is always read, in both modes",
g4.memMode == lgRetained
clearEnv()
# ── rack alias ───────────────────────────────────────────────────────────────
proc testRackAlias() =
clearEnv()
putEnv(LegacyRackEnvName, "both")
let m = loadRackMembership()
check "rack: TR_RACK_BITBRAIN=both still selects LEADGAIN (id 16)",
m[LeadGainId] == rmBoth and
vBulletAdmitted(LeadGainId, rm1v1, m, true)
putEnv(LG_NET_SWITCH_ENV, "1")
let m2 = loadRackMembership()
check "rack: TR_BITBRAIN_NET=1 hands the name to the new ADE+SBC gun (id 17)",
m2[LeadGainId] == rmOff and m2.len == 18
clearEnv()
putEnv("TR_RACK_LEADGAIN", "both")
putEnv(LegacyRackEnvName, "off")
let m3 = loadRackMembership()
check "rack: the new name wins over the legacy alias",
m3[LeadGainId] == rmBoth
clearEnv()
# ── the deprecation line ─────────────────────────────────────────────────────
proc testDeprecationLine() =
clearEnv()
check "depr: no legacy knobs => no deprecation line",
lgDeprecationLine().len == 0
putEnv("TR_BITBRAIN_GAINS", "1.0")
let line = lgDeprecationLine()
check "depr: a legacy knob produces exactly one line naming the new name",
line.startsWith("[depr]") and
line.contains("TR_BITBRAIN_GAINS -> TR_LEADGAIN_GAINS") and
line.contains(NewPrefix)
putEnv(LG_NET_SWITCH_ENV, "1")
check "depr: the line is silent once the switch is flipped",
lgDeprecationLine().len == 0
clearEnv()
# ── shipped-default parity ───────────────────────────────────────────────────
proc testDefaultParity() =
clearEnv()
let m = loadRackMembership()
var onlyPattern = true
for i in 0..<RackGunNames.len:
let want = if i == 5: rmBoth else: rmOff
if m[i] != want: onlyPattern = false
check "parity: a clean env still loads the shipped onlyPattern rack", onlyPattern
check "parity: the rack is still 18 guns at id 16 = LEADGAIN",
RackGunNames.len == 18 and RackGunNames[LeadGainId] == "LEADGAIN"
var g = initLeadGainGun()
check "parity: a clean env resolves the shipped candidate set",
g.cands == @[0.0, 0.25, 0.5, 0.75, 1.0] and g.memMode == lgPerRound
clearEnv()
testAliasTableIsDisjoint()
testSwitch()
testKnobAlias()
testRackAlias()
testDeprecationLine()
testDefaultParity()
if failures > 0:
echo "\n", failures, " check(s) FAILED"
quit(1)
echo "\nAll LEADGAIN legacy-alias checks passed."