e9302bc9f6
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>
207 lines
8.6 KiB
Nim
207 lines
8.6 KiB
Nim
## Offline guard for the TM pattern gun rack registration (id 14) and the
|
|
## default-path parity proof.
|
|
##
|
|
## Covers:
|
|
## * RackGunNames / DefaultRackMembership carry the new gun, defaulting `off`;
|
|
## * `TR_RACK_TMPATTERN` parses both|1v1|melee|off like every other gun;
|
|
## * the DEFAULT membership table is the shipped `onlyPattern` rack (Pattern
|
|
## admitted, TMPATTERN and every other gun `off`);
|
|
## * an EXPLICIT old-rack membership (guns 0..13 both, gun 14 off) still
|
|
## reproduces the pre-change all-`both` 14-gun selection, RNG draw for RNG
|
|
## draw, through both `bestGun` and the live `selectGun` — so admitting
|
|
## TMPATTERN never perturbs the old rack when it is selected explicitly;
|
|
## * `initTmRadialGun()` selects the radial target mode;
|
|
## * the deferred-label fix (Task 2) resolves every fired virtual bullet:
|
|
## a radial-mode replay ends with `labelMisses == 0` and non-zero training.
|
|
##
|
|
## Run: nim c -r common_libs/tests/test_tm_pattern_registration.nim
|
|
|
|
import std/[random, tables, os]
|
|
import gun_harness/gun_interface
|
|
import gun_harness/virtual_bullets
|
|
import gun_harness/selector
|
|
import gun_harness/offline_range
|
|
import guns/tm_pattern
|
|
|
|
const TmPatternId = 14
|
|
|
|
var failures = 0
|
|
proc check(name: string, ok: bool) =
|
|
if ok: echo "PASS: ", name
|
|
else: echo "FAIL: ", name; inc failures
|
|
|
|
proc recordHit(fw: var FitnessWindow, hit: bool) =
|
|
fw.hits[fw.head] = hit
|
|
fw.head = (fw.head + 1) mod WindowSize
|
|
inc fw.count
|
|
|
|
proc seedWindow(t: var VirtualTracker, targetId, gunId, binIdx, hits, misses: int) =
|
|
if targetId notin t.fitness:
|
|
t.fitness[targetId] = newSeq[GunFitness](t.numGuns)
|
|
var fw = addr t.fitness[targetId][gunId].bins[binIdx]
|
|
for _ in 0..<hits: recordHit(fw[], true)
|
|
for _ in 0..<misses: recordHit(fw[], false)
|
|
|
|
proc seedOldRack(t: var VirtualTracker, targetId: int) =
|
|
## 14 deterministic rates for the old rack.
|
|
seedWindow(t, targetId, 0, 0, 60, 40)
|
|
seedWindow(t, targetId, 1, 0, 40, 60)
|
|
seedWindow(t, targetId, 2, 0, 80, 20)
|
|
seedWindow(t, targetId, 3, 0, 55, 45)
|
|
seedWindow(t, targetId, 4, 0, 20, 80)
|
|
seedWindow(t, targetId, 5, 0, 70, 30)
|
|
seedWindow(t, targetId, 6, 0, 45, 55)
|
|
seedWindow(t, targetId, 7, 0, 65, 35)
|
|
seedWindow(t, targetId, 8, 0, 30, 70)
|
|
seedWindow(t, targetId, 9, 0, 50, 50)
|
|
seedWindow(t, targetId, 10, 0, 35, 65)
|
|
seedWindow(t, targetId, 11, 0, 75, 25)
|
|
seedWindow(t, targetId, 12, 0, 25, 75)
|
|
seedWindow(t, targetId, 13, 0, 52, 48)
|
|
|
|
# ── registration table ────────────────────────────────────────────────────────
|
|
|
|
proc testTable() =
|
|
check "rack: RackGunNames has 18 entries", RackGunNames.len == 18
|
|
check "rack: the new gun is named TMPATTERN at id 14",
|
|
RackGunNames[TmPatternId] == "TMPATTERN"
|
|
check "rack: the new gun defaults to `off`",
|
|
DefaultRackMembership[TmPatternId] == rmOff
|
|
var onlyPattern = true
|
|
for i in 0..<RackGunNames.len:
|
|
let want = if i == 5: rmBoth else: rmOff
|
|
if DefaultRackMembership[i] != want: onlyPattern = false
|
|
check "rack: the shipped default is the onlyPattern rack", onlyPattern
|
|
|
|
const PatternId = 5
|
|
|
|
proc oldRackMembership(): array[15, RackMembership] =
|
|
## The pre-change rack as an explicit table: guns 0..13 `both`, TMPATTERN off.
|
|
for i in 0..<TmPatternId: result[i] = rmBoth
|
|
result[TmPatternId] = rmOff
|
|
|
|
proc testDefaultAdmitsOnlyPattern() =
|
|
check "default membership admits only PATTERN (1v1)",
|
|
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId]
|
|
check "default membership admits only PATTERN (melee)",
|
|
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId]
|
|
|
|
proc testEnvOverride() =
|
|
for name in RackGunNames: delEnv("TR_RACK_" & name)
|
|
putEnv("TR_RACK_TMPATTERN", "both")
|
|
check "env: TR_RACK_TMPATTERN=both admits gun 14",
|
|
loadRackMembership()[TmPatternId] == rmBoth
|
|
putEnv("TR_RACK_TMPATTERN", "1v1")
|
|
check "env: TR_RACK_TMPATTERN=1v1 parses",
|
|
loadRackMembership()[TmPatternId] == rmOnly1v1
|
|
putEnv("TR_RACK_TMPATTERN", "melee")
|
|
check "env: TR_RACK_TMPATTERN=melee parses",
|
|
loadRackMembership()[TmPatternId] == rmOnlyMelee
|
|
putEnv("TR_RACK_TMPATTERN", "off")
|
|
check "env: TR_RACK_TMPATTERN=off parses",
|
|
loadRackMembership()[TmPatternId] == rmOff
|
|
delEnv("TR_RACK_TMPATTERN")
|
|
|
|
# ── default-path parity (RNG draw for RNG draw) ───────────────────────────────
|
|
|
|
proc testDefaultSelectionParity() =
|
|
## The OLD rack: 14 guns, all-`both`, empty membership (pre-change call).
|
|
## The NEW rack: 15 guns, EXPLICIT old-rack membership (gun 14 off). Selection
|
|
## must be identical draw for draw, because gun 14 is filtered out before any
|
|
## RNG use. (The shipped DEFAULT is now `onlyPattern`; this explicit table
|
|
## preserves the TMPATTERN-addition parity proof independent of that default.)
|
|
let oldRack = oldRackMembership()
|
|
var oldT = initTracker(14)
|
|
seedOldRack(oldT, 7)
|
|
var newT = initTracker(15)
|
|
seedOldRack(newT, 7)
|
|
# Give gun 14 data too: it must still be excluded by the default membership.
|
|
seedWindow(newT, 7, TmPatternId, 0, 100, 0)
|
|
|
|
randomize(20250922)
|
|
var oldSeq: seq[int]
|
|
for _ in 0..<400: oldSeq.add oldT.bestGun(7)
|
|
randomize(20250922)
|
|
var newSeq: seq[int]
|
|
for _ in 0..<400:
|
|
newSeq.add newT.bestGun(7, rackMode = rm1v1, membership = oldRack)
|
|
check "parity: old-rack-membership 15-gun bestGun == 14-gun rack, RNG draw for draw",
|
|
oldSeq == newSeq
|
|
check "parity: gun 14 is never selected under the old-rack membership",
|
|
TmPatternId notin newSeq
|
|
|
|
# Same through the live hysteresis path.
|
|
var oldH = initTracker(14); seedOldRack(oldH, 7)
|
|
var newH = initTracker(15); seedOldRack(newH, 7)
|
|
seedWindow(newH, 7, TmPatternId, 0, 100, 0)
|
|
randomize(4242)
|
|
var oldHSeq: seq[int]
|
|
for tick in 0..<400: oldHSeq.add oldH.selectGun(7, tick = tick)
|
|
randomize(4242)
|
|
var newHSeq: seq[int]
|
|
for tick in 0..<400:
|
|
newHSeq.add newH.selectGun(7, tick = tick, rackMode = rm1v1,
|
|
membership = oldRack)
|
|
check "parity: old-rack-membership 15-gun selectGun == 14-gun rack, RNG draw for draw",
|
|
oldHSeq == newHSeq
|
|
|
|
# Forcing TMPATTERN alone DOES change selection (it is forceable).
|
|
var forced = initTracker(15)
|
|
seedOldRack(forced, 7)
|
|
seedWindow(forced, 7, TmPatternId, 0, 100, 0)
|
|
var onlyTmp: array[15, RackMembership]
|
|
for i in 0..<15: onlyTmp[i] = rmOff
|
|
onlyTmp[TmPatternId] = rmBoth
|
|
check "force: with every other gun off, TMPATTERN is the only candidate",
|
|
admittedGuns(15, rm1v1, onlyTmp) == @[TmPatternId]
|
|
randomize(9)
|
|
var forcedSeq: seq[int]
|
|
for _ in 0..<50:
|
|
forcedSeq.add forced.bestGun(7, rackMode = rm1v1, membership = onlyTmp)
|
|
var allTmp = true
|
|
for g in forcedSeq:
|
|
if g != TmPatternId: allTmp = false
|
|
check "force: the forced-alone rack always returns TMPATTERN", allTmp
|
|
|
|
# ── radial mode + deferred-label fix ─────────────────────────────────────────
|
|
|
|
proc testRadialInit() =
|
|
let g = initTmRadialGun()
|
|
check "init: initTmRadialGun() selects the radial target mode",
|
|
g.targetMode == tmRadial
|
|
|
|
proc testDeferredLabel() =
|
|
## Drive a radial-mode gun over a synthetic fixture and prove the deferred
|
|
## label resolves every bullet: no label misses, and training happened.
|
|
let fx = synthesizeOscillator(ticks = 400)
|
|
var g2 = initTmRadialGun()
|
|
randomize(3)
|
|
var tracker = initTracker(1, bmPoint)
|
|
for state in fx.states:
|
|
var preds: array[len(PowerBins), GunPrediction]
|
|
for b in 0..<len(PowerBins):
|
|
preds[b] = g2.predict(state, bulletSpeed(PowerBins[b]))
|
|
tracker.spawnBullets(0, preds, state, fx.enemyId)
|
|
var et: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
|
et[fx.enemyId] = (x: state.enemyX, y: state.enemyY,
|
|
lastSeenTick: state.tick, alive: true)
|
|
tracker.tickBullets(state, et,
|
|
proc(gunId: GunId, binIdx: int, e: FeedbackEvent) = g2.onResult(e))
|
|
check "deferred: a radial replay trains (totalObs > 0)", g2.totalObs > 0
|
|
check "deferred: no label misses remain (labelMisses == 0)", g2.labelMisses == 0
|
|
check "deferred: the pending queue never overflowed", g2.pendingDropped == 0
|
|
check "deferred: the radial head is scored above chance (radTotal > 0)",
|
|
g2.radTotal > 0 and g2.radCorrect > 0
|
|
|
|
when isMainModule:
|
|
testTable()
|
|
testDefaultAdmitsOnlyPattern()
|
|
testEnvOverride()
|
|
testDefaultSelectionParity()
|
|
testRadialInit()
|
|
testDeferredLabel()
|
|
if failures > 0:
|
|
echo "\n", failures, " check(s) FAILED"
|
|
quit(1)
|
|
echo "\nAll TM pattern registration checks passed."
|