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:
@@ -31,7 +31,6 @@ import guns/tm_selector
|
||||
import guns/tm_pattern
|
||||
import guns/tm_horizon
|
||||
import guns/lead_gain
|
||||
import guns/bitbrain_net
|
||||
import movements/phantom_meteor
|
||||
import movements/rammer
|
||||
import movements/ram_decision
|
||||
@@ -151,7 +150,7 @@ let ResultLog* =
|
||||
block:
|
||||
let v = getEnv("TR_RESULT_LOG", "1").strip().toLowerAscii()
|
||||
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`;
|
||||
## 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
|
||||
## 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
|
||||
## byte-for-byte unchanged. `TR_RACK_BITBRAIN` still selects it while
|
||||
## `TR_BITBRAIN_NET` is off (backward compatibility; see `gun_harness/selector`).
|
||||
## byte-for-byte unchanged. `TR_RACK_BITBRAIN` still selects it unconditionally
|
||||
## (backward compatibility; see `gun_harness/selector`).
|
||||
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*) ───────────────────────
|
||||
## OFF by default: the shipped bot draws nothing. When on, the tracker's
|
||||
## virtual bullets are overlaid so the training signal the selector learns
|
||||
@@ -257,7 +246,6 @@ type
|
||||
tmPattern: TmPatternGun
|
||||
tmHorizon: TmHorizonGun
|
||||
leadGain: LeadGainGun
|
||||
bbn: BitbrainNetGun
|
||||
mover: TFILModule
|
||||
ringMover: TFILRingModule
|
||||
strafeMover: StrafeModule
|
||||
@@ -961,7 +949,6 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
# LEADGAIN: per-round wipe always clears observations/labels; `perRound` mode
|
||||
# also wipes its SBCs (the gate test's winning regime).
|
||||
bot.leadGain.resetRoundState()
|
||||
bot.bbn.resetRoundState()
|
||||
if e.roundNumber <= 1:
|
||||
bot.leadGain.resetLearning("round1")
|
||||
bot.isRamming = false
|
||||
@@ -1044,7 +1031,6 @@ method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
|
||||
# boundary. The round-1 fallback in `onRoundStarted` covers a missed callback.
|
||||
bot.tmHorizon.resetLearning("game_start")
|
||||
bot.leadGain.resetLearning("game_start")
|
||||
bot.bbn.resetLearning("game_start")
|
||||
# minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1
|
||||
bot.initialEnemyCount = e.gameSetup.minNumberOfParticipants - 1
|
||||
# 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).
|
||||
discard bot.tmHorizon.targetChanged(candidateId)
|
||||
discard bot.leadGain.targetChanged(candidateId)
|
||||
discard bot.bbn.targetChanged(candidateId)
|
||||
if bot.currentTargetId >= 0:
|
||||
bot.lastKnownTargetId = bot.currentTargetId
|
||||
|
||||
@@ -1298,7 +1283,6 @@ method run*(bot: ModularBot) =
|
||||
var tmpPreds: array[len(PowerBins), GunPrediction]
|
||||
var tmhPreds: array[len(PowerBins), GunPrediction]
|
||||
var lgPreds: array[len(PowerBins), GunPrediction]
|
||||
var bbnPreds: array[len(PowerBins), GunPrediction]
|
||||
# ── TR_VBULLET_ADMIT_ONLY gate ──────────────────────────────────────
|
||||
# Rack membership used to filter only SELECTION, so every unselected gun
|
||||
# still ran predict()+spawnBullets() each tick to feed a fitness table the
|
||||
@@ -1315,18 +1299,7 @@ method run*(bot: ModularBot) =
|
||||
admit[gi] = ModuleVBullets and
|
||||
vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership,
|
||||
VBulletAdmitOnly or gi == TmPatternId or
|
||||
gi == TmHorizonId or gi == LeadGainId or
|
||||
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))
|
||||
gi == TmHorizonId or gi == LeadGainId)
|
||||
for i in 0..<len(PowerBins):
|
||||
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]))
|
||||
@@ -1352,9 +1325,6 @@ method run*(bot: ModularBot) =
|
||||
# LEADGAIN: Pattern base, lead scaled by a per-band learned gain.
|
||||
if admit[LeadGainId]:
|
||||
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[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)
|
||||
if admit[LeadGainId] and not gunDisabled(LeadGainId):
|
||||
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
|
||||
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 TmHorizonId: bot.tmHorizon.onResult(fe)
|
||||
of LeadGainId: bot.leadGain.onResult(fe)
|
||||
of BitbrainNetId: bot.bbn.onResult(fe)
|
||||
else: discard
|
||||
if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss
|
||||
if DebugDrawOn and VBulletDebugOn:
|
||||
@@ -1472,7 +1439,6 @@ method run*(bot: ModularBot) =
|
||||
of TmPatternId: bot.tmPattern.predict(bot.lastState, bulletSpeed(power))
|
||||
of TmHorizonId: bot.tmHorizon.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))
|
||||
let aimTarget = aimAngle(getX(), getY(), pred.x, pred.y)
|
||||
|
||||
@@ -1540,7 +1506,7 @@ proc seedSelectorRng() =
|
||||
|
||||
when isMainModule:
|
||||
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(),
|
||||
linear: LinearGun(),
|
||||
circular: CircularGun(),
|
||||
@@ -1558,7 +1524,6 @@ when isMainModule:
|
||||
tmPattern: initTmRadialGun(),
|
||||
tmHorizon: initTmHorizonGun(),
|
||||
leadGain: initLeadGainGun(),
|
||||
bbn: initBitbrainNetGun(),
|
||||
radar: RadarLockModule(),
|
||||
meleeRadar: initAdaptiveMeleeRadar(),
|
||||
mover: TFILModule(debugGraphics: DebugDrawOn),
|
||||
@@ -1598,6 +1563,5 @@ when isMainModule:
|
||||
tmHorizon: bot.tmHorizon,
|
||||
patternMatcher: bot.patternMatcher,
|
||||
leadGain: bot.leadGain,
|
||||
bitbrainNet: bot.bbn,
|
||||
))
|
||||
start(bot, botJsonPath)
|
||||
|
||||
@@ -32,7 +32,6 @@ import movements/wave_surfer
|
||||
import movements/learned_surfer
|
||||
import guns/tm_horizon
|
||||
import guns/lead_gain
|
||||
import guns/bitbrain_net
|
||||
import guns/pattern_matcher
|
||||
import vbullet_draw
|
||||
import geo_overlay
|
||||
@@ -66,7 +65,6 @@ type
|
||||
disabledGuns*: HashSet[int]
|
||||
tmHorizon*: TmHorizonGun
|
||||
leadGain*: LeadGainGun
|
||||
bitbrainNet*: BitbrainNetGun
|
||||
patternMatcher*: PatternMatcherGun
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -117,8 +115,8 @@ proc sourceOfPresence(name: string): string =
|
||||
|
||||
proc legacyNameFor(name: string): string =
|
||||
## 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.
|
||||
if netSwitchOwnsBitbrainName(): return ""
|
||||
## the knob has no legacy name. This gun is the sole owner of the
|
||||
## `TR_BITBRAIN_*` prefix (the ADE+SBC gun that briefly shared it was retired).
|
||||
if not name.startsWith(NewPrefix): return ""
|
||||
let suffix = name[NewPrefix.len .. ^1]
|
||||
for s in LegacyKnobEnvNames:
|
||||
@@ -141,9 +139,6 @@ proc sourceOfLgPresence(name: string): string =
|
||||
else:
|
||||
sourceOfPresence(name)
|
||||
|
||||
proc sbcModeName(m: SbcMode): string {.inline.} =
|
||||
(if m == smCounted: "counted" else: "bitset")
|
||||
|
||||
proc disabledGunsString(guns: HashSet[int]): string =
|
||||
if guns.len == 0: return "none"
|
||||
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_RESET_ON_TARGET", onOff(lg.resetOnTarget),
|
||||
sourceOfLg(LG_RESET_ON_TARGET_ENV))
|
||||
# The disambiguation switch + the deprecation line it gates, in the report so
|
||||
# an owner staring at a `[depr]` line can see exactly why it fired.
|
||||
emit("TR_BITBRAIN_NET", onOff(netSwitchOwnsBitbrainName()),
|
||||
sourceOfPresence("TR_BITBRAIN_NET"))
|
||||
# The deprecation line the legacy alias layer can print, in the report so an
|
||||
# owner staring at a `[depr]` line can see exactly which knob fired.
|
||||
let dep = lgDeprecationLine()
|
||||
if dep.len > 0: emit("legacy TR_BITBRAIN_* aliases honoured", "yes", "legacy")
|
||||
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 ────────────────────────────────────
|
||||
# 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.
|
||||
@@ -514,7 +481,7 @@ proc printModules(ctx: EnvReportContext) =
|
||||
# mode = enabled in that rack.
|
||||
for i in 0..<len(RackGunNames):
|
||||
var src = sourceOf(RackEnvPrefix & RackGunNames[i])
|
||||
if src == "default" and not netSwitchOwnsBitbrainName():
|
||||
if src == "default":
|
||||
for (key, gid) in RackLegacyAlias:
|
||||
if gid == i and getEnv(key, "").len > 0:
|
||||
src = if isFromEnvFile(key): "legacy/.env" else: "legacy"
|
||||
@@ -621,26 +588,28 @@ proc knownEnvNames*(): seq[string] =
|
||||
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_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,
|
||||
# LEGACY aliases of the corrector knobs above (honoured while
|
||||
# LG_NET_SWITCH_ENV is off). Registered so the tree-scan guard does not
|
||||
# flag the owner's pre-rename .env as unknown.
|
||||
LG_DECAY_FRAC_ENV, LG_SEED_ENV, LG_RESET_ON_TARGET_ENV,
|
||||
# LEGACY aliases of the corrector knobs above, honoured UNCONDITIONALLY
|
||||
# (this gun is the sole owner of the `TR_BITBRAIN_*` prefix). Registered so
|
||||
# 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_DECAY", "TR_BITBRAIN_DECAY_FRAC", "TR_BITBRAIN_LOG",
|
||||
"TR_BITBRAIN_RESET_ON_TARGET", "TR_BITBRAIN_N", "TR_BITBRAIN_NADE",
|
||||
"TR_BITBRAIN_RANGE", "TR_BITBRAIN_WARMUP", "TR_BITBRAIN_ADAPT",
|
||||
"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
|
||||
for g in RackGunNames:
|
||||
result.add RackEnvPrefix & g
|
||||
# NOTE: the legacy rack alias `TR_RACK_BITBRAIN` needs no extra entry — it is
|
||||
# the CURRENT name of rack id 17, so the RackGunNames loop above already
|
||||
# registers it. See gun_harness/selector's RackLegacyAlias for how it is
|
||||
# routed to LEADGAIN while LG_NET_SWITCH_ENV is off.
|
||||
# the legacy rack alias `TR_RACK_BITBRAIN` is no longer in `RackGunNames`
|
||||
# (it is LEADGAIN's old name), so register it explicitly. See
|
||||
# gun_harness/selector's RackLegacyAlias.
|
||||
for (key, _) in RackLegacyAlias:
|
||||
result.add key
|
||||
# inline reads with no exported constant (the guard test scans for these)
|
||||
for n in [
|
||||
"GUN_SELECTOR_WINDOW", "GUN_SELECTOR_MINOBS", "GUN_SELECTOR_TIE",
|
||||
|
||||
@@ -22,7 +22,6 @@ import virtual_bullets
|
||||
# TR_RACK_KNN=off -> removed from both racks
|
||||
# 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_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
|
||||
# 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_WALLBOUNCE=both TR_RACK_ACCEL=both TR_RACK_STOPSHOT=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_BITBRAIN=both TR_BITBRAIN_NET=1 ./ModularBot
|
||||
# TR_RACK_KNN=both TR_RACK_TMSELECT=both TR_RACK_LEADGAIN=both ./ModularBot
|
||||
#
|
||||
# The mode itself is derived from SERVER truth (`getEnemyCount()`), never from
|
||||
# the tracker's known-enemy count, by `rackMode` in virtual_bullets — the same
|
||||
# transition the radar uses.
|
||||
|
||||
const
|
||||
RackGunNames*: array[18, string] = [
|
||||
RackGunNames*: array[17, string] = [
|
||||
"HEADON", "LINEAR", "TSETLIN", "CIRCULAR", "GUESSFACTOR", "PATTERN",
|
||||
"WALLBOUNCE", "ACCEL", "STOPSHOT", "DISPLACE", "AVGLEAD", "DECAYGF",
|
||||
"KNN", "TMSELECT", "TMPATTERN", "TMHORIZON", "LEADGAIN", "BITBRAIN"]
|
||||
"KNN", "TMSELECT", "TMPATTERN", "TMHORIZON", "LEADGAIN"]
|
||||
RackEnvPrefix* = "TR_RACK_"
|
||||
## 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
|
||||
@@ -64,8 +62,10 @@ const
|
||||
## (`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
|
||||
## 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
|
||||
## now belong to the ADE+SBC gun `guns/bitbrain_net.nim` (rack id 17).
|
||||
## literals still holds. It OWNS the `BITBRAIN` name and the `TR_BITBRAIN_*`
|
||||
## 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;
|
||||
## every other gun is `off`. The selection mechanism is untouched and remains
|
||||
## 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
|
||||
## VirtualTracker ring head — and every other gun's learning order — is
|
||||
## unchanged.
|
||||
DefaultRackMembership*: array[18, RackMembership] = [
|
||||
DefaultRackMembership*: array[17, RackMembership] = [
|
||||
rmOff, # 0 HEADON — off (measured: worst over-selected gun)
|
||||
rmOff, # 1 LINEAR — off
|
||||
rmOff, # 2 TSETLIN — off
|
||||
@@ -91,41 +91,23 @@ const
|
||||
rmOff, # 13 TMSELECT — off
|
||||
rmOff, # 14 TMPATTERN — off (already shipped off; TM pattern gun)
|
||||
rmOff, # 15 TMHORIZON — off (horizon-based TM corrector; expected to lose)
|
||||
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)
|
||||
rmOff] # 16 LEADGAIN — off (per-range-band lead-gain corrector)
|
||||
## 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
|
||||
## things that admit those guns and an unset environment is byte-for-byte the
|
||||
## shipped Pattern-only rack.
|
||||
## wiring, so `TR_RACK_LEADGAIN=both` (or the legacy `TR_RACK_BITBRAIN=both`)
|
||||
## are the ONLY things that admit that gun, and an unset environment is
|
||||
## byte-for-byte the shipped Pattern-only rack.
|
||||
|
||||
const
|
||||
## ── BACKWARD COMPATIBILITY: the legacy rack knob names ────────────────────
|
||||
## `TR_RACK_<gun name>` is derived from `RackGunNames`, so renaming a gun
|
||||
## 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.
|
||||
## `TR_RACK_BITBRAIN` is the ONE genuinely ambiguous legacy name — the rack is
|
||||
## keyed by gun name, and the new ADE+SBC gun is now the one called
|
||||
## `BITBRAIN`. It is disambiguated by the same switch the knobs use,
|
||||
## `TR_BITBRAIN_NET` (default 0):
|
||||
## * 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.)
|
||||
## `TR_RACK_BITBRAIN` is the old name of the gun that is now called
|
||||
## `LEADGAIN` (id 16) — and with the ADE+SBC gun (old id 17) retired, nothing
|
||||
## else can claim it, so the mapping is UNCONDITIONAL. There is no namespace
|
||||
## switch any more: `TR_RACK_BITBRAIN` always selects LEADGAIN.
|
||||
RackLegacyAlias*: array[1, (string, int)] = [("TR_RACK_BITBRAIN", 16)]
|
||||
RackLegacyAliasGunName* = "LEADGAIN" ## what the legacy name selects today
|
||||
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
|
||||
RackLegacyAliasGunName* = "LEADGAIN" ## what the legacy name selects
|
||||
|
||||
proc parseRackMembership*(value: string): RackMembership =
|
||||
## Parse a `TR_RACK_<GUN>` value. Empty / unknown values fall back to the
|
||||
@@ -148,17 +130,11 @@ proc loadRackMembership*(): array[len(RackGunNames), RackMembership] =
|
||||
## corresponding CURRENT name is unset, so a migrated config always wins.
|
||||
result = DefaultRackMembership
|
||||
var legacyTouched: seq[string]
|
||||
let netOwns = netSwitchOwnsBitbrainName()
|
||||
for i in 0..<len(RackGunNames):
|
||||
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, "")
|
||||
if v.len > 0:
|
||||
result[i] = parseRackMembership(v)
|
||||
if not netOwns:
|
||||
for (key, gid) in RackLegacyAlias:
|
||||
let current = RackEnvPrefix & RackGunNames[gid]
|
||||
if getEnv(key, "").len > 0 and getEnv(current, "").len == 0:
|
||||
@@ -167,9 +143,8 @@ proc loadRackMembership*(): array[len(RackGunNames), RackMembership] =
|
||||
if legacyTouched.len > 0:
|
||||
let gid = RackLegacyAlias[0][1]
|
||||
stderr.writeLine("[depr] legacy rack knob " & legacyTouched.join(",") &
|
||||
" now names the ADE+SBC gun (BITBRAIN, rack id 17); it still selects " &
|
||||
RackGunNames[gid] & " (rack id " & $gid &
|
||||
") until TR_BITBRAIN_NET=1. Set TR_RACK_" & RackGunNames[gid] &
|
||||
" is the old name of TR_RACK_" & RackGunNames[gid] & " (rack id " & $gid &
|
||||
"); it still selects it. Set TR_RACK_" & RackGunNames[gid] &
|
||||
" to make it explicit.")
|
||||
|
||||
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
|
||||
# over the named, ADMITTED guns instead of `chooseFromFit`'s ranking.
|
||||
#
|
||||
# TR_RACK_SHARE=pattern:50,bitbrain:50
|
||||
# TR_RACK_SHARE=pattern:0.7,bitbrain:0.3
|
||||
# TR_RACK_SHARE=pattern:50,leadgain:50
|
||||
# TR_RACK_SHARE=pattern:0.7,leadgain:0.3
|
||||
#
|
||||
# Values are RELATIVE weights (fractions or percentages — only the ratio
|
||||
# matters) and names are case-insensitive `RackGunNames` (the `GunNames` rack
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
`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
|
||||
for Pattern's lead, separately per range band.** The rack id is unchanged. The
|
||||
real ADE+SBC gun is `guns/bitbrain_net.nim` (rack id 17).
|
||||
for Pattern's lead, separately per range band.** The rack id is unchanged, and
|
||||
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`)
|
||||
|
||||
The `TR_BITBRAIN_*` names your `.env` already contains still work, and still
|
||||
select **this** gun. The disambiguation is one switch, `TR_BITBRAIN_NET`
|
||||
(default `0`):
|
||||
select **this** gun. There is nothing to disambiguate any more, so the alias
|
||||
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 |
|
||||
| `1` | the new ADE+SBC gun (rack id 17) |
|
||||
| `TR_BITBRAIN_<X>` (14 frozen suffixes) | always a legacy alias for `TR_LEADGAIN_<X>` |
|
||||
| `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
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
@@ -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_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_RACK_LEADGAIN` / `TR_RACK_BITBRAIN` | the rack admission switch |
|
||||
| `TR_BITBRAIN_NET` | `0` (default) keeps `TR_BITBRAIN_*` legacy; `1` hands the namespace to the new ADE+SBC gun |
|
||||
| `TR_RACK_LEADGAIN` / `TR_RACK_BITBRAIN` | the rack admission switch (the legacy name is unconditional) |
|
||||
|
||||
**INERT** — kept only so old configs and the boot report don't warn; never touch the gain learner:
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
## 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.**
|
||||
## `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
|
||||
## the separate `guns/bitbrain_net.nim` at rack id 17.
|
||||
## is UNCHANGED (many tests assert the id literals) and the `BITBRAIN` name is
|
||||
## 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) ────────────────────────────
|
||||
## 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
|
||||
## unchanged; they no longer affect the gain learner. The generic
|
||||
## `common_libs/bitbrain/` library is untouched and still tested by
|
||||
## `test_bitbrain.nim`, and the new ADE+SBC gun that actually uses it is
|
||||
## `guns/bitbrain_net.nim` (rack id 17).
|
||||
## `test_bitbrain.nim` — `movements/learned_surfer.nim` still imports
|
||||
## `bitbrain/sbc`, so the library is NOT the gun.
|
||||
##
|
||||
## ── BACKWARD COMPATIBILITY: the `TR_BITBRAIN_*` legacy aliases ───────────────
|
||||
## 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
|
||||
## the OLD corrector's knobs and MUST keep working unchanged. The
|
||||
## `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):
|
||||
## the OLD corrector's knobs and MUST keep working unchanged.
|
||||
##
|
||||
## * `TR_BITBRAIN_NET` UNSET / 0 → LEGACY MODE. Every `TR_BITBRAIN_<X>` name
|
||||
## listed in `LegacyKnobEnvNames` is a legacy alias for this gun's
|
||||
## `TR_LEADGAIN_<X>`, and the new ADE+SBC gun is OFF. This is the owner's
|
||||
## current configuration, so its behaviour is unchanged.
|
||||
## * `TR_BITBRAIN_NET=1` → NEW-NETWORK MODE. `TR_BITBRAIN_<X>` names
|
||||
## the NEW gun's knobs (see `bitbrain_net.nim`) and this gun reads ONLY
|
||||
## `TR_LEADGAIN_<X>`.
|
||||
##
|
||||
## 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`.
|
||||
## The `TR_BITBRAIN_*` prefix was briefly shared with the ADE+SBC gun, which
|
||||
## had a two-mode disambiguator, `TR_BITBRAIN_NET`. That gun was RETIRED and
|
||||
## removed (it did not learn; the owner watched it live and threw it away), so
|
||||
## the legacy namespace is now the ONLY namespace and the mapping is
|
||||
## UNCONDITIONAL: every `TR_BITBRAIN_<X>` in the frozen `LegacyKnobEnvNames`
|
||||
## set is an alias for `TR_LEADGAIN_<X>`, and `TR_RACK_BITBRAIN` always selects
|
||||
## 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.
|
||||
##
|
||||
## ── TR_LEADGAIN_GAINS (the candidate set as an env knob) ─────────────────────
|
||||
## `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_RANGE_ENV* = "TR_LEADGAIN_RANGE" ## (legacy class half-range; inert)
|
||||
LG_LOG_ENV* = "TR_LEADGAIN_LOG" ## 1 = per-change [lg] log
|
||||
## ── the one switch that disambiguates the legacy `TR_BITBRAIN_*` names ────
|
||||
## Read by BOTH guns (see `bitbrain_net.nim`). Unset/0 => the `TR_BITBRAIN_*`
|
||||
## names are LEGACY aliases for this gun; 1 => they belong to the new ADE+SBC
|
||||
## gun. It is also the new gun's master on/off switch.
|
||||
LG_NET_SWITCH_ENV* = "TR_BITBRAIN_NET"
|
||||
## ── the legacy `TR_BITBRAIN_*` namespace ──────────────────────────────────
|
||||
## This gun OWNS the `TR_BITBRAIN_*` prefix unconditionally (the ADE+SBC gun
|
||||
## that briefly shared it was retired), so the frozen suffix set below maps
|
||||
## every legacy name onto this gun's `TR_LEADGAIN_<X>` with no switch.
|
||||
LG_MIN_OBS_ENV* = "TR_LEADGAIN_MIN_OBS" ## samples before a band is trusted
|
||||
LG_WARMUP_ENV* = "TR_LEADGAIN_WARMUP" ## (legacy; inert)
|
||||
LG_ADAPT_ENV* = "TR_LEADGAIN_ADAPT" ## (legacy; inert)
|
||||
@@ -288,10 +281,9 @@ const
|
||||
LegacyPrefix* = "TR_BITBRAIN_"
|
||||
NewPrefix* = "TR_LEADGAIN_"
|
||||
## 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
|
||||
## other `TR_BITBRAIN_*` name belongs to the new ADE+SBC gun
|
||||
## (`bitbrain_net.nim`). The two sets are DISJOINT by construction, so the
|
||||
## mapping is total and deterministic — no name is claimed twice.
|
||||
## `TR_BITBRAIN_<X>` in this set is a legacy alias for `TR_LEADGAIN_<X>`.
|
||||
## This gun is the sole owner of the prefix (the ADE+SBC gun was retired), so
|
||||
## the mapping is total and unconditional — no name is claimed twice.
|
||||
LegacyKnobEnvNames* = [
|
||||
"GAINS", "MEM", "MIN_OBS", "DECAY", "DECAY_FRAC", "LOG", "RESET_ON_TARGET",
|
||||
"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
|
||||
## 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
|
||||
## two can never drift apart. It is the CURRENT name of rack id 17, which is
|
||||
## why it is not a distinct string.
|
||||
## two can never drift apart. It is NOT a current rack name any more (it was
|
||||
## `RackGunNames[RackNetGunId]` while the retired ADE+SBC gun existed).
|
||||
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
|
||||
|
||||
proc lgDeprecationLine*(): string =
|
||||
## The single clear deprecation line the owner sees. Names every legacy
|
||||
## `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.
|
||||
if netSwitchOn(): return ""
|
||||
var parts: seq[string]
|
||||
for suffix in LegacyKnobEnvNames:
|
||||
let old = LegacyPrefix & suffix
|
||||
@@ -329,17 +311,14 @@ proc lgDeprecationLine*(): string =
|
||||
if parts.len == 0: return ""
|
||||
result = "[depr] " & LegacyPrefix & "* is the OLD lead-gain corrector's namespace; " &
|
||||
"it was renamed to " & NewPrefix & "* (gun LEADGAIN, rack id 16). " &
|
||||
"Still honoured: " & parts.join("; ") &
|
||||
". The new ADE+SBC gun owns the " & LegacyPrefix &
|
||||
"* names once " & LG_NET_SWITCH_ENV & "=1."
|
||||
"Still honoured: " & parts.join("; ") & "."
|
||||
|
||||
proc lgEnv(name: string): string =
|
||||
## 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
|
||||
## wins when both are set, so a migrated config is authoritative.
|
||||
## `TR_BITBRAIN_<X>` alias. The NEW name always wins when both are set, so a
|
||||
## migrated config is authoritative.
|
||||
var v = getEnv(name, "")
|
||||
if v.len > 0: return v
|
||||
if netSwitchOn(): return ""
|
||||
let suffix = if name.startsWith(NewPrefix): name[NewPrefix.len .. ^1] else: ""
|
||||
if suffix.len == 0: return ""
|
||||
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()
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import std/[os, strformat, strutils, times, math]
|
||||
import gun_harness/[gun_interface, virtual_bullets, prediction_quality]
|
||||
import guns/[head_on, pattern_matcher, tm_horizon, lead_gain, bitbrain_net]
|
||||
import guns/[head_on, pattern_matcher, tm_horizon, lead_gain]
|
||||
|
||||
# arm indices (fixed order = fixed output)
|
||||
const
|
||||
@@ -43,16 +43,12 @@ const
|
||||
# is the rule the corrector must match; it needs no learning (range is known at fire
|
||||
# time). The table was selected in-sample from this corpus.
|
||||
A_BAND* = 13
|
||||
## The REAL ADE+SBC gun (rack id 17, `guns/bitbrain_net.nim`): Pattern base plus
|
||||
## a fine-grained, class-resolved angular correction from the network. The gun
|
||||
## is default OFF (`TR_BITBRAIN_NET=0`), so the arm turns its own switch on
|
||||
## here — the ruler is the one place that must exercise it.
|
||||
A_BBN* = 14
|
||||
BandGainTable* = [1.0, 1.0, 1.0, 0.0, 0.0]
|
||||
## (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",
|
||||
"PatternGain2.0", "PatternGain3.0", "NaiveLinear", "TMHorizon", "LeadGain",
|
||||
"PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain",
|
||||
"BitBrainNet"]
|
||||
"PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain"]
|
||||
|
||||
const
|
||||
## The five sub-unity gain arms, in increasing order, resolved to arm indices.
|
||||
@@ -102,7 +98,6 @@ type Ctx = object
|
||||
naive: NaiveLinearGun
|
||||
tmh: TmHorizonGun
|
||||
lg: LeadGainGun
|
||||
bbn: BitbrainNetGun
|
||||
headon: HeadOnGun
|
||||
st: WorldState
|
||||
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 bl = wrap180(bearingDeg(ox, oy, bp.x, bp.y) - los)
|
||||
arms[A_LG].record(rng, wrap180(bl - targetLead), bl, targetLead)
|
||||
# BITBRAIN (ADE+SBC): Pattern base + the network's fine-grained correction
|
||||
let np2 = predict(ctx.bbn, ctx.st, speed)
|
||||
let nl2 = wrap180(bearingDeg(ox, oy, np2.x, np2.y) - los)
|
||||
arms[A_BBN].record(rng, wrap180(nl2 - targetLead), nl2, targetLead)
|
||||
|
||||
proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var ShotStat,
|
||||
doShots: bool, timing: bool, cont: bool): int =
|
||||
@@ -197,7 +188,6 @@ proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var S
|
||||
naive: NaiveLinearGun(lastTick: -1),
|
||||
tmh: initTmHorizonGun(),
|
||||
lg: initLeadGainGun(),
|
||||
bbn: initBitbrainNetGun(),
|
||||
headon: HeadOnGun(),
|
||||
st: WorldState(arenaWidth: c.arenaW, arenaHeight: c.arenaH),
|
||||
enemy: newSeq[EnemyInfo](1))
|
||||
@@ -313,10 +303,9 @@ proc main() =
|
||||
let mPat = overallMean(arms, A_PATTERN)
|
||||
let mTmh = overallMean(arms, A_TMH)
|
||||
let mLg = overallMean(arms, A_LG)
|
||||
let mBbn = overallMean(arms, A_BBN)
|
||||
let mLin = overallMean(arms, A_NAIVE)
|
||||
let ordOk = mHead > mPat and mHead > mTmh and mHead > mLg and mHead > mBbn
|
||||
echo fmt"3. HeadOn (static LOS) mean|err| = {mHead:.3f} deg vs Pattern {mPat:.3f} / TMHorizon {mTmh:.3f} / LeadGain {mLg:.3f} / BitBrainNet {mBbn:.3f}"
|
||||
let ordOk = mHead > mPat and mHead > mTmh and mHead > mLg
|
||||
echo fmt"3. HeadOn (static LOS) mean|err| = {mHead:.3f} deg vs Pattern {mPat:.3f} / TMHorizon {mTmh:.3f} / LeadGain {mLg:.3f}"
|
||||
let ordMsg = if ordOk: "OK (static gun worst among real guns)" else: "UNEXPECTED: a predictive gun is worse than static LOS"
|
||||
echo fmt" -> {ordMsg}"
|
||||
echo fmt" NaiveLinear mean|err| = {mLin:.3f} deg (over-leads; see the lead-gain sweep for why a larger"
|
||||
@@ -335,7 +324,7 @@ proc main() =
|
||||
echo "=".repeat(120)
|
||||
echo "HEADROOM -- the direct answer: how far each arm is from the oracle ceiling, per band"
|
||||
echo "=" .repeat(120)
|
||||
let hdr = "band Pattern n Pattern|err| Pattern hpx Oracle hpx headroom pp naive hpx TMHoriz hpx LeadGain hpx BitBrainNet hpx"
|
||||
let hdr = "band Pattern n Pattern|err| Pattern hpx Oracle hpx headroom pp naive hpx TMHoriz hpx LeadGain hpx"
|
||||
echo hdr
|
||||
echo "-".repeat(hdr.len)
|
||||
for b in 0 ..< NBands:
|
||||
@@ -343,10 +332,8 @@ proc main() =
|
||||
let orc = arms[A_ORACLE].bands[b]
|
||||
let hp = pat.hitProxy
|
||||
let ohp = orc.hitProxy
|
||||
echo fmt"{BandLabels[b]:<9} {pat.n:>8} {fmt3(meanAbs(pat)):>12} {fmt4(hp):>12} {fmt4(ohp):>12} {ohp - hp:>13.4f} {fmt4(arms[A_NAIVE].bands[b].hitProxy):>11} {fmt4(arms[A_TMH].bands[b].hitProxy):>12} {fmt4(arms[A_LG].bands[b].hitProxy):>13} {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 "BitBrainNet hpx = the ADE+SBC gun (rack id 17), Pattern base + a class-resolved"
|
||||
echo "angular correction. VETO-CAPABLE OFFLINE CHECK ONLY, never a live claim."
|
||||
echo "hitProxy = fraction of tick-bins aimed within atan(18/range) of the true interception point."
|
||||
echo "headroom pp = oracle hitProxy - Pattern hitProxy = the absolute hit-probability points available"
|
||||
echo "to a perfect predictor (the campaign is playing for a slice of this)."
|
||||
|
||||
@@ -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."
|
||||
@@ -73,7 +73,7 @@ proc testKnownNames() =
|
||||
check "known set has no duplicates", known.len == s.len
|
||||
check "known set only holds TR_*/GUN_* names",
|
||||
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)
|
||||
for name in ["TR_MOVEMENT", "TR_POWER_ENERGY_MIN", "TR_TMHORIZON_NSTATES",
|
||||
"GUN_VBULLET_METRIC", "TR_ENV_REPORT", "GUN_SELECTOR_SEED",
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
##
|
||||
## The owner's live `.env` predates the rename and carries `TR_RACK_BITBRAIN`,
|
||||
## `TR_BITBRAIN_GAINS`, `TR_BITBRAIN_MEM`, `TR_BITBRAIN_LOG`. Those names used to
|
||||
## address the corrector that is now called LEADGAIN, and they MUST keep working,
|
||||
## because the `TR_BITBRAIN_*` prefix now belongs to the new ADE+SBC gun
|
||||
## (`guns/bitbrain_net.nim`, rack id 17). This test pins the disambiguation:
|
||||
## address the corrector that is now called LEADGAIN, and they MUST keep working.
|
||||
##
|
||||
## * `TR_BITBRAIN_NET` unset/0 -> LEGACY MODE: every `TR_BITBRAIN_<X>` in the
|
||||
## frozen `LegacyKnobEnvNames` set is an alias for `TR_LEADGAIN_<X>`, and
|
||||
## `TR_RACK_BITBRAIN` still selects rack id 16.
|
||||
## * `TR_BITBRAIN_NET=1` -> NEW-NETWORK MODE: the `TR_BITBRAIN_*` names
|
||||
## belong to the new gun; this gun reads only `TR_LEADGAIN_<X>`.
|
||||
## The ADE+SBC gun that briefly owned the `TR_BITBRAIN_*` prefix (rack id 17,
|
||||
## `guns/bitbrain_net.nim`) was RETIRED and removed, so there is nothing left to
|
||||
## disambiguate: this gun is the SOLE owner of that namespace and
|
||||
##
|
||||
## The two name sets are DISJOINT, so the mapping is total and deterministic.
|
||||
## No Java, no battle, no network build.
|
||||
## * every `TR_BITBRAIN_<X>` in the frozen `LegacyKnobEnvNames` set is an
|
||||
## 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
|
||||
|
||||
@@ -35,7 +34,7 @@ proc clearEnv() =
|
||||
delEnv(NewPrefix & suffix)
|
||||
delEnv(LegacyRackEnvName)
|
||||
delEnv("TR_RACK_" & RackGunNames[LeadGainId])
|
||||
delEnv(LG_NET_SWITCH_ENV)
|
||||
delEnv("TR_BITBRAIN_NET")
|
||||
|
||||
# ── the alias table itself ───────────────────────────────────────────────────
|
||||
|
||||
@@ -61,19 +60,29 @@ proc testAliasTableIsDisjoint() =
|
||||
|
||||
# ── 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()
|
||||
check "switch: unset => legacy mode (new ADE+SBC namespace disowned)",
|
||||
not netSwitchOn()
|
||||
putEnv(LG_NET_SWITCH_ENV, "0")
|
||||
check "switch: 0 => legacy mode", not netSwitchOn()
|
||||
putEnv(LG_NET_SWITCH_ENV, "1")
|
||||
check "switch: 1 => new-network mode", netSwitchOn()
|
||||
putEnv(LG_NET_SWITCH_ENV, "yes")
|
||||
check "switch: yes => new-network mode", netSwitchOn()
|
||||
putEnv(LG_NET_SWITCH_ENV, "banana")
|
||||
check "switch: an unknown value is NOT new-network mode (fails safe)",
|
||||
not netSwitchOn()
|
||||
putEnv("TR_BITBRAIN_MEM", "decay")
|
||||
putEnv("TR_BITBRAIN_LOG", "1")
|
||||
putEnv(LegacyRackEnvName, "both")
|
||||
var g = initLeadGainGun()
|
||||
check "ns: the legacy prefix is unconditional (no switch set at all)",
|
||||
g.memMode == lgDecay and g.logEnabled
|
||||
putEnv("TR_BITBRAIN_NET", "1")
|
||||
var g1 = initLeadGainGun()
|
||||
check "ns: a stale TR_BITBRAIN_NET=1 does not disown the legacy knob names",
|
||||
g1.memMode == lgDecay and g1.logEnabled
|
||||
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()
|
||||
|
||||
# ── knob aliasing ────────────────────────────────────────────────────────────
|
||||
@@ -93,13 +102,13 @@ proc testKnobAlias() =
|
||||
putEnv("TR_LEADGAIN_GAINS", "0.5,0.75")
|
||||
var g2 = initLeadGainGun()
|
||||
check "alias: the NEW name wins when both are set", g2.cands == @[0.5, 0.75]
|
||||
# the switch disowns the whole legacy namespace
|
||||
putEnv(LG_NET_SWITCH_ENV, "1")
|
||||
# the legacy namespace is unconditional, even next to a stale NET switch
|
||||
putEnv("TR_BITBRAIN_NET", "1")
|
||||
delEnv("TR_LEADGAIN_GAINS")
|
||||
var g3 = initLeadGainGun()
|
||||
check "alias: TR_BITBRAIN_NET=1 makes the legacy names inert for this gun",
|
||||
g3.memMode == lgPerRound and not g3.logEnabled and
|
||||
g3.cands == @[0.0, 0.25, 0.5, 0.75, 1.0]
|
||||
check "alias: a stale TR_BITBRAIN_NET=1 does NOT make the legacy names inert",
|
||||
g3.memMode == lgDecay and g3.logEnabled and
|
||||
g3.cands == @[0.25, 0.5, 1.0]
|
||||
# ... and then the NEW names still work
|
||||
putEnv("TR_LEADGAIN_MEM", "retained")
|
||||
var g4 = initLeadGainGun()
|
||||
@@ -116,10 +125,10 @@ proc testRackAlias() =
|
||||
check "rack: TR_RACK_BITBRAIN=both still selects LEADGAIN (id 16)",
|
||||
m[LeadGainId] == rmBoth and
|
||||
vBulletAdmitted(LeadGainId, rm1v1, m, true)
|
||||
putEnv(LG_NET_SWITCH_ENV, "1")
|
||||
putEnv("TR_BITBRAIN_NET", "1")
|
||||
let m2 = loadRackMembership()
|
||||
check "rack: TR_BITBRAIN_NET=1 hands the name to the new ADE+SBC gun (id 17)",
|
||||
m2[LeadGainId] == rmOff and m2.len == 18
|
||||
check "rack: a stale TR_BITBRAIN_NET=1 no longer disowns the name — LEADGAIN (id 16) stays selected",
|
||||
m2[LeadGainId] == rmBoth and m2.len == NumRackGuns
|
||||
clearEnv()
|
||||
putEnv("TR_RACK_LEADGAIN", "both")
|
||||
putEnv(LegacyRackEnvName, "off")
|
||||
@@ -140,9 +149,9 @@ proc testDeprecationLine() =
|
||||
line.startsWith("[depr]") and
|
||||
line.contains("TR_BITBRAIN_GAINS -> TR_LEADGAIN_GAINS") and
|
||||
line.contains(NewPrefix)
|
||||
putEnv(LG_NET_SWITCH_ENV, "1")
|
||||
check "depr: the line is silent once the switch is flipped",
|
||||
lgDeprecationLine().len == 0
|
||||
putEnv("TR_BITBRAIN_NET", "1")
|
||||
check "depr: the line still fires with a stale TR_BITBRAIN_NET=1 present",
|
||||
lgDeprecationLine().len > 0
|
||||
clearEnv()
|
||||
|
||||
# ── shipped-default parity ───────────────────────────────────────────────────
|
||||
@@ -155,15 +164,15 @@ proc testDefaultParity() =
|
||||
let want = if i == 5: rmBoth else: rmOff
|
||||
if m[i] != want: onlyPattern = false
|
||||
check "parity: a clean env still loads the shipped onlyPattern rack", onlyPattern
|
||||
check "parity: the rack is still 18 guns at id 16 = LEADGAIN",
|
||||
RackGunNames.len == 18 and RackGunNames[LeadGainId] == "LEADGAIN"
|
||||
check "parity: the rack is still 17 guns at id 16 = LEADGAIN",
|
||||
RackGunNames.len == 17 and RackGunNames[LeadGainId] == "LEADGAIN"
|
||||
var g = initLeadGainGun()
|
||||
check "parity: a clean env resolves the shipped candidate set",
|
||||
g.cands == @[0.0, 0.25, 0.5, 0.75, 1.0] and g.memMode == lgPerRound
|
||||
clearEnv()
|
||||
|
||||
testAliasTableIsDisjoint()
|
||||
testSwitch()
|
||||
testNamespaceIsUnconditional()
|
||||
testKnobAlias()
|
||||
testRackAlias()
|
||||
testDeprecationLine()
|
||||
|
||||
@@ -37,7 +37,7 @@ proc testTable() =
|
||||
if DefaultRackMembership[i] != want: onlyPattern = false
|
||||
check "rack: the shipped default is still the onlyPattern rack", onlyPattern
|
||||
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",
|
||||
not vBulletAdmitted(LeadGainId, rm1v1, DefaultRackMembership, true)
|
||||
|
||||
@@ -49,7 +49,7 @@ proc testEnvOverride() =
|
||||
m[LeadGainId] == rmBoth and
|
||||
vBulletAdmitted(LeadGainId, rm1v1, m, true)
|
||||
check "env: admitting LEADGAIN leaves Pattern as the only other member",
|
||||
admittedGuns(18, rm1v1, m) == @[PatternId, LeadGainId]
|
||||
admittedGuns(NumRackGuns, rm1v1, m) == @[PatternId, LeadGainId]
|
||||
clearRackEnv()
|
||||
|
||||
proc testLazyAndRngClean() =
|
||||
|
||||
@@ -158,14 +158,14 @@ proc testDefaultsOnlyPattern() =
|
||||
DefaultRackMembership[12] == rmOff and DefaultRackMembership[13] == rmOff and
|
||||
DefaultRackMembership[14] == rmOff
|
||||
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",
|
||||
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||
admittedGuns(17, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||
let loaded = loadRackMembership()
|
||||
check "defaults: with a clean environment loadRackMembership() == shipped table",
|
||||
loaded == DefaultRackMembership
|
||||
check "defaults: RackGunNames covers the shipped 18-gun rack",
|
||||
RackGunNames.len == 18 and DefaultRackMembership.len == 18
|
||||
check "defaults: RackGunNames covers the shipped 17-gun rack",
|
||||
RackGunNames.len == 17 and DefaultRackMembership.len == 17
|
||||
|
||||
proc testFloorRespectsAdmission() =
|
||||
## 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.
|
||||
clearRackEnv()
|
||||
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:
|
||||
if name != "TMPATTERN": putEnv("TR_RACK_" & name, "both")
|
||||
let m = loadRackMembership()
|
||||
delEnv("TR_BITBRAIN_NET")
|
||||
var full = true
|
||||
for i in 0..<RackGunNames.len:
|
||||
let want = if i == 14: rmOff else: rmBoth
|
||||
if m[i] != want: full = false
|
||||
check "revert: the documented one-liner restores the all-`both` full rack", full
|
||||
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]
|
||||
check "revert: the legacy TR_RACK_BITBRAIN alone still selects LEADGAIN only",
|
||||
loadRackMembership()[17] == rmOff
|
||||
admittedGuns(17, rm1v1, m) == @[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16]
|
||||
clearRackEnv()
|
||||
# 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()
|
||||
|
||||
proc testEnvOverrides() =
|
||||
|
||||
@@ -14,17 +14,23 @@
|
||||
## The fix derives every gun-indexed width from `NumRackGuns` (== `len(RackGunNames)`).
|
||||
## 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;
|
||||
## B. source shape — `ModularBot.nim` contains NO literal width/loop bound
|
||||
## narrower than `NumRackGuns` for the gun-indexed
|
||||
## accounting, and sizes them with `NumRackGuns`;
|
||||
## C. accounting — an in-process replay of the real shape (`array[NumRackGuns]`
|
||||
## 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
|
||||
## 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
|
||||
## 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"
|
||||
statsPath = "/tmp/rack_stat_width_stats.jsonl"
|
||||
PatternId = 5
|
||||
NewestId = NumRackGuns - 1 ## 17, the id that regressed
|
||||
NewestId = NumRackGuns - 1 ## 16 (LEADGAIN), the newest rack id
|
||||
LegacyLeadGainId = 16
|
||||
|
||||
var failures = 0
|
||||
@@ -53,7 +59,7 @@ proc check(name: string, ok: bool) =
|
||||
|
||||
proc testRackTable() =
|
||||
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",
|
||||
DefaultRackMembership.len == NumRackGuns
|
||||
var seen = initTable[string, bool]()
|
||||
@@ -156,21 +162,31 @@ proc testAccounting() =
|
||||
if gunRealHits[NewestId] != 50: inRange = false
|
||||
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
|
||||
# range. Prove the id that regressed is exactly the one that overflowed.
|
||||
check "C3: the regressed id (17) is out of range of the old array[17, int]",
|
||||
NewestId >= 17
|
||||
# The bug in one line, in its GENERAL form: an accounting array hard-wired to
|
||||
# a literal is out of range the moment the rack grows. Prove the fix
|
||||
# (`NumRackGuns`) tracks a rack that grows by one more gun, where the id that
|
||||
# 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
|
||||
# LEADGAIN (16), and id 17 is NOT admitted.
|
||||
delEnv("TR_BITBRAIN_NET")
|
||||
# The legacy namespace: TR_RACK_BITBRAIN is now unambiguously LEADGAIN's old
|
||||
# name (the ADE+SBC gun that briefly owned it was retired), so it always
|
||||
# admits LEADGAIN (16) — the newest id — and nothing else.
|
||||
for n in RackGunNames: delEnv("TR_RACK_" & n)
|
||||
delEnv("TR_BITBRAIN_NET")
|
||||
putEnv("TR_RACK_BITBRAIN", "both")
|
||||
let legacy = loadRackMembership()
|
||||
check "C4: legacy TR_RACK_BITBRAIN admits LEADGAIN (16)",
|
||||
legacy[LegacyLeadGainId] == rmBoth
|
||||
check "C5: legacy namespace never admits id 17",
|
||||
legacy[NewestId] == rmOff
|
||||
check "C5: legacy TR_RACK_BITBRAIN alone adds LEADGAIN to the shipped Pattern",
|
||||
admittedGuns(NumRackGuns, rm1v1, legacy) == @[PatternId, LegacyLeadGainId]
|
||||
delEnv("TR_RACK_BITBRAIN")
|
||||
|
||||
# ── D. live proof, only with the JARs ────────────────────────────────────────
|
||||
@@ -203,7 +219,7 @@ proc live() =
|
||||
var linesOut: seq[string]
|
||||
for l in readFile(backup).splitLines():
|
||||
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
|
||||
writeFile(dotenv, linesOut.join("\n"))
|
||||
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 ────────────────────────────────────────────────────────
|
||||
|
||||
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",
|
||||
RackGunNames[TmPatternId] == "TMPATTERN"
|
||||
check "rack: the new gun defaults to `off`",
|
||||
@@ -82,9 +82,9 @@ proc oldRackMembership(): array[15, RackMembership] =
|
||||
|
||||
proc testDefaultAdmitsOnlyPattern() =
|
||||
check "default membership admits only PATTERN (1v1)",
|
||||
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||
admittedGuns(NumRackGuns, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||
check "default membership admits only PATTERN (melee)",
|
||||
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||
admittedGuns(NumRackGuns, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||
|
||||
proc testEnvOverride() =
|
||||
for name in RackGunNames: delEnv("TR_RACK_" & name)
|
||||
|
||||
@@ -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`.
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
|
||||
@@ -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
|
||||
comparison. **INFERRED:** that no share schedule whatsoever could ever help
|
||||
(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**.
|
||||
|
||||
Reference in New Issue
Block a user