59c5af0499
TR_RACK_BITBRAIN=both is what the owner's live .env carries, and with the new ADE+SBC gun registered at id 17 under the SAME rack name that value was also landing on id 17 - so a gun that was not enabled (TR_BITBRAIN_NET unset) was admitted into the rack and its placeholder predictions were pushed into the shared VirtualTracker ring, which shifts every other gun's learning order. While the namespace is LEGACY, loadRackMembership now skips id 17's TR_RACK_BITBRAIN entirely, so that value addresses ONLY the gun it always addressed (LEADGAIN, id 16). ModularBot additionally gates admission on BitbrainNetGun.gunAdmitted(), and test_bitbrain_net pins the truth table: over 6 (rack, switch) settings there is NO configuration that admits the gun while leaving it disabled. Guards: test_env_report 25, test_rack_membership 49 (was 48; the revert one-liner now sets TR_BITBRAIN_NET=1 and one truth-table check was added), test_tm_pattern_registration 20, test_bitbrain 56, test_gun_harness 39, test_tfil_commit_env 30, test_lead_gain_registration 13, test_lead_gain_legacy 24, test_bitbrain_net 44. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
326 lines
13 KiB
Nim
326 lines
13 KiB
Nim
## Does the BITBRAIN gun (rack id 17) actually ENGAGE the ADE+SBC network?
|
|
##
|
|
## Linking is not engagement. These checks drive the real gun over synthetic
|
|
## states and assert the observable consequences of a live network:
|
|
## * the network is BUILT on first predict and is 0 bytes before that (lazy);
|
|
## * two DIFFERENT inputs give DIFFERENT class outputs (the ADE layer is
|
|
## discriminating, not a constant);
|
|
## * learning a sample CHANGES the memory (the SBC head is writing);
|
|
## * `learn` is idempotent on a bitset SBC (setting the same class twice is a
|
|
## no-op) and monotone on a counted one;
|
|
## * the shipped knobs really move RAM and the derived geometry;
|
|
## * the input is the CONFIGURED block set: a disabled block shrinks it, a
|
|
## wider block grows it, and `TR_BITBRAIN_INPUT` pins it exactly;
|
|
## * construction is RNG-clean (the default path cannot perturb the selector).
|
|
##
|
|
## No Java, no battle, no fixtures.
|
|
##
|
|
## Run: nim c -r --path:common_libs common_libs/tests/test_bitbrain_net.nim
|
|
|
|
import std/[os, random, math, strutils, sequtils]
|
|
import gun_harness/gun_interface
|
|
import gun_harness/virtual_bullets
|
|
import gun_harness/selector
|
|
import guns/bitbrain_net
|
|
|
|
const BitbrainNetId = 17
|
|
const PatternId = 5
|
|
|
|
var failures = 0
|
|
proc check(name: string, ok: bool) =
|
|
if ok: echo "PASS: ", name
|
|
else: echo "FAIL: ", name; inc failures
|
|
|
|
proc clearEnv() =
|
|
for n in BitbrainNetEnvNames: delEnv(n)
|
|
# the master switch is shared with LEADGAIN, so it is NOT in
|
|
# BitbrainNetEnvNames and must be cleared explicitly here
|
|
delEnv("TR_BITBRAIN_NET")
|
|
for n in RackGunNames: delEnv("TR_RACK_" & n)
|
|
|
|
proc mkState(t: int, ex, ey, sx, sy, eh: float): WorldState =
|
|
WorldState(arenaWidth: 800, arenaHeight: 600, tick: t,
|
|
enemyX: ex, enemyY: ey, enemyHeading: eh, enemySpeed: 8,
|
|
selfX: sx, selfY: sy, selfHeading: 0, selfSpeed: 8,
|
|
selfEnergy: 100, enemyEnergy: 100, selfRadarHeading: 0)
|
|
|
|
proc armNet() =
|
|
## A small, fast, deterministic configuration: TR_BITBRAIN_NET must be on for
|
|
## the gun to engage at all.
|
|
putEnv(BBN_NET_ENV, "1")
|
|
putEnv(BBN_NADES_ENV, "64")
|
|
putEnv(BBN_CLASSES_ENV, "8")
|
|
putEnv(BBN_MINOBS_ENV, "1")
|
|
putEnv(BBN_INPUT_ENV, "24")
|
|
|
|
# ── registration / default-off ───────────────────────────────────────────────
|
|
|
|
proc testRegistration() =
|
|
clearEnv()
|
|
check "rack: BITBRAIN is registered at id 17 and defaults to off",
|
|
RackGunNames.len == 18 and
|
|
RackGunNames[BitbrainNetId] == "BITBRAIN" and
|
|
DefaultRackMembership[BitbrainNetId] == rmOff
|
|
check "rack: the shipped default still admits only Pattern",
|
|
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId] and
|
|
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId]
|
|
check "gate: the gun is NOT spawned under the default rack",
|
|
not vBulletAdmitted(BitbrainNetId, rm1v1, DefaultRackMembership, true)
|
|
var g = initBitbrainNetGun()
|
|
check "default OFF: TR_BITBRAIN_NET unset leaves the gun disabled",
|
|
not g.enabled and g.networkBytes() == 0
|
|
# With the switch on but the gun not built, predict must not spend anything.
|
|
armNet()
|
|
var g2 = initBitbrainNetGun()
|
|
check "lazy: an enabled but unused gun still holds 0 bytes",
|
|
g2.enabled and g2.networkBytes() == 0
|
|
|
|
proc testRackEnv() =
|
|
clearEnv()
|
|
putEnv("TR_RACK_BITBRAIN", "both")
|
|
putEnv(BBN_NET_ENV, "1")
|
|
let m = loadRackMembership()
|
|
check "env: TR_RACK_BITBRAIN=both + TR_BITBRAIN_NET=1 admits id 17",
|
|
m[BitbrainNetId] == rmBoth and
|
|
vBulletAdmitted(BitbrainNetId, rm1v1, m, true)
|
|
# A pre-rename .env carries TR_RACK_BITBRAIN=both WITHOUT the master switch.
|
|
# That must not admit the gun: a disabled gun's placeholder predictions would
|
|
# enter the shared VirtualTracker ring and shift every other gun's learning
|
|
# order, i.e. it would break the default path.
|
|
clearEnv()
|
|
putEnv("TR_RACK_BITBRAIN", "both") # exactly what a pre-rename .env carries
|
|
let mLegacy = loadRackMembership()
|
|
check "env: TR_RACK_BITBRAIN with the switch OFF still means LEADGAIN (id 16)",
|
|
mLegacy[BitbrainNetId] == rmOff and mLegacy[16] == rmBoth
|
|
# TR_RACK_BITBRAIN and TR_BITBRAIN_NET are gated by the SAME switch, so there
|
|
# is no env configuration that admits the gun without enabling it. Pin that:
|
|
# a disabled gun must never reach the shared VirtualTracker ring, because its
|
|
# placeholder predictions would shift every other gun's learning order.
|
|
var consistent = true
|
|
for (rackVal, netVal) in [("both", ""), ("both", "0"), ("both", "1"),
|
|
("off", "1"), ("1v1", "1"), ("1v1", "")]:
|
|
clearEnv()
|
|
putEnv("TR_RACK_BITBRAIN", rackVal)
|
|
if netVal.len > 0: putEnv(BBN_NET_ENV, netVal)
|
|
let mm = loadRackMembership()
|
|
let inRack = mm[BitbrainNetId] != rmOff
|
|
let admitted = initBitbrainNetGun().gunAdmitted(inRack)
|
|
if inRack != admitted: consistent = false
|
|
if admitted and not initBitbrainNetGun().enabled: consistent = false
|
|
check "env: TR_RACK_BITBRAIN alone can NEVER admit a disabled id 17 (6 settings)",
|
|
consistent
|
|
clearEnv()
|
|
clearEnv()
|
|
|
|
# ── the knobs really resolve ─────────────────────────────────────────────────
|
|
|
|
proc testKnobs() =
|
|
clearEnv()
|
|
armNet()
|
|
putEnv(BBN_INPUT_ENV, "33")
|
|
putEnv(BBN_CLASSES_ENV, "17")
|
|
putEnv(BBN_NADES_ENV, "128")
|
|
putEnv(BBN_WIDTHS_ENV, "3,5,7,9")
|
|
putEnv(BBN_SPAN_ENV, "25")
|
|
putEnv(BBN_MODE_ENV, "counted")
|
|
putEnv(BBN_DECAY_EVERY_ENV, "11")
|
|
putEnv(BBN_DECAY_SHIFT_ENV, "2")
|
|
putEnv(BBN_MINOBS_ENV, "7")
|
|
var g = initBitbrainNetGun()
|
|
check "knob: input width", g.inputWidth == 33
|
|
check "knob: nClasses (output resolution)", g.nClasses == 17
|
|
check "knob: nAde per AD", g.nAde == 128
|
|
check "knob: clause widths (one AD per width)", g.widths == @[3, 5, 7, 9]
|
|
check "knob: class half-range", g.maxDeg == 25.0
|
|
check "knob: SBC mode", g.mode == smCounted
|
|
check "knob: counted-mode decay knobs",
|
|
g.decayEvery == 11 and g.decayShift == 2
|
|
check "knob: minObs", g.minObs == 7
|
|
putEnv(BBN_MODE_ENV, "bitset")
|
|
check "knob: bitset mode", initBitbrainNetGun().mode == smBitset
|
|
clearEnv()
|
|
|
|
# ── the input is a CONFIGURED SET OF BLOCKS ──────────────────────────────────
|
|
|
|
proc testFeatureBlocks() =
|
|
clearEnv()
|
|
armNet()
|
|
delEnv(BBN_INPUT_ENV)
|
|
var g = initBitbrainNetGun()
|
|
check "features: the shipped block set is 47 slots",
|
|
derivedSlots(g.blockWidths) == BB_DEFAULT_SLOTS and
|
|
g.inputWidth == BB_DEFAULT_SLOTS
|
|
check "features: every shipped block is enabled by default",
|
|
g.blockWidths.allIt(it > 0)
|
|
# disable two blocks -> the input shrinks by exactly their widths
|
|
delEnv(BBN_INPUT_ENV)
|
|
putEnv(BBN_FEATURES_ENV, "epos:0,bull:0")
|
|
var g2 = initBitbrainNetGun()
|
|
check "features: a disabled block removes its slots",
|
|
g2.inputWidth == BB_DEFAULT_SLOTS - 8 - 4
|
|
# widen one block -> the input grows by exactly the extra slots
|
|
putEnv(BBN_FEATURES_ENV, "dist:9")
|
|
var g3 = initBitbrainNetGun()
|
|
check "features: a wider block adds its slots",
|
|
g3.inputWidth == BB_DEFAULT_SLOTS - 10 + 18
|
|
# TR_BITBRAIN_INPUT pins the width whatever the blocks say
|
|
putEnv(BBN_FEATURES_ENV, "dist:9")
|
|
putEnv(BBN_INPUT_ENV, "64")
|
|
var g4 = initBitbrainNetGun()
|
|
check "features: TR_BITBRAIN_INPUT pins the width",
|
|
g4.inputWidth == 64
|
|
putEnv(BBN_INPUT_ENV, "9")
|
|
var g5 = initBitbrainNetGun()
|
|
check "features: TR_BITBRAIN_INPUT can shrink below the block total",
|
|
g5.inputWidth == 9
|
|
# the unknown-block warning path must not change the widths
|
|
putEnv(BBN_FEATURES_ENV, "banana,hzn:3")
|
|
var g6 = initBitbrainNetGun()
|
|
check "features: an unknown block is ignored, the rest still apply",
|
|
g6.blockWidths[blockNameIndex("hzn")] == 3 and
|
|
g6.blockWidths[blockNameIndex("epos")] == 4
|
|
clearEnv()
|
|
|
|
# ── the network really engages ───────────────────────────────────────────────
|
|
|
|
proc testNetworkEngages() =
|
|
clearEnv()
|
|
armNet()
|
|
var g = initBitbrainNetGun()
|
|
let st = mkState(1, 600, 300, 100, 300, 0)
|
|
var input = buildInput(g, st)
|
|
check "engage: the input vector is exactly inputWidth long",
|
|
input.len == g.inputWidth
|
|
check "engage: the input is a 0/255 thermometer, not a dead constant",
|
|
input.allIt(it == 0'u8 or it == BBN_SBC_VALUE) and
|
|
input.anyIt(it == BBN_SBC_VALUE)
|
|
check "engage: no network is built before the gun is used",
|
|
g.networkBytes() == 0
|
|
discard predict(g, st, 11.0)
|
|
check "engage: the gun builds the network on first use",
|
|
g.networkBytes() > 0
|
|
# two different inputs -> different class outputs
|
|
g.buildNet()
|
|
let a = buildInput(g, mkState(1, 600, 300, 100, 300, 0))
|
|
let b = buildInput(g, mkState(1, 200, 500, 700, 100, 90))
|
|
check "engage: two different inputs give DIFFERENT bit vectors", a != b
|
|
# An untrained SBC has no evidence, so the argmax is trivially 0. The real
|
|
# engagement test is: teach it a rule, then check the readout separates.
|
|
for i in 0 ..< 300:
|
|
let half = (i mod 2 == 0)
|
|
let x = buildInput(g, mkState(i, if half: 200.0 else: 700.0,
|
|
150.0 + float(i mod 7), 100, 500, float(i mod 3)))
|
|
g.learnSample(x, if half: 0 else: 7)
|
|
var labels = newSeq[int](100)
|
|
for i in 0 ..< 100:
|
|
let half = (i mod 2 == 0)
|
|
let x = buildInput(g, mkState(i, if half: 200.0 else: 700.0,
|
|
150.0 + float(i mod 7), 100, 500, float(i mod 3)))
|
|
labels[i] = g.inferClass(x)
|
|
var differ = false
|
|
for i in 1 ..< labels.len:
|
|
if labels[i] != labels[0]: differ = true
|
|
check "engage: a TRAINED ADE+SBC head returns DIFFERENT classes for different states",
|
|
differ
|
|
var agree = 0
|
|
for i in 0 ..< labels.len:
|
|
if (labels[i] == 0) == (i mod 2 == 0): inc agree
|
|
check "engage: the readout separates the two taught populations",
|
|
agree >= 80
|
|
# learning changes the memory
|
|
g.resetLearning()
|
|
let before = g.sbcBytesOf()
|
|
let beforeOcc = g.sbcsOf()[0].occupancy()
|
|
g.learnSample(a, 3)
|
|
check "engage: learning one sample WRITES the SBC memory",
|
|
g.sbcsOf()[0].occupancy() > beforeOcc
|
|
check "engage: memoryBytes is unchanged by a learn (no realloc)",
|
|
g.sbcBytesOf() == before
|
|
# bitset learn is idempotent
|
|
clearEnv()
|
|
armNet()
|
|
putEnv(BBN_MODE_ENV, "bitset")
|
|
putEnv(BBN_WIDTHS_ENV, "2,3,4") # denser clauses so coincidences actually fire
|
|
var gb = initBitbrainNetGun()
|
|
gb.buildNet()
|
|
gb.learnSample(a, 2)
|
|
let occ1 = gb.net.sbcs[0].occupancy()
|
|
for _ in 0 ..< 49: gb.learnSample(a, 2)
|
|
check "engage: a bitset learn really writes the memory", occ1 > 0.0
|
|
check "engage: bitset learn is idempotent (50x learn == 1x)",
|
|
abs(gb.net.sbcs[0].occupancy() - occ1) < 1e-12
|
|
# counted learn is monotone: the same cell keeps gaining evidence
|
|
putEnv(BBN_MODE_ENV, "counted")
|
|
putEnv(BBN_WIDTHS_ENV, "2,3,4")
|
|
var gc = initBitbrainNetGun()
|
|
gc.buildNet()
|
|
gc.learnSample(a, 2)
|
|
let ev2 = gc.evidenceFor(a, 2)
|
|
for _ in 0 ..< 4: gc.learnSample(a, 2)
|
|
check "engage: counted learn is MONOTONE (5 learns beat 1)",
|
|
ev2 > 0 and gc.evidenceFor(a, 2) > ev2
|
|
check "engage: the counted mode really holds saturating counters",
|
|
gc.sbcsOf()[0].mode == smCounted
|
|
clearEnv()
|
|
|
|
# ── the readout is a fine-grained correction, and it is gated ────────────────
|
|
|
|
proc testReadout() =
|
|
clearEnv()
|
|
armNet()
|
|
putEnv(BBN_MINOBS_ENV, "1000")
|
|
var g = initBitbrainNetGun()
|
|
let st = mkState(1, 600, 300, 100, 300, 0)
|
|
discard predict(g, st, 11.0)
|
|
check "readout: below minObs the correction is exactly zero",
|
|
g.lastShiftDeg == 0.0 and not isWarmedUp(g)
|
|
putEnv(BBN_MINOBS_ENV, "1")
|
|
var g2 = initBitbrainNetGun()
|
|
# feed a stream so labels resolve and the learner engages
|
|
var t = 0
|
|
for _ in 0 ..< 400:
|
|
let ft = float(t)
|
|
let st = mkState(t, 100.0 + 2.0 * ft, 200.0 + 1.1 * ft, 400, 300,
|
|
20.0 * sin(ft * 0.2))
|
|
discard predict(g2, st, 11.0)
|
|
inc t
|
|
check "readout: a resolved stream trains the net", g2.trained > 100
|
|
check "readout: the net becomes warmed up past minObs", isWarmedUp(g2)
|
|
check "readout: the correction is a bounded, fine-grained angle",
|
|
g2.lastShiftDeg >= -g2.maxDeg - 1e-9 and
|
|
g2.lastShiftDeg <= g2.maxDeg + 1e-9
|
|
check "readout: the gun's output is a POINT (Pattern base + correction)",
|
|
predict(g2, mkState(400, 500, 300, 400, 300, 0), 11.0).x != 0.0
|
|
check "readout: ADE threshold adaptation ran (online calibration)",
|
|
g2.adapts > 0
|
|
clearEnv()
|
|
|
|
# ── RNG parity ───────────────────────────────────────────────────────────────
|
|
|
|
proc testRngClean() =
|
|
clearEnv()
|
|
randomize(1234)
|
|
let a = rand(1_000_000)
|
|
randomize(1234)
|
|
armNet()
|
|
var g = initBitbrainNetGun()
|
|
g.buildNet()
|
|
discard predict(g, mkState(1, 300, 300, 100, 100, 0), 11.0)
|
|
let b = rand(1_000_000)
|
|
check "parity: building + running the net does not perturb the global RNG",
|
|
a == b
|
|
clearEnv()
|
|
|
|
testRegistration()
|
|
testRackEnv()
|
|
testKnobs()
|
|
testFeatureBlocks()
|
|
testNetworkEngages()
|
|
testReadout()
|
|
testRngClean()
|
|
|
|
if failures > 0:
|
|
echo "\n", failures, " check(s) FAILED"
|
|
quit(1)
|
|
echo "\nAll BITBRAIN (ADE+SBC) engagement checks passed."
|