diff --git a/ModularBot_garage/src/ModularBot.nim b/ModularBot_garage/src/ModularBot.nim index b6688c7..6bf6cf6 100644 --- a/ModularBot_garage/src/ModularBot.nim +++ b/ModularBot_garage/src/ModularBot.nim @@ -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..` alias for a `TR_LEADGAIN_` 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.. 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", diff --git a/common_libs/gun_harness/selector.nim b/common_libs/gun_harness/selector.nim index 417ebe3..77025ec 100644 --- a/common_libs/gun_harness/selector.nim +++ b/common_libs/gun_harness/selector.nim @@ -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_` 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_` value. Empty / unknown values fall back to the @@ -148,28 +130,21 @@ proc loadRackMembership*(): array[len(RackGunNames), RackMembership] = ## corresponding CURRENT name is unset, so a migrated config always wins. result = DefaultRackMembership var legacyTouched: seq[string] - let netOwns = netSwitchOwnsBitbrainName() for i in 0.. 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: - result[gid] = parseRackMembership(getEnv(key, "")) - legacyTouched.add key + for (key, gid) in RackLegacyAlias: + let current = RackEnvPrefix & RackGunNames[gid] + if getEnv(key, "").len > 0 and getEnv(current, "").len == 0: + result[gid] = parseRackMembership(getEnv(key, "")) + legacyTouched.add key 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 diff --git a/common_libs/guns/bitbrain_net.README.md b/common_libs/guns/bitbrain_net.README.md deleted file mode 100644 index b69aaa7..0000000 --- a/common_libs/guns/bitbrain_net.README.md +++ /dev/null @@ -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. diff --git a/common_libs/guns/bitbrain_net.nim b/common_libs/guns/bitbrain_net.nim deleted file mode 100644 index 7248958..0000000 --- a/common_libs/guns/bitbrain_net.nim +++ /dev/null @@ -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..= 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 diff --git a/common_libs/guns/lead_gain.README.md b/common_libs/guns/lead_gain.README.md index b15346d..d73c720 100644 --- a/common_libs/guns/lead_gain.README.md +++ b/common_libs/guns/lead_gain.README.md @@ -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_` (14 frozen suffixes) | always a legacy alias for `TR_LEADGAIN_` | +| `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: diff --git a/common_libs/guns/lead_gain.nim b/common_libs/guns/lead_gain.nim index 2e1ab4a..982e958 100644 --- a/common_libs/guns/lead_gain.nim +++ b/common_libs/guns/lead_gain.nim @@ -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_` name -## listed in `LegacyKnobEnvNames` is a legacy alias for this gun's -## `TR_LEADGAIN_`, 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_` names -## the NEW gun's knobs (see `bitbrain_net.nim`) and this gun reads ONLY -## `TR_LEADGAIN_`. -## -## 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_` in the frozen `LegacyKnobEnvNames` +## set is an alias for `TR_LEADGAIN_`, 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_` 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_` in this set is a legacy alias for `TR_LEADGAIN_`; 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_` in this set is a legacy alias for `TR_LEADGAIN_`. + ## 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_` knob, falling back to the legacy - ## `TR_BITBRAIN_` alias while `TR_BITBRAIN_NET` is off. The NEW name always - ## wins when both are set, so a migrated config is authoritative. + ## `TR_BITBRAIN_` 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: diff --git a/common_libs/tests/measure_bitbrain_scaling.nim b/common_libs/tests/measure_bitbrain_scaling.nim deleted file mode 100644 index 7dde35a..0000000 --- a/common_libs/tests/measure_bitbrain_scaling.nim +++ /dev/null @@ -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() diff --git a/common_libs/tests/run_prediction_quality.nim b/common_libs/tests/run_prediction_quality.nim index f5d425d..064f6d7 100644 --- a/common_libs/tests/run_prediction_quality.nim +++ b/common_libs/tests/run_prediction_quality.nim @@ -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)." diff --git a/common_libs/tests/test_bitbrain_net.nim b/common_libs/tests/test_bitbrain_net.nim deleted file mode 100644 index 03cca70..0000000 --- a/common_libs/tests/test_bitbrain_net.nim +++ /dev/null @@ -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." diff --git a/common_libs/tests/test_env_report.nim b/common_libs/tests/test_env_report.nim index 0beecf7..9f75837 100644 --- a/common_libs/tests/test_env_report.nim +++ b/common_libs/tests/test_env_report.nim @@ -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", diff --git a/common_libs/tests/test_lead_gain_legacy.nim b/common_libs/tests/test_lead_gain_legacy.nim index 6ce5c5a..ca998bf 100644 --- a/common_libs/tests/test_lead_gain_legacy.nim +++ b/common_libs/tests/test_lead_gain_legacy.nim @@ -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_` in the -## frozen `LegacyKnobEnvNames` set is an alias for `TR_LEADGAIN_`, 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_`. +## 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_` in the frozen `LegacyKnobEnvNames` set is an +## alias for `TR_LEADGAIN_`, 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() diff --git a/common_libs/tests/test_lead_gain_registration.nim b/common_libs/tests/test_lead_gain_registration.nim index bdc220d..f4c7af8 100644 --- a/common_libs/tests/test_lead_gain_registration.nim +++ b/common_libs/tests/test_lead_gain_registration.nim @@ -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() = diff --git a/common_libs/tests/test_rack_membership.nim b/common_libs/tests/test_rack_membership.nim index 47b8b5b..036e63e 100644 --- a/common_libs/tests/test_rack_membership.nim +++ b/common_libs/tests/test_rack_membership.nim @@ -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..= 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)." diff --git a/common_libs/tests/test_tm_pattern_registration.nim b/common_libs/tests/test_tm_pattern_registration.nim index c8ef367..c351c1b 100644 --- a/common_libs/tests/test_tm_pattern_registration.nim +++ b/common_libs/tests/test_tm_pattern_registration.nim @@ -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) diff --git a/docs/bitbrain_campaign.md b/docs/bitbrain_campaign.md index eba4eab..a11773d 100644 --- a/docs/bitbrain_campaign.md +++ b/docs/bitbrain_campaign.md @@ -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 `/run.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_` in the frozen 14-suffix alias set always means + `TR_LEADGAIN_`. 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`. diff --git a/docs/gun_campaign.md b/docs/gun_campaign.md index 36c424e..1e9518d 100644 --- a/docs/gun_campaign.md +++ b/docs/gun_campaign.md @@ -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**.