j142 retire the ADE+SBC gun (rack id 17): the owner watched it, it does not learn, throw it away

Remove guns/bitbrain_net.nim (+README), test_bitbrain_net.nim,
measure_bitbrain_scaling.nim, rack id 17 and all of its plumbing in
selector.nim / ModularBot.nim / env_report.nim, the TR_BITBRAIN_NET switch
and the NEW-NETWORK TR_BITBRAIN_* knobs, and the BitBrainNet arm of
run_prediction_quality.nim.

With id 17 gone there is nothing to disambiguate, so the legacy namespace
becomes the ONLY one: TR_RACK_BITBRAIN always selects id 16 LEADGAIN and
every TR_BITBRAIN_<X> in the frozen 14-suffix alias set always means
TR_LEADGAIN_<X>. The alias layer and its [depr] line stay.

KEPT: the common_libs/bitbrain/ SBC library (learned_surfer imports
bitbrain/sbc), lead_gain at id 16 with env TR_LEADGAIN_* and log tag [lg],
and the c9b6753 crash fix (NumRackGuns widths + test_rack_stat_width).

Tombstone: docs/bitbrain_campaign.md ## RETIRED and one cross-reference line
in docs/gun_campaign.md. Shipped defaults unchanged: clean env -> rack
active 1v1 = PATTERN, movement default strafe.
This commit is contained in:
2026-09-26 19:20:23 +02:00
parent c9b67530db
commit 5e32ec16df
18 changed files with 279 additions and 1732 deletions
+5 -41
View File
@@ -31,7 +31,6 @@ import guns/tm_selector
import guns/tm_pattern import guns/tm_pattern
import guns/tm_horizon import guns/tm_horizon
import guns/lead_gain import guns/lead_gain
import guns/bitbrain_net
import movements/phantom_meteor import movements/phantom_meteor
import movements/rammer import movements/rammer
import movements/ram_decision import movements/ram_decision
@@ -151,7 +150,7 @@ let ResultLog* =
block: block:
let v = getEnv("TR_RESULT_LOG", "1").strip().toLowerAscii() let v = getEnv("TR_RESULT_LOG", "1").strip().toLowerAscii()
not (v == "0" or v == "false" or v == "no" or v == "off") not (v == "0" or v == "false" or v == "no" or v == "off")
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect", "TMPattern", "TMHorizon", "LeadGain", "BitBrainNet"] const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect", "TMPattern", "TMHorizon", "LeadGain"]
## Rack id of the new TM pattern gun. It defaults to `TR_RACK_TMPATTERN=off`; ## Rack id of the new TM pattern gun. It defaults to `TR_RACK_TMPATTERN=off`;
## unlike the other guns, its virtual-bullet spawn is gated on rack admission ## unlike the other guns, its virtual-bullet spawn is gated on rack admission
@@ -171,20 +170,10 @@ const TmHorizonId = 15
## is unchanged). It defaults to `TR_RACK_LEADGAIN=off` and its virtual-bullet ## is unchanged). It defaults to `TR_RACK_LEADGAIN=off` and its virtual-bullet
## spawn is gated on rack admission, so the shipped default never spawns it and ## spawn is gated on rack admission, so the shipped default never spawns it and
## the shared tracker ring head — hence every other gun's learning order — is ## the shared tracker ring head — hence every other gun's learning order — is
## byte-for-byte unchanged. `TR_RACK_BITBRAIN` still selects it while ## byte-for-byte unchanged. `TR_RACK_BITBRAIN` still selects it unconditionally
## `TR_BITBRAIN_NET` is off (backward compatibility; see `gun_harness/selector`). ## (backward compatibility; see `gun_harness/selector`).
const LeadGainId = 16 const LeadGainId = 16
## Rack id of the ADE+SBC gun — the gun that actually runs the algorithm
## (`guns/bitbrain_net.nim`). It takes the `BITBRAIN` name and the
## `TR_BITBRAIN_*` knob prefix that the renamed corrector gave up. TWO switches
## admit it, and both default off: `TR_RACK_BITBRAIN=both` in the rack table and
## `TR_BITBRAIN_NET=1` in the gun itself (the switch that also disowns the
## legacy `TR_BITBRAIN_*` aliases — see `gun_harness/selector`). Its
## virtual-bullet spawn is gated on rack admission, so the shipped default never
## spawns it and the shared tracker ring head is byte-for-byte unchanged.
const BitbrainNetId = 17
## ── virtual-bullet debug overlay (TR_VBULLET_DEBUG*) ─────────────────────── ## ── virtual-bullet debug overlay (TR_VBULLET_DEBUG*) ───────────────────────
## OFF by default: the shipped bot draws nothing. When on, the tracker's ## OFF by default: the shipped bot draws nothing. When on, the tracker's
## virtual bullets are overlaid so the training signal the selector learns ## virtual bullets are overlaid so the training signal the selector learns
@@ -257,7 +246,6 @@ type
tmPattern: TmPatternGun tmPattern: TmPatternGun
tmHorizon: TmHorizonGun tmHorizon: TmHorizonGun
leadGain: LeadGainGun leadGain: LeadGainGun
bbn: BitbrainNetGun
mover: TFILModule mover: TFILModule
ringMover: TFILRingModule ringMover: TFILRingModule
strafeMover: StrafeModule strafeMover: StrafeModule
@@ -961,7 +949,6 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
# LEADGAIN: per-round wipe always clears observations/labels; `perRound` mode # LEADGAIN: per-round wipe always clears observations/labels; `perRound` mode
# also wipes its SBCs (the gate test's winning regime). # also wipes its SBCs (the gate test's winning regime).
bot.leadGain.resetRoundState() bot.leadGain.resetRoundState()
bot.bbn.resetRoundState()
if e.roundNumber <= 1: if e.roundNumber <= 1:
bot.leadGain.resetLearning("round1") bot.leadGain.resetLearning("round1")
bot.isRamming = false bot.isRamming = false
@@ -1044,7 +1031,6 @@ method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
# boundary. The round-1 fallback in `onRoundStarted` covers a missed callback. # boundary. The round-1 fallback in `onRoundStarted` covers a missed callback.
bot.tmHorizon.resetLearning("game_start") bot.tmHorizon.resetLearning("game_start")
bot.leadGain.resetLearning("game_start") bot.leadGain.resetLearning("game_start")
bot.bbn.resetLearning("game_start")
# minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1 # minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1
bot.initialEnemyCount = e.gameSetup.minNumberOfParticipants - 1 bot.initialEnemyCount = e.gameSetup.minNumberOfParticipants - 1
# New battle: reset the cumulative outcome counters and remember how many # New battle: reset the cumulative outcome counters and remember how many
@@ -1153,7 +1139,6 @@ method run*(bot: ModularBot) =
# TR_TMHORIZON_RESET_ON_TARGET, default on; no-op in 1v1). # TR_TMHORIZON_RESET_ON_TARGET, default on; no-op in 1v1).
discard bot.tmHorizon.targetChanged(candidateId) discard bot.tmHorizon.targetChanged(candidateId)
discard bot.leadGain.targetChanged(candidateId) discard bot.leadGain.targetChanged(candidateId)
discard bot.bbn.targetChanged(candidateId)
if bot.currentTargetId >= 0: if bot.currentTargetId >= 0:
bot.lastKnownTargetId = bot.currentTargetId bot.lastKnownTargetId = bot.currentTargetId
@@ -1298,7 +1283,6 @@ method run*(bot: ModularBot) =
var tmpPreds: array[len(PowerBins), GunPrediction] var tmpPreds: array[len(PowerBins), GunPrediction]
var tmhPreds: array[len(PowerBins), GunPrediction] var tmhPreds: array[len(PowerBins), GunPrediction]
var lgPreds: array[len(PowerBins), GunPrediction] var lgPreds: array[len(PowerBins), GunPrediction]
var bbnPreds: array[len(PowerBins), GunPrediction]
# ── TR_VBULLET_ADMIT_ONLY gate ────────────────────────────────────── # ── TR_VBULLET_ADMIT_ONLY gate ──────────────────────────────────────
# Rack membership used to filter only SELECTION, so every unselected gun # Rack membership used to filter only SELECTION, so every unselected gun
# still ran predict()+spawnBullets() each tick to feed a fitness table the # still ran predict()+spawnBullets() each tick to feed a fitness table the
@@ -1315,18 +1299,7 @@ method run*(bot: ModularBot) =
admit[gi] = ModuleVBullets and admit[gi] = ModuleVBullets and
vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership, vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership,
VBulletAdmitOnly or gi == TmPatternId or VBulletAdmitOnly or gi == TmPatternId or
gi == TmHorizonId or gi == LeadGainId or gi == TmHorizonId or gi == LeadGainId)
gi == BitbrainNetId) and
# BITBRAIN (id 17) needs BOTH switches. The rack table alone
# is not enough: a pre-rename `.env` still carries
# `TR_RACK_BITBRAIN=both` (it used to select LEADGAIN), and
# admitting a DISABLED gun would let its placeholder
# predictions into the shared VirtualTracker ring and shift
# every other gun's learning order. With this gate an
# incomplete configuration is exactly as inert as an unset
# one.
(gi != BitbrainNetId or
bot.bbn.gunAdmitted(true))
for i in 0..<len(PowerBins): for i in 0..<len(PowerBins):
if admit[0]: headsUp[i] = bot.headOn.predict(bot.lastState, bulletSpeed(PowerBins[i])) if admit[0]: headsUp[i] = bot.headOn.predict(bot.lastState, bulletSpeed(PowerBins[i]))
if admit[1]: linPreds[i] = bot.linear.predict(bot.lastState, bulletSpeed(PowerBins[i])) if admit[1]: linPreds[i] = bot.linear.predict(bot.lastState, bulletSpeed(PowerBins[i]))
@@ -1352,9 +1325,6 @@ method run*(bot: ModularBot) =
# LEADGAIN: Pattern base, lead scaled by a per-band learned gain. # LEADGAIN: Pattern base, lead scaled by a per-band learned gain.
if admit[LeadGainId]: if admit[LeadGainId]:
lgPreds[i] = bot.leadGain.predict(bot.lastState, bulletSpeed(PowerBins[i])) lgPreds[i] = bot.leadGain.predict(bot.lastState, bulletSpeed(PowerBins[i]))
# BITBRAIN: Pattern base + a fine-grained ADE/SBC angular correction.
if admit[BitbrainNetId]:
bbnPreds[i] = bot.bbn.predict(bot.lastState, bulletSpeed(PowerBins[i]))
if admit[0]: bot.tracker.spawnBullets(0, headsUp, bot.lastState, tid) if admit[0]: bot.tracker.spawnBullets(0, headsUp, bot.lastState, tid)
if admit[1] and not gunDisabled(1): bot.tracker.spawnBullets(1, linPreds, bot.lastState, tid) if admit[1] and not gunDisabled(1): bot.tracker.spawnBullets(1, linPreds, bot.lastState, tid)
@@ -1378,8 +1348,6 @@ method run*(bot: ModularBot) =
bot.tracker.spawnBullets(TmHorizonId, tmhPreds, bot.lastState, tid) bot.tracker.spawnBullets(TmHorizonId, tmhPreds, bot.lastState, tid)
if admit[LeadGainId] and not gunDisabled(LeadGainId): if admit[LeadGainId] and not gunDisabled(LeadGainId):
bot.tracker.spawnBullets(LeadGainId, lgPreds, bot.lastState, tid) bot.tracker.spawnBullets(LeadGainId, lgPreds, bot.lastState, tid)
if admit[BitbrainNetId] and not gunDisabled(BitbrainNetId):
bot.tracker.spawnBullets(BitbrainNetId, bbnPreds, bot.lastState, tid)
# Build slim enemy table for tickBullets # Build slim enemy table for tickBullets
var enemyPositions: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]] var enemyPositions: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
@@ -1416,7 +1384,6 @@ method run*(bot: ModularBot) =
of TmPatternId: bot.tmPattern.onResult(fe) of TmPatternId: bot.tmPattern.onResult(fe)
of TmHorizonId: bot.tmHorizon.onResult(fe) of TmHorizonId: bot.tmHorizon.onResult(fe)
of LeadGainId: bot.leadGain.onResult(fe) of LeadGainId: bot.leadGain.onResult(fe)
of BitbrainNetId: bot.bbn.onResult(fe)
else: discard else: discard
if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss
if DebugDrawOn and VBulletDebugOn: if DebugDrawOn and VBulletDebugOn:
@@ -1472,7 +1439,6 @@ method run*(bot: ModularBot) =
of TmPatternId: bot.tmPattern.predict(bot.lastState, bulletSpeed(power)) of TmPatternId: bot.tmPattern.predict(bot.lastState, bulletSpeed(power))
of TmHorizonId: bot.tmHorizon.predict(bot.lastState, bulletSpeed(power)) of TmHorizonId: bot.tmHorizon.predict(bot.lastState, bulletSpeed(power))
of LeadGainId: bot.leadGain.predict(bot.lastState, bulletSpeed(power)) of LeadGainId: bot.leadGain.predict(bot.lastState, bulletSpeed(power))
of BitbrainNetId: bot.bbn.predict(bot.lastState, bulletSpeed(power))
else: bot.headOn.predict(bot.lastState, bulletSpeed(power)) else: bot.headOn.predict(bot.lastState, bulletSpeed(power))
let aimTarget = aimAngle(getX(), getY(), pred.x, pred.y) let aimTarget = aimAngle(getX(), getY(), pred.x, pred.y)
@@ -1540,7 +1506,7 @@ proc seedSelectorRng() =
when isMainModule: when isMainModule:
var bot = ModularBot( var bot = ModularBot(
tracker: vb.initTracker(NumRackGuns), # 0: HeadOn, 1: Linear, 2: Tsetlin, 3: Circular, 4: GuessFactor, 5: Pattern, 6: WallBounce, 7: Accel, 8: StopShot, 9: Displace, 10: AvgLead, 11: DecayGF, 12: KNN, 13: TMSelect, 14: TMPattern, 15: TMHorizon, 16: LeadGain, 17: BitBrainNet tracker: vb.initTracker(NumRackGuns), # 0: HeadOn, 1: Linear, 2: Tsetlin, 3: Circular, 4: GuessFactor, 5: Pattern, 6: WallBounce, 7: Accel, 8: StopShot, 9: Displace, 10: AvgLead, 11: DecayGF, 12: KNN, 13: TMSelect, 14: TMPattern, 15: TMHorizon, 16: LeadGain
headOn: HeadOnGun(), headOn: HeadOnGun(),
linear: LinearGun(), linear: LinearGun(),
circular: CircularGun(), circular: CircularGun(),
@@ -1558,7 +1524,6 @@ when isMainModule:
tmPattern: initTmRadialGun(), tmPattern: initTmRadialGun(),
tmHorizon: initTmHorizonGun(), tmHorizon: initTmHorizonGun(),
leadGain: initLeadGainGun(), leadGain: initLeadGainGun(),
bbn: initBitbrainNetGun(),
radar: RadarLockModule(), radar: RadarLockModule(),
meleeRadar: initAdaptiveMeleeRadar(), meleeRadar: initAdaptiveMeleeRadar(),
mover: TFILModule(debugGraphics: DebugDrawOn), mover: TFILModule(debugGraphics: DebugDrawOn),
@@ -1598,6 +1563,5 @@ when isMainModule:
tmHorizon: bot.tmHorizon, tmHorizon: bot.tmHorizon,
patternMatcher: bot.patternMatcher, patternMatcher: bot.patternMatcher,
leadGain: bot.leadGain, leadGain: bot.leadGain,
bitbrainNet: bot.bbn,
)) ))
start(bot, botJsonPath) start(bot, botJsonPath)
+18 -49
View File
@@ -32,7 +32,6 @@ import movements/wave_surfer
import movements/learned_surfer import movements/learned_surfer
import guns/tm_horizon import guns/tm_horizon
import guns/lead_gain import guns/lead_gain
import guns/bitbrain_net
import guns/pattern_matcher import guns/pattern_matcher
import vbullet_draw import vbullet_draw
import geo_overlay import geo_overlay
@@ -66,7 +65,6 @@ type
disabledGuns*: HashSet[int] disabledGuns*: HashSet[int]
tmHorizon*: TmHorizonGun tmHorizon*: TmHorizonGun
leadGain*: LeadGainGun leadGain*: LeadGainGun
bitbrainNet*: BitbrainNetGun
patternMatcher*: PatternMatcherGun patternMatcher*: PatternMatcherGun
# ── helpers ────────────────────────────────────────────────────────────────── # ── helpers ──────────────────────────────────────────────────────────────────
@@ -117,8 +115,8 @@ proc sourceOfPresence(name: string): string =
proc legacyNameFor(name: string): string = proc legacyNameFor(name: string): string =
## The legacy `TR_BITBRAIN_<X>` alias for a `TR_LEADGAIN_<X>` knob, or "" when ## The legacy `TR_BITBRAIN_<X>` alias for a `TR_LEADGAIN_<X>` knob, or "" when
## the knob has no legacy name / the namespace now belongs to the new gun. ## the knob has no legacy name. This gun is the sole owner of the
if netSwitchOwnsBitbrainName(): return "" ## `TR_BITBRAIN_*` prefix (the ADE+SBC gun that briefly shared it was retired).
if not name.startsWith(NewPrefix): return "" if not name.startsWith(NewPrefix): return ""
let suffix = name[NewPrefix.len .. ^1] let suffix = name[NewPrefix.len .. ^1]
for s in LegacyKnobEnvNames: for s in LegacyKnobEnvNames:
@@ -141,9 +139,6 @@ proc sourceOfLgPresence(name: string): string =
else: else:
sourceOfPresence(name) sourceOfPresence(name)
proc sbcModeName(m: SbcMode): string {.inline.} =
(if m == smCounted: "counted" else: "bitset")
proc disabledGunsString(guns: HashSet[int]): string = proc disabledGunsString(guns: HashSet[int]): string =
if guns.len == 0: return "none" if guns.len == 0: return "none"
var ids: seq[int] var ids: seq[int]
@@ -425,40 +420,12 @@ proc printEffectiveValues(ctx: EnvReportContext) =
emit("TR_LEADGAIN_SEED", $lg.seed, sourceOfLg(LG_SEED_ENV)) emit("TR_LEADGAIN_SEED", $lg.seed, sourceOfLg(LG_SEED_ENV))
emit("TR_LEADGAIN_RESET_ON_TARGET", onOff(lg.resetOnTarget), emit("TR_LEADGAIN_RESET_ON_TARGET", onOff(lg.resetOnTarget),
sourceOfLg(LG_RESET_ON_TARGET_ENV)) sourceOfLg(LG_RESET_ON_TARGET_ENV))
# The disambiguation switch + the deprecation line it gates, in the report so # The deprecation line the legacy alias layer can print, in the report so an
# an owner staring at a `[depr]` line can see exactly why it fired. # owner staring at a `[depr]` line can see exactly which knob fired.
emit("TR_BITBRAIN_NET", onOff(netSwitchOwnsBitbrainName()),
sourceOfPresence("TR_BITBRAIN_NET"))
let dep = lgDeprecationLine() let dep = lgDeprecationLine()
if dep.len > 0: emit("legacy TR_BITBRAIN_* aliases honoured", "yes", "legacy") if dep.len > 0: emit("legacy TR_BITBRAIN_* aliases honoured", "yes", "legacy")
else: emit("legacy TR_BITBRAIN_* aliases honoured", "no", "default") else: emit("legacy TR_BITBRAIN_* aliases honoured", "no", "default")
# ── the ADE+SBC gun (rack id 17) ─────────────────────────────────────────
# Two switches admit it, both default off: `TR_RACK_BITBRAIN` (rack table) and
# `TR_BITBRAIN_NET` (the gun's own master switch, which is also the switch
# that disowns the legacy `TR_BITBRAIN_*` aliases above).
let bbn = ctx.bitbrainNet
emit("TR_BITBRAIN_NET", onOff(bbn.enabled), sourceOfPresence(BBN_NET_ENV))
emit("TR_BITBRAIN_INPUT", $bbn.inputWidth, sourceOf(BBN_INPUT_ENV))
emit("TR_BITBRAIN_FEATURES", bbn.features, sourceOf(BBN_FEATURES_ENV))
emit("TR_BITBRAIN_NCLASSES", $bbn.nClasses, sourceOf(BBN_CLASSES_ENV))
emit("TR_BITBRAIN_NADES", $bbn.nAde, sourceOf(BBN_NADES_ENV))
emit("TR_BITBRAIN_WIDTHS", bbn.widths.join(","), sourceOf(BBN_WIDTHS_ENV))
emit("TR_BITBRAIN_SPAN", $bbn.maxDeg, sourceOf(BBN_SPAN_ENV))
emit("TR_BITBRAIN_MODE", sbcModeName(bbn.mode), sourceOf(BBN_MODE_ENV))
emit("TR_BITBRAIN_DECAY_EVERY", $bbn.decayEvery, sourceOf(BBN_DECAY_EVERY_ENV))
emit("TR_BITBRAIN_DECAY_SHIFT", $bbn.decayShift, sourceOf(BBN_DECAY_SHIFT_ENV))
emit("TR_BITBRAIN_MINOBS", $bbn.minObs, sourceOf(BBN_MINOBS_ENV))
emit("TR_BITBRAIN_ADAPT_EVERY", $bbn.adaptEvery, sourceOf(BBN_ADAPT_ENV))
emit("TR_BITBRAIN_TARGET", $bbn.targetRate, sourceOf(BBN_TARGET_ENV))
emit("TR_BITBRAIN_NETSEED", $bbn.seed, sourceOf(BBN_SEED_ENV))
emit("TR_BITBRAIN_NETLOG", onOff(bbn.logEnabled),
sourceOfPresence(BBN_LOG_ENV))
emit("bitbrain_net.ram", $bbn.networkBytes, "default") ## 0 until the lazy build
emit("bitbrain_net.admitted", onOff(bbn.gunAdmitted(
ActiveRackMembership[17] != rmOff)),
"default") ## rack table AND TR_BITBRAIN_NET; both default off
# ── Pattern match-shape + radial knobs ──────────────────────────────────── # ── Pattern match-shape + radial knobs ────────────────────────────────────
# These are resolved lazily inside `predict` (which has not run at boot), so # These are resolved lazily inside `predict` (which has not run at boot), so
# unless something already forced them we report the raw env value and say so. # unless something already forced them we report the raw env value and say so.
@@ -514,7 +481,7 @@ proc printModules(ctx: EnvReportContext) =
# mode = enabled in that rack. # mode = enabled in that rack.
for i in 0..<len(RackGunNames): for i in 0..<len(RackGunNames):
var src = sourceOf(RackEnvPrefix & RackGunNames[i]) var src = sourceOf(RackEnvPrefix & RackGunNames[i])
if src == "default" and not netSwitchOwnsBitbrainName(): if src == "default":
for (key, gid) in RackLegacyAlias: for (key, gid) in RackLegacyAlias:
if gid == i and getEnv(key, "").len > 0: if gid == i and getEnv(key, "").len > 0:
src = if isFromEnvFile(key): "legacy/.env" else: "legacy" src = if isFromEnvFile(key): "legacy/.env" else: "legacy"
@@ -621,26 +588,28 @@ proc knownEnvNames*(): seq[string] =
TMH_RETRAIN_EVERY_ENV, TMH_EPOCHS_ENV, TMH_RETRAIN_EVERY_ENV, TMH_EPOCHS_ENV,
LG_MEM_ENV, LG_GAINS_ENV, LG_N_ENV, LG_NADE_ENV, LG_RANGE_ENV, LG_LOG_ENV, LG_MEM_ENV, LG_GAINS_ENV, LG_N_ENV, LG_NADE_ENV, LG_RANGE_ENV, LG_LOG_ENV,
LG_MIN_OBS_ENV, LG_WARMUP_ENV, LG_ADAPT_ENV, LG_CALIB_ENV, LG_DECAY_ENV, LG_MIN_OBS_ENV, LG_WARMUP_ENV, LG_ADAPT_ENV, LG_CALIB_ENV, LG_DECAY_ENV,
LG_DECAY_FRAC_ENV, LG_SEED_ENV, LG_RESET_ON_TARGET_ENV, LG_NET_SWITCH_ENV, LG_DECAY_FRAC_ENV, LG_SEED_ENV, LG_RESET_ON_TARGET_ENV,
# LEGACY aliases of the corrector knobs above (honoured while # LEGACY aliases of the corrector knobs above, honoured UNCONDITIONALLY
# LG_NET_SWITCH_ENV is off). Registered so the tree-scan guard does not # (this gun is the sole owner of the `TR_BITBRAIN_*` prefix). Registered so
# flag the owner's pre-rename .env as unknown. # the tree-scan guard does not flag the owner's pre-rename .env as unknown.
"TR_BITBRAIN_GAINS", "TR_BITBRAIN_MEM", "TR_BITBRAIN_MIN_OBS", "TR_BITBRAIN_GAINS", "TR_BITBRAIN_MEM", "TR_BITBRAIN_MIN_OBS",
"TR_BITBRAIN_DECAY", "TR_BITBRAIN_DECAY_FRAC", "TR_BITBRAIN_LOG", "TR_BITBRAIN_DECAY", "TR_BITBRAIN_DECAY_FRAC", "TR_BITBRAIN_LOG",
"TR_BITBRAIN_RESET_ON_TARGET", "TR_BITBRAIN_N", "TR_BITBRAIN_NADE", "TR_BITBRAIN_RESET_ON_TARGET", "TR_BITBRAIN_N", "TR_BITBRAIN_NADE",
"TR_BITBRAIN_RANGE", "TR_BITBRAIN_WARMUP", "TR_BITBRAIN_ADAPT", "TR_BITBRAIN_RANGE", "TR_BITBRAIN_WARMUP", "TR_BITBRAIN_ADAPT",
"TR_BITBRAIN_CALIB", "TR_BITBRAIN_SEED", "TR_BITBRAIN_CALIB", "TR_BITBRAIN_SEED",
# common_libs/bitbrain (counted-SBC library) reads these inline; job j102 # common_libs/bitbrain (the SBC LIBRARY, which STAYS — `learned_surfer`
# imports bitbrain/sbc) reads these three inline; they are the library's own
# knobs, not the retired ADE+SBC gun's (that gun, rack id 17, was removed).
"TR_BITBRAIN_MODE", "TR_BITBRAIN_DECAY_EVERY", "TR_BITBRAIN_DECAY_SHIFT",
] ]
# the ADE+SBC gun (rack id 17) owns the rest of the TR_BITBRAIN_* namespace
for n in BitbrainNetEnvNames: result.add n
# rack names are constructed from the prefix + gun table, not spelled out # rack names are constructed from the prefix + gun table, not spelled out
for g in RackGunNames: for g in RackGunNames:
result.add RackEnvPrefix & g result.add RackEnvPrefix & g
# NOTE: the legacy rack alias `TR_RACK_BITBRAIN` needs no extra entry — it is # the legacy rack alias `TR_RACK_BITBRAIN` is no longer in `RackGunNames`
# the CURRENT name of rack id 17, so the RackGunNames loop above already # (it is LEADGAIN's old name), so register it explicitly. See
# registers it. See gun_harness/selector's RackLegacyAlias for how it is # gun_harness/selector's RackLegacyAlias.
# routed to LEADGAIN while LG_NET_SWITCH_ENV is off. for (key, _) in RackLegacyAlias:
result.add key
# inline reads with no exported constant (the guard test scans for these) # inline reads with no exported constant (the guard test scans for these)
for n in [ for n in [
"GUN_SELECTOR_WINDOW", "GUN_SELECTOR_MINOBS", "GUN_SELECTOR_TIE", "GUN_SELECTOR_WINDOW", "GUN_SELECTOR_MINOBS", "GUN_SELECTOR_TIE",
+26 -51
View File
@@ -22,7 +22,6 @@ import virtual_bullets
# TR_RACK_KNN=off -> removed from both racks # TR_RACK_KNN=off -> removed from both racks
# TR_RACK_TMPATTERN=off (shipped default for the new TM pattern gun) # TR_RACK_TMPATTERN=off (shipped default for the new TM pattern gun)
# TR_RACK_LEADGAIN=both -> the per-range-band lead-gain corrector (id 16) # TR_RACK_LEADGAIN=both -> the per-range-band lead-gain corrector (id 16)
# TR_RACK_BITBRAIN=both -> the ADE+SBC gun (id 17); also needs TR_BITBRAIN_NET=1
# #
# SHIPPED DEFAULT IS `onlyPattern`: Pattern (id 5) is admitted in both racks and # SHIPPED DEFAULT IS `onlyPattern`: Pattern (id 5) is admitted in both racks and
# every other gun is `off`. This is a deliberate, measured decision, not a # every other gun is `off`. This is a deliberate, measured decision, not a
@@ -39,18 +38,17 @@ import virtual_bullets
# TR_RACK_TSETLIN=both TR_RACK_CIRCULAR=both TR_RACK_GUESSFACTOR=both \ # TR_RACK_TSETLIN=both TR_RACK_CIRCULAR=both TR_RACK_GUESSFACTOR=both \
# TR_RACK_WALLBOUNCE=both TR_RACK_ACCEL=both TR_RACK_STOPSHOT=both \ # TR_RACK_WALLBOUNCE=both TR_RACK_ACCEL=both TR_RACK_STOPSHOT=both \
# TR_RACK_DISPLACE=both TR_RACK_AVGLEAD=both TR_RACK_DECAYGF=both \ # TR_RACK_DISPLACE=both TR_RACK_AVGLEAD=both TR_RACK_DECAYGF=both \
# TR_RACK_KNN=both TR_RACK_TMSELECT=both TR_RACK_LEADGAIN=both \ # TR_RACK_KNN=both TR_RACK_TMSELECT=both TR_RACK_LEADGAIN=both ./ModularBot
# TR_RACK_BITBRAIN=both TR_BITBRAIN_NET=1 ./ModularBot
# #
# The mode itself is derived from SERVER truth (`getEnemyCount()`), never from # The mode itself is derived from SERVER truth (`getEnemyCount()`), never from
# the tracker's known-enemy count, by `rackMode` in virtual_bullets — the same # the tracker's known-enemy count, by `rackMode` in virtual_bullets — the same
# transition the radar uses. # transition the radar uses.
const const
RackGunNames*: array[18, string] = [ RackGunNames*: array[17, string] = [
"HEADON", "LINEAR", "TSETLIN", "CIRCULAR", "GUESSFACTOR", "PATTERN", "HEADON", "LINEAR", "TSETLIN", "CIRCULAR", "GUESSFACTOR", "PATTERN",
"WALLBOUNCE", "ACCEL", "STOPSHOT", "DISPLACE", "AVGLEAD", "DECAYGF", "WALLBOUNCE", "ACCEL", "STOPSHOT", "DISPLACE", "AVGLEAD", "DECAYGF",
"KNN", "TMSELECT", "TMPATTERN", "TMHORIZON", "LEADGAIN", "BITBRAIN"] "KNN", "TMSELECT", "TMPATTERN", "TMHORIZON", "LEADGAIN"]
RackEnvPrefix* = "TR_RACK_" RackEnvPrefix* = "TR_RACK_"
## THE rack size, derived from `RackGunNames`. Every per-gun-indexed array and ## THE rack size, derived from `RackGunNames`. Every per-gun-indexed array and
## loop in the bot and in the harness MUST be sized with this, never with a ## loop in the bot and in the harness MUST be sized with this, never with a
@@ -64,8 +62,10 @@ const
## (`guns/lead_gain.nim`). It USED to be called BITBRAIN; the name now ## (`guns/lead_gain.nim`). It USED to be called BITBRAIN; the name now
## describes its internals (it learns a multiplier for Pattern's lead per ## describes its internals (it learns a multiplier for Pattern's lead per
## range band). Its rack id is UNCHANGED so every test that asserts the id ## range band). Its rack id is UNCHANGED so every test that asserts the id
## literals still holds. The `BITBRAIN` name and the `TR_BITBRAIN_*` prefix ## literals still holds. It OWNS the `BITBRAIN` name and the `TR_BITBRAIN_*`
## now belong to the ADE+SBC gun `guns/bitbrain_net.nim` (rack id 17). ## env prefix unconditionally: the ADE+SBC gun that briefly took them (rack
## id 17, `guns/bitbrain_net.nim`) was RETIRED and removed — see
## `docs/bitbrain_campaign.md` §RETIRED.
## SHIPPED DEFAULT: `onlyPattern`. Pattern (id 5) is admitted in both racks; ## SHIPPED DEFAULT: `onlyPattern`. Pattern (id 5) is admitted in both racks;
## every other gun is `off`. The selection mechanism is untouched and remains ## every other gun is `off`. The selection mechanism is untouched and remains
## fully functional — only the rack's membership changed. Re-enable any gun ## fully functional — only the rack's membership changed. Re-enable any gun
@@ -74,7 +74,7 @@ const
## it never spawns a virtual bullet unless explicitly enabled, so the shared ## it never spawns a virtual bullet unless explicitly enabled, so the shared
## VirtualTracker ring head — and every other gun's learning order — is ## VirtualTracker ring head — and every other gun's learning order — is
## unchanged. ## unchanged.
DefaultRackMembership*: array[18, RackMembership] = [ DefaultRackMembership*: array[17, RackMembership] = [
rmOff, # 0 HEADON — off (measured: worst over-selected gun) rmOff, # 0 HEADON — off (measured: worst over-selected gun)
rmOff, # 1 LINEAR — off rmOff, # 1 LINEAR — off
rmOff, # 2 TSETLIN — off rmOff, # 2 TSETLIN — off
@@ -91,41 +91,23 @@ const
rmOff, # 13 TMSELECT — off rmOff, # 13 TMSELECT — off
rmOff, # 14 TMPATTERN — off (already shipped off; TM pattern gun) rmOff, # 14 TMPATTERN — off (already shipped off; TM pattern gun)
rmOff, # 15 TMHORIZON — off (horizon-based TM corrector; expected to lose) rmOff, # 15 TMHORIZON — off (horizon-based TM corrector; expected to lose)
rmOff, # 16 LEADGAIN — off (per-range-band lead-gain corrector) rmOff] # 16 LEADGAIN — off (per-range-band lead-gain corrector)
rmOff] # 17 BITBRAIN — off (the real ADE+SBC gun; also needs TR_BITBRAIN_NET=1)
## NOTE: the table is registered in the SAME commit as the gun id and the live ## NOTE: the table is registered in the SAME commit as the gun id and the live
## wiring, so `TR_RACK_LEADGAIN=both` / `TR_RACK_BITBRAIN=both` are the ONLY ## wiring, so `TR_RACK_LEADGAIN=both` (or the legacy `TR_RACK_BITBRAIN=both`)
## things that admit those guns and an unset environment is byte-for-byte the ## are the ONLY things that admit that gun, and an unset environment is
## shipped Pattern-only rack. ## byte-for-byte the shipped Pattern-only rack.
const const
## ── BACKWARD COMPATIBILITY: the legacy rack knob names ──────────────────── ## ── BACKWARD COMPATIBILITY: the legacy rack knob names ────────────────────
## `TR_RACK_<gun name>` is derived from `RackGunNames`, so renaming a gun ## `TR_RACK_<gun name>` is derived from `RackGunNames`, so renaming a gun
## silently retires its old switch. These entries keep an old switch alive: ## silently retires its old switch. These entries keep an old switch alive:
## each maps a legacy `TR_RACK_*` name onto the gun id it used to address. ## each maps a legacy `TR_RACK_*` name onto the gun id it used to address.
## `TR_RACK_BITBRAIN` is the ONE genuinely ambiguous legacy name — the rack is ## `TR_RACK_BITBRAIN` is the old name of the gun that is now called
## keyed by gun name, and the new ADE+SBC gun is now the one called ## `LEADGAIN` (id 16) — and with the ADE+SBC gun (old id 17) retired, nothing
## `BITBRAIN`. It is disambiguated by the same switch the knobs use, ## else can claim it, so the mapping is UNCONDITIONAL. There is no namespace
## `TR_BITBRAIN_NET` (default 0): ## switch any more: `TR_RACK_BITBRAIN` always selects LEADGAIN.
## * unset/0 -> LEGACY: `TR_RACK_BITBRAIN` selects LEADGAIN (id 16), the gun
## it always selected, and the new gun stays off (the shipped default);
## * 1 -> `TR_RACK_BITBRAIN` selects the new BITBRAIN gun (id 17).
## (The new gun additionally requires `TR_BITBRAIN_NET=1` in its own
## `predict`, so even an explicitly racked `TR_RACK_BITBRAIN=both` cannot turn
## it on while the namespace is still in legacy mode.)
RackLegacyAlias*: array[1, (string, int)] = [("TR_RACK_BITBRAIN", 16)] RackLegacyAlias*: array[1, (string, int)] = [("TR_RACK_BITBRAIN", 16)]
RackLegacyAliasGunName* = "LEADGAIN" ## what the legacy name selects today RackLegacyAliasGunName* = "LEADGAIN" ## what the legacy name selects
RackNetGunId* = 17 ## the ADE+SBC gun that owns the name once switched
proc netSwitchOwnsBitbrainName*(): bool =
## `TR_BITBRAIN_NET` (default 0) is THE disambiguation switch for the whole
## `TR_BITBRAIN_*` namespace. Unset/0 => the namespace is LEGACY and belongs
## to the renamed lead-gain corrector; 1 => it belongs to the ADE+SBC gun.
## Defined here (and identically in `guns/lead_gain.nim`) because
## `gun_harness/selector` must not depend on a concrete gun module.
case getEnv("TR_BITBRAIN_NET", "").strip().toLowerAscii()
of "1", "true", "yes", "on": true
else: false
proc parseRackMembership*(value: string): RackMembership = proc parseRackMembership*(value: string): RackMembership =
## Parse a `TR_RACK_<GUN>` value. Empty / unknown values fall back to the ## Parse a `TR_RACK_<GUN>` value. Empty / unknown values fall back to the
@@ -148,28 +130,21 @@ proc loadRackMembership*(): array[len(RackGunNames), RackMembership] =
## corresponding CURRENT name is unset, so a migrated config always wins. ## corresponding CURRENT name is unset, so a migrated config always wins.
result = DefaultRackMembership result = DefaultRackMembership
var legacyTouched: seq[string] var legacyTouched: seq[string]
let netOwns = netSwitchOwnsBitbrainName()
for i in 0..<len(RackGunNames): for i in 0..<len(RackGunNames):
let key = RackEnvPrefix & RackGunNames[i] let key = RackEnvPrefix & RackGunNames[i]
# While the namespace is LEGACY, `TR_RACK_BITBRAIN` must address ONLY the
# gun it always addressed (LEADGAIN, id 16) — never id 17, or a pre-rename
# `.env` would silently admit a gun that did not exist then.
if not netOwns and i == RackNetGunId: continue
let v = getEnv(key, "") let v = getEnv(key, "")
if v.len > 0: if v.len > 0:
result[i] = parseRackMembership(v) result[i] = parseRackMembership(v)
if not netOwns: for (key, gid) in RackLegacyAlias:
for (key, gid) in RackLegacyAlias: let current = RackEnvPrefix & RackGunNames[gid]
let current = RackEnvPrefix & RackGunNames[gid] if getEnv(key, "").len > 0 and getEnv(current, "").len == 0:
if getEnv(key, "").len > 0 and getEnv(current, "").len == 0: result[gid] = parseRackMembership(getEnv(key, ""))
result[gid] = parseRackMembership(getEnv(key, "")) legacyTouched.add key
legacyTouched.add key
if legacyTouched.len > 0: if legacyTouched.len > 0:
let gid = RackLegacyAlias[0][1] let gid = RackLegacyAlias[0][1]
stderr.writeLine("[depr] legacy rack knob " & legacyTouched.join(",") & stderr.writeLine("[depr] legacy rack knob " & legacyTouched.join(",") &
" now names the ADE+SBC gun (BITBRAIN, rack id 17); it still selects " & " is the old name of TR_RACK_" & RackGunNames[gid] & " (rack id " & $gid &
RackGunNames[gid] & " (rack id " & $gid & "); it still selects it. Set TR_RACK_" & RackGunNames[gid] &
") until TR_BITBRAIN_NET=1. Set TR_RACK_" & RackGunNames[gid] &
" to make it explicit.") " to make it explicit.")
let ActiveRackMembership* = loadRackMembership() let ActiveRackMembership* = loadRackMembership()
@@ -213,8 +188,8 @@ proc rackActive*(membership: openArray[RackMembership], mode: RackMode): string
# a POLICY: when set, the live selection is a deterministic deficit-round-robin # a POLICY: when set, the live selection is a deterministic deficit-round-robin
# over the named, ADMITTED guns instead of `chooseFromFit`'s ranking. # over the named, ADMITTED guns instead of `chooseFromFit`'s ranking.
# #
# TR_RACK_SHARE=pattern:50,bitbrain:50 # TR_RACK_SHARE=pattern:50,leadgain:50
# TR_RACK_SHARE=pattern:0.7,bitbrain:0.3 # TR_RACK_SHARE=pattern:0.7,leadgain:0.3
# #
# Values are RELATIVE weights (fractions or percentages — only the ratio # Values are RELATIVE weights (fractions or percentages — only the ratio
# matters) and names are case-insensitive `RackGunNames` (the `GunNames` rack # matters) and names are case-insensitive `RackGunNames` (the `GunNames` rack
-193
View File
@@ -1,193 +0,0 @@
# bitbrain_net — quick recap (inputs / outputs)
Recap card. Everything below is read off `common_libs/guns/bitbrain_net.nim`.
Companion to the library's own README (`common_libs/bitbrain/README.md`) and to
`common_libs/guns/lead_gain.README.md` (the *other* gun, the per-range-band
lead-gain corrector at rack id 16).
## What it is
The **real ADE+SBC gun**: an ADE layer (thresholded random projections with
**online threshold adaptation**) feeding the SBC head from
`common_libs/bitbrain/`, with the **counted + decay** mode available (the mode
that delivers forgetting and true per-class probabilities —
`docs/bitbrain_counted_sbc.md`).
It took the `BITBRAIN` rack name (id **17**) and the `TR_BITBRAIN_*` knob prefix
that the renamed corrector gave up. **Default OFF.** It needs
`TR_RACK_BITBRAIN=both` in the rack table **and** `TR_BITBRAIN_NET=1` — the one
switch that decides whether the `TR_BITBRAIN_*` namespace is legacy
(`LEADGAIN`'s) or the new gun's, and the new gun's master on/off at the same
time. The two are gated by the SAME switch, so no env configuration can admit
the gun while leaving it disabled
(`test_bitbrain_net.nim` pins that over a 6-setting truth table) — which
matters, because a pre-rename `.env` still carries `TR_RACK_BITBRAIN=both` and a
disabled gun's placeholder predictions in the shared `VirtualTracker` ring would
shift every other gun's learning order. The network is built lazily, so an unset
environment never allocates a byte.
## OUTPUT SHAPE — a fine-grained correction ON TOP of Pattern
**Not** a direct aim point from the argmax class. The class-resolved angular
correction added to Pattern's bearing is the shape `docs/bitbrain_gate.md`
actually measured, and Pattern is already a strong predictor, so the net's job
is the small signed *residual*, not the whole aim.
| Output | Formula / meaning |
|---|---|
| Correction | `shift = Σ_k P(k)·centre_k / Σ_k P(k)` — the **probability-weighted mean of the `nClasses` class centres** under `inferProb`, in degrees, over ±`TR_BITBRAIN_SPAN` |
| Aim point | `tmhApplyShift(self, Pattern prediction, shift)`. Below `TR_BITBRAIN_MINOBS` resolved samples, or when the posterior has no mass, `shift == 0.0` and **Pattern's prediction is returned unchanged** |
| `[bbn]` log (only `TR_BITBRAIN_NETLOG=1`, change-gated) | `t`, `shift` (deg), `class` (argmax), `in`, `ncl`, `nAde`, `mode`, `trained`, `adapts`, `pend`, `dropped` |
| Does **NOT** output | an aim point of its own; it never discards Pattern |
## INPUT — a CONFIGURED SET OF FEATURE BLOCKS
The input vector is a concatenation of feature blocks, each independently
selectable (`TR_BITBRAIN_FEATURES`) and with a settable width. A block is a list
of scalar quantities; a block of width `W` lays each quantity out as a `W`-slot
thermometer code, so **width == resolution**. Slots are `0`/`255` uint8, which the
ADE scorer centres at 127 (`DefaultCenter`): a synapse *matches* when its
polarity agrees with the slot, so a random ADE fires iff its `w` synapses all
match — a thresholded random projection with firing rate `2^-w`, which online
homeostasis then drives toward `TR_BITBRAIN_TARGET`.
**The default block set is 52 slots:**
| block | quantities | width | slots | quantity |
|---|---:|---:|---:|---|
| `epos` | 2 | 4 | 8 | enemy offset from us, x and y, over the arena span |
| `evel` | 2 | 3 | 6 | enemy speed; enemy heading minus the bearing to us |
| `eturn` | 2 | 2 | 4 | turn direction this tick; turn consistency over the ring |
| `eself` | 2 | 2 | 4 | our speed; our heading minus the bearing to the enemy |
| `dist` | 2 | 5 | 10 | range; signed range rate over the last 10 ticks |
| `bear` | 1 | 4 | 4 | relative bearing (enemy bearing minus our heading) |
| `walls` | 4 | 2 | 8 | distance to each of the four arena walls |
| `bull` | 2 | 2 | 4 | live-bullet count; nearest bullet's signed lateral offset |
| `hzn` | 1 | 4 | 4 | bullet flight time to the current range |
`docs/state_window_gate.md` measured that a long **temporal window** of states
destroys recurrence, so there is deliberately **no window block**: the only
history-derived inputs are the three rate/turn quantities above (a 12-tick
ring), i.e. the same causal information Pattern itself uses.
## KNOB TABLE
| Env | Default | Meaning |
|---|---|---|
| `TR_RACK_BITBRAIN` | `off` | rack admission for id 17 (the *current* name of the rack key) |
| `TR_BITBRAIN_NET` | `0` | **master switch + namespace disambiguator.** `0` = the gun is off AND the `TR_BITBRAIN_*` names are LEGACY aliases of `LEADGAIN` (and `TR_RACK_BITBRAIN` selects id 16); `1` = the gun is on and the names below are this gun's (and `TR_RACK_BITBRAIN` selects id 17) |
| `TR_BITBRAIN_INPUT` | 52 | total input slots; pads or truncates the block layout so the ADE codes can never index out of range |
| `TR_BITBRAIN_FEATURES` | all blocks at their shipped width | `name:W` list, comma separated. `name:0` switches a block OFF; an unlisted block keeps its shipped width; an unknown name warns and is ignored |
| `TR_BITBRAIN_NCLASSES` | 8 | output resolution |
| `TR_BITBRAIN_NADES` | 256 | ADEs per address decoder (RAM is **quadratic** in this) |
| `TR_BITBRAIN_WIDTHS` | `4,5,6` | ADE clause widths; one AD per width, one cross-AD SBC per pair (3 widths → 3 SBCs) |
| `TR_BITBRAIN_SPAN` | `40.0` | class half-range, degrees |
| `TR_BITBRAIN_MODE` | `counted` | `bitset` \| `counted` (saturating counters + decay) |
| `TR_BITBRAIN_DECAY_EVERY` | 64 | counted mode: learns between global decay passes |
| `TR_BITBRAIN_DECAY_SHIFT` | 3 | counted mode: `c -= c shr shift` per pass (`0` disables) |
| `TR_BITBRAIN_MINOBS` | 32 | resolved samples before the correction is applied at all |
| `TR_BITBRAIN_ADAPT_EVERY` | 200 | inputs between ADE threshold-adaptation passes |
| `TR_BITBRAIN_TARGET` | 0.01 | the paper's target ADE firing rate |
| `TR_BITBRAIN_NETSEED` | 20240921 | network seed (a **private** RNG, never the global one) |
| `TR_BITBRAIN_NETLOG` | `0` | `1` = emit the `[bbn]` line |
| `TR_BITBRAIN_CALIB_EVERY` | — | compat alias for `TR_BITBRAIN_ADAPT_EVERY` |
| `TR_BITBRAIN_NET_RESET_ON_TARGET` | `1` | wipe the SBC counters when the enemy id changes |
## HOW TO TURN IT ON
```
TR_BITBRAIN_NET=1 # the master switch — the legacy aliases go quiet
TR_RACK_BITBRAIN=both
TR_RACK_PATTERN=off
TR_BITBRAIN_NETLOG=1
```
## MEASURED SCALING (RAM / ms-per-tick / quality)
`common_libs/tests/measure_bitbrain_scaling.nim`, replaying 3 recorded live runs
(37 412 recorded ticks, 149 650 tick × power-bin samples), `-d:release`,
single-threaded, one `predict` per power bin per tick — the same call pattern
as the live loop. The timed region contains **only** the gun's `predict` calls:
the interception solve is done once, up front, so the ruler's own cost cannot
contaminate the timing.
| arm | in | nCl | nAde | mode | RAM B | SBC B | AD B | ms/tick | % of 13.16 | mean\|err\|° | hit% | Pattern\|err\|° |
|---|---:|---:|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|
| input-small | 26 | 8 | 256 | counted | 1 594 368 | 1 572 864 | 21 504 | 2.60 | 20 % | 17.329 | 8.00 | 16.964 |
| input-medium | 52 | 8 | 256 | counted | 1 594 368 | 1 572 864 | 21 504 | 2.70 | 21 % | 17.270 | 8.04 | 16.964 |
| input-large | 84 | 8 | 256 | counted | 1 594 368 | 1 572 864 | 21 504 | 2.54 | 19 % | 17.259 | 8.06 | 16.964 |
| classes-small | 52 | 2 | 256 | counted | 414 720 | 393 216 | 21 504 | 1.07 | 8 % | 17.201 | 8.12 | 16.964 |
| classes-medium| 52 | 8 | 256 | counted | 1 594 368 | 1 572 864 | 21 504 | 2.70 | 21 % | 17.270 | 8.04 | 16.964 |
| classes-large | 52 | 64 | 256 | counted |12 604 416 |12 582 912 | 21 504 | 19.56 | 149 % | 17.115 | 8.27 | 16.964 |
| nAde-small | 52 | 8 | 64 | counted | 103 680 | 98 304 | 5 376 | 0.30 | 2 % | 17.254 | 8.08 | 16.964 |
| nAde-medium | 52 | 8 | 256 | counted | 1 594 368 | 1 572 864 | 21 504 | 2.72 | 21 % | 17.270 | 8.04 | 16.964 |
| nAde-large | 52 | 8 | 512 | counted | 6 334 464 | 6 291 456 | 43 008 | 9.76 | 74 % | 17.264 | 8.03 | 16.964 |
| mode-bitset | 52 | 8 | 256 | bitset | 218 112 | 196 608 | 21 504 | 2.72 | 21 % | 16.975 | 8.53 | 16.964 |
| mode-counted | 52 | 8 | 256 | counted | 1 594 368 | 1 572 864 | 21 504 | 2.71 | 21 % | 17.270 | 8.04 | 16.964 |
Reproduce:
```bash
nim c -r -d:release --path:common_libs common_libs/tests/measure_bitbrain_scaling.nim --limit 3
```
### The scaling laws, as MEASURED
* **RAM is dominated by the SBC tensors (98.6 % at the default).** One SBC is
`nAde² × nClasses` cells. Bitset: 1 bit/cell. Counted: 1 byte/cell. The
measured counted/bitset factor is **7.30×** on the default geometry
(1 594 368 / 218 112), which is the bitset tensor's 1/8-of-a-`uint32`-slot
overhead — the tensors are `uint32`-slot-packed, so 8 classes share slots.
* **RAM vs `nClasses`: exactly linear.** 2 → 8 → 64 classes: 0.41 → 1.59 → 12.60 MB
(4× and 32× for 4× and 8× the classes). RAM vs `nAde`: **quadratic** —
64 → 256 → 512 gives 0.10 → 1.59 → 6.33 MB (4× then 16× for 4× then 2×).
* **RAM vs input width: FLAT.** 26 → 52 → 84 slots: 1 594 368 B in all three
cases. The input width only sets how many of the `2^inputWidth` synapses the
ADE codes may draw from; the codes are still `nAde × w` per AD. The only
width-dependent term is the ADE scoring cost and the tiny `AD B` column
(unchanged here because it is dominated by `nAde × w`, not by input width).
* **ms/tick vs input width: flat** (2.60 / 2.70 / 2.54 ms). vs `nClasses`:
**linear** (1.07 / 2.70 / 19.56 ms — the 64-class arm busts the 13.16 ms
budget at 149 %). vs `nAde`: **quadratic-ish** (0.30 / 2.72 / 9.76 ms), because
the ADE pass is `O(nAde × w)` and the SBC read is `O(|row| × |col|)`.
* **counted vs bitset: same time, 7.3× the RAM** (2.71 vs 2.72 ms/tick).
### Capacity vs accuracy — the direct answer
**More capacity buys essentially nothing here; it costs RAM and, past a point,
the tick budget.** Across a 100× range of RAM (0.10 MB → 12.60 MB) the offline
mean |angular error| moves from 17.254° to 17.115° — a 0.14° spread around
Pattern's own 16.964°, and the sign of the effect flips across the `nClasses`
axis (17.201° at 2 classes, 17.115° at 64), so it is not a trend, it is noise.
The only arm that *helps* is `mode-bitset` (16.975° vs 17.270° counted), and
even that costs nothing in accuracy terms until you switch to counted for
forgetting — at which point you pay 7.3× the RAM and lose the 0.3°.
The honest reading is that the information ceiling here is the **state**, not
the classifier: `docs/bitbrain_gate.md` measured ~1 bit of information in a
53-bit input, and this sweep reproduces that at 26, 52 and 84 slots alike. The
corrector is consistently **slightly WORSE than Pattern** offline (17.2° vs
17.0°), which is the same verdict the campaign already reached for every
additive-shift design. Capacity is not the binding constraint and buying more of
it is not the fix.
**VETO-CAPABLE CHECK ONLY.** Per `docs/offline_harness_trust.md` this is the
single trustworthy use of the offline harness — per-gun single-tick prediction
quality on a fixed trajectory. It is **not** a live result: nothing here says
anything about damage, survival or round wins, and it is never presented as one.
## Tests
```bash
# 44 checks: the gun really engages the network (not just that it links)
nim c -r -d:release --path:common_libs common_libs/tests/test_bitbrain_net.nim
# the library itself (56 checks, unchanged)
nim c -r -d:release --path:common_libs common_libs/tests/test_bitbrain.nim
```
`test_bitbrain_net.nim` proves engagement by observation, not by linkage: the
network is 0 bytes before first use, a **trained** head returns different
classes for different states and separates two taught populations, a learn
visibly raises SBC occupancy, a bitset learn is idempotent while a counted
learn's evidence is monotone, ADE threshold adaptation runs, no env setting can
admit a disabled gun, and construction + first predict leave the global RNG
untouched.
-679
View File
@@ -1,679 +0,0 @@
## bitbrain_net.nim — BITBRAIN (rack id 17): the REAL ADE+SBC gun.
##
## The gun at rack id 16 used to be called `BITBRAIN` while containing no network
## at all; it is now `LEADGAIN` (`guns/lead_gain.nim`, a per-range-band lead-gain
## corrector). This module 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 — the mode that delivers forgetting and true per-class
## probabilities (`docs/bitbrain_counted_sbc.md`).
##
## ── OUTPUT SHAPE: a fine-grained aim correction ON TOP OF Pattern ────────────
## Not a direct aim point from the argmax class. Two reasons:
## 1. the gate test that motivated this (`docs/bitbrain_gate.md`) measured
## exactly this shape — a class-resolved correction added to Pattern's
## bearing — so this is the shape with a published measurement behind it;
## 2. Pattern's prediction is already a strong, fully-learned predictor. The
## network's job is the RESIDUAL angular error, which is a small, signed,
## zero-centred quantity; replacing the aim point outright would throw away
## Pattern entirely on the (measured) bet that the net beats it.
## The correction is the PROBABILITY-WEIGHTED MEAN of the class centres under
## `inferProb`, i.e. a continuous readout over `nClasses` discrete cells rather
## than a step function of the argmax. Below `TR_BITBRAIN_MINOBS` resolved
## samples — or when the readout has no evidence — the correction is exactly 0
## and the gun returns Pattern's prediction unchanged.
##
## ── THE INPUT IS A CONFIGURED SET OF FEATURE BLOCKS ─────────────────────────
## The input vector is a concatenation of FEATURE BLOCKS, each independently
## selectable (`TR_BITBRAIN_FEATURES`) and with a settable width. A block is a
## list of scalar QUANTITIES; a block of width `W` lays each quantity out as a
## thermometer (unary) code of `W` slots, so width == resolution: a wider block
## distinguishes more states of that quantity. Slots are 0/255 uint8, which the
## ADE scorer centres at 127 (`DefaultCenter`), so a synapse "matches" when its
## polarity agrees with the slot and a random ADE fires iff its `w` synapses all
## match — a clean thresholded random projection with firing rate 2^-w.
##
## The DEFAULT block set (52 slots) and what each slot means:
##
## block quant. slots quantity
## epos 2 8 enemy offset from us, x and y, over the arena span
## evel 2 6 enemy speed; enemy heading minus the bearing to us
## eturn 2 4 turn direction this tick; turn consistency over 10
## eself 2 4 our speed; our heading minus the bearing to the enemy
## dist 2 10 range; range rate over the last 10 ticks
## bear 1 4 relative bearing (enemy bearing minus our heading)
## walls 4 8 distance to each of the four arena walls
## bull 2 4 live bullet count; nearest bullet's signed lateral offset
## hzn 1 4 bullet flight time to the current range, h = dist / speed
##
## `docs/state_window_gate.md` measured that a long TEMPORAL WINDOW of states
## destroys recurrence, so there is deliberately NO window block here: the only
## history-derived inputs are the 3 rate/turn quantities above (10 ticks), which
## is the same causal information Pattern itself uses.
##
## ── DEFAULT OFF / PARITY ────────────────────────────────────────────────────
## Admitted ONLY when `TR_RACK_BITBRAIN=both` AND `TR_BITBRAIN_NET=1`. Both
## default off, so the shipped rack never calls `predict`, the network is never
## built (`ensureInit` is lazy), and the shipped bot is byte-for-byte unchanged.
## The default target RNG is a PRIVATE `initRand(seed)`, so construction cannot
## perturb the global selector RNG either.
import std/[math, os, strutils, strformat, algorithm, random]
import gun_harness/gun_interface
import guns/tm_horizon
import guns/pattern_matcher
import bitbrain/bitbrain
export ade, sbc
const
## ── env knobs (all resolved once at gun construction) ─────────────────────
BBN_INPUT_ENV* = "TR_BITBRAIN_INPUT" ## total input slots
BBN_CLASSES_ENV* = "TR_BITBRAIN_NCLASSES" ## output resolution
BBN_NADES_ENV* = "TR_BITBRAIN_NADES" ## ADEs per AD
BBN_WIDTHS_ENV* = "TR_BITBRAIN_WIDTHS" ## clause widths, one AD per width
BBN_FEATURES_ENV* = "TR_BITBRAIN_FEATURES" ## block:name:width,...
BBN_SPAN_ENV* = "TR_BITBRAIN_SPAN" ## class half-range, degrees
BBN_MINOBS_ENV* = "TR_BITBRAIN_MINOBS" ## resolved samples before trusting
BBN_LOG_ENV* = "TR_BITBRAIN_NETLOG" ## 1 = per-change [bbn] log
BBN_ADAPT_ENV* = "TR_BITBRAIN_ADAPT_EVERY"## ADE threshold adaptation interval
BBN_CALIB_ENV* = "TR_BITBRAIN_CALIB_EVERY"## (compat alias; same interval)
BBN_SEED_ENV* = "TR_BITBRAIN_NETSEED" ## network seed
BBN_TARGET_ENV* = "TR_BITBRAIN_TARGET" ## ADE target firing rate
BBN_RESET_ON_TARGET_ENV* = "TR_BITBRAIN_NET_RESET_ON_TARGET"
BBN_NET_ENV* = "TR_BITBRAIN_NET" ## master switch (legacy disambiguator)
## The library's own knobs (read inside `common_libs/bitbrain`), re-declared
## here so `knownEnvNames()` and the boot report cover the whole gun.
BBN_MODE_ENV* = "TR_BITBRAIN_MODE"
BBN_DECAY_EVERY_ENV* = "TR_BITBRAIN_DECAY_EVERY"
BBN_DECAY_SHIFT_ENV* = "TR_BITBRAIN_DECAY_SHIFT"
## Every env name this gun reads, for the tree-scan guard's known set.
BitbrainNetEnvNames* = [
BBN_INPUT_ENV, BBN_CLASSES_ENV, BBN_NADES_ENV, BBN_WIDTHS_ENV,
BBN_FEATURES_ENV, BBN_SPAN_ENV, BBN_MINOBS_ENV, BBN_LOG_ENV,
BBN_ADAPT_ENV, BBN_CALIB_ENV, BBN_SEED_ENV, BBN_TARGET_ENV,
BBN_RESET_ON_TARGET_ENV, BBN_MODE_ENV, BBN_DECAY_EVERY_ENV,
BBN_DECAY_SHIFT_ENV]
## (`BBN_NET_ENV` == `TR_BITBRAIN_NET` is deliberately absent: it is already
## registered as `LG_NET_SWITCH_ENV`, the one switch both guns share.)
## ── feature blocks ────────────────────────────────────────────────────────
BB_BLOCKS* = [
("epos", 2, 4), ("evel", 2, 3), ("eturn", 2, 2), ("eself", 2, 2),
("dist", 2, 5), ("bear", 1, 4), ("walls", 4, 2), ("bull", 2, 2),
("hzn", 1, 4)]
## Their shipped widths sum to BB_DEFAULT_SLOTS (8+6+4+4+10+4+8+4+4 = 52).
BB_DEFAULT_SLOTS* = 52
BB_MAX_SLOTS* = 4096
## ── defaults ──────────────────────────────────────────────────────────────
BBN_NCLASSES_DEF = 8 ## output resolution
BBN_NADES_DEF = 256 ## ADEs per AD
BBN_WIDTHS_DEF* = @[4, 5, 6] ## clause widths: one AD per width, 3 cross SBCs
BBN_SPAN_DEF = 40.0
BBN_MINOBS_DEF = 32 ## resolved samples before the net is trusted
BBN_ADAPT_DEF = 200 ## inputs between threshold-adaptation passes
BBN_TARGET_DEF = 0.01 ## paper's target ADE firing rate
BBN_SEED_DEF = 20240921
BBN_PENDING_CAP* = 512
BBN_HIST* = 12 ## the short causal history ring (see the note)
BBN_SBC_VALUE* = 255'u8 ## a "set" thermometer slot
BBN_SLOT_CENTRE = 127 ## DefaultCenter; a synapse matches when 0 or 255
type
Block = tuple[name: string, nQuant: int, width: int]
Pending = object
fireTick: int
horizon: int
selfX, selfY: float
baseBearing: float
input: seq[uint8]
klass: int
HistSample = object
x, y, heading: float
BitbrainNetGun* = object
tmh: TmHorizonGun
initialized: bool
## ── resolved config (boot report) ────────────────────────────────────────
enabled*: bool
inputWidth*: int
nClasses*: int
nAde*: int
widths*: seq[int]
blockWidths*: seq[int] ## per BB_BLOCKS entry; 0 = block disabled
features*: string ## the resolved block spec, for the boot report
maxDeg*: float
minObs*: int
adaptEvery*: int
targetRate*: float
seed*: int64
logEnabled*: bool
resetOnTarget*: bool
mode*: SbcMode
decayEvery*: int
decayShift*: int
## ── the network ─────────────────────────────────────────────────────────
net*: BitBrain
built: bool
## ── learner state ───────────────────────────────────────────────────────
trained*: int
sinceAdapt: int
adapts*: int
sinceEnqTick: int
sinceEnqBucket: int
pending: array[BBN_PENDING_CAP, Pending]
pendingCount*: int
pendingDropped*: int
observedTargetId*: int
lastTick: int
## ── readout ─────────────────────────────────────────────────────────────
lastShiftDeg*: float
lastClass*: int
corrections*: int
lastLogKey: string
## ── the short causal history the rate/turn blocks need ─────────────────
## DELIBERATELY SHORT (12 ticks, and only 3 derived quantities use it):
## `docs/state_window_gate.md` measured that feeding a long TEMPORAL WINDOW
## of states destroys the recurrence this network depends on. This is not
## a window block — it is the same 10-tick information Pattern uses.
hist: seq[HistSample]
# ── config helpers ───────────────────────────────────────────────────────────
proc envStrBbn(name: string): string {.inline.} =
let v = getEnv(name, "")
if v.len > 0: v.strip() else: ""
proc envIntBbn(name: string, default: int): int =
let v = envStrBbn(name)
if v.len == 0: return default
try: parseInt(v) except ValueError: default
proc envFloatBbn(name: string, default: float): float =
let v = envStrBbn(name)
if v.len == 0: return default
try: parseFloat(v) except ValueError: default
proc envBoolBbn(name: string, default: bool): bool =
case envStrBbn(name).toLowerAscii()
of "1", "true", "yes", "on": true
of "0", "false", "no", "off": false
else: default
proc netOn*(): bool =
## The master switch. Default OFF: this gun is never admitted by an unset
## environment, and while it is off the `TR_BITBRAIN_*` names are the LEGACY
## aliases of the LEADGAIN corrector (`guns/lead_gain.nim`).
envBoolBbn(BBN_NET_ENV, false)
proc blockNames*(): string =
## The known block names, comma separated (used in the unknown-block warning).
for i in 0 ..< BB_BLOCKS.len:
if i > 0: result.add ","
result.add BB_BLOCKS[i][0]
proc parseBlockWidths*(value: string): seq[int] =
## Parse `TR_BITBRAIN_FEATURES` — a comma-separated `name[:W]` list. A block
## that is NOT listed keeps its shipped width; an explicitly listed block may
## be switched off with width 0. Unknown names are ignored (with a stderr
## warning) so a typo cannot silently change the input size. Unset/empty ->
## the shipped widths, byte-identical.
result = newSeq[int](BB_BLOCKS.len)
for i, b in BB_BLOCKS: result[i] = b[2]
if value.len == 0: return
for part in value.split(','):
let p = part.strip()
if p.len == 0: continue
let ci = p.find(':')
let nm = (if ci < 0: p else: p[0..<ci]).strip().toLowerAscii()
var w = -1
if ci >= 0:
try: w = parseInt(p[ci+1..^1].strip())
except ValueError: w = -1
var found = false
for i, b in BB_BLOCKS:
if b[0] == nm:
found = true
result[i] = (if w < 0: b[2] else: w)
if not found:
stderr.writeLine("[bbn] unknown feature block '" & nm & "' in " &
BBN_FEATURES_ENV & "; ignored (known: " &
blockNames() & ")")
for w in result.mitems: w = max(0, min(w, 64))
proc parseWidths*(value: string): seq[int] =
## Parse `TR_BITBRAIN_WIDTHS` — the ADE clause widths, one AD per width, one
## cross-AD SBC per unordered pair. Unset -> `BBN_WIDTHS_DEF`.
if value.len == 0: return BBN_WIDTHS_DEF
for tok in value.split(','):
let t = tok.strip()
if t.len == 0: continue
var w: int
try: w = parseInt(t) except ValueError: continue
if w >= 1 and w <= 64: result.add w
if result.len == 0: return BBN_WIDTHS_DEF
proc blockWidthsString*(widths: seq[int]): string =
## The resolved block spec as the env's own form (boot report).
for i, w in widths:
if i > 0: result.add ","
if w == 0: result.add BB_BLOCKS[i][0] & ":0"
elif w == BB_BLOCKS[i][2]: result.add BB_BLOCKS[i][0]
else: result.add BB_BLOCKS[i][0] & ":" & $w
proc derivedSlots*(widths: seq[int]): int =
for i, b in BB_BLOCKS: result += b[1] * widths[i]
proc blockName*(i: int): string {.inline.} = BB_BLOCKS[i][0]
proc blockQuantities*(i: int): int {.inline.} = BB_BLOCKS[i][1]
# ── construction ─────────────────────────────────────────────────────────────
proc initBitbrainNetGun*(): BitbrainNetGun =
result.enabled = netOn()
result.blockWidths = parseBlockWidths(envStrBbn(BBN_FEATURES_ENV))
result.features = blockWidthsString(result.blockWidths)
let derived = derivedSlots(result.blockWidths)
result.inputWidth = clamp(envIntBbn(BBN_INPUT_ENV, derived), 1, BB_MAX_SLOTS)
result.nClasses = clamp(envIntBbn(BBN_CLASSES_ENV, BBN_NCLASSES_DEF), 2, 4096)
result.nAde = clamp(envIntBbn(BBN_NADES_ENV, BBN_NADES_DEF), 8, 8192)
result.widths = parseWidths(envStrBbn(BBN_WIDTHS_ENV))
result.maxDeg = clamp(envFloatBbn(BBN_SPAN_ENV, BBN_SPAN_DEF), 1.0, 180.0)
result.minObs = max(1, envIntBbn(BBN_MINOBS_ENV, BBN_MINOBS_DEF))
result.adaptEvery = max(1, envIntBbn(BBN_ADAPT_ENV, envIntBbn(BBN_CALIB_ENV, BBN_ADAPT_DEF)))
result.targetRate = clamp(envFloatBbn(BBN_TARGET_ENV, BBN_TARGET_DEF), 0.0001, 0.5)
result.seed = int64(envIntBbn(BBN_SEED_ENV, BBN_SEED_DEF))
result.logEnabled = envBoolBbn(BBN_LOG_ENV, false)
result.resetOnTarget = envBoolBbn(BBN_RESET_ON_TARGET_ENV, true)
# The SBC storage mode + its decay knobs are the LIBRARY's env names, read
# here so the gun's resolved config (and the boot report) is the real one.
result.mode = envSbcMode(smCounted)
result.decayEvery = envDecayEvery(64)
result.decayShift = envDecayShift(3)
result.lastTick = -1
result.sinceEnqTick = -1
result.sinceEnqBucket = -1
result.observedTargetId = -1
proc buildNet*(g: var BitbrainNetGun) =
## Build the ADE layers + SBC head. LAZY: the shipped rack never calls it.
## Deterministic given the seed, and it uses a PRIVATE `initRand`, so it can
## never perturb the global selector RNG.
if g.built: return
g.built = true
var rng = initRand(g.seed)
# The clause width must not exceed the input width or `initRandomAddressDecoder`
# cannot draw `w` distinct indices; clamp the widths to the input width.
var widths = g.widths
for i, w in widths: widths[i] = min(w, max(2, g.inputWidth))
g.net = buildRandomBitBrain(widths = widths, nAde = g.nAde,
inputWidth = g.inputWidth, nClasses = g.nClasses,
seed = g.seed, mode = g.mode,
decayEvery = g.decayEvery,
decayShift = g.decayShift)
# The initial threshold is 0, which for 0/255 slots centred at 127 fires every
# ADE whose `w` synapses all match — rate 2^-w. Homeostatic adaptation then
# drives each ADE toward the paper's ~1% target rate, online, unsupervised.
for ad in g.net.ades.mitems:
for i in 0 ..< ad.nAde: ad.thresholds[i] = int32(127 * ad.width - 1)
g.tmh = initTmHorizonGun()
proc ensureInit*(g: var BitbrainNetGun) =
if g.initialized: return
g.initialized = true
if not g.enabled: return
g.buildNet()
# ── the input vector ─────────────────────────────────────────────────────────
proc pushTherm(dst: var seq[uint8], v: float, w: int) =
## Lay one quantity out as a `w`-slot thermometer code over [0, 1]. `w` == 0
## disables the quantity.
if w <= 0: return
var level = int(clamp(v, 0.0, 0.999999) * float(w))
if level < 0: level = 0
if level > w - 1: level = w - 1
for j in 0 ..< w:
dst.add(if j < level: BBN_SBC_VALUE else: 0'u8)
proc wrapRadNet(r: float): float {.inline.} =
result = r
while result > PI: result -= 2.0 * PI
while result < -PI: result += 2.0 * PI
proc pushHist(g: var BitbrainNetGun, state: WorldState) =
## Ring of the last `BBN_HIST` states. One push per tick, in `predict`.
if g.hist.len == 0:
for _ in 0 ..< BBN_HIST:
g.hist.add HistSample(x: state.enemyX, y: state.enemyY,
heading: state.enemyHeading)
g.hist.insert(HistSample(x: state.enemyX, y: state.enemyY,
heading: state.enemyHeading), 0)
g.hist.setLen(BBN_HIST)
proc turnSignal(g: BitbrainNetGun, state: WorldState): (float, float) =
## (turn direction this tick, turn consistency over the ring). 0.5 = straight,
## 0/1 = a full left/right turn; consistency = fraction of the ring's
## consecutive steps that turn the SAME way.
if g.hist.len < 3:
return (0.5, 0.5)
let d0 = wrapRadNet(degToRad(state.enemyHeading - g.hist[0].heading))
let dir = 0.5 + 0.5 * (if d0 > 0.0: 1.0 elif d0 < 0.0: -1.0 else: 0.0)
var same = 0
var total = 0
for i in 0 ..< g.hist.len - 1:
let d = wrapRadNet(degToRad(g.hist[i].heading - g.hist[i+1].heading))
if abs(d) < 1e-9: continue
inc total
if (d > 0) == (d0 > 0.0): inc same
(dir, if total == 0: 0.5 else: float(same) / float(total))
proc rangeSignal(g: BitbrainNetGun, state: WorldState,
dist: float): float =
## Signed range rate over the ring, in [-1, 1] (approaching / opening), scaled
## to +-400 px per tick and clamped. One quantity, ten ticks of history.
if g.hist.len < 2: return 0.0
let dPrev = hypot(g.hist[0].x - state.selfX, g.hist[0].y - state.selfY)
let step = (dist - dPrev) / float(max(1, g.hist.len - 1))
clamp(step / 400.0, -1.0, 1.0)
proc bulletSignal(g: BitbrainNetGun, state: WorldState): (float, float) =
## (live-bullet count / 4, nearest bullet's signed lateral offset in [-1,1]).
## The nearest known bullet is the one whose own last-seen tick is the most
## recent; with no bullet knowledge both are neutral.
var best = -1
var bestAge = high(int)
for e in state.enemies:
if e.lastSeenTick < 0: continue
let age = state.tick - e.lastSeenTick
if age < bestAge: bestAge = age; best = e.id
if best < 0: return (0.0, 0.0)
for e in state.enemies:
if e.id != best: continue
let toE = arctan2(e.y - state.selfY, e.x - state.selfX)
let toB = arctan2(state.enemyY - state.selfY, state.enemyX - state.selfX)
let lat = wrapRadNet(toB - toE)
return (0.25, clamp(lat / 0.5, -1.0, 1.0))
(0.0, 0.0)
proc buildInput*(g: var BitbrainNetGun, state: WorldState): seq[uint8] =
## (see the header for the block table)
## The configured feature-block vector for this state. Block order is fixed
## (`BB_BLOCKS`); each block's slot count is `g.blockWidths[i]`. The result is
## padded with zero slots or truncated to exactly `g.inputWidth`, so the
## ADE codes (drawn once at build time over `inputWidth`) can never index out
## of range no matter how the two knobs are combined.
result = newSeqOfCap[uint8](g.inputWidth)
let bw = g.blockWidths
let ex = state.enemyX
let ey = state.enemyY
let sx = state.selfX
let sy = state.selfY
let wAll = max(1.0, state.arenaWidth)
let hAll = max(1.0, state.arenaHeight)
let dx = ex - sx
let dy = ey - sy
let dist = max(1e-6, hypot(dx, dy))
let bearing = arctan2(dy, dx)
let selfHdg = degToRad(state.selfHeading)
let enemyHdg = degToRad(state.enemyHeading)
for bi in 0 ..< BB_BLOCKS.len:
let w = bw[bi]
if w <= 0: continue
case bi
of 0: # epos — enemy offset over the arena
pushTherm(result, 0.5 + dx / wAll, w div 2)
pushTherm(result, 0.5 + dy / hAll, w - w div 2)
of 1: # evel — speed, heading vs the lane
pushTherm(result, state.enemySpeed / 16.0, w div 2)
pushTherm(result, 0.5 + 0.5 * sin(enemyHdg - bearing), w - w div 2)
of 2: # eturn — turn now, consistency over 10
let (dir, cons) = turnSignal(g, state)
pushTherm(result, dir, w div 2)
pushTherm(result, cons, w - w div 2)
of 3: # eself — our speed, our heading error
pushTherm(result, state.selfSpeed / 16.0, w div 2)
pushTherm(result, 0.5 + 0.5 * sin(selfHdg - bearing), w - w div 2)
of 4: # dist — range, range rate over 10
pushTherm(result, dist / 1000.0, w div 2)
pushTherm(result, 0.5 + 0.5 * rangeSignal(g, state, dist), w - w div 2)
of 5: # bear — relative bearing
pushTherm(result, (bearing - selfHdg + PI) / (2.0 * PI), w)
of 6: # walls — distance to each wall
pushTherm(result, ex / wAll, w div 4)
pushTherm(result, (wAll - ex) / wAll, w div 4)
pushTherm(result, ey / hAll, w div 4)
pushTherm(result, (hAll - ey) / hAll, w - 3 * (w div 4))
of 7: # bull — live bullets, lateral offset
let (n, lat) = bulletSignal(g, state)
pushTherm(result, n / 4.0, w div 2)
pushTherm(result, 0.5 + 0.5 * lat, w - w div 2)
of 8: # hzn — bullet flight time
pushTherm(result, float(tmhHorizonFor(dist, 11.0)) / 50.0, w)
else: discard
if result.len < g.inputWidth:
for _ in result.len ..< g.inputWidth: result.add 0'u8
elif result.len > g.inputWidth:
result.setLen(g.inputWidth)
# ── class geometry ───────────────────────────────────────────────────────────
proc classCenterDeg*(k, nClasses: int, maxDeg: float): float {.inline.} =
## Centre (degrees) of correction class `k` over ±maxDeg.
-maxDeg + (float(k) + 0.5) * (2.0 * maxDeg / float(nClasses))
proc classOf*(errRad, maxDeg: float, nClasses: int): int {.inline.} =
## Bin a signed angular error (radians) into one of `nClasses` bins.
let x = radToDeg(errRad)
var k = int((x + maxDeg) / (2.0 * maxDeg) * float(nClasses))
if k < 0: k = 0
if k >= nClasses: k = nClasses - 1
k
# ── readout ──────────────────────────────────────────────────────────────────
proc netShiftDeg*(g: var BitbrainNetGun, input: openArray[uint8]): float =
## The fine-grained correction: the PROBABILITY-WEIGHTED MEAN of the class
## centres over the SBC posterior. Returns 0.0 when there is no evidence.
let (label, scores) = g.net.inferProb(input)
var sum = 0.0
var tot = 0.0
for k in 0 ..< scores.len:
sum += scores[k] * classCenterDeg(k, g.nClasses, g.maxDeg)
tot += scores[k]
g.lastClass = label
if tot <= 0.0: 0.0 else: sum / tot
proc adaptThresholds*(g: var BitbrainNetGun, input: openArray[uint8]) =
## Online homeostasis (unsupervised): accumulate this input's ADE firings and
## every `adaptEvery` inputs nudge each threshold toward `targetRate`.
for ad in g.net.ades.mitems: ad.accumulateFiring(input)
inc g.sinceAdapt
if g.sinceAdapt >= g.adaptEvery:
for ad in g.net.ades.mitems:
ad.adaptThresholds(interval = g.sinceAdapt, targetRate = g.targetRate)
g.sinceAdapt = 0
inc g.adapts
proc resolvePending(g: var BitbrainNetGun, state: WorldState) =
## Prequential label resolution: `h` ticks after the fire, the enemy's OBSERVED
## bearing from the firing position is the FACT; the required correction is
## `observedBearing - baseBearing`, binned into a class, and learned.
var w = 0
for i in 0 ..< g.pendingCount:
let p = g.pending[i]
let due = p.fireTick + p.horizon
if due > state.tick:
g.pending[w] = p
inc w
elif due == state.tick:
let obs = tmhObservedAt(g.tmh, state.tick, p.selfX, p.selfY)
if obs.ok and (state.tick - obs.lastSeenTick) <= TMH_STALE_MAX:
let err = wrapRadNet(obs.bearing - p.baseBearing)
g.net.learn(p.input, classOf(err, g.maxDeg, g.nClasses))
inc g.trained
else:
inc g.pendingDropped
else:
inc g.pendingDropped
g.pendingCount = w
proc bbnLog(g: var BitbrainNetGun, state: WorldState, shiftDeg: float) =
if not g.logEnabled: return
let key = fmt"{shiftDeg:.2f}"
if key == g.lastLogKey: return
g.lastLogKey = key
echo fmt"[bbn] t={state.tick} shift={shiftDeg:+.2f}deg class={g.lastClass} " &
fmt"in={g.inputWidth} ncl={g.nClasses} nAde={g.nAde} " &
fmt"mode={($g.mode)[7..^1]} trained={g.trained} adapts={g.adapts} " &
fmt"pend={g.pendingCount} dropped={g.pendingDropped}"
# ── reset hooks ──────────────────────────────────────────────────────────────
proc resetRoundState*(g: var BitbrainNetGun) =
if not g.initialized: return
g.tmh.resetRoundState()
g.pendingCount = 0
g.hist.setLen(0)
g.lastTick = -1
g.sinceEnqTick = -1
g.sinceEnqBucket = -1
g.lastLogKey = ""
proc resetLearning*(g: var BitbrainNetGun, reason = "") =
## Per-battle / per-enemy wipe of the SBC counters. The ADEs (codes and
## adapted thresholds) survive: they are unsupervised structure, not labels.
if not g.initialized: return
g.net.resetLearning()
g.trained = 0
g.sinceAdapt = 0
g.adapts = 0
g.corrections = 0
g.lastShiftDeg = 0.0
g.observedTargetId = -1
g.resetRoundState()
if reason.len > 0 and g.logEnabled: echo fmt"[bbn-reset] reason={reason}"
proc targetChanged*(g: var BitbrainNetGun, enemyId: int): bool =
if not g.resetOnTarget: return false
if enemyId < 0: return false
if g.observedTargetId >= 0 and enemyId != g.observedTargetId:
g.resetLearning("target_change")
g.observedTargetId = enemyId
return true
g.observedTargetId = enemyId
false
# ── Gun interface ────────────────────────────────────────────────────────────
proc isWarmedUp*(g: BitbrainNetGun): bool {.inline.} =
## Ready as soon as it has enough resolved samples to have a readout; before
## that it is the identity on Pattern, which is always a valid prediction.
g.trained >= g.minObs
proc networkBytes*(g: BitbrainNetGun): int =
## RAM held by the network (ADs + SBC tensors). 0 before the lazy build.
if not g.built: 0 else: g.net.memoryBytes
proc sbcBytes*(g: BitbrainNetGun): int =
if not g.built: 0 else: g.net.sbcMemoryBytes
proc predict*(g: var BitbrainNetGun, state: WorldState,
bulletSpeed: float): GunPrediction =
g.ensureInit()
## A disabled gun must never be called: ModularBot gates admission on
## `g.enabled`, and this branch is only a belt-and-braces guard. It returns
## the CURRENT position (a valid, if useless, prediction) rather than anything
## that could poison a shared tracker.
if not g.enabled: return GunPrediction(x: state.enemyX, y: state.enemyY)
if state.tick < g.lastTick: g.resetRoundState()
if state.tick != g.lastTick:
tmhUpdateHistory(g.tmh, state)
g.resolvePending(state)
g.pushHist(state)
g.lastTick = state.tick
# The base is Pattern; BitBrain only adds a fine-grained correction to it.
let base = g.tmh.pattern.predict(state, bulletSpeed)
if bulletSpeed <= 0.0: return base
let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
let h = tmhHorizonFor(dist, bulletSpeed)
let hb = tmhHorizonBucket(h)
var input = buildInput(g, state)
g.adaptThresholds(input)
# One deferred training sample per (tick, horizon bucket): `predict` runs once
# per power bin, so all four horizons contribute evidence.
if g.sinceEnqTick != state.tick or g.sinceEnqBucket != hb:
if g.pendingCount < BBN_PENDING_CAP:
let los = arctan2(state.enemyY - state.selfY, state.enemyX - state.selfX)
let baseBearing = arctan2(base.y - state.selfY, base.x - state.selfX)
g.pending[g.pendingCount] = Pending(
fireTick: state.tick, horizon: h, selfX: state.selfX, selfY: state.selfY,
baseBearing: baseBearing, input: input,
klass: classOf(wrapRadNet(baseBearing - los), g.maxDeg, g.nClasses))
inc g.pendingCount
else:
inc g.pendingDropped
g.sinceEnqTick = state.tick
g.sinceEnqBucket = hb
g.lastShiftDeg = 0.0
if g.trained >= g.minObs:
g.lastShiftDeg = g.netShiftDeg(input)
g.bbnLog(state, g.lastShiftDeg)
if abs(g.lastShiftDeg) < 1e-9: return base
inc g.corrections
tmhApplyShift(state.selfX, state.selfY, base.x, base.y, g.lastShiftDeg)
proc onResult*(g: var BitbrainNetGun, e: FeedbackEvent) =
## Labels come from our own observation ring, not from virtual-bullet
## feedback. The hook exists for the rack.
discard
proc blockNameIndex*(name: string): int =
## Index of the named block in `BB_BLOCKS`, or -1. Used by the tests and the
## boot report so a block is addressed by its NAME, never by a bare index.
for i in 0 ..< BB_BLOCKS.len:
if BB_BLOCKS[i][0] == name: return i
-1
proc inferClass*(g: var BitbrainNetGun, input: openArray[uint8]): int =
## The argmax class of the SBC posterior. Exposed (rather than letting callers
## reach into `g.net`) so the gun's network stays an implementation detail and
## so this name cannot be shadowed by the SBC-level `inferProb` export.
g.net.inferProb(input).label
proc sbcBytesOf*(g: BitbrainNetGun): int =
let net = g.net
net.sbcMemoryBytes()
proc sbcsOf*(g: BitbrainNetGun): seq[Sbc] = g.net.sbcs
proc learnSample*(g: var BitbrainNetGun, input: openArray[uint8], klass: int) =
## One supervised online step: drive the ADE layer and set/increment the class
## in every observed coincidence. Exposed so tests can drive the learner
## without replaying a whole deferred-label stream.
g.net.learn(input, klass)
proc evidenceFor*(g: var BitbrainNetGun, input: openArray[uint8], klass: int): int =
## Total SBC evidence the network currently holds for `klass` on `input` (the
## summed counters, or the set-bit count in bitset mode). Unlike
## `occupancy` — which saturates once a cell is non-zero — this GROWS with
## every counted learn, so it is the observable that proves the counted mode
## is accumulating rather than just flipping bits.
g.net.infer(input).counts[klass]
proc gunAdmitted*(g: BitbrainNetGun, rackAdmitted: bool): bool {.inline.} =
## The FULL admission predicate for rack id 17: the rack table AND this gun's
## own master switch. Both default off, so an incomplete configuration is
## exactly as inert as an unset one — which matters, because a pre-rename
## `.env` still carries `TR_RACK_BITBRAIN=both` and must not be able to push a
## disabled gun's placeholder predictions into the shared VirtualTracker ring.
rackAdmitted and g.enabled
+12 -10
View File
@@ -7,19 +7,22 @@ Recap card. Everything below is read off `common_libs/guns/lead_gain.nim`.
This gun is **LEADGAIN** (rack id 16, `TR_LEADGAIN_*`). It used to be called This gun is **LEADGAIN** (rack id 16, `TR_LEADGAIN_*`). It used to be called
`BITBRAIN` and to live in `guns/bitbrain_gun.nim`, but the ADE+SBC network was `BITBRAIN` and to live in `guns/bitbrain_gun.nim`, but the ADE+SBC network was
removed when it was rebuilt into what it actually is: **it learns a multiplier removed when it was rebuilt into what it actually is: **it learns a multiplier
for Pattern's lead, separately per range band.** The rack id is unchanged. The for Pattern's lead, separately per range band.** The rack id is unchanged, and
real ADE+SBC gun is `guns/bitbrain_net.nim` (rack id 17). it OWNS the `BITBRAIN` name and the `TR_BITBRAIN_*` prefix unconditionally:
the ADE+SBC gun that briefly took both (rack id 17, `guns/bitbrain_net.nim`) was
RETIRED and removed — see `docs/bitbrain_campaign.md` §RETIRED.
### Backward compatibility (read this before editing your `.env`) ### Backward compatibility (read this before editing your `.env`)
The `TR_BITBRAIN_*` names your `.env` already contains still work, and still The `TR_BITBRAIN_*` names your `.env` already contains still work, and still
select **this** gun. The disambiguation is one switch, `TR_BITBRAIN_NET` select **this** gun. There is nothing to disambiguate any more, so the alias
(default `0`): layer is unconditional:
| `TR_BITBRAIN_NET` | who owns `TR_BITBRAIN_*` | | name | meaning |
|---|---| |---|---|
| unset / `0` | LEGACY — these are aliases for `TR_LEADGAIN_*`; the new ADE+SBC gun is off | | `TR_BITBRAIN_<X>` (14 frozen suffixes) | always a legacy alias for `TR_LEADGAIN_<X>` |
| `1` | the new ADE+SBC gun (rack id 17) | | `TR_RACK_BITBRAIN` | always selects LEADGAIN (rack id 16) |
| `TR_BITBRAIN_NET` | **RETIRED** — no longer read; a stale value in a `.env` is an unrecognised variable and the boot report warns about it |
So the owner's existing `TR_RACK_LEADGAIN=both TR_BITBRAIN_GAINS=… TR_BITBRAIN_MEM=decay So the owner's existing `TR_RACK_LEADGAIN=both TR_BITBRAIN_GAINS=… TR_BITBRAIN_MEM=decay
TR_BITBRAIN_LOG=1` keeps behaving exactly as before, and one `[depr]` line on TR_BITBRAIN_LOG=1` keeps behaving exactly as before, and one `[depr]` line on
@@ -32,7 +35,7 @@ Migrated names are `TR_LEADGAIN_GAINS`, `_MEM`, `_MIN_OBS`, `_DECAY`,
## What it is today ## What it is today
- A **lead-gain corrector on top of Pattern's prediction**. It scales Pattern's lead over the line of sight by a learned gain. - A **lead-gain corrector on top of Pattern's prediction**. It scales Pattern's lead over the line of sight by a learned gain.
- The **ADE/SBC neural network is REMOVED from this gun** (it lives in `guns/bitbrain_net.nim` now). The generic `common_libs/bitbrain/` library is intact and tested separately (`test_bitbrain.nim`, 56 checks). - The **ADE/SBC neural network is REMOVED from this gun and the project**. The generic `common_libs/bitbrain/` library is intact and tested separately (`test_bitbrain.nim`, 56 checks) — `movements/learned_surfer.nim` still imports `bitbrain/sbc`, so the LIBRARY is not the gun.
## INPUTS ## INPUTS
@@ -91,8 +94,7 @@ sets are disjoint, so nothing is claimed twice.
| `TR_LEADGAIN_DECAY_FRAC` / `TR_BITBRAIN_DECAY_FRAC` | per-decay shrink of the hit counts (`lgApplyDecay`) | | `TR_LEADGAIN_DECAY_FRAC` / `TR_BITBRAIN_DECAY_FRAC` | per-decay shrink of the hit counts (`lgApplyDecay`) |
| `TR_LEADGAIN_LOG` / `TR_BITBRAIN_LOG` | `1` = emit the `[lg]` line | | `TR_LEADGAIN_LOG` / `TR_BITBRAIN_LOG` | `1` = emit the `[lg]` line |
| `TR_LEADGAIN_RESET_ON_TARGET` / `TR_BITBRAIN_RESET_ON_TARGET` | wipe learning when the enemy id changes (`targetChanged`) | | `TR_LEADGAIN_RESET_ON_TARGET` / `TR_BITBRAIN_RESET_ON_TARGET` | wipe learning when the enemy id changes (`targetChanged`) |
| `TR_RACK_LEADGAIN` / `TR_RACK_BITBRAIN` | the rack admission switch | | `TR_RACK_LEADGAIN` / `TR_RACK_BITBRAIN` | the rack admission switch (the legacy name is unconditional) |
| `TR_BITBRAIN_NET` | `0` (default) keeps `TR_BITBRAIN_*` legacy; `1` hands the namespace to the new ADE+SBC gun |
**INERT** — kept only so old configs and the boot report don't warn; never touch the gain learner: **INERT** — kept only so old configs and the boot report don't warn; never touch the gain learner:
+27 -48
View File
@@ -5,8 +5,10 @@
## but its ADE+SBC network was removed when it was rebuilt into what it actually ## but its ADE+SBC network was removed when it was rebuilt into what it actually
## is: **it learns a multiplier for Pattern's lead, separately per range band.** ## is: **it learns a multiplier for Pattern's lead, separately per range band.**
## `LEADGAIN` says that; `BITBRAIN` (a neural network) did not. The rack id 16 ## `LEADGAIN` says that; `BITBRAIN` (a neural network) did not. The rack id 16
## is UNCHANGED (many tests assert the id literals) and the real ADE+SBC gun is ## is UNCHANGED (many tests assert the id literals) and the `BITBRAIN` name is
## the separate `guns/bitbrain_net.nim` at rack id 17. ## the gun's to keep: the ADE+SBC gun that briefly held it (rack id 17,
## `guns/bitbrain_net.nim`) was RETIRED and removed — see
## `docs/bitbrain_campaign.md` §RETIRED.
## ##
## ── WHY THE FILE WAS REBUILT (Phase 0/1 evidence) ──────────────────────────── ## ── WHY THE FILE WAS REBUILT (Phase 0/1 evidence) ────────────────────────────
## The previous design was an ADDITIVE angular shift: an ADE+SBC network ## The previous design was an ADDITIVE angular shift: an ADE+SBC network
@@ -66,30 +68,22 @@
## report (`env_report.nim`) and the registration guard tests keep working ## report (`env_report.nim`) and the registration guard tests keep working
## unchanged; they no longer affect the gain learner. The generic ## unchanged; they no longer affect the gain learner. The generic
## `common_libs/bitbrain/` library is untouched and still tested by ## `common_libs/bitbrain/` library is untouched and still tested by
## `test_bitbrain.nim`, and the new ADE+SBC gun that actually uses it is ## `test_bitbrain.nim` — `movements/learned_surfer.nim` still imports
## `guns/bitbrain_net.nim` (rack id 17). ## `bitbrain/sbc`, so the library is NOT the gun.
## ##
## ── BACKWARD COMPATIBILITY: the `TR_BITBRAIN_*` legacy aliases ─────────────── ## ── BACKWARD COMPATIBILITY: the `TR_BITBRAIN_*` legacy aliases ───────────────
## The owner's live `.env` predates the rename and contains `TR_RACK_BITBRAIN`, ## The owner's live `.env` predates the rename and contains `TR_RACK_BITBRAIN`,
## `TR_BITBRAIN_GAINS`, `TR_BITBRAIN_MEM`, `TR_BITBRAIN_LOG`, … Those names are ## `TR_BITBRAIN_GAINS`, `TR_BITBRAIN_MEM`, `TR_BITBRAIN_LOG`, … Those names are
## the OLD corrector's knobs and MUST keep working unchanged. The ## the OLD corrector's knobs and MUST keep working unchanged.
## `TR_BITBRAIN_*` prefix, however, now belongs to the NEW ADE+SBC gun
## (`guns/bitbrain_net.nim`). The two uses are separated by ONE deterministic
## switch, `TR_BITBRAIN_NET` (the new gun's master switch, default 0 = off):
## ##
## * `TR_BITBRAIN_NET` UNSET / 0 → LEGACY MODE. Every `TR_BITBRAIN_<X>` name ## The `TR_BITBRAIN_*` prefix was briefly shared with the ADE+SBC gun, which
## listed in `LegacyKnobEnvNames` is a legacy alias for this gun's ## had a two-mode disambiguator, `TR_BITBRAIN_NET`. That gun was RETIRED and
## `TR_LEADGAIN_<X>`, and the new ADE+SBC gun is OFF. This is the owner's ## removed (it did not learn; the owner watched it live and threw it away), so
## current configuration, so its behaviour is unchanged. ## the legacy namespace is now the ONLY namespace and the mapping is
## * `TR_BITBRAIN_NET=1` → NEW-NETWORK MODE. `TR_BITBRAIN_<X>` names ## UNCONDITIONAL: every `TR_BITBRAIN_<X>` in the frozen `LegacyKnobEnvNames`
## the NEW gun's knobs (see `bitbrain_net.nim`) and this gun reads ONLY ## set is an alias for `TR_LEADGAIN_<X>`, and `TR_RACK_BITBRAIN` always selects
## `TR_LEADGAIN_<X>`. ## this gun (rack id 16). A stale `TR_BITBRAIN_NET=1` left in a `.env` is now an
## ## unrecognised variable: the boot report warns about it and ignores it.
## The legacy sets are DISJOINT (see `legacyKnobEnvNames` / the new gun's
## `netKnobEnvNames`), so no name is ever claimed by both. `TR_RACK_BITBRAIN` is
## the one genuinely ambiguous name (the rack is keyed by gun name and the new
## gun is now the one called `BITBRAIN`); it is resolved by the SAME switch —
## see `selector.nim`'s `RackLegacyAliases`.
## ##
## ── TR_LEADGAIN_GAINS (the candidate set as an env knob) ───────────────────── ## ── TR_LEADGAIN_GAINS (the candidate set as an env knob) ─────────────────────
## `TR_LEADGAIN_GAINS` is a comma-separated candidate list, e.g. ## `TR_LEADGAIN_GAINS` is a comma-separated candidate list, e.g.
@@ -121,11 +115,10 @@ const
LG_NADE_ENV* = "TR_LEADGAIN_NADE" ## (legacy ADE count; inert) LG_NADE_ENV* = "TR_LEADGAIN_NADE" ## (legacy ADE count; inert)
LG_RANGE_ENV* = "TR_LEADGAIN_RANGE" ## (legacy class half-range; inert) LG_RANGE_ENV* = "TR_LEADGAIN_RANGE" ## (legacy class half-range; inert)
LG_LOG_ENV* = "TR_LEADGAIN_LOG" ## 1 = per-change [lg] log LG_LOG_ENV* = "TR_LEADGAIN_LOG" ## 1 = per-change [lg] log
## ── the one switch that disambiguates the legacy `TR_BITBRAIN_*` names ──── ## ── the legacy `TR_BITBRAIN_*` namespace ──────────────────────────────────
## Read by BOTH guns (see `bitbrain_net.nim`). Unset/0 => the `TR_BITBRAIN_*` ## This gun OWNS the `TR_BITBRAIN_*` prefix unconditionally (the ADE+SBC gun
## names are LEGACY aliases for this gun; 1 => they belong to the new ADE+SBC ## that briefly shared it was retired), so the frozen suffix set below maps
## gun. It is also the new gun's master on/off switch. ## every legacy name onto this gun's `TR_LEADGAIN_<X>` with no switch.
LG_NET_SWITCH_ENV* = "TR_BITBRAIN_NET"
LG_MIN_OBS_ENV* = "TR_LEADGAIN_MIN_OBS" ## samples before a band is trusted LG_MIN_OBS_ENV* = "TR_LEADGAIN_MIN_OBS" ## samples before a band is trusted
LG_WARMUP_ENV* = "TR_LEADGAIN_WARMUP" ## (legacy; inert) LG_WARMUP_ENV* = "TR_LEADGAIN_WARMUP" ## (legacy; inert)
LG_ADAPT_ENV* = "TR_LEADGAIN_ADAPT" ## (legacy; inert) LG_ADAPT_ENV* = "TR_LEADGAIN_ADAPT" ## (legacy; inert)
@@ -288,10 +281,9 @@ const
LegacyPrefix* = "TR_BITBRAIN_" LegacyPrefix* = "TR_BITBRAIN_"
NewPrefix* = "TR_LEADGAIN_" NewPrefix* = "TR_LEADGAIN_"
## The COMPLETE, FROZEN set of the old corrector's knob suffixes. A ## The COMPLETE, FROZEN set of the old corrector's knob suffixes. A
## `TR_BITBRAIN_<X>` in this set is a legacy alias for `TR_LEADGAIN_<X>`; any ## `TR_BITBRAIN_<X>` in this set is a legacy alias for `TR_LEADGAIN_<X>`.
## other `TR_BITBRAIN_*` name belongs to the new ADE+SBC gun ## This gun is the sole owner of the prefix (the ADE+SBC gun was retired), so
## (`bitbrain_net.nim`). The two sets are DISJOINT by construction, so the ## the mapping is total and unconditional — no name is claimed twice.
## mapping is total and deterministic — no name is claimed twice.
LegacyKnobEnvNames* = [ LegacyKnobEnvNames* = [
"GAINS", "MEM", "MIN_OBS", "DECAY", "DECAY_FRAC", "LOG", "RESET_ON_TARGET", "GAINS", "MEM", "MIN_OBS", "DECAY", "DECAY_FRAC", "LOG", "RESET_ON_TARGET",
"N", "NADE", "RANGE", "WARMUP", "ADAPT", "CALIB", "SEED"] "N", "NADE", "RANGE", "WARMUP", "ADAPT", "CALIB", "SEED"]
@@ -299,26 +291,16 @@ const
## for the boot report). A deprecation line is only worth printing for these ## for the boot report). A deprecation line is only worth printing for these
## plus the inert ones, because a stale inert name is still a stale name. ## plus the inert ones, because a stale inert name is still a stale name.
## The legacy rack knob, from `gun_harness/selector`'s alias table so the ## The legacy rack knob, from `gun_harness/selector`'s alias table so the
## two can never drift apart. It is the CURRENT name of rack id 17, which is ## two can never drift apart. It is NOT a current rack name any more (it was
## why it is not a distinct string. ## `RackGunNames[RackNetGunId]` while the retired ADE+SBC gun existed).
LegacyRackEnvName* = RackLegacyAlias[0][0] LegacyRackEnvName* = RackLegacyAlias[0][0]
proc netSwitchOn*(): bool =
## `TR_BITBRAIN_NET` unset/0 => the `TR_BITBRAIN_*` names are LEGACY aliases
## for this gun. 1 => they belong to the new ADE+SBC gun. The same predicate
## is defined in `gun_harness/selector` (`netSwitchOwnsBitbrainName`), which
## cannot import a concrete gun module.
case getEnv(LG_NET_SWITCH_ENV, "").strip().toLowerAscii()
of "1", "true", "yes", "on": true
else: false
var deprecationShown = false var deprecationShown = false
proc lgDeprecationLine*(): string = proc lgDeprecationLine*(): string =
## The single clear deprecation line the owner sees. Names every legacy ## The single clear deprecation line the owner sees. Names every legacy
## `TR_BITBRAIN_*` knob that is actually set in the environment and the new ## `TR_BITBRAIN_*` knob that is actually set in the environment and the new
## name that now owns it. Empty when there is nothing to migrate. ## name that now owns it. Empty when there is nothing to migrate.
if netSwitchOn(): return ""
var parts: seq[string] var parts: seq[string]
for suffix in LegacyKnobEnvNames: for suffix in LegacyKnobEnvNames:
let old = LegacyPrefix & suffix let old = LegacyPrefix & suffix
@@ -329,17 +311,14 @@ proc lgDeprecationLine*(): string =
if parts.len == 0: return "" if parts.len == 0: return ""
result = "[depr] " & LegacyPrefix & "* is the OLD lead-gain corrector's namespace; " & result = "[depr] " & LegacyPrefix & "* is the OLD lead-gain corrector's namespace; " &
"it was renamed to " & NewPrefix & "* (gun LEADGAIN, rack id 16). " & "it was renamed to " & NewPrefix & "* (gun LEADGAIN, rack id 16). " &
"Still honoured: " & parts.join("; ") & "Still honoured: " & parts.join("; ") & "."
". The new ADE+SBC gun owns the " & LegacyPrefix &
"* names once " & LG_NET_SWITCH_ENV & "=1."
proc lgEnv(name: string): string = proc lgEnv(name: string): string =
## Read a `TR_LEADGAIN_<X>` knob, falling back to the legacy ## Read a `TR_LEADGAIN_<X>` knob, falling back to the legacy
## `TR_BITBRAIN_<X>` alias while `TR_BITBRAIN_NET` is off. The NEW name always ## `TR_BITBRAIN_<X>` alias. The NEW name always wins when both are set, so a
## wins when both are set, so a migrated config is authoritative. ## migrated config is authoritative.
var v = getEnv(name, "") var v = getEnv(name, "")
if v.len > 0: return v if v.len > 0: return v
if netSwitchOn(): return ""
let suffix = if name.startsWith(NewPrefix): name[NewPrefix.len .. ^1] else: "" let suffix = if name.startsWith(NewPrefix): name[NewPrefix.len .. ^1] else: ""
if suffix.len == 0: return "" if suffix.len == 0: return ""
for s in LegacyKnobEnvNames: for s in LegacyKnobEnvNames:
@@ -1,246 +0,0 @@
## SCALING SWEEP for the ADE+SBC gun (rack id 17) — RAM vs ms/tick vs quality.
##
## THE QUESTION: "how do inference, timing and size of RAM scale with input and
## output size?" This answers it on the REAL gun by replaying a recorded live
## corpus through it, not from theory.
##
## For each setting it reports:
## RAM — `memoryBytes` (ADs + SBC tensors) and the SBC tensor alone,
## with the counted-vs-bitset factor spelled out;
## ms/tick — wall time per RECORDED TICK over the real replay, against the
## project's 13.16 ms/tick budget. One `predict` per power bin
## per tick, which is what the live loop does, so the number
## is directly comparable with the budget;
## quality — the OFFLINE ruler: mean |angular error| against the true
## interception point (`gun_harness/prediction_quality`), plus
## the hit proxy (|err| <= atan(18/range)).
##
## VETO-CAPABLE CHECK ONLY. Per `docs/offline_harness_trust.md` the offline
## harness is trustworthy for per-gun, single-tick prediction quality on a FIXED
## trajectory and for NOTHING that flows through the closed loop. A win here is
## NOT a live win and is never presented as one.
##
## Usage:
## nim c -r -d:release --path:common_libs common_libs/tests/measure_bitbrain_scaling.nim \
## [--corpus /tmp/tfil_ab2/out] [--limit N]
##
## Env knobs are set per SETTING by this program (putEnv), so the sweep is a
## pure-env experiment: no recompile between arms.
import std/[os, strformat, strutils, times, math, sequtils, algorithm]
import gun_harness/gun_interface
import gun_harness/prediction_quality
import gun_harness/virtual_bullets
import guns/pattern_matcher
import guns/bitbrain_net
const
BudgetMsPerTick* = 13.16 ## the project's live tick budget
type
Setting = object
label: string
inputWidth: int
nClasses: int
nAde: int
widths: string
features: string
mode: string
Row = object
s: Setting
ramBytes: int
inputWidth: int
sbcBytes: int
adBytes: int
msPerTick: float
meanAbsDeg: float
hitProxy: float
patternAbsDeg: float
n: int
# ── the sweep grid ───────────────────────────────────────────────────────────
const FullFeatures = "epos:4,evel:3,eturn:2,eself:2,dist:5,bear:4,walls:2,bull:2,hzn:4"
proc smallFeatures(): string = "epos:2,evel:2,eturn:1,eself:1,dist:2,bear:2,walls:1,bull:1,hzn:2"
proc largeFeatures(): string = "epos:6,evel:5,eturn:4,eself:4,dist:8,bear:6,walls:3,bull:3,hzn:6"
proc settings(): seq[Setting] =
## Three points along the INPUT axis (classes/geometry held at the default) and
## three along the OUTPUT axis (input held at the default 52), all in BOTH
## storage modes, because the counted/bitset factor is part of the answer.
let base = Setting(label: "default", inputWidth: 0, nClasses: 8, nAde: 256,
widths: "4,5,6", features: FullFeatures, mode: "counted")
var inp: seq[Setting]
for (lbl, feat, w) in [("input-small", smallFeatures(), 0),
("input-medium", FullFeatures, 0),
("input-large", largeFeatures(), 0)]:
inp.add Setting(label: lbl, inputWidth: w, nClasses: 8, nAde: 256,
widths: "4,5,6", features: feat, mode: "counted")
var outp: seq[Setting]
for (lbl, nc) in [("classes-small", 2), ("classes-medium", 8),
("classes-large", 64)]:
outp.add Setting(label: lbl, inputWidth: 0, nClasses: nc, nAde: 256,
widths: "4,5,6", features: FullFeatures, mode: "counted")
# the nAde axis is the third one, because RAM is quadratic in it
var ade: seq[Setting]
for (lbl, n) in [("nAde-small", 64), ("nAde-medium", 256), ("nAde-large", 512)]:
ade.add Setting(label: lbl, inputWidth: 0, nClasses: 8, nAde: n,
widths: "4,5,6", features: FullFeatures, mode: "counted")
var both: seq[Setting]
for m in ["bitset", "counted"]:
both.add Setting(label: "mode-" & m, inputWidth: 0, nClasses: 8, nAde: 256,
widths: "4,5,6", features: FullFeatures, mode: m)
result = inp & outp & ade & both
discard base
# ── one arm ──────────────────────────────────────────────────────────────────
proc applySetting(s: Setting) =
for n in BitbrainNetEnvNames: delEnv(n)
putEnv(BBN_NET_ENV, "1")
putEnv(BBN_FEATURES_ENV, s.features)
if s.inputWidth > 0: putEnv(BBN_INPUT_ENV, $s.inputWidth)
putEnv(BBN_CLASSES_ENV, $s.nClasses)
putEnv(BBN_NADES_ENV, $s.nAde)
putEnv(BBN_WIDTHS_ENV, s.widths)
putEnv(BBN_MODE_ENV, s.mode)
putEnv(BBN_MINOBS_ENV, "1")
putEnv(BBN_DECAY_EVERY_ENV, "64")
putEnv(BBN_DECAY_SHIFT_ENV, "3")
# ── the corpus, turned once into a fixed sample set ─────────────────────────
#
# The interception solve (the ruler) is INDEPENDENT of the arm, so it is done
# ONCE and cached. That does two things: every arm is scored on byte-identical
# labels, and the timed region contains ONLY the gun's `predict` calls — the
# ruler's own cost cannot contaminate the ms/tick number.
type
Sample = object
st: WorldState
speed: float
targetLead: float
range: float
tol: float
proc buildSamples(runs: seq[string]): seq[Sample] =
for rp in runs:
let c = loadCorpus(rp)
if c.n == 0: continue
for r in 0 ..< c.rStart.len:
let base = int(c.rStart[r]) - c.base
let cnt = int(c.rCount[r])
let iEnd = base + cnt
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])
let baseState = WorldState(
arenaWidth: c.arenaW, arenaHeight: c.arenaH, tick: localTick,
enemyX: c.ex(i), enemyY: c.ey(i), enemyHeading: c.eh(i),
enemySpeed: c.es(i), enemyEnergy: c.ee(i),
selfX: ox, selfY: oy, selfHeading: c.sh(i), selfSpeed: c.ss(i),
selfEnergy: c.se(i), selfRadarHeading: c.sh(i))
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, true)
if not ib.ok: continue
result.add Sample(st: baseState, speed: speed,
targetLead: wrap180(ib.bearing - los),
range: ib.range, tol: tolDeg(ib.range))
proc runArm(s: Setting, samples: seq[Sample]): Row =
applySetting(s)
var g = initBitbrainNetGun()
var pat = PatternMatcherGun()
var sumAbs = 0.0
var sumPat = 0.0
var hits = 0
var n = 0
# `samples` is ordered round-by-round, so the gun sees rounds in order.
var t0 = epochTime()
for smp in samples:
let bp = predict(g, smp.st, smp.speed)
let los = bearingDeg(smp.st.selfX, smp.st.selfY,
smp.st.enemyX, smp.st.enemyY)
let bl = wrap180(bearingDeg(smp.st.selfX, smp.st.selfY, bp.x, bp.y) - los)
let err = abs(wrap180(bl - smp.targetLead))
sumAbs += err
if err <= smp.tol: inc hits
let pp = predict(pat, smp.st, smp.speed)
let pl = wrap180(bearingDeg(smp.st.selfX, smp.st.selfY, pp.x, pp.y) - los)
sumPat += abs(wrap180(pl - smp.targetLead))
inc n
let elapsed = epochTime() - t0
# ms per recorded TICK: `n` samples over `len(PowerBins)` samples per tick.
let ticks = max(1, samples.len div len(PowerBins))
result = Row(s: s, inputWidth: g.inputWidth, ramBytes: g.networkBytes(),
sbcBytes: g.sbcBytes(),
adBytes: g.networkBytes() - g.sbcBytes(),
msPerTick: elapsed * 1000.0 / float(ticks),
meanAbsDeg: if n > 0: sumAbs / float(n) else: NaN,
hitProxy: if n > 0: float(hits) / float(n) else: NaN,
patternAbsDeg: if n > 0: sumPat / float(n) else: NaN,
n: n)
# ── driver ───────────────────────────────────────────────────────────────────
proc main() =
var corpusRoot = "/tmp/tfil_ab2/out"
var limit = 4
var i = 1
while i <= paramCount():
case paramStr(i)
of "--corpus": inc i; corpusRoot = paramStr(i)
of "--limit": inc i; limit = parseInt(paramStr(i))
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 under " & corpusRoot); quit(1)
echo "=".repeat(118)
echo "BITBRAIN (ADE+SBC, rack id 17) SCALING -- RAM / ms-per-tick / offline prediction quality"
echo "=".repeat(118)
echo fmt"corpus : {corpusRoot} ({runs.len} recorded run(s))"
echo fmt"nAde : ADEs per address decoder; the SBC tensor is nAde^2 x nClasses"
echo " cells, so RAM is QUADRATIC in nAde and LINEAR in nClasses."
echo fmt"budget : {BudgetMsPerTick} ms/tick (one predict per power bin per recorded tick)"
echo "quality : mean |angular error| vs the true interception point, over every"
echo " tick x power-bin. VETO-CAPABLE OFFLINE CHECK ONLY (docs/offline_harness_trust.md):"
echo " a win here is NOT a live win."
echo ""
stderr.writeLine("building the ruler sample set once from " & $runs.len &
" run(s)...")
let tSamp = epochTime()
let samples = buildSamples(runs)
echo fmt"sample set : {samples.len} tick x power-bin samples " &
fmt"({samples.len div max(1, len(PowerBins))} recorded ticks) in " &
fmt"{epochTime()-tSamp:.1f}s — arm-independent, so the timed region below"
echo " contains ONLY the gun's predict calls."
echo ""
var rows: seq[Row]
for s in settings(): rows.add runArm(s, samples)
# Pattern is the same on every arm (it never reads the net's knobs), so the
# Pattern column is taken from one arm and is identical for all of them.
let patRef = rows[1]
echo "arm in nCl nAde mode RAM B SBC B AD B " &
"ms/tick %bud mean|err| hit% Pattern|err|"
echo "-".repeat(118)
for r in rows:
echo fmt"{r.s.label:<15} {r.inputWidth:>4} {r.s.nClasses:>5} " &
fmt"{r.s.nAde:>5} {r.s.mode:<8} {r.ramBytes:>8} {r.sbcBytes:>9} " &
fmt"{r.adBytes:>7} {r.msPerTick:>8.3f} {100.0*r.msPerTick/BudgetMsPerTick:>6.2f} " &
fmt"{r.meanAbsDeg:>10.3f} {100.0*r.hitProxy:>6.2f} {patRef.patternAbsDeg:>13.3f}"
echo ""
echo "(in = the resolved input width, i.e. the CONFIGURED feature-block total;"
echo " Pattern|err| is the shipped Pattern gun on the same ticks and is identical"
echo " across arms, since Pattern never reads any of these knobs.)"
for n in BitbrainNetEnvNames: delEnv(n)
main()
+8 -21
View File
@@ -16,7 +16,7 @@
import std/[os, strformat, strutils, times, math] import std/[os, strformat, strutils, times, math]
import gun_harness/[gun_interface, virtual_bullets, prediction_quality] import gun_harness/[gun_interface, virtual_bullets, prediction_quality]
import guns/[head_on, pattern_matcher, tm_horizon, lead_gain, bitbrain_net] import guns/[head_on, pattern_matcher, tm_horizon, lead_gain]
# arm indices (fixed order = fixed output) # arm indices (fixed order = fixed output)
const const
@@ -43,16 +43,12 @@ const
# is the rule the corrector must match; it needs no learning (range is known at fire # 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. # time). The table was selected in-sample from this corpus.
A_BAND* = 13 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] BandGainTable* = [1.0, 1.0, 1.0, 0.0, 0.0]
## (The ADE+SBC gun that used to be arm 14 — rack id 17, `guns/bitbrain_net.nim`
## — was RETIRED and removed. See `docs/bitbrain_campaign.md` §RETIRED.)
ArmNames* = ["Oracle", "OracleQuant", "HeadOn", "Pattern", "PatternGain1.5", ArmNames* = ["Oracle", "OracleQuant", "HeadOn", "Pattern", "PatternGain1.5",
"PatternGain2.0", "PatternGain3.0", "NaiveLinear", "TMHorizon", "LeadGain", "PatternGain2.0", "PatternGain3.0", "NaiveLinear", "TMHorizon", "LeadGain",
"PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain", "PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain"]
"BitBrainNet"]
const const
## The five sub-unity gain arms, in increasing order, resolved to arm indices. ## The five sub-unity gain arms, in increasing order, resolved to arm indices.
@@ -102,7 +98,6 @@ type Ctx = object
naive: NaiveLinearGun naive: NaiveLinearGun
tmh: TmHorizonGun tmh: TmHorizonGun
lg: LeadGainGun lg: LeadGainGun
bbn: BitbrainNetGun
headon: HeadOnGun headon: HeadOnGun
st: WorldState st: WorldState
enemy: seq[EnemyInfo] enemy: seq[EnemyInfo]
@@ -180,10 +175,6 @@ proc runRound(ctx: var Ctx, arms: var seq[ArmAcc], r: int) =
let bp = predict(ctx.lg, ctx.st, speed) let bp = predict(ctx.lg, ctx.st, speed)
let bl = wrap180(bearingDeg(ox, oy, bp.x, bp.y) - los) let bl = wrap180(bearingDeg(ox, oy, bp.x, bp.y) - los)
arms[A_LG].record(rng, wrap180(bl - targetLead), bl, targetLead) 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, proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var ShotStat,
doShots: bool, timing: bool, cont: bool): int = doShots: bool, timing: bool, cont: bool): int =
@@ -197,7 +188,6 @@ proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var S
naive: NaiveLinearGun(lastTick: -1), naive: NaiveLinearGun(lastTick: -1),
tmh: initTmHorizonGun(), tmh: initTmHorizonGun(),
lg: initLeadGainGun(), lg: initLeadGainGun(),
bbn: initBitbrainNetGun(),
headon: HeadOnGun(), headon: HeadOnGun(),
st: WorldState(arenaWidth: c.arenaW, arenaHeight: c.arenaH), st: WorldState(arenaWidth: c.arenaW, arenaHeight: c.arenaH),
enemy: newSeq[EnemyInfo](1)) enemy: newSeq[EnemyInfo](1))
@@ -313,10 +303,9 @@ proc main() =
let mPat = overallMean(arms, A_PATTERN) let mPat = overallMean(arms, A_PATTERN)
let mTmh = overallMean(arms, A_TMH) let mTmh = overallMean(arms, A_TMH)
let mLg = overallMean(arms, A_LG) let mLg = overallMean(arms, A_LG)
let mBbn = overallMean(arms, A_BBN)
let mLin = overallMean(arms, A_NAIVE) let mLin = overallMean(arms, A_NAIVE)
let ordOk = mHead > mPat and mHead > mTmh and mHead > mLg and mHead > mBbn 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} / BitBrainNet {mBbn:.3f}" 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" 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" -> {ordMsg}"
echo fmt" NaiveLinear mean|err| = {mLin:.3f} deg (over-leads; see the lead-gain sweep for why a larger" echo fmt" NaiveLinear mean|err| = {mLin:.3f} deg (over-leads; see the lead-gain sweep for why a larger"
@@ -335,7 +324,7 @@ proc main() =
echo "=".repeat(120) echo "=".repeat(120)
echo "HEADROOM -- the direct answer: how far each arm is from the oracle ceiling, per band" echo "HEADROOM -- the direct answer: how far each arm is from the oracle ceiling, per band"
echo "=" .repeat(120) echo "=" .repeat(120)
let hdr = "band Pattern n Pattern|err| Pattern hpx Oracle hpx headroom pp naive hpx TMHoriz hpx LeadGain hpx BitBrainNet hpx" let hdr = "band Pattern n Pattern|err| Pattern hpx Oracle hpx headroom pp naive hpx TMHoriz hpx LeadGain hpx"
echo hdr echo hdr
echo "-".repeat(hdr.len) echo "-".repeat(hdr.len)
for b in 0 ..< NBands: for b in 0 ..< NBands:
@@ -343,10 +332,8 @@ proc main() =
let orc = arms[A_ORACLE].bands[b] let orc = arms[A_ORACLE].bands[b]
let hp = pat.hitProxy let hp = pat.hitProxy
let ohp = orc.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} {fmt4(arms[A_BBN].bands[b].hitProxy):>15}" 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 ""
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 "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 "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 "to a perfect predictor (the campaign is playing for a slice of this)."
-325
View File
@@ -1,325 +0,0 @@
## 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."
+1 -1
View File
@@ -73,7 +73,7 @@ proc testKnownNames() =
check "known set has no duplicates", known.len == s.len check "known set has no duplicates", known.len == s.len
check "known set only holds TR_*/GUN_* names", check "known set only holds TR_*/GUN_* names",
known.allIt(it.startsWith("TR_") or it.startsWith("GUN_")) known.allIt(it.startsWith("TR_") or it.startsWith("GUN_"))
check "known set covers all 18 rack names", check "known set covers all 17 rack names",
RackGunNames.allIt((RackEnvPrefix & it) in s) RackGunNames.allIt((RackEnvPrefix & it) in s)
for name in ["TR_MOVEMENT", "TR_POWER_ENERGY_MIN", "TR_TMHORIZON_NSTATES", for name in ["TR_MOVEMENT", "TR_POWER_ENERGY_MIN", "TR_TMHORIZON_NSTATES",
"GUN_VBULLET_METRIC", "TR_ENV_REPORT", "GUN_SELECTOR_SEED", "GUN_VBULLET_METRIC", "TR_ENV_REPORT", "GUN_SELECTOR_SEED",
+46 -37
View File
@@ -2,18 +2,17 @@
## ##
## The owner's live `.env` predates the rename and carries `TR_RACK_BITBRAIN`, ## 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 ## `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, ## 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 ## The ADE+SBC gun that briefly owned the `TR_BITBRAIN_*` prefix (rack id 17,
## frozen `LegacyKnobEnvNames` set is an alias for `TR_LEADGAIN_<X>`, and ## `guns/bitbrain_net.nim`) was RETIRED and removed, so there is nothing left to
## `TR_RACK_BITBRAIN` still selects rack id 16. ## disambiguate: this gun is the SOLE owner of that namespace and
## * `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. ## * every `TR_BITBRAIN_<X>` in the frozen `LegacyKnobEnvNames` set is an
## No Java, no battle, no network build. ## alias for `TR_LEADGAIN_<X>`, unconditionally;
## * `TR_RACK_BITBRAIN` selects rack id 16, unconditionally;
## * a stale `TR_BITBRAIN_NET=1` left in a `.env` is an unrecognised
## variable — the boot report warns about it and nothing reads it.
## ##
## Run: nim c -r common_libs/tests/test_lead_gain_legacy.nim ## Run: nim c -r common_libs/tests/test_lead_gain_legacy.nim
@@ -35,7 +34,7 @@ proc clearEnv() =
delEnv(NewPrefix & suffix) delEnv(NewPrefix & suffix)
delEnv(LegacyRackEnvName) delEnv(LegacyRackEnvName)
delEnv("TR_RACK_" & RackGunNames[LeadGainId]) delEnv("TR_RACK_" & RackGunNames[LeadGainId])
delEnv(LG_NET_SWITCH_ENV) delEnv("TR_BITBRAIN_NET")
# ── the alias table itself ─────────────────────────────────────────────────── # ── the alias table itself ───────────────────────────────────────────────────
@@ -61,19 +60,29 @@ proc testAliasTableIsDisjoint() =
# ── the switch ─────────────────────────────────────────────────────────────── # ── the switch ───────────────────────────────────────────────────────────────
proc testSwitch() = proc testNamespaceIsUnconditional() =
## There is no namespace switch any more. The ADE+SBC gun is gone, so a stale
## `TR_BITBRAIN_NET` left in an old `.env` is inert: it must neither disown
## the legacy knob aliases, nor the legacy rack knob, nor the deprecation line.
clearEnv() clearEnv()
check "switch: unset => legacy mode (new ADE+SBC namespace disowned)", putEnv("TR_BITBRAIN_MEM", "decay")
not netSwitchOn() putEnv("TR_BITBRAIN_LOG", "1")
putEnv(LG_NET_SWITCH_ENV, "0") putEnv(LegacyRackEnvName, "both")
check "switch: 0 => legacy mode", not netSwitchOn() var g = initLeadGainGun()
putEnv(LG_NET_SWITCH_ENV, "1") check "ns: the legacy prefix is unconditional (no switch set at all)",
check "switch: 1 => new-network mode", netSwitchOn() g.memMode == lgDecay and g.logEnabled
putEnv(LG_NET_SWITCH_ENV, "yes") putEnv("TR_BITBRAIN_NET", "1")
check "switch: yes => new-network mode", netSwitchOn() var g1 = initLeadGainGun()
putEnv(LG_NET_SWITCH_ENV, "banana") check "ns: a stale TR_BITBRAIN_NET=1 does not disown the legacy knob names",
check "switch: an unknown value is NOT new-network mode (fails safe)", g1.memMode == lgDecay and g1.logEnabled
not netSwitchOn() putEnv("TR_BITBRAIN_NET", "0")
var g2 = initLeadGainGun()
check "ns: a stale TR_BITBRAIN_NET=0 is equally inert", g2.memMode == lgDecay
putEnv("TR_BITBRAIN_NET", "1")
check "ns: a stale TR_BITBRAIN_NET=1 does not disown the legacy rack knob",
loadRackMembership()[LeadGainId] == rmBoth
check "ns: a stale TR_BITBRAIN_NET=1 does not suppress the deprecation line",
lgDeprecationLine().len > 0
clearEnv() clearEnv()
# ── knob aliasing ──────────────────────────────────────────────────────────── # ── knob aliasing ────────────────────────────────────────────────────────────
@@ -93,13 +102,13 @@ proc testKnobAlias() =
putEnv("TR_LEADGAIN_GAINS", "0.5,0.75") putEnv("TR_LEADGAIN_GAINS", "0.5,0.75")
var g2 = initLeadGainGun() var g2 = initLeadGainGun()
check "alias: the NEW name wins when both are set", g2.cands == @[0.5, 0.75] check "alias: the NEW name wins when both are set", g2.cands == @[0.5, 0.75]
# the switch disowns the whole legacy namespace # the legacy namespace is unconditional, even next to a stale NET switch
putEnv(LG_NET_SWITCH_ENV, "1") putEnv("TR_BITBRAIN_NET", "1")
delEnv("TR_LEADGAIN_GAINS") delEnv("TR_LEADGAIN_GAINS")
var g3 = initLeadGainGun() var g3 = initLeadGainGun()
check "alias: TR_BITBRAIN_NET=1 makes the legacy names inert for this gun", check "alias: a stale TR_BITBRAIN_NET=1 does NOT make the legacy names inert",
g3.memMode == lgPerRound and not g3.logEnabled and g3.memMode == lgDecay and g3.logEnabled and
g3.cands == @[0.0, 0.25, 0.5, 0.75, 1.0] g3.cands == @[0.25, 0.5, 1.0]
# ... and then the NEW names still work # ... and then the NEW names still work
putEnv("TR_LEADGAIN_MEM", "retained") putEnv("TR_LEADGAIN_MEM", "retained")
var g4 = initLeadGainGun() var g4 = initLeadGainGun()
@@ -116,10 +125,10 @@ proc testRackAlias() =
check "rack: TR_RACK_BITBRAIN=both still selects LEADGAIN (id 16)", check "rack: TR_RACK_BITBRAIN=both still selects LEADGAIN (id 16)",
m[LeadGainId] == rmBoth and m[LeadGainId] == rmBoth and
vBulletAdmitted(LeadGainId, rm1v1, m, true) vBulletAdmitted(LeadGainId, rm1v1, m, true)
putEnv(LG_NET_SWITCH_ENV, "1") putEnv("TR_BITBRAIN_NET", "1")
let m2 = loadRackMembership() let m2 = loadRackMembership()
check "rack: TR_BITBRAIN_NET=1 hands the name to the new ADE+SBC gun (id 17)", check "rack: a stale TR_BITBRAIN_NET=1 no longer disowns the name — LEADGAIN (id 16) stays selected",
m2[LeadGainId] == rmOff and m2.len == 18 m2[LeadGainId] == rmBoth and m2.len == NumRackGuns
clearEnv() clearEnv()
putEnv("TR_RACK_LEADGAIN", "both") putEnv("TR_RACK_LEADGAIN", "both")
putEnv(LegacyRackEnvName, "off") putEnv(LegacyRackEnvName, "off")
@@ -140,9 +149,9 @@ proc testDeprecationLine() =
line.startsWith("[depr]") and line.startsWith("[depr]") and
line.contains("TR_BITBRAIN_GAINS -> TR_LEADGAIN_GAINS") and line.contains("TR_BITBRAIN_GAINS -> TR_LEADGAIN_GAINS") and
line.contains(NewPrefix) line.contains(NewPrefix)
putEnv(LG_NET_SWITCH_ENV, "1") putEnv("TR_BITBRAIN_NET", "1")
check "depr: the line is silent once the switch is flipped", check "depr: the line still fires with a stale TR_BITBRAIN_NET=1 present",
lgDeprecationLine().len == 0 lgDeprecationLine().len > 0
clearEnv() clearEnv()
# ── shipped-default parity ─────────────────────────────────────────────────── # ── shipped-default parity ───────────────────────────────────────────────────
@@ -155,15 +164,15 @@ proc testDefaultParity() =
let want = if i == 5: rmBoth else: rmOff let want = if i == 5: rmBoth else: rmOff
if m[i] != want: onlyPattern = false if m[i] != want: onlyPattern = false
check "parity: a clean env still loads the shipped onlyPattern rack", onlyPattern check "parity: a clean env still loads the shipped onlyPattern rack", onlyPattern
check "parity: the rack is still 18 guns at id 16 = LEADGAIN", check "parity: the rack is still 17 guns at id 16 = LEADGAIN",
RackGunNames.len == 18 and RackGunNames[LeadGainId] == "LEADGAIN" RackGunNames.len == 17 and RackGunNames[LeadGainId] == "LEADGAIN"
var g = initLeadGainGun() var g = initLeadGainGun()
check "parity: a clean env resolves the shipped candidate set", 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 g.cands == @[0.0, 0.25, 0.5, 0.75, 1.0] and g.memMode == lgPerRound
clearEnv() clearEnv()
testAliasTableIsDisjoint() testAliasTableIsDisjoint()
testSwitch() testNamespaceIsUnconditional()
testKnobAlias() testKnobAlias()
testRackAlias() testRackAlias()
testDeprecationLine() testDeprecationLine()
@@ -37,7 +37,7 @@ proc testTable() =
if DefaultRackMembership[i] != want: onlyPattern = false if DefaultRackMembership[i] != want: onlyPattern = false
check "rack: the shipped default is still the onlyPattern rack", onlyPattern check "rack: the shipped default is still the onlyPattern rack", onlyPattern
check "rack: the default rack admits only Pattern (1v1)", check "rack: the default rack admits only Pattern (1v1)",
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId] admittedGuns(NumRackGuns, rm1v1, DefaultRackMembership) == @[PatternId]
check "gate: LEADGAIN is NOT spawned under the default rack", check "gate: LEADGAIN is NOT spawned under the default rack",
not vBulletAdmitted(LeadGainId, rm1v1, DefaultRackMembership, true) not vBulletAdmitted(LeadGainId, rm1v1, DefaultRackMembership, true)
@@ -49,7 +49,7 @@ proc testEnvOverride() =
m[LeadGainId] == rmBoth and m[LeadGainId] == rmBoth and
vBulletAdmitted(LeadGainId, rm1v1, m, true) vBulletAdmitted(LeadGainId, rm1v1, m, true)
check "env: admitting LEADGAIN leaves Pattern as the only other member", check "env: admitting LEADGAIN leaves Pattern as the only other member",
admittedGuns(18, rm1v1, m) == @[PatternId, LeadGainId] admittedGuns(NumRackGuns, rm1v1, m) == @[PatternId, LeadGainId]
clearRackEnv() clearRackEnv()
proc testLazyAndRngClean() = proc testLazyAndRngClean() =
+12 -12
View File
@@ -158,14 +158,14 @@ proc testDefaultsOnlyPattern() =
DefaultRackMembership[12] == rmOff and DefaultRackMembership[13] == rmOff and DefaultRackMembership[12] == rmOff and DefaultRackMembership[13] == rmOff and
DefaultRackMembership[14] == rmOff DefaultRackMembership[14] == rmOff
check "defaults: the default rack admits ONLY Pattern in 1v1", check "defaults: the default rack admits ONLY Pattern in 1v1",
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId] admittedGuns(17, rm1v1, DefaultRackMembership) == @[PatternId]
check "defaults: the default rack admits ONLY Pattern in melee", check "defaults: the default rack admits ONLY Pattern in melee",
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId] admittedGuns(17, rmMelee, DefaultRackMembership) == @[PatternId]
let loaded = loadRackMembership() let loaded = loadRackMembership()
check "defaults: with a clean environment loadRackMembership() == shipped table", check "defaults: with a clean environment loadRackMembership() == shipped table",
loaded == DefaultRackMembership loaded == DefaultRackMembership
check "defaults: RackGunNames covers the shipped 18-gun rack", check "defaults: RackGunNames covers the shipped 17-gun rack",
RackGunNames.len == 18 and DefaultRackMembership.len == 18 RackGunNames.len == 17 and DefaultRackMembership.len == 17
proc testFloorRespectsAdmission() = proc testFloorRespectsAdmission() =
## The FLOOR path (`bestRate <= 0` or below the floor rate) historically fell ## The FLOOR path (`bestRate <= 0` or below the floor rate) historically fell
@@ -204,23 +204,23 @@ proc testRevertOverrideRestoresFullRack() =
## stays `off`). This must reproduce the old full rack without a rebuild. ## stays `off`). This must reproduce the old full rack without a rebuild.
clearRackEnv() clearRackEnv()
putEnv("TR_RACK_PATTERN", "both") putEnv("TR_RACK_PATTERN", "both")
# The ADE+SBC gun (id 17) additionally needs TR_BITBRAIN_NET=1; without the
# switch TR_RACK_BITBRAIN is the LEGACY name for LEADGAIN (id 16), so the
# documented one-liner has to flip the namespace over as well.
putEnv("TR_BITBRAIN_NET", "1")
for name in RackGunNames: for name in RackGunNames:
if name != "TMPATTERN": putEnv("TR_RACK_" & name, "both") if name != "TMPATTERN": putEnv("TR_RACK_" & name, "both")
let m = loadRackMembership() let m = loadRackMembership()
delEnv("TR_BITBRAIN_NET")
var full = true var full = true
for i in 0..<RackGunNames.len: for i in 0..<RackGunNames.len:
let want = if i == 14: rmOff else: rmBoth let want = if i == 14: rmOff else: rmBoth
if m[i] != want: full = false if m[i] != want: full = false
check "revert: the documented one-liner restores the all-`both` full rack", full check "revert: the documented one-liner restores the all-`both` full rack", full
check "revert: 1v1 rack admits every gun again (TMPATTERN excluded)", check "revert: 1v1 rack admits every gun again (TMPATTERN excluded)",
admittedGuns(18, rm1v1, m) == @[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17] admittedGuns(17, rm1v1, m) == @[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16]
check "revert: the legacy TR_RACK_BITBRAIN alone still selects LEADGAIN only", clearRackEnv()
loadRackMembership()[17] == rmOff # The ADE+SBC gun (old rack id 17) is RETIRED, so `TR_RACK_BITBRAIN` is now
# unambiguously LEADGAIN's old name: alone, it admits LEADGAIN and nothing else.
putEnv("TR_RACK_BITBRAIN", "both")
let mLegacy = loadRackMembership()
check "revert: the legacy TR_RACK_BITBRAIN alone selects LEADGAIN (16) only",
mLegacy[16] == rmBoth and admittedGuns(17, rm1v1, mLegacy) == @[5, 16]
clearRackEnv() clearRackEnv()
proc testEnvOverrides() = proc testEnvOverrides() =
+30 -14
View File
@@ -14,17 +14,23 @@
## The fix derives every gun-indexed width from `NumRackGuns` (== `len(RackGunNames)`). ## The fix derives every gun-indexed width from `NumRackGuns` (== `len(RackGunNames)`).
## This test is the guard that would have caught it: ## This test is the guard that would have caught it:
## ##
## A. rack table — `NumRackGuns` is 18, 18 DISTINCT names, and the ## A. rack table — `NumRackGuns` is 17, 17 DISTINCT names, and the
## membership table and the legacy alias target are in range; ## membership table and the legacy alias target are in range;
## B. source shape — `ModularBot.nim` contains NO literal width/loop bound ## B. source shape — `ModularBot.nim` contains NO literal width/loop bound
## narrower than `NumRackGuns` for the gun-indexed ## narrower than `NumRackGuns` for the gun-indexed
## accounting, and sizes them with `NumRackGuns`; ## accounting, and sizes them with `NumRackGuns`;
## C. accounting — an in-process replay of the real shape (`array[NumRackGuns]` ## C. accounting — an in-process replay of the real shape (`array[NumRackGuns]`
## indexed by every admitted id) never goes out of range, ## indexed by every admitted id) never goes out of range,
## and gun 17 IS accounted; ## and the newest id IS accounted, even when the rack
## grows by one more gun;
## D. live (opt-in) — with a rack that admits ONLY the newest id, the bot ## D. live (opt-in) — with a rack that admits ONLY the newest id, the bot
## selects it, fires and lands hits (skipped without JARs). ## selects it, fires and lands hits (skipped without JARs).
## ##
## The ADE+SBC gun (rack id 17) was later RETIRED, so the rack is back to 17 and
## the newest id is LEADGAIN (16). The guard is unchanged in shape and in
## strength: C3 now proves the fix survives a rack that grows again, which is
## the general form of the bug that shipped.
##
## Run: nim c -r common_libs/tests/test_rack_stat_width.nim ## Run: nim c -r common_libs/tests/test_rack_stat_width.nim
## Live: TR_SERVER_JAR=... TR_BATTLE_RUNNER=... nim c -r common_libs/tests/test_rack_stat_width.nim ## Live: TR_SERVER_JAR=... TR_BATTLE_RUNNER=... nim c -r common_libs/tests/test_rack_stat_width.nim
@@ -41,7 +47,7 @@ const
adversaryDir = repoRoot / "common_libs" / "test_framework" / "adversaries" / "SittingDuck" adversaryDir = repoRoot / "common_libs" / "test_framework" / "adversaries" / "SittingDuck"
statsPath = "/tmp/rack_stat_width_stats.jsonl" statsPath = "/tmp/rack_stat_width_stats.jsonl"
PatternId = 5 PatternId = 5
NewestId = NumRackGuns - 1 ## 17, the id that regressed NewestId = NumRackGuns - 1 ## 16 (LEADGAIN), the newest rack id
LegacyLeadGainId = 16 LegacyLeadGainId = 16
var failures = 0 var failures = 0
@@ -53,7 +59,7 @@ proc check(name: string, ok: bool) =
proc testRackTable() = proc testRackTable() =
check "A1: NumRackGuns is derived from RackGunNames", check "A1: NumRackGuns is derived from RackGunNames",
NumRackGuns == RackGunNames.len and NumRackGuns == 18 NumRackGuns == RackGunNames.len and NumRackGuns == 17
check "A2: rack membership table has one entry per gun", check "A2: rack membership table has one entry per gun",
DefaultRackMembership.len == NumRackGuns DefaultRackMembership.len == NumRackGuns
var seen = initTable[string, bool]() var seen = initTable[string, bool]()
@@ -156,21 +162,31 @@ proc testAccounting() =
if gunRealHits[NewestId] != 50: inRange = false if gunRealHits[NewestId] != 50: inRange = false
check "C2: accounting stays in range and records the newest gun", inRange check "C2: accounting stays in range and records the newest gun", inRange
# The bug in one line: with the OLD `array[17, int]`, `NewestId` is out of # The bug in one line, in its GENERAL form: an accounting array hard-wired to
# range. Prove the id that regressed is exactly the one that overflowed. # a literal is out of range the moment the rack grows. Prove the fix
check "C3: the regressed id (17) is out of range of the old array[17, int]", # (`NumRackGuns`) tracks a rack that grows by one more gun, where the id that
NewestId >= 17 # regressed in j141 (17) would be a legal, accounted index.
var grown: array[NumRackGuns + 1, int]
var grownOk = true
for gid in 0 .. NumRackGuns:
try:
inc grown[gid]
except IndexDefect:
grownOk = false
check "C3: a rack one gun WIDER is still fully covered by the NumRackGuns width",
grownOk and grown[NumRackGuns] == 1
# The legacy namespace: TR_RACK_BITBRAIN with TR_BITBRAIN_NET unset addresses # The legacy namespace: TR_RACK_BITBRAIN is now unambiguously LEADGAIN's old
# LEADGAIN (16), and id 17 is NOT admitted. # name (the ADE+SBC gun that briefly owned it was retired), so it always
delEnv("TR_BITBRAIN_NET") # admits LEADGAIN (16) — the newest id — and nothing else.
for n in RackGunNames: delEnv("TR_RACK_" & n) for n in RackGunNames: delEnv("TR_RACK_" & n)
delEnv("TR_BITBRAIN_NET")
putEnv("TR_RACK_BITBRAIN", "both") putEnv("TR_RACK_BITBRAIN", "both")
let legacy = loadRackMembership() let legacy = loadRackMembership()
check "C4: legacy TR_RACK_BITBRAIN admits LEADGAIN (16)", check "C4: legacy TR_RACK_BITBRAIN admits LEADGAIN (16)",
legacy[LegacyLeadGainId] == rmBoth legacy[LegacyLeadGainId] == rmBoth
check "C5: legacy namespace never admits id 17", check "C5: legacy TR_RACK_BITBRAIN alone adds LEADGAIN to the shipped Pattern",
legacy[NewestId] == rmOff admittedGuns(NumRackGuns, rm1v1, legacy) == @[PatternId, LegacyLeadGainId]
delEnv("TR_RACK_BITBRAIN") delEnv("TR_RACK_BITBRAIN")
# ── D. live proof, only with the JARs ──────────────────────────────────────── # ── D. live proof, only with the JARs ────────────────────────────────────────
@@ -203,7 +219,7 @@ proc live() =
var linesOut: seq[string] var linesOut: seq[string]
for l in readFile(backup).splitLines(): for l in readFile(backup).splitLines():
if l.strip().startsWith("GUN_STATS_PATH"): linesOut.add "GUN_STATS_PATH=" & statsPath if l.strip().startsWith("GUN_STATS_PATH"): linesOut.add "GUN_STATS_PATH=" & statsPath
elif l.strip().startsWith("TR_RACK_BITBRAIN"): linesOut.add "TR_RACK_BITBRAIN=both" elif l.strip().startsWith("TR_RACK_BITBRAIN"): linesOut.add "TR_RACK_LEADGAIN=both"
else: linesOut.add l else: linesOut.add l
writeFile(dotenv, linesOut.join("\n")) writeFile(dotenv, linesOut.join("\n"))
echo "NOTE: live section wrote a temporary out/.env (backup at out/.env.j141-backup)." echo "NOTE: live section wrote a temporary out/.env (backup at out/.env.j141-backup)."
@@ -62,7 +62,7 @@ proc seedOldRack(t: var VirtualTracker, targetId: int) =
# ── registration table ──────────────────────────────────────────────────────── # ── registration table ────────────────────────────────────────────────────────
proc testTable() = proc testTable() =
check "rack: RackGunNames has 18 entries", RackGunNames.len == 18 check "rack: RackGunNames has 17 entries", RackGunNames.len == 17
check "rack: the new gun is named TMPATTERN at id 14", check "rack: the new gun is named TMPATTERN at id 14",
RackGunNames[TmPatternId] == "TMPATTERN" RackGunNames[TmPatternId] == "TMPATTERN"
check "rack: the new gun defaults to `off`", check "rack: the new gun defaults to `off`",
@@ -82,9 +82,9 @@ proc oldRackMembership(): array[15, RackMembership] =
proc testDefaultAdmitsOnlyPattern() = proc testDefaultAdmitsOnlyPattern() =
check "default membership admits only PATTERN (1v1)", check "default membership admits only PATTERN (1v1)",
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId] admittedGuns(NumRackGuns, rm1v1, DefaultRackMembership) == @[PatternId]
check "default membership admits only PATTERN (melee)", check "default membership admits only PATTERN (melee)",
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId] admittedGuns(NumRackGuns, rmMelee, DefaultRackMembership) == @[PatternId]
proc testEnvOverride() = proc testEnvOverride() =
for name in RackGunNames: delEnv("TR_RACK_" & name) for name in RackGunNames: delEnv("TR_RACK_" & name)
+74
View File
@@ -626,3 +626,77 @@ should be chosen for **plausible large effect**, not for a sub-MDE gain slope.
`python3 tools/ab/ab_range_bands.py /tmp/ab_leadgain --reference control`. `python3 tools/ab/ab_range_bands.py /tmp/ab_leadgain --reference control`.
Liveness from `<arm>/run<N>.bot.stdout.log` (`[env]`), applied gain from the Liveness from `<arm>/run<N>.bot.stdout.log` (`[env]`), applied gain from the
same file (`[bb]`). Fixtures: `/tmp/ab_leadgain` (ephemeral, as is the corpus). same file (`[bb]`). Fixtures: `/tmp/ab_leadgain` (ephemeral, as is the corpus).
---
## RETIRED: the ADE+SBC gun *(rack id 17, `guns/bitbrain_net.nim` — REMOVED)*
**The owner's decision (verbatim):** *"not learning, i watched it, throw it away
and we forget about it"*. This section is the tombstone. **Nothing below is
rewritten history** — Phases 0-2 above stand as written. The ADE+SBC gun
(`common_libs/guns/bitbrain_net.nim`, rack id 17, env `TR_BITBRAIN_NET` +
`TR_BITBRAIN_*`, log tag `[bbn]`) and its 44-check test and its scaling harness
are **DELETED from the tree**. The generic SBC/ADE **library** at
`common_libs/bitbrain/` **STAYS** — it is not the gun, and
`common_libs/movements/learned_surfer.nim` still imports `bitbrain/sbc` (still
56 checks in `test_bitbrain.nim`).
### (a) The offline sweep: more RAM, no better aim — and never better than Pattern
The scaling sweep replayed 3 recorded live runs through the real gun across a
**100x RAM range (0.10 -> 12.60 MB)**. Mean |angular error| against the true
interception point moved only:
| RAM (MB) | mean \|err\| (deg) |
|---|---|
| 0.10 | 17.254 |
| … (intermediate arms) | monotonically ↓ by <0.2 deg total |
| 12.60 | 17.115 |
**Pattern, on the same corpus, scores 16.964 deg.** So the ADE+SBC gun was
**consistently slightly WORSE than Pattern at EVERY capacity** — a 126x RAM
budget bought 0.139 deg, and the endpoint was still 0.151 deg *behind* the gun
it was correcting. The measured scaling confirmed the shape of the cost but not
the shape of the benefit: RAM is **linear in `nClasses`**, **quadratic in
`nAde`**, and **7.3x for counted mode** over bitset.
### (b) The budget made 360 classes unreachable anyway
At **64 classes** the gun already cost **19.56 ms/tick = 149% of the project's
13.16 ms/tick budget** — over budget before the interesting settings were
reached. The configuration the design actually wanted (360 classes) was
therefore **never affordable**, whatever its quality.
### (c) The owner's live observation: it does not learn within a round
The live run settled it: **400 virtual shots, 0 hits.** Not a weak learner, not a
tuned one — no movement of the counts inside a round. The owner watched it and
called it.
### (d) Same information ceiling as the gate test
This is the **same ~1-bit information ceiling** measured in
`docs/bitbrain_gate_test.md`: ~55 000 samples cannot separate 16-64 correction
classes that must differ by 1-2 deg, the SBC is idempotent so the per-class
counts blur toward uniform, and the readout regresses to the mean. Phase 0's
gains sweep (above) found the same thing from the amplitude side.
### (e) Status: the gun is removed, the library is not
* **REMOVED:** `common_libs/guns/bitbrain_net.nim`,
`common_libs/guns/bitbrain_net.README.md`,
`common_libs/tests/test_bitbrain_net.nim`,
`common_libs/tests/measure_bitbrain_scaling.nim`, rack id 17 and its whole
admit/dispatch/stat-width plumbing, the `bbn` gun field in `ModularBot.nim`,
the `TR_BITBRAIN_NET` switch and the NEW-NETWORK `TR_BITBRAIN_*` knob names,
and the `BitBrainNet` arm of `run_prediction_quality.nim`.
* **KEPT:** `common_libs/bitbrain/` (the SBC library, used by
`learned_surfer`), and the `c9b6753` crash fix (`NumRackGuns`-derived
per-gun array widths + `test_rack_stat_width.nim`).
* **SIMPLIFICATION:** with nothing left to disambiguate, the legacy namespace is
the ONLY namespace. `TR_RACK_BITBRAIN` always selects **id 16 LEADGAIN**, and
every `TR_BITBRAIN_<X>` in the frozen 14-suffix alias set always means
`TR_LEADGAIN_<X>`. A stale `TR_BITBRAIN_NET=1` in an old `.env` is now an
unrecognised variable: the boot report warns and ignores it. See
`common_libs/guns/lead_gain.README.md` and
`common_libs/tests/test_lead_gain_legacy.nim`.
+15
View File
@@ -1153,3 +1153,18 @@ round-boundary bug and its fix, all 4 forced arms' applied shares (exactly
per-opponent delta, CI, MDE, sign and sign-flip test, and the policy-isolation per-opponent delta, CI, MDE, sign and sign-flip test, and the policy-isolation
comparison. **INFERRED:** that no share schedule whatsoever could ever help comparison. **INFERRED:** that no share schedule whatsoever could ever help
(this batch bounds only the shares it tested, at this MDE and panel). (this batch bounds only the shares it tested, at this MDE and panel).
---
## Cross-reference: the ADE+SBC gun (rack id 17) is RETIRED
The `bitbrain` arm named in earlier batches of this ledger is NOT the gun
shipped at id 16. The **ADE+SBC gun** that briefly held the `BITBRAIN` rack name
and the `TR_BITBRAIN_*` env prefix at **rack id 17** was **retired and removed
from the tree** (owner: *"not learning, i watched it, throw it away and we
forget about it"*). Its tombstone, evidence and removal list are in
`docs/bitbrain_campaign.md` §RETIRED. The `bitbrain` verdict in this ledger —
nothing beat Pattern — is unchanged; this line only records that the id-17
experiment that followed it is closed, so the two ledgers do not contradict
each other. The surviving "BitBrain-named" capability is the id-16 lead-gain
corrector, now called **LEADGAIN**.