j140 rebuild a real BitBrain gun: ADE+SBC at rack id 17, default off, and measure its scaling
The BITBRAIN name was sitting on a gun with no network in it. This is the gun
that actually runs the algorithm: an ADE layer (thresholded random projections
with ONLINE threshold adaptation) feeding the SBC head from
common_libs/bitbrain/, with the counted+decay mode available.
common_libs/guns/bitbrain_net.nim the gun
rack name BITBRAIN, rack id 17 (rack 17 -> 18 guns), both new guns default OFF
admitted by TR_RACK_BITBRAIN=both AND TR_BITBRAIN_NET=1 (the switch that also
disowns LEADGAIN's legacy TR_BITBRAIN_* aliases)
OUTPUT: a fine-grained aim CORRECTION on top of Pattern - the probability-
weighted mean of the nClasses class centres under inferProb - not a direct aim
point from the argmax. That is the shape docs/bitbrain_gate.md measured, and
Pattern is already a strong predictor, so the net's job is the signed residual.
Below TR_BITBRAIN_MINOBS the shift is exactly 0 and Pattern is returned
unchanged.
INPUT: a CONFIGURED set of FEATURE BLOCKS (TR_BITBRAIN_FEATURES=name:W), each
block's width == its resolution, laid out as a thermometer code over 0/255 slots
(so an ADE synapse 'matches' when its polarity agrees with the slot and a random
ADE fires iff its w synapses all match, rate 2^-w). Default is 52 slots over 9
blocks. NO long temporal window, per docs/state_window_gate.md: the only history
is a 12-tick ring feeding three rate/turn quantities.
Every knob env-configurable: _INPUT (width), _NCLASSES, _NADES, _WIDTHS
(clause widths), _FEATURES, _SPAN, _MODE, _DECAY_EVERY, _DECAY_SHIFT,
_MINOBS, _ADAPT_EVERY, _TARGET, _NETSEED, _NETLOG, _NET_RESET_ON_TARGET.
MEASURED SCALING (measure_bitbrain_scaling.nim, 3 recorded runs, 37412 ticks,
-d:release, one predict per power bin per tick, timed region = predicts only):
RAM 1.59 MB default (98.6% SBC tensors); linear in nClasses, QUADRATIC in
nAde, FLAT in input width; counted/bitset = 7.30x on RAM, ~1x on time.
ms/tick 2.70 default = 21% of the 13.16 ms budget; 64 classes busts it (149%),
nAde 512 uses 74%, nAde 64 uses 2%.
CAPACITY vs ACCURACY: over a 100x RAM range the offline mean |err| moves
17.254 -> 17.115 deg around Pattern's 16.964, and the sign flips along the
nClasses axis, so it is noise, not a trend. The corrector is consistently
slightly WORSE than Pattern. The ceiling is the STATE, not the classifier.
VETO-CAPABLE OFFLINE CHECK ONLY (docs/offline_harness_trust.md), never
presented as a live win.
ENGAGEMENT is proven, not assumed: test_bitbrain_net.nim (42 checks) shows 0
bytes before first use, different inputs -> different class outputs, a learn
raises SBC occupancy, bitset learn idempotent while counted learn is monotone,
threshold adaptation runs, and the global RNG is untouched.
Parity: shipped rack still onlyPattern, shipped movement still strafe. Guards:
test_env_report 25, test_rack_membership 48, test_tm_pattern_registration 20,
test_lead_gain_registration 13, test_lead_gain_legacy 24, test_bitbrain 56,
test_gun_harness 39, test_tfil_commit_env 30, test_bitbrain_net 42.
Clean archive build: [SuccessX].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
## ModularBot — plugin gun architecture tracer bullet.
|
## ModularBot — plugin gun architecture tracer bullet.
|
||||||
## Guns: HeadOnGun (0), LinearGun (1), TsetlinGun (2), CircularGun (3), GFGun (4), PatternMatcherGun (5), WallBounceGun (6), AccelGun (7), StopShotGun (8), DisplacementGun (9), AveragedLeadGun (10), DecayGFGun (11), KNNGun (12), TmSelectorGun (13), TmPatternGun (14), TmHorizonGun (15), LeadGainGun (16) via GunHarness.
|
## Guns: HeadOnGun (0), LinearGun (1), TsetlinGun (2), CircularGun (3), GFGun (4), PatternMatcherGun (5), WallBounceGun (6), AccelGun (7), StopShotGun (8), DisplacementGun (9), AveragedLeadGun (10), DecayGFGun (11), KNNGun (12), TmSelectorGun (13), TmPatternGun (14), TmHorizonGun (15), LeadGainGun (16), BitbrainNetGun (17) via GunHarness.
|
||||||
## Radar: RadarLockModule (1v1) / AdaptiveMeleeRadarModule (2+ enemies), auto-switched per tick.
|
## Radar: RadarLockModule (1v1) / AdaptiveMeleeRadarModule (2+ enemies), auto-switched per tick.
|
||||||
## Movement: OscillatorModule (perpendicular strafing).
|
## Movement: OscillatorModule (perpendicular strafing).
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ import guns/tm_selector
|
|||||||
import guns/tm_pattern
|
import guns/tm_pattern
|
||||||
import guns/tm_horizon
|
import guns/tm_horizon
|
||||||
import guns/lead_gain
|
import guns/lead_gain
|
||||||
|
import guns/bitbrain_net
|
||||||
import movements/phantom_meteor
|
import movements/phantom_meteor
|
||||||
import movements/rammer
|
import movements/rammer
|
||||||
import movements/ram_decision
|
import movements/ram_decision
|
||||||
@@ -150,7 +151,7 @@ let ResultLog* =
|
|||||||
block:
|
block:
|
||||||
let v = getEnv("TR_RESULT_LOG", "1").strip().toLowerAscii()
|
let v = getEnv("TR_RESULT_LOG", "1").strip().toLowerAscii()
|
||||||
not (v == "0" or v == "false" or v == "no" or v == "off")
|
not (v == "0" or v == "false" or v == "no" or v == "off")
|
||||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect", "TMPattern", "TMHorizon", "LeadGain"]
|
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect", "TMPattern", "TMHorizon", "LeadGain", "BitBrainNet"]
|
||||||
|
|
||||||
## Rack id of the new TM pattern gun. It defaults to `TR_RACK_TMPATTERN=off`;
|
## Rack id of the new TM pattern gun. It defaults to `TR_RACK_TMPATTERN=off`;
|
||||||
## unlike the other guns, its virtual-bullet spawn is gated on rack admission
|
## unlike the other guns, its virtual-bullet spawn is gated on rack admission
|
||||||
@@ -174,6 +175,16 @@ const TmHorizonId = 15
|
|||||||
## `TR_BITBRAIN_NET` is off (backward compatibility; see `gun_harness/selector`).
|
## `TR_BITBRAIN_NET` is off (backward compatibility; see `gun_harness/selector`).
|
||||||
const LeadGainId = 16
|
const LeadGainId = 16
|
||||||
|
|
||||||
|
## Rack id of the ADE+SBC gun — the gun that actually runs the algorithm
|
||||||
|
## (`guns/bitbrain_net.nim`). It takes the `BITBRAIN` name and the
|
||||||
|
## `TR_BITBRAIN_*` knob prefix that the renamed corrector gave up. TWO switches
|
||||||
|
## admit it, and both default off: `TR_RACK_BITBRAIN=both` in the rack table and
|
||||||
|
## `TR_BITBRAIN_NET=1` in the gun itself (the switch that also disowns the
|
||||||
|
## legacy `TR_BITBRAIN_*` aliases — see `gun_harness/selector`). Its
|
||||||
|
## virtual-bullet spawn is gated on rack admission, so the shipped default never
|
||||||
|
## spawns it and the shared tracker ring head is byte-for-byte unchanged.
|
||||||
|
const BitbrainNetId = 17
|
||||||
|
|
||||||
## ── virtual-bullet debug overlay (TR_VBULLET_DEBUG*) ───────────────────────
|
## ── virtual-bullet debug overlay (TR_VBULLET_DEBUG*) ───────────────────────
|
||||||
## OFF by default: the shipped bot draws nothing. When on, the tracker's
|
## OFF by default: the shipped bot draws nothing. When on, the tracker's
|
||||||
## virtual bullets are overlaid so the training signal the selector learns
|
## virtual bullets are overlaid so the training signal the selector learns
|
||||||
@@ -246,6 +257,7 @@ type
|
|||||||
tmPattern: TmPatternGun
|
tmPattern: TmPatternGun
|
||||||
tmHorizon: TmHorizonGun
|
tmHorizon: TmHorizonGun
|
||||||
leadGain: LeadGainGun
|
leadGain: LeadGainGun
|
||||||
|
bbn: BitbrainNetGun
|
||||||
mover: TFILModule
|
mover: TFILModule
|
||||||
ringMover: TFILRingModule
|
ringMover: TFILRingModule
|
||||||
strafeMover: StrafeModule
|
strafeMover: StrafeModule
|
||||||
@@ -943,6 +955,7 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
|||||||
# LEADGAIN: per-round wipe always clears observations/labels; `perRound` mode
|
# LEADGAIN: per-round wipe always clears observations/labels; `perRound` mode
|
||||||
# also wipes its SBCs (the gate test's winning regime).
|
# also wipes its SBCs (the gate test's winning regime).
|
||||||
bot.leadGain.resetRoundState()
|
bot.leadGain.resetRoundState()
|
||||||
|
bot.bbn.resetRoundState()
|
||||||
if e.roundNumber <= 1:
|
if e.roundNumber <= 1:
|
||||||
bot.leadGain.resetLearning("round1")
|
bot.leadGain.resetLearning("round1")
|
||||||
bot.isRamming = false
|
bot.isRamming = false
|
||||||
@@ -1025,6 +1038,7 @@ method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
|
|||||||
# boundary. The round-1 fallback in `onRoundStarted` covers a missed callback.
|
# boundary. The round-1 fallback in `onRoundStarted` covers a missed callback.
|
||||||
bot.tmHorizon.resetLearning("game_start")
|
bot.tmHorizon.resetLearning("game_start")
|
||||||
bot.leadGain.resetLearning("game_start")
|
bot.leadGain.resetLearning("game_start")
|
||||||
|
bot.bbn.resetLearning("game_start")
|
||||||
# minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1
|
# minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1
|
||||||
bot.initialEnemyCount = e.gameSetup.minNumberOfParticipants - 1
|
bot.initialEnemyCount = e.gameSetup.minNumberOfParticipants - 1
|
||||||
# New battle: reset the cumulative outcome counters and remember how many
|
# New battle: reset the cumulative outcome counters and remember how many
|
||||||
@@ -1133,6 +1147,7 @@ method run*(bot: ModularBot) =
|
|||||||
# TR_TMHORIZON_RESET_ON_TARGET, default on; no-op in 1v1).
|
# TR_TMHORIZON_RESET_ON_TARGET, default on; no-op in 1v1).
|
||||||
discard bot.tmHorizon.targetChanged(candidateId)
|
discard bot.tmHorizon.targetChanged(candidateId)
|
||||||
discard bot.leadGain.targetChanged(candidateId)
|
discard bot.leadGain.targetChanged(candidateId)
|
||||||
|
discard bot.bbn.targetChanged(candidateId)
|
||||||
if bot.currentTargetId >= 0:
|
if bot.currentTargetId >= 0:
|
||||||
bot.lastKnownTargetId = bot.currentTargetId
|
bot.lastKnownTargetId = bot.currentTargetId
|
||||||
|
|
||||||
@@ -1277,6 +1292,7 @@ method run*(bot: ModularBot) =
|
|||||||
var tmpPreds: array[len(PowerBins), GunPrediction]
|
var tmpPreds: array[len(PowerBins), GunPrediction]
|
||||||
var tmhPreds: array[len(PowerBins), GunPrediction]
|
var tmhPreds: array[len(PowerBins), GunPrediction]
|
||||||
var lgPreds: array[len(PowerBins), GunPrediction]
|
var lgPreds: array[len(PowerBins), GunPrediction]
|
||||||
|
var bbnPreds: array[len(PowerBins), GunPrediction]
|
||||||
# ── TR_VBULLET_ADMIT_ONLY gate ──────────────────────────────────────
|
# ── TR_VBULLET_ADMIT_ONLY gate ──────────────────────────────────────
|
||||||
# Rack membership used to filter only SELECTION, so every unselected gun
|
# Rack membership used to filter only SELECTION, so every unselected gun
|
||||||
# still ran predict()+spawnBullets() each tick to feed a fitness table the
|
# still ran predict()+spawnBullets() each tick to feed a fitness table the
|
||||||
@@ -1288,12 +1304,13 @@ method run*(bot: ModularBot) =
|
|||||||
# owning gun's onResult, so attribution survives a mid-round rack change.
|
# owning gun's onResult, so attribution survives a mid-round rack change.
|
||||||
# TMPATTERN and TMHORIZON keep their own admission gate even when the knob
|
# TMPATTERN and TMHORIZON keep their own admission gate even when the knob
|
||||||
# is 0, so TR_VBULLET_ADMIT_ONLY=0 reproduces the exact pre-change rack.
|
# is 0, so TR_VBULLET_ADMIT_ONLY=0 reproduces the exact pre-change rack.
|
||||||
var admit: array[17, bool]
|
var admit: array[18, bool]
|
||||||
for gi in 0..<17:
|
for gi in 0..<18:
|
||||||
admit[gi] = ModuleVBullets and
|
admit[gi] = ModuleVBullets and
|
||||||
vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership,
|
vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership,
|
||||||
VBulletAdmitOnly or gi == TmPatternId or
|
VBulletAdmitOnly or gi == TmPatternId or
|
||||||
gi == TmHorizonId or gi == LeadGainId)
|
gi == TmHorizonId or gi == LeadGainId or
|
||||||
|
gi == BitbrainNetId)
|
||||||
for i in 0..<len(PowerBins):
|
for i in 0..<len(PowerBins):
|
||||||
if admit[0]: headsUp[i] = bot.headOn.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
if admit[0]: headsUp[i] = bot.headOn.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||||
if admit[1]: linPreds[i] = bot.linear.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
if admit[1]: linPreds[i] = bot.linear.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||||
@@ -1319,6 +1336,9 @@ method run*(bot: ModularBot) =
|
|||||||
# LEADGAIN: Pattern base, lead scaled by a per-band learned gain.
|
# LEADGAIN: Pattern base, lead scaled by a per-band learned gain.
|
||||||
if admit[LeadGainId]:
|
if admit[LeadGainId]:
|
||||||
lgPreds[i] = bot.leadGain.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
lgPreds[i] = bot.leadGain.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||||
|
# BITBRAIN: Pattern base + a fine-grained ADE/SBC angular correction.
|
||||||
|
if admit[BitbrainNetId]:
|
||||||
|
bbnPreds[i] = bot.bbn.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||||
|
|
||||||
if admit[0]: bot.tracker.spawnBullets(0, headsUp, bot.lastState, tid)
|
if admit[0]: bot.tracker.spawnBullets(0, headsUp, bot.lastState, tid)
|
||||||
if admit[1] and not gunDisabled(1): bot.tracker.spawnBullets(1, linPreds, bot.lastState, tid)
|
if admit[1] and not gunDisabled(1): bot.tracker.spawnBullets(1, linPreds, bot.lastState, tid)
|
||||||
@@ -1342,6 +1362,8 @@ method run*(bot: ModularBot) =
|
|||||||
bot.tracker.spawnBullets(TmHorizonId, tmhPreds, bot.lastState, tid)
|
bot.tracker.spawnBullets(TmHorizonId, tmhPreds, bot.lastState, tid)
|
||||||
if admit[LeadGainId] and not gunDisabled(LeadGainId):
|
if admit[LeadGainId] and not gunDisabled(LeadGainId):
|
||||||
bot.tracker.spawnBullets(LeadGainId, lgPreds, bot.lastState, tid)
|
bot.tracker.spawnBullets(LeadGainId, lgPreds, bot.lastState, tid)
|
||||||
|
if admit[BitbrainNetId] and not gunDisabled(BitbrainNetId):
|
||||||
|
bot.tracker.spawnBullets(BitbrainNetId, bbnPreds, bot.lastState, tid)
|
||||||
|
|
||||||
# Build slim enemy table for tickBullets
|
# Build slim enemy table for tickBullets
|
||||||
var enemyPositions: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
var enemyPositions: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
||||||
@@ -1378,6 +1400,7 @@ method run*(bot: ModularBot) =
|
|||||||
of TmPatternId: bot.tmPattern.onResult(fe)
|
of TmPatternId: bot.tmPattern.onResult(fe)
|
||||||
of TmHorizonId: bot.tmHorizon.onResult(fe)
|
of TmHorizonId: bot.tmHorizon.onResult(fe)
|
||||||
of LeadGainId: bot.leadGain.onResult(fe)
|
of LeadGainId: bot.leadGain.onResult(fe)
|
||||||
|
of BitbrainNetId: bot.bbn.onResult(fe)
|
||||||
else: discard
|
else: discard
|
||||||
if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss
|
if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss
|
||||||
if DebugDrawOn and VBulletDebugOn:
|
if DebugDrawOn and VBulletDebugOn:
|
||||||
@@ -1433,6 +1456,7 @@ method run*(bot: ModularBot) =
|
|||||||
of TmPatternId: bot.tmPattern.predict(bot.lastState, bulletSpeed(power))
|
of TmPatternId: bot.tmPattern.predict(bot.lastState, bulletSpeed(power))
|
||||||
of TmHorizonId: bot.tmHorizon.predict(bot.lastState, bulletSpeed(power))
|
of TmHorizonId: bot.tmHorizon.predict(bot.lastState, bulletSpeed(power))
|
||||||
of LeadGainId: bot.leadGain.predict(bot.lastState, bulletSpeed(power))
|
of LeadGainId: bot.leadGain.predict(bot.lastState, bulletSpeed(power))
|
||||||
|
of BitbrainNetId: bot.bbn.predict(bot.lastState, bulletSpeed(power))
|
||||||
else: bot.headOn.predict(bot.lastState, bulletSpeed(power))
|
else: bot.headOn.predict(bot.lastState, bulletSpeed(power))
|
||||||
let aimTarget = aimAngle(getX(), getY(), pred.x, pred.y)
|
let aimTarget = aimAngle(getX(), getY(), pred.x, pred.y)
|
||||||
|
|
||||||
@@ -1500,7 +1524,7 @@ proc seedSelectorRng() =
|
|||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
var bot = ModularBot(
|
var bot = ModularBot(
|
||||||
tracker: vb.initTracker(17), # 0: HeadOn, 1: Linear, 2: Tsetlin, 3: Circular, 4: GuessFactor, 5: Pattern, 6: WallBounce, 7: Accel, 8: StopShot, 9: Displace, 10: AvgLead, 11: DecayGF, 12: KNN, 13: TMSelect, 14: TMPattern, 15: TMHorizon, 16: LeadGain
|
tracker: vb.initTracker(18), # 0: HeadOn, 1: Linear, 2: Tsetlin, 3: Circular, 4: GuessFactor, 5: Pattern, 6: WallBounce, 7: Accel, 8: StopShot, 9: Displace, 10: AvgLead, 11: DecayGF, 12: KNN, 13: TMSelect, 14: TMPattern, 15: TMHorizon, 16: LeadGain, 17: BitBrainNet
|
||||||
headOn: HeadOnGun(),
|
headOn: HeadOnGun(),
|
||||||
linear: LinearGun(),
|
linear: LinearGun(),
|
||||||
circular: CircularGun(),
|
circular: CircularGun(),
|
||||||
@@ -1518,6 +1542,7 @@ when isMainModule:
|
|||||||
tmPattern: initTmRadialGun(),
|
tmPattern: initTmRadialGun(),
|
||||||
tmHorizon: initTmHorizonGun(),
|
tmHorizon: initTmHorizonGun(),
|
||||||
leadGain: initLeadGainGun(),
|
leadGain: initLeadGainGun(),
|
||||||
|
bbn: initBitbrainNetGun(),
|
||||||
radar: RadarLockModule(),
|
radar: RadarLockModule(),
|
||||||
meleeRadar: initAdaptiveMeleeRadar(),
|
meleeRadar: initAdaptiveMeleeRadar(),
|
||||||
mover: TFILModule(debugGraphics: DebugDrawOn),
|
mover: TFILModule(debugGraphics: DebugDrawOn),
|
||||||
@@ -1557,5 +1582,6 @@ when isMainModule:
|
|||||||
tmHorizon: bot.tmHorizon,
|
tmHorizon: bot.tmHorizon,
|
||||||
patternMatcher: bot.patternMatcher,
|
patternMatcher: bot.patternMatcher,
|
||||||
leadGain: bot.leadGain,
|
leadGain: bot.leadGain,
|
||||||
|
bitbrainNet: bot.bbn,
|
||||||
))
|
))
|
||||||
start(bot, botJsonPath)
|
start(bot, botJsonPath)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import movements/wave_surfer
|
|||||||
import movements/learned_surfer
|
import movements/learned_surfer
|
||||||
import guns/tm_horizon
|
import guns/tm_horizon
|
||||||
import guns/lead_gain
|
import guns/lead_gain
|
||||||
|
import guns/bitbrain_net
|
||||||
import guns/pattern_matcher
|
import guns/pattern_matcher
|
||||||
import vbullet_draw
|
import vbullet_draw
|
||||||
import geo_overlay
|
import geo_overlay
|
||||||
@@ -65,6 +66,7 @@ type
|
|||||||
disabledGuns*: HashSet[int]
|
disabledGuns*: HashSet[int]
|
||||||
tmHorizon*: TmHorizonGun
|
tmHorizon*: TmHorizonGun
|
||||||
leadGain*: LeadGainGun
|
leadGain*: LeadGainGun
|
||||||
|
bitbrainNet*: BitbrainNetGun
|
||||||
patternMatcher*: PatternMatcherGun
|
patternMatcher*: PatternMatcherGun
|
||||||
|
|
||||||
# ── helpers ──────────────────────────────────────────────────────────────────
|
# ── helpers ──────────────────────────────────────────────────────────────────
|
||||||
@@ -139,6 +141,9 @@ proc sourceOfLgPresence(name: string): string =
|
|||||||
else:
|
else:
|
||||||
sourceOfPresence(name)
|
sourceOfPresence(name)
|
||||||
|
|
||||||
|
proc sbcModeName(m: SbcMode): string {.inline.} =
|
||||||
|
(if m == smCounted: "counted" else: "bitset")
|
||||||
|
|
||||||
proc disabledGunsString(guns: HashSet[int]): string =
|
proc disabledGunsString(guns: HashSet[int]): string =
|
||||||
if guns.len == 0: return "none"
|
if guns.len == 0: return "none"
|
||||||
var ids: seq[int]
|
var ids: seq[int]
|
||||||
@@ -428,6 +433,29 @@ proc printEffectiveValues(ctx: EnvReportContext) =
|
|||||||
if dep.len > 0: emit("legacy TR_BITBRAIN_* aliases honoured", "yes", "legacy")
|
if dep.len > 0: emit("legacy TR_BITBRAIN_* aliases honoured", "yes", "legacy")
|
||||||
else: emit("legacy TR_BITBRAIN_* aliases honoured", "no", "default")
|
else: emit("legacy TR_BITBRAIN_* aliases honoured", "no", "default")
|
||||||
|
|
||||||
|
# ── the ADE+SBC gun (rack id 17) ─────────────────────────────────────────
|
||||||
|
# Two switches admit it, both default off: `TR_RACK_BITBRAIN` (rack table) and
|
||||||
|
# `TR_BITBRAIN_NET` (the gun's own master switch, which is also the switch
|
||||||
|
# that disowns the legacy `TR_BITBRAIN_*` aliases above).
|
||||||
|
let bbn = ctx.bitbrainNet
|
||||||
|
emit("TR_BITBRAIN_NET", onOff(bbn.enabled), sourceOfPresence(BBN_NET_ENV))
|
||||||
|
emit("TR_BITBRAIN_INPUT", $bbn.inputWidth, sourceOf(BBN_INPUT_ENV))
|
||||||
|
emit("TR_BITBRAIN_FEATURES", bbn.features, sourceOf(BBN_FEATURES_ENV))
|
||||||
|
emit("TR_BITBRAIN_NCLASSES", $bbn.nClasses, sourceOf(BBN_CLASSES_ENV))
|
||||||
|
emit("TR_BITBRAIN_NADES", $bbn.nAde, sourceOf(BBN_NADES_ENV))
|
||||||
|
emit("TR_BITBRAIN_WIDTHS", bbn.widths.join(","), sourceOf(BBN_WIDTHS_ENV))
|
||||||
|
emit("TR_BITBRAIN_SPAN", $bbn.maxDeg, sourceOf(BBN_SPAN_ENV))
|
||||||
|
emit("TR_BITBRAIN_MODE", sbcModeName(bbn.mode), sourceOf(BBN_MODE_ENV))
|
||||||
|
emit("TR_BITBRAIN_DECAY_EVERY", $bbn.decayEvery, sourceOf(BBN_DECAY_EVERY_ENV))
|
||||||
|
emit("TR_BITBRAIN_DECAY_SHIFT", $bbn.decayShift, sourceOf(BBN_DECAY_SHIFT_ENV))
|
||||||
|
emit("TR_BITBRAIN_MINOBS", $bbn.minObs, sourceOf(BBN_MINOBS_ENV))
|
||||||
|
emit("TR_BITBRAIN_ADAPT_EVERY", $bbn.adaptEvery, sourceOf(BBN_ADAPT_ENV))
|
||||||
|
emit("TR_BITBRAIN_TARGET", $bbn.targetRate, sourceOf(BBN_TARGET_ENV))
|
||||||
|
emit("TR_BITBRAIN_NETSEED", $bbn.seed, sourceOf(BBN_SEED_ENV))
|
||||||
|
emit("TR_BITBRAIN_NETLOG", onOff(bbn.logEnabled),
|
||||||
|
sourceOfPresence(BBN_LOG_ENV))
|
||||||
|
emit("bitbrain_net.ram", $bbn.networkBytes, "default") ## 0 until the lazy build
|
||||||
|
|
||||||
# ── Pattern match-shape + radial knobs ────────────────────────────────────
|
# ── Pattern match-shape + radial knobs ────────────────────────────────────
|
||||||
# These are resolved lazily inside `predict` (which has not run at boot), so
|
# These are resolved lazily inside `predict` (which has not run at boot), so
|
||||||
# unless something already forced them we report the raw env value and say so.
|
# unless something already forced them we report the raw env value and say so.
|
||||||
@@ -600,14 +628,16 @@ proc knownEnvNames*(): seq[string] =
|
|||||||
"TR_BITBRAIN_RANGE", "TR_BITBRAIN_WARMUP", "TR_BITBRAIN_ADAPT",
|
"TR_BITBRAIN_RANGE", "TR_BITBRAIN_WARMUP", "TR_BITBRAIN_ADAPT",
|
||||||
"TR_BITBRAIN_CALIB", "TR_BITBRAIN_SEED",
|
"TR_BITBRAIN_CALIB", "TR_BITBRAIN_SEED",
|
||||||
# common_libs/bitbrain (counted-SBC library) reads these inline; job j102
|
# common_libs/bitbrain (counted-SBC library) reads these inline; job j102
|
||||||
"TR_BITBRAIN_MODE", "TR_BITBRAIN_DECAY_EVERY", "TR_BITBRAIN_DECAY_SHIFT",
|
|
||||||
]
|
]
|
||||||
|
# the ADE+SBC gun (rack id 17) owns the rest of the TR_BITBRAIN_* namespace
|
||||||
|
for n in BitbrainNetEnvNames: result.add n
|
||||||
# rack names are constructed from the prefix + gun table, not spelled out
|
# rack names are constructed from the prefix + gun table, not spelled out
|
||||||
for g in RackGunNames:
|
for g in RackGunNames:
|
||||||
result.add RackEnvPrefix & g
|
result.add RackEnvPrefix & g
|
||||||
# the LEGACY rack name of the renamed corrector (still honoured while
|
# NOTE: the legacy rack alias `TR_RACK_BITBRAIN` needs no extra entry — it is
|
||||||
# LG_NET_SWITCH_ENV is off — see gun_harness/selector's RackLegacyAlias)
|
# the CURRENT name of rack id 17, so the RackGunNames loop above already
|
||||||
for (key, _) in RackLegacyAlias: result.add key
|
# registers it. See gun_harness/selector's RackLegacyAlias for how it is
|
||||||
|
# routed to LEADGAIN while LG_NET_SWITCH_ENV is off.
|
||||||
# inline reads with no exported constant (the guard test scans for these)
|
# inline reads with no exported constant (the guard test scans for these)
|
||||||
for n in [
|
for n in [
|
||||||
"GUN_SELECTOR_WINDOW", "GUN_SELECTOR_MINOBS", "GUN_SELECTOR_TIE",
|
"GUN_SELECTOR_WINDOW", "GUN_SELECTOR_MINOBS", "GUN_SELECTOR_TIE",
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import virtual_bullets
|
|||||||
# TR_RACK_DISPLACE=melee -> melee rack only
|
# TR_RACK_DISPLACE=melee -> melee rack only
|
||||||
# TR_RACK_KNN=off -> removed from both racks
|
# TR_RACK_KNN=off -> removed from both racks
|
||||||
# TR_RACK_TMPATTERN=off (shipped default for the new TM pattern gun)
|
# TR_RACK_TMPATTERN=off (shipped default for the new TM pattern gun)
|
||||||
|
# TR_RACK_LEADGAIN=both -> the per-range-band lead-gain corrector (id 16)
|
||||||
|
# TR_RACK_BITBRAIN=both -> the ADE+SBC gun (id 17); also needs TR_BITBRAIN_NET=1
|
||||||
#
|
#
|
||||||
# SHIPPED DEFAULT IS `onlyPattern`: Pattern (id 5) is admitted in both racks and
|
# SHIPPED DEFAULT IS `onlyPattern`: Pattern (id 5) is admitted in both racks and
|
||||||
# every other gun is `off`. This is a deliberate, measured decision, not a
|
# every other gun is `off`. This is a deliberate, measured decision, not a
|
||||||
@@ -37,17 +39,18 @@ import virtual_bullets
|
|||||||
# TR_RACK_TSETLIN=both TR_RACK_CIRCULAR=both TR_RACK_GUESSFACTOR=both \
|
# TR_RACK_TSETLIN=both TR_RACK_CIRCULAR=both TR_RACK_GUESSFACTOR=both \
|
||||||
# TR_RACK_WALLBOUNCE=both TR_RACK_ACCEL=both TR_RACK_STOPSHOT=both \
|
# TR_RACK_WALLBOUNCE=both TR_RACK_ACCEL=both TR_RACK_STOPSHOT=both \
|
||||||
# TR_RACK_DISPLACE=both TR_RACK_AVGLEAD=both TR_RACK_DECAYGF=both \
|
# TR_RACK_DISPLACE=both TR_RACK_AVGLEAD=both TR_RACK_DECAYGF=both \
|
||||||
# TR_RACK_KNN=both TR_RACK_TMSELECT=both ./ModularBot
|
# TR_RACK_KNN=both TR_RACK_TMSELECT=both TR_RACK_LEADGAIN=both \
|
||||||
|
# TR_RACK_BITBRAIN=both TR_BITBRAIN_NET=1 ./ModularBot
|
||||||
#
|
#
|
||||||
# The mode itself is derived from SERVER truth (`getEnemyCount()`), never from
|
# The mode itself is derived from SERVER truth (`getEnemyCount()`), never from
|
||||||
# the tracker's known-enemy count, by `rackMode` in virtual_bullets — the same
|
# the tracker's known-enemy count, by `rackMode` in virtual_bullets — the same
|
||||||
# transition the radar uses.
|
# transition the radar uses.
|
||||||
|
|
||||||
const
|
const
|
||||||
RackGunNames*: array[17, string] = [
|
RackGunNames*: array[18, string] = [
|
||||||
"HEADON", "LINEAR", "TSETLIN", "CIRCULAR", "GUESSFACTOR", "PATTERN",
|
"HEADON", "LINEAR", "TSETLIN", "CIRCULAR", "GUESSFACTOR", "PATTERN",
|
||||||
"WALLBOUNCE", "ACCEL", "STOPSHOT", "DISPLACE", "AVGLEAD", "DECAYGF",
|
"WALLBOUNCE", "ACCEL", "STOPSHOT", "DISPLACE", "AVGLEAD", "DECAYGF",
|
||||||
"KNN", "TMSELECT", "TMPATTERN", "TMHORIZON", "LEADGAIN"]
|
"KNN", "TMSELECT", "TMPATTERN", "TMHORIZON", "LEADGAIN", "BITBRAIN"]
|
||||||
RackEnvPrefix* = "TR_RACK_"
|
RackEnvPrefix* = "TR_RACK_"
|
||||||
## LEADGAIN (id 16) is the per-range-band lead-gain corrector
|
## LEADGAIN (id 16) is the per-range-band lead-gain corrector
|
||||||
## (`guns/lead_gain.nim`). It USED to be called BITBRAIN; the name now
|
## (`guns/lead_gain.nim`). It USED to be called BITBRAIN; the name now
|
||||||
@@ -63,7 +66,7 @@ const
|
|||||||
## it never spawns a virtual bullet unless explicitly enabled, so the shared
|
## it never spawns a virtual bullet unless explicitly enabled, so the shared
|
||||||
## VirtualTracker ring head — and every other gun's learning order — is
|
## VirtualTracker ring head — and every other gun's learning order — is
|
||||||
## unchanged.
|
## unchanged.
|
||||||
DefaultRackMembership*: array[17, RackMembership] = [
|
DefaultRackMembership*: array[18, RackMembership] = [
|
||||||
rmOff, # 0 HEADON — off (measured: worst over-selected gun)
|
rmOff, # 0 HEADON — off (measured: worst over-selected gun)
|
||||||
rmOff, # 1 LINEAR — off
|
rmOff, # 1 LINEAR — off
|
||||||
rmOff, # 2 TSETLIN — off
|
rmOff, # 2 TSETLIN — off
|
||||||
@@ -80,10 +83,12 @@ const
|
|||||||
rmOff, # 13 TMSELECT — off
|
rmOff, # 13 TMSELECT — off
|
||||||
rmOff, # 14 TMPATTERN — off (already shipped off; TM pattern gun)
|
rmOff, # 14 TMPATTERN — off (already shipped off; TM pattern gun)
|
||||||
rmOff, # 15 TMHORIZON — off (horizon-based TM corrector; expected to lose)
|
rmOff, # 15 TMHORIZON — off (horizon-based TM corrector; expected to lose)
|
||||||
rmOff] # 16 LEADGAIN — off (per-range-band lead-gain corrector)
|
rmOff, # 16 LEADGAIN — off (per-range-band lead-gain corrector)
|
||||||
## NOTE: the table is registered in the SAME commit as the gun id (16) and the
|
rmOff] # 17 BITBRAIN — off (the real ADE+SBC gun; also needs TR_BITBRAIN_NET=1)
|
||||||
## live wiring, so `TR_RACK_LEADGAIN=both` is the ONLY thing that admits it and
|
## NOTE: the table is registered in the SAME commit as the gun id and the live
|
||||||
## an unset environment is byte-for-byte the shipped Pattern-only rack.
|
## 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.
|
||||||
|
|
||||||
const
|
const
|
||||||
## ── BACKWARD COMPATIBILITY: the legacy rack knob names ────────────────────
|
## ── BACKWARD COMPATIBILITY: the legacy rack knob names ────────────────────
|
||||||
@@ -97,6 +102,9 @@ const
|
|||||||
## * unset/0 -> LEGACY: `TR_RACK_BITBRAIN` selects LEADGAIN (id 16), the gun
|
## * unset/0 -> LEGACY: `TR_RACK_BITBRAIN` selects LEADGAIN (id 16), the gun
|
||||||
## it always selected, and the new gun stays off (the shipped default);
|
## it always selected, and the new gun stays off (the shipped default);
|
||||||
## * 1 -> `TR_RACK_BITBRAIN` selects the new BITBRAIN gun (id 17).
|
## * 1 -> `TR_RACK_BITBRAIN` selects the new BITBRAIN gun (id 17).
|
||||||
|
## (The new gun additionally requires `TR_BITBRAIN_NET=1` in its own
|
||||||
|
## `predict`, so even an explicitly racked `TR_RACK_BITBRAIN=both` cannot turn
|
||||||
|
## it on while the namespace is still in legacy mode.)
|
||||||
RackLegacyAlias*: array[1, (string, int)] = [("TR_RACK_BITBRAIN", 16)]
|
RackLegacyAlias*: array[1, (string, int)] = [("TR_RACK_BITBRAIN", 16)]
|
||||||
RackLegacyAliasGunName* = "LEADGAIN" ## what the legacy name selects today
|
RackLegacyAliasGunName* = "LEADGAIN" ## what the legacy name selects today
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
# 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 BOTH
|
||||||
|
`TR_RACK_BITBRAIN` (rack table) and `TR_BITBRAIN_NET=1` (the gun's own master
|
||||||
|
switch, the same switch that disowns the legacy `TR_BITBRAIN_*` aliases of
|
||||||
|
`LEADGAIN`). 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.** `0` = off and the `TR_BITBRAIN_*` names are LEGACY aliases of `LEADGAIN`; `1` = on and the names below are the new gun's |
|
||||||
|
| `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
|
||||||
|
# 42 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, two different inputs give different class
|
||||||
|
outputs, a learn visibly raises SBC occupancy, a bitset learn is idempotent
|
||||||
|
while a counted learn is monotone, ADE threshold adaptation runs, and
|
||||||
|
construction + first predict leave the global RNG untouched.
|
||||||
@@ -0,0 +1,667 @@
|
|||||||
|
## bitbrain_net.nim — BITBRAIN (rack id 17): the REAL ADE+SBC gun.
|
||||||
|
##
|
||||||
|
## The gun at rack id 16 used to be called `BITBRAIN` while containing no network
|
||||||
|
## at all; it is now `LEADGAIN` (`guns/lead_gain.nim`, a per-range-band lead-gain
|
||||||
|
## corrector). This module is the gun that actually runs the algorithm: an ADE
|
||||||
|
## layer (thresholded random projections with ONLINE threshold adaptation)
|
||||||
|
## feeding the SBC head from `common_libs/bitbrain/`, with the COUNTED + DECAY
|
||||||
|
## mode available — the mode that delivers forgetting and true per-class
|
||||||
|
## probabilities (`docs/bitbrain_counted_sbc.md`).
|
||||||
|
##
|
||||||
|
## ── OUTPUT SHAPE: a fine-grained aim correction ON TOP OF Pattern ────────────
|
||||||
|
## Not a direct aim point from the argmax class. Two reasons:
|
||||||
|
## 1. the gate test that motivated this (`docs/bitbrain_gate.md`) measured
|
||||||
|
## exactly this shape — a class-resolved correction added to Pattern's
|
||||||
|
## bearing — so this is the shape with a published measurement behind it;
|
||||||
|
## 2. Pattern's prediction is already a strong, fully-learned predictor. The
|
||||||
|
## network's job is the RESIDUAL angular error, which is a small, signed,
|
||||||
|
## zero-centred quantity; replacing the aim point outright would throw away
|
||||||
|
## Pattern entirely on the (measured) bet that the net beats it.
|
||||||
|
## The correction is the PROBABILITY-WEIGHTED MEAN of the class centres under
|
||||||
|
## `inferProb`, i.e. a continuous readout over `nClasses` discrete cells rather
|
||||||
|
## than a step function of the argmax. Below `TR_BITBRAIN_MINOBS` resolved
|
||||||
|
## samples — or when the readout has no evidence — the correction is exactly 0
|
||||||
|
## and the gun returns Pattern's prediction unchanged.
|
||||||
|
##
|
||||||
|
## ── THE INPUT IS A CONFIGURED SET OF FEATURE BLOCKS ─────────────────────────
|
||||||
|
## The input vector is a concatenation of FEATURE BLOCKS, each independently
|
||||||
|
## selectable (`TR_BITBRAIN_FEATURES`) and with a settable width. A block is a
|
||||||
|
## list of scalar QUANTITIES; a block of width `W` lays each quantity out as a
|
||||||
|
## thermometer (unary) code of `W` slots, so width == resolution: a wider block
|
||||||
|
## distinguishes more states of that quantity. Slots are 0/255 uint8, which the
|
||||||
|
## ADE scorer centres at 127 (`DefaultCenter`), so a synapse "matches" when its
|
||||||
|
## polarity agrees with the slot and a random ADE fires iff its `w` synapses all
|
||||||
|
## match — a clean thresholded random projection with firing rate 2^-w.
|
||||||
|
##
|
||||||
|
## The DEFAULT block set (52 slots) and what each slot means:
|
||||||
|
##
|
||||||
|
## block quant. slots quantity
|
||||||
|
## epos 2 8 enemy offset from us, x and y, over the arena span
|
||||||
|
## evel 2 6 enemy speed; enemy heading minus the bearing to us
|
||||||
|
## eturn 2 4 turn direction this tick; turn consistency over 10
|
||||||
|
## eself 2 4 our speed; our heading minus the bearing to the enemy
|
||||||
|
## dist 2 10 range; range rate over the last 10 ticks
|
||||||
|
## bear 1 4 relative bearing (enemy bearing minus our heading)
|
||||||
|
## walls 4 8 distance to each of the four arena walls
|
||||||
|
## bull 2 4 live bullet count; nearest bullet's signed lateral offset
|
||||||
|
## hzn 1 4 bullet flight time to the current range, h = dist / speed
|
||||||
|
##
|
||||||
|
## `docs/state_window_gate.md` measured that a long TEMPORAL WINDOW of states
|
||||||
|
## destroys recurrence, so there is deliberately NO window block here: the only
|
||||||
|
## history-derived inputs are the 3 rate/turn quantities above (10 ticks), which
|
||||||
|
## is the same causal information Pattern itself uses.
|
||||||
|
##
|
||||||
|
## ── DEFAULT OFF / PARITY ────────────────────────────────────────────────────
|
||||||
|
## Admitted ONLY when `TR_RACK_BITBRAIN=both` AND `TR_BITBRAIN_NET=1`. Both
|
||||||
|
## default off, so the shipped rack never calls `predict`, the network is never
|
||||||
|
## built (`ensureInit` is lazy), and the shipped bot is byte-for-byte unchanged.
|
||||||
|
## The default target RNG is a PRIVATE `initRand(seed)`, so construction cannot
|
||||||
|
## perturb the global selector RNG either.
|
||||||
|
|
||||||
|
import std/[math, os, strutils, strformat, algorithm, random]
|
||||||
|
import gun_harness/gun_interface
|
||||||
|
import guns/tm_horizon
|
||||||
|
import guns/pattern_matcher
|
||||||
|
import bitbrain/bitbrain
|
||||||
|
|
||||||
|
export ade, sbc
|
||||||
|
|
||||||
|
const
|
||||||
|
## ── env knobs (all resolved once at gun construction) ─────────────────────
|
||||||
|
BBN_INPUT_ENV* = "TR_BITBRAIN_INPUT" ## total input slots
|
||||||
|
BBN_CLASSES_ENV* = "TR_BITBRAIN_NCLASSES" ## output resolution
|
||||||
|
BBN_NADES_ENV* = "TR_BITBRAIN_NADES" ## ADEs per AD
|
||||||
|
BBN_WIDTHS_ENV* = "TR_BITBRAIN_WIDTHS" ## clause widths, one AD per width
|
||||||
|
BBN_FEATURES_ENV* = "TR_BITBRAIN_FEATURES" ## block:name:width,...
|
||||||
|
BBN_SPAN_ENV* = "TR_BITBRAIN_SPAN" ## class half-range, degrees
|
||||||
|
BBN_MINOBS_ENV* = "TR_BITBRAIN_MINOBS" ## resolved samples before trusting
|
||||||
|
BBN_LOG_ENV* = "TR_BITBRAIN_NETLOG" ## 1 = per-change [bbn] log
|
||||||
|
BBN_ADAPT_ENV* = "TR_BITBRAIN_ADAPT_EVERY"## ADE threshold adaptation interval
|
||||||
|
BBN_CALIB_ENV* = "TR_BITBRAIN_CALIB_EVERY"## (compat alias; same interval)
|
||||||
|
BBN_SEED_ENV* = "TR_BITBRAIN_NETSEED" ## network seed
|
||||||
|
BBN_TARGET_ENV* = "TR_BITBRAIN_TARGET" ## ADE target firing rate
|
||||||
|
BBN_RESET_ON_TARGET_ENV* = "TR_BITBRAIN_NET_RESET_ON_TARGET"
|
||||||
|
BBN_NET_ENV* = "TR_BITBRAIN_NET" ## master switch (legacy disambiguator)
|
||||||
|
## The library's own knobs (read inside `common_libs/bitbrain`), re-declared
|
||||||
|
## here so `knownEnvNames()` and the boot report cover the whole gun.
|
||||||
|
BBN_MODE_ENV* = "TR_BITBRAIN_MODE"
|
||||||
|
BBN_DECAY_EVERY_ENV* = "TR_BITBRAIN_DECAY_EVERY"
|
||||||
|
BBN_DECAY_SHIFT_ENV* = "TR_BITBRAIN_DECAY_SHIFT"
|
||||||
|
## Every env name this gun reads, for the tree-scan guard's known set.
|
||||||
|
BitbrainNetEnvNames* = [
|
||||||
|
BBN_INPUT_ENV, BBN_CLASSES_ENV, BBN_NADES_ENV, BBN_WIDTHS_ENV,
|
||||||
|
BBN_FEATURES_ENV, BBN_SPAN_ENV, BBN_MINOBS_ENV, BBN_LOG_ENV,
|
||||||
|
BBN_ADAPT_ENV, BBN_CALIB_ENV, BBN_SEED_ENV, BBN_TARGET_ENV,
|
||||||
|
BBN_RESET_ON_TARGET_ENV, BBN_MODE_ENV, BBN_DECAY_EVERY_ENV,
|
||||||
|
BBN_DECAY_SHIFT_ENV]
|
||||||
|
## (`BBN_NET_ENV` == `TR_BITBRAIN_NET` is deliberately absent: it is already
|
||||||
|
## registered as `LG_NET_SWITCH_ENV`, the one switch both guns share.)
|
||||||
|
|
||||||
|
## ── feature blocks ────────────────────────────────────────────────────────
|
||||||
|
BB_BLOCKS* = [
|
||||||
|
("epos", 2, 4), ("evel", 2, 3), ("eturn", 2, 2), ("eself", 2, 2),
|
||||||
|
("dist", 2, 5), ("bear", 1, 4), ("walls", 4, 2), ("bull", 2, 2),
|
||||||
|
("hzn", 1, 4)]
|
||||||
|
## Their shipped widths sum to BB_DEFAULT_SLOTS (8+6+4+4+10+4+8+4+4 = 52).
|
||||||
|
BB_DEFAULT_SLOTS* = 52
|
||||||
|
BB_MAX_SLOTS* = 4096
|
||||||
|
|
||||||
|
## ── defaults ──────────────────────────────────────────────────────────────
|
||||||
|
BBN_NCLASSES_DEF = 8 ## output resolution
|
||||||
|
BBN_NADES_DEF = 256 ## ADEs per AD
|
||||||
|
BBN_WIDTHS_DEF* = @[4, 5, 6] ## clause widths: one AD per width, 3 cross SBCs
|
||||||
|
BBN_SPAN_DEF = 40.0
|
||||||
|
BBN_MINOBS_DEF = 32 ## resolved samples before the net is trusted
|
||||||
|
BBN_ADAPT_DEF = 200 ## inputs between threshold-adaptation passes
|
||||||
|
BBN_TARGET_DEF = 0.01 ## paper's target ADE firing rate
|
||||||
|
BBN_SEED_DEF = 20240921
|
||||||
|
BBN_PENDING_CAP* = 512
|
||||||
|
BBN_HIST* = 12 ## the short causal history ring (see the note)
|
||||||
|
BBN_SBC_VALUE* = 255'u8 ## a "set" thermometer slot
|
||||||
|
BBN_SLOT_CENTRE = 127 ## DefaultCenter; a synapse matches when 0 or 255
|
||||||
|
|
||||||
|
type
|
||||||
|
Block = tuple[name: string, nQuant: int, width: int]
|
||||||
|
|
||||||
|
Pending = object
|
||||||
|
fireTick: int
|
||||||
|
horizon: int
|
||||||
|
selfX, selfY: float
|
||||||
|
baseBearing: float
|
||||||
|
input: seq[uint8]
|
||||||
|
klass: int
|
||||||
|
|
||||||
|
HistSample = object
|
||||||
|
x, y, heading: float
|
||||||
|
|
||||||
|
BitbrainNetGun* = object
|
||||||
|
tmh: TmHorizonGun
|
||||||
|
initialized: bool
|
||||||
|
## ── resolved config (boot report) ────────────────────────────────────────
|
||||||
|
enabled*: bool
|
||||||
|
inputWidth*: int
|
||||||
|
nClasses*: int
|
||||||
|
nAde*: int
|
||||||
|
widths*: seq[int]
|
||||||
|
blockWidths*: seq[int] ## per BB_BLOCKS entry; 0 = block disabled
|
||||||
|
features*: string ## the resolved block spec, for the boot report
|
||||||
|
maxDeg*: float
|
||||||
|
minObs*: int
|
||||||
|
adaptEvery*: int
|
||||||
|
targetRate*: float
|
||||||
|
seed*: int64
|
||||||
|
logEnabled*: bool
|
||||||
|
resetOnTarget*: bool
|
||||||
|
mode*: SbcMode
|
||||||
|
decayEvery*: int
|
||||||
|
decayShift*: int
|
||||||
|
## ── the network ─────────────────────────────────────────────────────────
|
||||||
|
net*: BitBrain
|
||||||
|
built: bool
|
||||||
|
## ── learner state ───────────────────────────────────────────────────────
|
||||||
|
trained*: int
|
||||||
|
sinceAdapt: int
|
||||||
|
adapts*: int
|
||||||
|
sinceEnqTick: int
|
||||||
|
sinceEnqBucket: int
|
||||||
|
pending: array[BBN_PENDING_CAP, Pending]
|
||||||
|
pendingCount*: int
|
||||||
|
pendingDropped*: int
|
||||||
|
observedTargetId*: int
|
||||||
|
lastTick: int
|
||||||
|
## ── readout ─────────────────────────────────────────────────────────────
|
||||||
|
lastShiftDeg*: float
|
||||||
|
lastClass*: int
|
||||||
|
corrections*: int
|
||||||
|
lastLogKey: string
|
||||||
|
## ── the short causal history the rate/turn blocks need ─────────────────
|
||||||
|
## DELIBERATELY SHORT (12 ticks, and only 3 derived quantities use it):
|
||||||
|
## `docs/state_window_gate.md` measured that feeding a long TEMPORAL WINDOW
|
||||||
|
## of states destroys the recurrence this network depends on. This is not
|
||||||
|
## a window block — it is the same 10-tick information Pattern uses.
|
||||||
|
hist: seq[HistSample]
|
||||||
|
|
||||||
|
# ── config helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc envStrBbn(name: string): string {.inline.} =
|
||||||
|
let v = getEnv(name, "")
|
||||||
|
if v.len > 0: v.strip() else: ""
|
||||||
|
|
||||||
|
proc envIntBbn(name: string, default: int): int =
|
||||||
|
let v = envStrBbn(name)
|
||||||
|
if v.len == 0: return default
|
||||||
|
try: parseInt(v) except ValueError: default
|
||||||
|
|
||||||
|
proc envFloatBbn(name: string, default: float): float =
|
||||||
|
let v = envStrBbn(name)
|
||||||
|
if v.len == 0: return default
|
||||||
|
try: parseFloat(v) except ValueError: default
|
||||||
|
|
||||||
|
proc envBoolBbn(name: string, default: bool): bool =
|
||||||
|
case envStrBbn(name).toLowerAscii()
|
||||||
|
of "1", "true", "yes", "on": true
|
||||||
|
of "0", "false", "no", "off": false
|
||||||
|
else: default
|
||||||
|
|
||||||
|
proc netOn*(): bool =
|
||||||
|
## The master switch. Default OFF: this gun is never admitted by an unset
|
||||||
|
## environment, and while it is off the `TR_BITBRAIN_*` names are the LEGACY
|
||||||
|
## aliases of the LEADGAIN corrector (`guns/lead_gain.nim`).
|
||||||
|
envBoolBbn(BBN_NET_ENV, false)
|
||||||
|
|
||||||
|
proc blockNames*(): string =
|
||||||
|
## The known block names, comma separated (used in the unknown-block warning).
|
||||||
|
for i in 0 ..< BB_BLOCKS.len:
|
||||||
|
if i > 0: result.add ","
|
||||||
|
result.add BB_BLOCKS[i][0]
|
||||||
|
|
||||||
|
proc parseBlockWidths*(value: string): seq[int] =
|
||||||
|
## Parse `TR_BITBRAIN_FEATURES` — a comma-separated `name[:W]` list. A block
|
||||||
|
## that is NOT listed keeps its shipped width; an explicitly listed block may
|
||||||
|
## be switched off with width 0. Unknown names are ignored (with a stderr
|
||||||
|
## warning) so a typo cannot silently change the input size. Unset/empty ->
|
||||||
|
## the shipped widths, byte-identical.
|
||||||
|
result = newSeq[int](BB_BLOCKS.len)
|
||||||
|
for i, b in BB_BLOCKS: result[i] = b[2]
|
||||||
|
if value.len == 0: return
|
||||||
|
for part in value.split(','):
|
||||||
|
let p = part.strip()
|
||||||
|
if p.len == 0: continue
|
||||||
|
let ci = p.find(':')
|
||||||
|
let nm = (if ci < 0: p else: p[0..<ci]).strip().toLowerAscii()
|
||||||
|
var w = -1
|
||||||
|
if ci >= 0:
|
||||||
|
try: w = parseInt(p[ci+1..^1].strip())
|
||||||
|
except ValueError: w = -1
|
||||||
|
var found = false
|
||||||
|
for i, b in BB_BLOCKS:
|
||||||
|
if b[0] == nm:
|
||||||
|
found = true
|
||||||
|
result[i] = (if w < 0: b[2] else: w)
|
||||||
|
if not found:
|
||||||
|
stderr.writeLine("[bbn] unknown feature block '" & nm & "' in " &
|
||||||
|
BBN_FEATURES_ENV & "; ignored (known: " &
|
||||||
|
blockNames() & ")")
|
||||||
|
for w in result.mitems: w = max(0, min(w, 64))
|
||||||
|
|
||||||
|
proc parseWidths*(value: string): seq[int] =
|
||||||
|
## Parse `TR_BITBRAIN_WIDTHS` — the ADE clause widths, one AD per width, one
|
||||||
|
## cross-AD SBC per unordered pair. Unset -> `BBN_WIDTHS_DEF`.
|
||||||
|
if value.len == 0: return BBN_WIDTHS_DEF
|
||||||
|
for tok in value.split(','):
|
||||||
|
let t = tok.strip()
|
||||||
|
if t.len == 0: continue
|
||||||
|
var w: int
|
||||||
|
try: w = parseInt(t) except ValueError: continue
|
||||||
|
if w >= 1 and w <= 64: result.add w
|
||||||
|
if result.len == 0: return BBN_WIDTHS_DEF
|
||||||
|
|
||||||
|
proc blockWidthsString*(widths: seq[int]): string =
|
||||||
|
## The resolved block spec as the env's own form (boot report).
|
||||||
|
for i, w in widths:
|
||||||
|
if i > 0: result.add ","
|
||||||
|
if w == 0: result.add BB_BLOCKS[i][0] & ":0"
|
||||||
|
elif w == BB_BLOCKS[i][2]: result.add BB_BLOCKS[i][0]
|
||||||
|
else: result.add BB_BLOCKS[i][0] & ":" & $w
|
||||||
|
|
||||||
|
proc derivedSlots*(widths: seq[int]): int =
|
||||||
|
for i, b in BB_BLOCKS: result += b[1] * widths[i]
|
||||||
|
|
||||||
|
proc blockName*(i: int): string {.inline.} = BB_BLOCKS[i][0]
|
||||||
|
proc blockQuantities*(i: int): int {.inline.} = BB_BLOCKS[i][1]
|
||||||
|
|
||||||
|
# ── construction ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc initBitbrainNetGun*(): BitbrainNetGun =
|
||||||
|
result.enabled = netOn()
|
||||||
|
result.blockWidths = parseBlockWidths(envStrBbn(BBN_FEATURES_ENV))
|
||||||
|
result.features = blockWidthsString(result.blockWidths)
|
||||||
|
let derived = derivedSlots(result.blockWidths)
|
||||||
|
result.inputWidth = clamp(envIntBbn(BBN_INPUT_ENV, derived), 1, BB_MAX_SLOTS)
|
||||||
|
result.nClasses = clamp(envIntBbn(BBN_CLASSES_ENV, BBN_NCLASSES_DEF), 2, 4096)
|
||||||
|
result.nAde = clamp(envIntBbn(BBN_NADES_ENV, BBN_NADES_DEF), 8, 8192)
|
||||||
|
result.widths = parseWidths(envStrBbn(BBN_WIDTHS_ENV))
|
||||||
|
result.maxDeg = clamp(envFloatBbn(BBN_SPAN_ENV, BBN_SPAN_DEF), 1.0, 180.0)
|
||||||
|
result.minObs = max(1, envIntBbn(BBN_MINOBS_ENV, BBN_MINOBS_DEF))
|
||||||
|
result.adaptEvery = max(1, envIntBbn(BBN_ADAPT_ENV, envIntBbn(BBN_CALIB_ENV, BBN_ADAPT_DEF)))
|
||||||
|
result.targetRate = clamp(envFloatBbn(BBN_TARGET_ENV, BBN_TARGET_DEF), 0.0001, 0.5)
|
||||||
|
result.seed = int64(envIntBbn(BBN_SEED_ENV, BBN_SEED_DEF))
|
||||||
|
result.logEnabled = envBoolBbn(BBN_LOG_ENV, false)
|
||||||
|
result.resetOnTarget = envBoolBbn(BBN_RESET_ON_TARGET_ENV, true)
|
||||||
|
# The SBC storage mode + its decay knobs are the LIBRARY's env names, read
|
||||||
|
# here so the gun's resolved config (and the boot report) is the real one.
|
||||||
|
result.mode = envSbcMode(smCounted)
|
||||||
|
result.decayEvery = envDecayEvery(64)
|
||||||
|
result.decayShift = envDecayShift(3)
|
||||||
|
result.lastTick = -1
|
||||||
|
result.sinceEnqTick = -1
|
||||||
|
result.sinceEnqBucket = -1
|
||||||
|
result.observedTargetId = -1
|
||||||
|
|
||||||
|
proc buildNet*(g: var BitbrainNetGun) =
|
||||||
|
## Build the ADE layers + SBC head. LAZY: the shipped rack never calls it.
|
||||||
|
## Deterministic given the seed, and it uses a PRIVATE `initRand`, so it can
|
||||||
|
## never perturb the global selector RNG.
|
||||||
|
if g.built: return
|
||||||
|
g.built = true
|
||||||
|
var rng = initRand(g.seed)
|
||||||
|
# The clause width must not exceed the input width or `initRandomAddressDecoder`
|
||||||
|
# cannot draw `w` distinct indices; clamp the widths to the input width.
|
||||||
|
var widths = g.widths
|
||||||
|
for i, w in widths: widths[i] = min(w, max(2, g.inputWidth))
|
||||||
|
g.net = buildRandomBitBrain(widths = widths, nAde = g.nAde,
|
||||||
|
inputWidth = g.inputWidth, nClasses = g.nClasses,
|
||||||
|
seed = g.seed, mode = g.mode,
|
||||||
|
decayEvery = g.decayEvery,
|
||||||
|
decayShift = g.decayShift)
|
||||||
|
# The initial threshold is 0, which for 0/255 slots centred at 127 fires every
|
||||||
|
# ADE whose `w` synapses all match — rate 2^-w. Homeostatic adaptation then
|
||||||
|
# drives each ADE toward the paper's ~1% target rate, online, unsupervised.
|
||||||
|
for ad in g.net.ades.mitems:
|
||||||
|
for i in 0 ..< ad.nAde: ad.thresholds[i] = int32(127 * ad.width - 1)
|
||||||
|
g.tmh = initTmHorizonGun()
|
||||||
|
|
||||||
|
proc ensureInit*(g: var BitbrainNetGun) =
|
||||||
|
if g.initialized: return
|
||||||
|
g.initialized = true
|
||||||
|
if not g.enabled: return
|
||||||
|
g.buildNet()
|
||||||
|
|
||||||
|
# ── the input vector ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc pushTherm(dst: var seq[uint8], v: float, w: int) =
|
||||||
|
## Lay one quantity out as a `w`-slot thermometer code over [0, 1]. `w` == 0
|
||||||
|
## disables the quantity.
|
||||||
|
if w <= 0: return
|
||||||
|
var level = int(clamp(v, 0.0, 0.999999) * float(w))
|
||||||
|
if level < 0: level = 0
|
||||||
|
if level > w - 1: level = w - 1
|
||||||
|
for j in 0 ..< w:
|
||||||
|
dst.add(if j < level: BBN_SBC_VALUE else: 0'u8)
|
||||||
|
|
||||||
|
proc wrapRadNet(r: float): float {.inline.} =
|
||||||
|
result = r
|
||||||
|
while result > PI: result -= 2.0 * PI
|
||||||
|
while result < -PI: result += 2.0 * PI
|
||||||
|
|
||||||
|
proc pushHist(g: var BitbrainNetGun, state: WorldState) =
|
||||||
|
## Ring of the last `BBN_HIST` states. One push per tick, in `predict`.
|
||||||
|
if g.hist.len == 0:
|
||||||
|
for _ in 0 ..< BBN_HIST:
|
||||||
|
g.hist.add HistSample(x: state.enemyX, y: state.enemyY,
|
||||||
|
heading: state.enemyHeading)
|
||||||
|
g.hist.insert(HistSample(x: state.enemyX, y: state.enemyY,
|
||||||
|
heading: state.enemyHeading), 0)
|
||||||
|
g.hist.setLen(BBN_HIST)
|
||||||
|
|
||||||
|
proc turnSignal(g: BitbrainNetGun, state: WorldState): (float, float) =
|
||||||
|
## (turn direction this tick, turn consistency over the ring). 0.5 = straight,
|
||||||
|
## 0/1 = a full left/right turn; consistency = fraction of the ring's
|
||||||
|
## consecutive steps that turn the SAME way.
|
||||||
|
if g.hist.len < 3:
|
||||||
|
return (0.5, 0.5)
|
||||||
|
let d0 = wrapRadNet(degToRad(state.enemyHeading - g.hist[0].heading))
|
||||||
|
let dir = 0.5 + 0.5 * (if d0 > 0.0: 1.0 elif d0 < 0.0: -1.0 else: 0.0)
|
||||||
|
var same = 0
|
||||||
|
var total = 0
|
||||||
|
for i in 0 ..< g.hist.len - 1:
|
||||||
|
let d = wrapRadNet(degToRad(g.hist[i].heading - g.hist[i+1].heading))
|
||||||
|
if abs(d) < 1e-9: continue
|
||||||
|
inc total
|
||||||
|
if (d > 0) == (d0 > 0.0): inc same
|
||||||
|
(dir, if total == 0: 0.5 else: float(same) / float(total))
|
||||||
|
|
||||||
|
proc rangeSignal(g: BitbrainNetGun, state: WorldState,
|
||||||
|
dist: float): float =
|
||||||
|
## Signed range rate over the ring, in [-1, 1] (approaching / opening), scaled
|
||||||
|
## to +-400 px per tick and clamped. One quantity, ten ticks of history.
|
||||||
|
if g.hist.len < 2: return 0.0
|
||||||
|
let dPrev = hypot(g.hist[0].x - state.selfX, g.hist[0].y - state.selfY)
|
||||||
|
let step = (dist - dPrev) / float(max(1, g.hist.len - 1))
|
||||||
|
clamp(step / 400.0, -1.0, 1.0)
|
||||||
|
|
||||||
|
proc bulletSignal(g: BitbrainNetGun, state: WorldState): (float, float) =
|
||||||
|
## (live-bullet count / 4, nearest bullet's signed lateral offset in [-1,1]).
|
||||||
|
## The nearest known bullet is the one whose own last-seen tick is the most
|
||||||
|
## recent; with no bullet knowledge both are neutral.
|
||||||
|
var best = -1
|
||||||
|
var bestAge = high(int)
|
||||||
|
for e in state.enemies:
|
||||||
|
if e.lastSeenTick < 0: continue
|
||||||
|
let age = state.tick - e.lastSeenTick
|
||||||
|
if age < bestAge: bestAge = age; best = e.id
|
||||||
|
if best < 0: return (0.0, 0.0)
|
||||||
|
for e in state.enemies:
|
||||||
|
if e.id != best: continue
|
||||||
|
let toE = arctan2(e.y - state.selfY, e.x - state.selfX)
|
||||||
|
let toB = arctan2(state.enemyY - state.selfY, state.enemyX - state.selfX)
|
||||||
|
let lat = wrapRadNet(toB - toE)
|
||||||
|
return (0.25, clamp(lat / 0.5, -1.0, 1.0))
|
||||||
|
(0.0, 0.0)
|
||||||
|
|
||||||
|
proc buildInput*(g: var BitbrainNetGun, state: WorldState): seq[uint8] =
|
||||||
|
## (see the header for the block table)
|
||||||
|
## The configured feature-block vector for this state. Block order is fixed
|
||||||
|
## (`BB_BLOCKS`); each block's slot count is `g.blockWidths[i]`. The result is
|
||||||
|
## padded with zero slots or truncated to exactly `g.inputWidth`, so the
|
||||||
|
## ADE codes (drawn once at build time over `inputWidth`) can never index out
|
||||||
|
## of range no matter how the two knobs are combined.
|
||||||
|
result = newSeqOfCap[uint8](g.inputWidth)
|
||||||
|
let bw = g.blockWidths
|
||||||
|
let ex = state.enemyX
|
||||||
|
let ey = state.enemyY
|
||||||
|
let sx = state.selfX
|
||||||
|
let sy = state.selfY
|
||||||
|
let wAll = max(1.0, state.arenaWidth)
|
||||||
|
let hAll = max(1.0, state.arenaHeight)
|
||||||
|
let dx = ex - sx
|
||||||
|
let dy = ey - sy
|
||||||
|
let dist = max(1e-6, hypot(dx, dy))
|
||||||
|
let bearing = arctan2(dy, dx)
|
||||||
|
let selfHdg = degToRad(state.selfHeading)
|
||||||
|
let enemyHdg = degToRad(state.enemyHeading)
|
||||||
|
|
||||||
|
for bi in 0 ..< BB_BLOCKS.len:
|
||||||
|
let w = bw[bi]
|
||||||
|
if w <= 0: continue
|
||||||
|
case bi
|
||||||
|
of 0: # epos — enemy offset over the arena
|
||||||
|
pushTherm(result, 0.5 + dx / wAll, w div 2)
|
||||||
|
pushTherm(result, 0.5 + dy / hAll, w - w div 2)
|
||||||
|
of 1: # evel — speed, heading vs the lane
|
||||||
|
pushTherm(result, state.enemySpeed / 16.0, w div 2)
|
||||||
|
pushTherm(result, 0.5 + 0.5 * sin(enemyHdg - bearing), w - w div 2)
|
||||||
|
of 2: # eturn — turn now, consistency over 10
|
||||||
|
let (dir, cons) = turnSignal(g, state)
|
||||||
|
pushTherm(result, dir, w div 2)
|
||||||
|
pushTherm(result, cons, w - w div 2)
|
||||||
|
of 3: # eself — our speed, our heading error
|
||||||
|
pushTherm(result, state.selfSpeed / 16.0, w div 2)
|
||||||
|
pushTherm(result, 0.5 + 0.5 * sin(selfHdg - bearing), w - w div 2)
|
||||||
|
of 4: # dist — range, range rate over 10
|
||||||
|
pushTherm(result, dist / 1000.0, w div 2)
|
||||||
|
pushTherm(result, 0.5 + 0.5 * rangeSignal(g, state, dist), w - w div 2)
|
||||||
|
of 5: # bear — relative bearing
|
||||||
|
pushTherm(result, (bearing - selfHdg + PI) / (2.0 * PI), w)
|
||||||
|
of 6: # walls — distance to each wall
|
||||||
|
pushTherm(result, ex / wAll, w div 4)
|
||||||
|
pushTherm(result, (wAll - ex) / wAll, w div 4)
|
||||||
|
pushTherm(result, ey / hAll, w div 4)
|
||||||
|
pushTherm(result, (hAll - ey) / hAll, w - 3 * (w div 4))
|
||||||
|
of 7: # bull — live bullets, lateral offset
|
||||||
|
let (n, lat) = bulletSignal(g, state)
|
||||||
|
pushTherm(result, n / 4.0, w div 2)
|
||||||
|
pushTherm(result, 0.5 + 0.5 * lat, w - w div 2)
|
||||||
|
of 8: # hzn — bullet flight time
|
||||||
|
pushTherm(result, float(tmhHorizonFor(dist, 11.0)) / 50.0, w)
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
if result.len < g.inputWidth:
|
||||||
|
for _ in result.len ..< g.inputWidth: result.add 0'u8
|
||||||
|
elif result.len > g.inputWidth:
|
||||||
|
result.setLen(g.inputWidth)
|
||||||
|
|
||||||
|
# ── class geometry ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc classCenterDeg*(k, nClasses: int, maxDeg: float): float {.inline.} =
|
||||||
|
## Centre (degrees) of correction class `k` over ±maxDeg.
|
||||||
|
-maxDeg + (float(k) + 0.5) * (2.0 * maxDeg / float(nClasses))
|
||||||
|
|
||||||
|
proc classOf*(errRad, maxDeg: float, nClasses: int): int {.inline.} =
|
||||||
|
## Bin a signed angular error (radians) into one of `nClasses` bins.
|
||||||
|
let x = radToDeg(errRad)
|
||||||
|
var k = int((x + maxDeg) / (2.0 * maxDeg) * float(nClasses))
|
||||||
|
if k < 0: k = 0
|
||||||
|
if k >= nClasses: k = nClasses - 1
|
||||||
|
k
|
||||||
|
|
||||||
|
# ── readout ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc netShiftDeg*(g: var BitbrainNetGun, input: openArray[uint8]): float =
|
||||||
|
## The fine-grained correction: the PROBABILITY-WEIGHTED MEAN of the class
|
||||||
|
## centres over the SBC posterior. Returns 0.0 when there is no evidence.
|
||||||
|
let (label, scores) = g.net.inferProb(input)
|
||||||
|
var sum = 0.0
|
||||||
|
var tot = 0.0
|
||||||
|
for k in 0 ..< scores.len:
|
||||||
|
sum += scores[k] * classCenterDeg(k, g.nClasses, g.maxDeg)
|
||||||
|
tot += scores[k]
|
||||||
|
g.lastClass = label
|
||||||
|
if tot <= 0.0: 0.0 else: sum / tot
|
||||||
|
|
||||||
|
proc adaptThresholds*(g: var BitbrainNetGun, input: openArray[uint8]) =
|
||||||
|
## Online homeostasis (unsupervised): accumulate this input's ADE firings and
|
||||||
|
## every `adaptEvery` inputs nudge each threshold toward `targetRate`.
|
||||||
|
for ad in g.net.ades.mitems: ad.accumulateFiring(input)
|
||||||
|
inc g.sinceAdapt
|
||||||
|
if g.sinceAdapt >= g.adaptEvery:
|
||||||
|
for ad in g.net.ades.mitems:
|
||||||
|
ad.adaptThresholds(interval = g.sinceAdapt, targetRate = g.targetRate)
|
||||||
|
g.sinceAdapt = 0
|
||||||
|
inc g.adapts
|
||||||
|
|
||||||
|
proc resolvePending(g: var BitbrainNetGun, state: WorldState) =
|
||||||
|
## Prequential label resolution: `h` ticks after the fire, the enemy's OBSERVED
|
||||||
|
## bearing from the firing position is the FACT; the required correction is
|
||||||
|
## `observedBearing - baseBearing`, binned into a class, and learned.
|
||||||
|
var w = 0
|
||||||
|
for i in 0 ..< g.pendingCount:
|
||||||
|
let p = g.pending[i]
|
||||||
|
let due = p.fireTick + p.horizon
|
||||||
|
if due > state.tick:
|
||||||
|
g.pending[w] = p
|
||||||
|
inc w
|
||||||
|
elif due == state.tick:
|
||||||
|
let obs = tmhObservedAt(g.tmh, state.tick, p.selfX, p.selfY)
|
||||||
|
if obs.ok and (state.tick - obs.lastSeenTick) <= TMH_STALE_MAX:
|
||||||
|
let err = wrapRadNet(obs.bearing - p.baseBearing)
|
||||||
|
g.net.learn(p.input, classOf(err, g.maxDeg, g.nClasses))
|
||||||
|
inc g.trained
|
||||||
|
else:
|
||||||
|
inc g.pendingDropped
|
||||||
|
else:
|
||||||
|
inc g.pendingDropped
|
||||||
|
g.pendingCount = w
|
||||||
|
|
||||||
|
proc bbnLog(g: var BitbrainNetGun, state: WorldState, shiftDeg: float) =
|
||||||
|
if not g.logEnabled: return
|
||||||
|
let key = fmt"{shiftDeg:.2f}"
|
||||||
|
if key == g.lastLogKey: return
|
||||||
|
g.lastLogKey = key
|
||||||
|
echo fmt"[bbn] t={state.tick} shift={shiftDeg:+.2f}deg class={g.lastClass} " &
|
||||||
|
fmt"in={g.inputWidth} ncl={g.nClasses} nAde={g.nAde} " &
|
||||||
|
fmt"mode={($g.mode)[7..^1]} trained={g.trained} adapts={g.adapts} " &
|
||||||
|
fmt"pend={g.pendingCount} dropped={g.pendingDropped}"
|
||||||
|
|
||||||
|
# ── reset hooks ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc resetRoundState*(g: var BitbrainNetGun) =
|
||||||
|
if not g.initialized: return
|
||||||
|
g.tmh.resetRoundState()
|
||||||
|
g.pendingCount = 0
|
||||||
|
g.hist.setLen(0)
|
||||||
|
g.lastTick = -1
|
||||||
|
g.sinceEnqTick = -1
|
||||||
|
g.sinceEnqBucket = -1
|
||||||
|
g.lastLogKey = ""
|
||||||
|
|
||||||
|
proc resetLearning*(g: var BitbrainNetGun, reason = "") =
|
||||||
|
## Per-battle / per-enemy wipe of the SBC counters. The ADEs (codes and
|
||||||
|
## adapted thresholds) survive: they are unsupervised structure, not labels.
|
||||||
|
if not g.initialized: return
|
||||||
|
g.net.resetLearning()
|
||||||
|
g.trained = 0
|
||||||
|
g.sinceAdapt = 0
|
||||||
|
g.adapts = 0
|
||||||
|
g.corrections = 0
|
||||||
|
g.lastShiftDeg = 0.0
|
||||||
|
g.observedTargetId = -1
|
||||||
|
g.resetRoundState()
|
||||||
|
if reason.len > 0 and g.logEnabled: echo fmt"[bbn-reset] reason={reason}"
|
||||||
|
|
||||||
|
proc targetChanged*(g: var BitbrainNetGun, enemyId: int): bool =
|
||||||
|
if not g.resetOnTarget: return false
|
||||||
|
if enemyId < 0: return false
|
||||||
|
if g.observedTargetId >= 0 and enemyId != g.observedTargetId:
|
||||||
|
g.resetLearning("target_change")
|
||||||
|
g.observedTargetId = enemyId
|
||||||
|
return true
|
||||||
|
g.observedTargetId = enemyId
|
||||||
|
false
|
||||||
|
|
||||||
|
# ── Gun interface ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc isWarmedUp*(g: BitbrainNetGun): bool {.inline.} =
|
||||||
|
## Ready as soon as it has enough resolved samples to have a readout; before
|
||||||
|
## that it is the identity on Pattern, which is always a valid prediction.
|
||||||
|
g.trained >= g.minObs
|
||||||
|
|
||||||
|
proc networkBytes*(g: BitbrainNetGun): int =
|
||||||
|
## RAM held by the network (ADs + SBC tensors). 0 before the lazy build.
|
||||||
|
if not g.built: 0 else: g.net.memoryBytes
|
||||||
|
|
||||||
|
proc sbcBytes*(g: BitbrainNetGun): int =
|
||||||
|
if not g.built: 0 else: g.net.sbcMemoryBytes
|
||||||
|
|
||||||
|
proc predict*(g: var BitbrainNetGun, state: WorldState,
|
||||||
|
bulletSpeed: float): GunPrediction =
|
||||||
|
g.ensureInit()
|
||||||
|
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]
|
||||||
@@ -111,6 +111,7 @@ import std/[math, os, strutils, strformat, algorithm]
|
|||||||
import gun_harness/gun_interface
|
import gun_harness/gun_interface
|
||||||
import guns/tm_horizon
|
import guns/tm_horizon
|
||||||
import guns/pattern_matcher
|
import guns/pattern_matcher
|
||||||
|
import gun_harness/selector
|
||||||
|
|
||||||
const
|
const
|
||||||
## ── env knobs (all resolved once at gun construction) ─────────────────────
|
## ── env knobs (all resolved once at gun construction) ─────────────────────
|
||||||
@@ -297,7 +298,10 @@ const
|
|||||||
## Knobs that actually change behaviour (the rest are inert configuration kept
|
## Knobs that actually change behaviour (the rest are inert configuration kept
|
||||||
## for the boot report). A deprecation line is only worth printing for these
|
## for the boot report). A deprecation line is only worth printing for these
|
||||||
## plus the inert ones, because a stale inert name is still a stale name.
|
## plus the inert ones, because a stale inert name is still a stale name.
|
||||||
LegacyRackEnvName* = "TR_RACK_BITBRAIN"
|
## 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.
|
||||||
|
LegacyRackEnvName* = RackLegacyAlias[0][0]
|
||||||
|
|
||||||
proc netSwitchOn*(): bool =
|
proc netSwitchOn*(): bool =
|
||||||
## `TR_BITBRAIN_NET` unset/0 => the `TR_BITBRAIN_*` names are LEGACY aliases
|
## `TR_BITBRAIN_NET` unset/0 => the `TR_BITBRAIN_*` names are LEGACY aliases
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
## SCALING SWEEP for the ADE+SBC gun (rack id 17) — RAM vs ms/tick vs quality.
|
||||||
|
##
|
||||||
|
## THE QUESTION: "how do inference, timing and size of RAM scale with input and
|
||||||
|
## output size?" This answers it on the REAL gun by replaying a recorded live
|
||||||
|
## corpus through it, not from theory.
|
||||||
|
##
|
||||||
|
## For each setting it reports:
|
||||||
|
## RAM — `memoryBytes` (ADs + SBC tensors) and the SBC tensor alone,
|
||||||
|
## with the counted-vs-bitset factor spelled out;
|
||||||
|
## ms/tick — wall time per RECORDED TICK over the real replay, against the
|
||||||
|
## project's 13.16 ms/tick budget. One `predict` per power bin
|
||||||
|
## per tick, which is what the live loop does, so the number
|
||||||
|
## is directly comparable with the budget;
|
||||||
|
## quality — the OFFLINE ruler: mean |angular error| against the true
|
||||||
|
## interception point (`gun_harness/prediction_quality`), plus
|
||||||
|
## the hit proxy (|err| <= atan(18/range)).
|
||||||
|
##
|
||||||
|
## VETO-CAPABLE CHECK ONLY. Per `docs/offline_harness_trust.md` the offline
|
||||||
|
## harness is trustworthy for per-gun, single-tick prediction quality on a FIXED
|
||||||
|
## trajectory and for NOTHING that flows through the closed loop. A win here is
|
||||||
|
## NOT a live win and is never presented as one.
|
||||||
|
##
|
||||||
|
## Usage:
|
||||||
|
## nim c -r -d:release --path:common_libs common_libs/tests/measure_bitbrain_scaling.nim \
|
||||||
|
## [--corpus /tmp/tfil_ab2/out] [--limit N]
|
||||||
|
##
|
||||||
|
## Env knobs are set per SETTING by this program (putEnv), so the sweep is a
|
||||||
|
## pure-env experiment: no recompile between arms.
|
||||||
|
|
||||||
|
import std/[os, strformat, strutils, times, math, sequtils, algorithm]
|
||||||
|
import gun_harness/gun_interface
|
||||||
|
import gun_harness/prediction_quality
|
||||||
|
import gun_harness/virtual_bullets
|
||||||
|
import guns/pattern_matcher
|
||||||
|
import guns/bitbrain_net
|
||||||
|
|
||||||
|
const
|
||||||
|
BudgetMsPerTick* = 13.16 ## the project's live tick budget
|
||||||
|
|
||||||
|
type
|
||||||
|
Setting = object
|
||||||
|
label: string
|
||||||
|
inputWidth: int
|
||||||
|
nClasses: int
|
||||||
|
nAde: int
|
||||||
|
widths: string
|
||||||
|
features: string
|
||||||
|
mode: string
|
||||||
|
|
||||||
|
Row = object
|
||||||
|
s: Setting
|
||||||
|
ramBytes: int
|
||||||
|
inputWidth: int
|
||||||
|
sbcBytes: int
|
||||||
|
adBytes: int
|
||||||
|
msPerTick: float
|
||||||
|
meanAbsDeg: float
|
||||||
|
hitProxy: float
|
||||||
|
patternAbsDeg: float
|
||||||
|
n: int
|
||||||
|
|
||||||
|
# ── the sweep grid ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const FullFeatures = "epos:4,evel:3,eturn:2,eself:2,dist:5,bear:4,walls:2,bull:2,hzn:4"
|
||||||
|
|
||||||
|
proc smallFeatures(): string = "epos:2,evel:2,eturn:1,eself:1,dist:2,bear:2,walls:1,bull:1,hzn:2"
|
||||||
|
proc largeFeatures(): string = "epos:6,evel:5,eturn:4,eself:4,dist:8,bear:6,walls:3,bull:3,hzn:6"
|
||||||
|
|
||||||
|
proc settings(): seq[Setting] =
|
||||||
|
## Three points along the INPUT axis (classes/geometry held at the default) and
|
||||||
|
## three along the OUTPUT axis (input held at the default 52), all in BOTH
|
||||||
|
## storage modes, because the counted/bitset factor is part of the answer.
|
||||||
|
let base = Setting(label: "default", inputWidth: 0, nClasses: 8, nAde: 256,
|
||||||
|
widths: "4,5,6", features: FullFeatures, mode: "counted")
|
||||||
|
var inp: seq[Setting]
|
||||||
|
for (lbl, feat, w) in [("input-small", smallFeatures(), 0),
|
||||||
|
("input-medium", FullFeatures, 0),
|
||||||
|
("input-large", largeFeatures(), 0)]:
|
||||||
|
inp.add Setting(label: lbl, inputWidth: w, nClasses: 8, nAde: 256,
|
||||||
|
widths: "4,5,6", features: feat, mode: "counted")
|
||||||
|
var outp: seq[Setting]
|
||||||
|
for (lbl, nc) in [("classes-small", 2), ("classes-medium", 8),
|
||||||
|
("classes-large", 64)]:
|
||||||
|
outp.add Setting(label: lbl, inputWidth: 0, nClasses: nc, nAde: 256,
|
||||||
|
widths: "4,5,6", features: FullFeatures, mode: "counted")
|
||||||
|
# the nAde axis is the third one, because RAM is quadratic in it
|
||||||
|
var ade: seq[Setting]
|
||||||
|
for (lbl, n) in [("nAde-small", 64), ("nAde-medium", 256), ("nAde-large", 512)]:
|
||||||
|
ade.add Setting(label: lbl, inputWidth: 0, nClasses: 8, nAde: n,
|
||||||
|
widths: "4,5,6", features: FullFeatures, mode: "counted")
|
||||||
|
var both: seq[Setting]
|
||||||
|
for m in ["bitset", "counted"]:
|
||||||
|
both.add Setting(label: "mode-" & m, inputWidth: 0, nClasses: 8, nAde: 256,
|
||||||
|
widths: "4,5,6", features: FullFeatures, mode: m)
|
||||||
|
result = inp & outp & ade & both
|
||||||
|
discard base
|
||||||
|
|
||||||
|
# ── one arm ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc applySetting(s: Setting) =
|
||||||
|
for n in BitbrainNetEnvNames: delEnv(n)
|
||||||
|
putEnv(BBN_NET_ENV, "1")
|
||||||
|
putEnv(BBN_FEATURES_ENV, s.features)
|
||||||
|
if s.inputWidth > 0: putEnv(BBN_INPUT_ENV, $s.inputWidth)
|
||||||
|
putEnv(BBN_CLASSES_ENV, $s.nClasses)
|
||||||
|
putEnv(BBN_NADES_ENV, $s.nAde)
|
||||||
|
putEnv(BBN_WIDTHS_ENV, s.widths)
|
||||||
|
putEnv(BBN_MODE_ENV, s.mode)
|
||||||
|
putEnv(BBN_MINOBS_ENV, "1")
|
||||||
|
putEnv(BBN_DECAY_EVERY_ENV, "64")
|
||||||
|
putEnv(BBN_DECAY_SHIFT_ENV, "3")
|
||||||
|
|
||||||
|
# ── the corpus, turned once into a fixed sample set ─────────────────────────
|
||||||
|
#
|
||||||
|
# The interception solve (the ruler) is INDEPENDENT of the arm, so it is done
|
||||||
|
# ONCE and cached. That does two things: every arm is scored on byte-identical
|
||||||
|
# labels, and the timed region contains ONLY the gun's `predict` calls — the
|
||||||
|
# ruler's own cost cannot contaminate the ms/tick number.
|
||||||
|
|
||||||
|
type
|
||||||
|
Sample = object
|
||||||
|
st: WorldState
|
||||||
|
speed: float
|
||||||
|
targetLead: float
|
||||||
|
range: float
|
||||||
|
tol: float
|
||||||
|
|
||||||
|
proc buildSamples(runs: seq[string]): seq[Sample] =
|
||||||
|
for rp in runs:
|
||||||
|
let c = loadCorpus(rp)
|
||||||
|
if c.n == 0: continue
|
||||||
|
for r in 0 ..< c.rStart.len:
|
||||||
|
let base = int(c.rStart[r]) - c.base
|
||||||
|
let cnt = int(c.rCount[r])
|
||||||
|
let iEnd = base + cnt
|
||||||
|
for i in base ..< iEnd:
|
||||||
|
let ox = c.sx(i)
|
||||||
|
let oy = c.sy(i)
|
||||||
|
let localTick = int(c.tick[i]) - int(c.rStart[r])
|
||||||
|
let baseState = WorldState(
|
||||||
|
arenaWidth: c.arenaW, arenaHeight: c.arenaH, tick: localTick,
|
||||||
|
enemyX: c.ex(i), enemyY: c.ey(i), enemyHeading: c.eh(i),
|
||||||
|
enemySpeed: c.es(i), enemyEnergy: c.ee(i),
|
||||||
|
selfX: ox, selfY: oy, selfHeading: c.sh(i), selfSpeed: c.ss(i),
|
||||||
|
selfEnergy: c.se(i), selfRadarHeading: c.sh(i))
|
||||||
|
let los = bearingDeg(ox, oy, c.ex(i), c.ey(i))
|
||||||
|
for bin in 0 ..< len(PowerBins):
|
||||||
|
let speed = bulletSpeed(PowerBins[bin])
|
||||||
|
let ib = interceptBearing(c, i, iEnd, ox, oy, speed, true)
|
||||||
|
if not ib.ok: continue
|
||||||
|
result.add Sample(st: baseState, speed: speed,
|
||||||
|
targetLead: wrap180(ib.bearing - los),
|
||||||
|
range: ib.range, tol: tolDeg(ib.range))
|
||||||
|
|
||||||
|
proc runArm(s: Setting, samples: seq[Sample]): Row =
|
||||||
|
applySetting(s)
|
||||||
|
var g = initBitbrainNetGun()
|
||||||
|
var pat = PatternMatcherGun()
|
||||||
|
var sumAbs = 0.0
|
||||||
|
var sumPat = 0.0
|
||||||
|
var hits = 0
|
||||||
|
var n = 0
|
||||||
|
# `samples` is ordered round-by-round, so the gun sees rounds in order.
|
||||||
|
var t0 = epochTime()
|
||||||
|
for smp in samples:
|
||||||
|
let bp = predict(g, smp.st, smp.speed)
|
||||||
|
let los = bearingDeg(smp.st.selfX, smp.st.selfY,
|
||||||
|
smp.st.enemyX, smp.st.enemyY)
|
||||||
|
let bl = wrap180(bearingDeg(smp.st.selfX, smp.st.selfY, bp.x, bp.y) - los)
|
||||||
|
let err = abs(wrap180(bl - smp.targetLead))
|
||||||
|
sumAbs += err
|
||||||
|
if err <= smp.tol: inc hits
|
||||||
|
let pp = predict(pat, smp.st, smp.speed)
|
||||||
|
let pl = wrap180(bearingDeg(smp.st.selfX, smp.st.selfY, pp.x, pp.y) - los)
|
||||||
|
sumPat += abs(wrap180(pl - smp.targetLead))
|
||||||
|
inc n
|
||||||
|
let elapsed = epochTime() - t0
|
||||||
|
# ms per recorded TICK: `n` samples over `len(PowerBins)` samples per tick.
|
||||||
|
let ticks = max(1, samples.len div len(PowerBins))
|
||||||
|
result = Row(s: s, inputWidth: g.inputWidth, ramBytes: g.networkBytes(),
|
||||||
|
sbcBytes: g.sbcBytes(),
|
||||||
|
adBytes: g.networkBytes() - g.sbcBytes(),
|
||||||
|
msPerTick: elapsed * 1000.0 / float(ticks),
|
||||||
|
meanAbsDeg: if n > 0: sumAbs / float(n) else: NaN,
|
||||||
|
hitProxy: if n > 0: float(hits) / float(n) else: NaN,
|
||||||
|
patternAbsDeg: if n > 0: sumPat / float(n) else: NaN,
|
||||||
|
n: n)
|
||||||
|
|
||||||
|
# ── driver ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc main() =
|
||||||
|
var corpusRoot = "/tmp/tfil_ab2/out"
|
||||||
|
var limit = 4
|
||||||
|
var i = 1
|
||||||
|
while i <= paramCount():
|
||||||
|
case paramStr(i)
|
||||||
|
of "--corpus": inc i; corpusRoot = paramStr(i)
|
||||||
|
of "--limit": inc i; limit = parseInt(paramStr(i))
|
||||||
|
else: stderr.writeLine("unknown arg: " & paramStr(i)); quit(2)
|
||||||
|
inc i
|
||||||
|
var runs = discoverRuns(corpusRoot)
|
||||||
|
if limit > 0 and runs.len > limit: runs.setLen(limit)
|
||||||
|
if runs.len == 0:
|
||||||
|
stderr.writeLine("no runs under " & corpusRoot); quit(1)
|
||||||
|
|
||||||
|
echo "=".repeat(118)
|
||||||
|
echo "BITBRAIN (ADE+SBC, rack id 17) SCALING -- RAM / ms-per-tick / offline prediction quality"
|
||||||
|
echo "=".repeat(118)
|
||||||
|
echo fmt"corpus : {corpusRoot} ({runs.len} recorded run(s))"
|
||||||
|
echo fmt"nAde : ADEs per address decoder; the SBC tensor is nAde^2 x nClasses"
|
||||||
|
echo " cells, so RAM is QUADRATIC in nAde and LINEAR in nClasses."
|
||||||
|
echo fmt"budget : {BudgetMsPerTick} ms/tick (one predict per power bin per recorded tick)"
|
||||||
|
echo "quality : mean |angular error| vs the true interception point, over every"
|
||||||
|
echo " tick x power-bin. VETO-CAPABLE OFFLINE CHECK ONLY (docs/offline_harness_trust.md):"
|
||||||
|
echo " a win here is NOT a live win."
|
||||||
|
echo ""
|
||||||
|
stderr.writeLine("building the ruler sample set once from " & $runs.len &
|
||||||
|
" run(s)...")
|
||||||
|
let tSamp = epochTime()
|
||||||
|
let samples = buildSamples(runs)
|
||||||
|
echo fmt"sample set : {samples.len} tick x power-bin samples " &
|
||||||
|
fmt"({samples.len div max(1, len(PowerBins))} recorded ticks) in " &
|
||||||
|
fmt"{epochTime()-tSamp:.1f}s — arm-independent, so the timed region below"
|
||||||
|
echo " contains ONLY the gun's predict calls."
|
||||||
|
echo ""
|
||||||
|
var rows: seq[Row]
|
||||||
|
for s in settings(): rows.add runArm(s, samples)
|
||||||
|
# Pattern is the same on every arm (it never reads the net's knobs), so the
|
||||||
|
# Pattern column is taken from one arm and is identical for all of them.
|
||||||
|
let patRef = rows[1]
|
||||||
|
|
||||||
|
echo "arm in nCl nAde mode RAM B SBC B AD B " &
|
||||||
|
"ms/tick %bud mean|err| hit% Pattern|err|"
|
||||||
|
echo "-".repeat(118)
|
||||||
|
for r in rows:
|
||||||
|
echo fmt"{r.s.label:<15} {r.inputWidth:>4} {r.s.nClasses:>5} " &
|
||||||
|
fmt"{r.s.nAde:>5} {r.s.mode:<8} {r.ramBytes:>8} {r.sbcBytes:>9} " &
|
||||||
|
fmt"{r.adBytes:>7} {r.msPerTick:>8.3f} {100.0*r.msPerTick/BudgetMsPerTick:>6.2f} " &
|
||||||
|
fmt"{r.meanAbsDeg:>10.3f} {100.0*r.hitProxy:>6.2f} {patRef.patternAbsDeg:>13.3f}"
|
||||||
|
echo ""
|
||||||
|
echo "(in = the resolved input width, i.e. the CONFIGURED feature-block total;"
|
||||||
|
echo " Pattern|err| is the shipped Pattern gun on the same ticks and is identical"
|
||||||
|
echo " across arms, since Pattern never reads any of these knobs.)"
|
||||||
|
for n in BitbrainNetEnvNames: delEnv(n)
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import std/[os, strformat, strutils, times, math]
|
import std/[os, strformat, strutils, times, math]
|
||||||
import gun_harness/[gun_interface, virtual_bullets, prediction_quality]
|
import gun_harness/[gun_interface, virtual_bullets, prediction_quality]
|
||||||
import guns/[head_on, pattern_matcher, tm_horizon, lead_gain]
|
import guns/[head_on, pattern_matcher, tm_horizon, lead_gain, bitbrain_net]
|
||||||
|
|
||||||
# arm indices (fixed order = fixed output)
|
# arm indices (fixed order = fixed output)
|
||||||
const
|
const
|
||||||
@@ -43,10 +43,16 @@ const
|
|||||||
# is the rule the corrector must match; it needs no learning (range is known at fire
|
# is the rule the corrector must match; it needs no learning (range is known at fire
|
||||||
# time). The table was selected in-sample from this corpus.
|
# time). The table was selected in-sample from this corpus.
|
||||||
A_BAND* = 13
|
A_BAND* = 13
|
||||||
|
## The REAL ADE+SBC gun (rack id 17, `guns/bitbrain_net.nim`): Pattern base plus
|
||||||
|
## a fine-grained, class-resolved angular correction from the network. The gun
|
||||||
|
## is default OFF (`TR_BITBRAIN_NET=0`), so the arm turns its own switch on
|
||||||
|
## here — the ruler is the one place that must exercise it.
|
||||||
|
A_BBN* = 14
|
||||||
BandGainTable* = [1.0, 1.0, 1.0, 0.0, 0.0]
|
BandGainTable* = [1.0, 1.0, 1.0, 0.0, 0.0]
|
||||||
ArmNames* = ["Oracle", "OracleQuant", "HeadOn", "Pattern", "PatternGain1.5",
|
ArmNames* = ["Oracle", "OracleQuant", "HeadOn", "Pattern", "PatternGain1.5",
|
||||||
"PatternGain2.0", "PatternGain3.0", "NaiveLinear", "TMHorizon", "LeadGain",
|
"PatternGain2.0", "PatternGain3.0", "NaiveLinear", "TMHorizon", "LeadGain",
|
||||||
"PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain"]
|
"PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain",
|
||||||
|
"BitBrainNet"]
|
||||||
|
|
||||||
const
|
const
|
||||||
## The five sub-unity gain arms, in increasing order, resolved to arm indices.
|
## The five sub-unity gain arms, in increasing order, resolved to arm indices.
|
||||||
@@ -96,6 +102,7 @@ type Ctx = object
|
|||||||
naive: NaiveLinearGun
|
naive: NaiveLinearGun
|
||||||
tmh: TmHorizonGun
|
tmh: TmHorizonGun
|
||||||
lg: LeadGainGun
|
lg: LeadGainGun
|
||||||
|
bbn: BitbrainNetGun
|
||||||
headon: HeadOnGun
|
headon: HeadOnGun
|
||||||
st: WorldState
|
st: WorldState
|
||||||
enemy: seq[EnemyInfo]
|
enemy: seq[EnemyInfo]
|
||||||
@@ -173,6 +180,10 @@ proc runRound(ctx: var Ctx, arms: var seq[ArmAcc], r: int) =
|
|||||||
let bp = predict(ctx.lg, ctx.st, speed)
|
let bp = predict(ctx.lg, ctx.st, speed)
|
||||||
let bl = wrap180(bearingDeg(ox, oy, bp.x, bp.y) - los)
|
let bl = wrap180(bearingDeg(ox, oy, bp.x, bp.y) - los)
|
||||||
arms[A_LG].record(rng, wrap180(bl - targetLead), bl, targetLead)
|
arms[A_LG].record(rng, wrap180(bl - targetLead), bl, targetLead)
|
||||||
|
# BITBRAIN (ADE+SBC): Pattern base + the network's fine-grained correction
|
||||||
|
let np2 = predict(ctx.bbn, ctx.st, speed)
|
||||||
|
let nl2 = wrap180(bearingDeg(ox, oy, np2.x, np2.y) - los)
|
||||||
|
arms[A_BBN].record(rng, wrap180(nl2 - targetLead), nl2, targetLead)
|
||||||
|
|
||||||
proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var ShotStat,
|
proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var ShotStat,
|
||||||
doShots: bool, timing: bool, cont: bool): int =
|
doShots: bool, timing: bool, cont: bool): int =
|
||||||
@@ -186,6 +197,7 @@ proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var S
|
|||||||
naive: NaiveLinearGun(lastTick: -1),
|
naive: NaiveLinearGun(lastTick: -1),
|
||||||
tmh: initTmHorizonGun(),
|
tmh: initTmHorizonGun(),
|
||||||
lg: initLeadGainGun(),
|
lg: initLeadGainGun(),
|
||||||
|
bbn: initBitbrainNetGun(),
|
||||||
headon: HeadOnGun(),
|
headon: HeadOnGun(),
|
||||||
st: WorldState(arenaWidth: c.arenaW, arenaHeight: c.arenaH),
|
st: WorldState(arenaWidth: c.arenaW, arenaHeight: c.arenaH),
|
||||||
enemy: newSeq[EnemyInfo](1))
|
enemy: newSeq[EnemyInfo](1))
|
||||||
@@ -213,6 +225,11 @@ proc fmt3(x: float): string =
|
|||||||
if x.classify in {fcNan, fcInf, fcNegInf}: "-" else: fmt"{x:.3f}"
|
if x.classify in {fcNan, fcInf, fcNegInf}: "-" else: fmt"{x:.3f}"
|
||||||
|
|
||||||
proc main() =
|
proc main() =
|
||||||
|
# The ADE+SBC gun is default OFF (two switches). This arm is the one place
|
||||||
|
# that must exercise it, so the ruler turns its master switch on for the whole
|
||||||
|
# process — every other arm is unaffected (they never read TR_BITBRAIN_*).
|
||||||
|
putEnv(BBN_NET_ENV, "1")
|
||||||
|
putEnv(BBN_MINOBS_ENV, "1")
|
||||||
var corpusRoot = "/tmp/tfil_ab2/out"
|
var corpusRoot = "/tmp/tfil_ab2/out"
|
||||||
var limit = 0
|
var limit = 0
|
||||||
var doShots = true
|
var doShots = true
|
||||||
@@ -296,9 +313,10 @@ proc main() =
|
|||||||
let mPat = overallMean(arms, A_PATTERN)
|
let mPat = overallMean(arms, A_PATTERN)
|
||||||
let mTmh = overallMean(arms, A_TMH)
|
let mTmh = overallMean(arms, A_TMH)
|
||||||
let mLg = overallMean(arms, A_LG)
|
let mLg = overallMean(arms, A_LG)
|
||||||
|
let mBbn = overallMean(arms, A_BBN)
|
||||||
let mLin = overallMean(arms, A_NAIVE)
|
let mLin = overallMean(arms, A_NAIVE)
|
||||||
let ordOk = mHead > mPat and mHead > mTmh and mHead > mLg
|
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}"
|
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 ordMsg = if ordOk: "OK (static gun worst among real guns)" else: "UNEXPECTED: a predictive gun is worse than static LOS"
|
let ordMsg = if ordOk: "OK (static gun worst among real guns)" else: "UNEXPECTED: a predictive gun is worse than static LOS"
|
||||||
echo fmt" -> {ordMsg}"
|
echo fmt" -> {ordMsg}"
|
||||||
echo fmt" NaiveLinear mean|err| = {mLin:.3f} deg (over-leads; see the lead-gain sweep for why a larger"
|
echo fmt" NaiveLinear mean|err| = {mLin:.3f} deg (over-leads; see the lead-gain sweep for why a larger"
|
||||||
@@ -317,7 +335,7 @@ proc main() =
|
|||||||
echo "=".repeat(120)
|
echo "=".repeat(120)
|
||||||
echo "HEADROOM -- the direct answer: how far each arm is from the oracle ceiling, per band"
|
echo "HEADROOM -- the direct answer: how far each arm is from the oracle ceiling, per band"
|
||||||
echo "=" .repeat(120)
|
echo "=" .repeat(120)
|
||||||
let hdr = "band Pattern n Pattern|err| Pattern hpx Oracle hpx headroom pp naive hpx TMHoriz hpx LeadGain hpx"
|
let hdr = "band Pattern n Pattern|err| Pattern hpx Oracle hpx headroom pp naive hpx TMHoriz hpx LeadGain hpx BitBrainNet hpx"
|
||||||
echo hdr
|
echo hdr
|
||||||
echo "-".repeat(hdr.len)
|
echo "-".repeat(hdr.len)
|
||||||
for b in 0 ..< NBands:
|
for b in 0 ..< NBands:
|
||||||
@@ -325,8 +343,10 @@ proc main() =
|
|||||||
let orc = arms[A_ORACLE].bands[b]
|
let orc = arms[A_ORACLE].bands[b]
|
||||||
let hp = pat.hitProxy
|
let hp = pat.hitProxy
|
||||||
let ohp = orc.hitProxy
|
let ohp = orc.hitProxy
|
||||||
echo fmt"{BandLabels[b]:<9} {pat.n:>8} {fmt3(meanAbs(pat)):>12} {fmt4(hp):>12} {fmt4(ohp):>12} {ohp - hp:>13.4f} {fmt4(arms[A_NAIVE].bands[b].hitProxy):>11} {fmt4(arms[A_TMH].bands[b].hitProxy):>12} {fmt4(arms[A_LG].bands[b].hitProxy):>13}"
|
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 ""
|
echo ""
|
||||||
|
echo "BitBrainNet hpx = the ADE+SBC gun (rack id 17), Pattern base + a class-resolved"
|
||||||
|
echo "angular correction. VETO-CAPABLE OFFLINE CHECK ONLY, never a live claim."
|
||||||
echo "hitProxy = fraction of tick-bins aimed within atan(18/range) of the true interception point."
|
echo "hitProxy = fraction of tick-bins aimed within atan(18/range) of the true interception point."
|
||||||
echo "headroom pp = oracle hitProxy - Pattern hitProxy = the absolute hit-probability points available"
|
echo "headroom pp = oracle hitProxy - Pattern hitProxy = the absolute hit-probability points available"
|
||||||
echo "to a perfect predictor (the campaign is playing for a slice of this)."
|
echo "to a perfect predictor (the campaign is playing for a slice of this)."
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
## 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)
|
||||||
|
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)
|
||||||
|
clearEnv()
|
||||||
|
|
||||||
|
# ── the knobs really resolve ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc testKnobs() =
|
||||||
|
clearEnv()
|
||||||
|
armNet()
|
||||||
|
putEnv(BBN_INPUT_ENV, "33")
|
||||||
|
putEnv(BBN_CLASSES_ENV, "17")
|
||||||
|
putEnv(BBN_NADES_ENV, "128")
|
||||||
|
putEnv(BBN_WIDTHS_ENV, "3,5,7,9")
|
||||||
|
putEnv(BBN_SPAN_ENV, "25")
|
||||||
|
putEnv(BBN_MODE_ENV, "counted")
|
||||||
|
putEnv(BBN_DECAY_EVERY_ENV, "11")
|
||||||
|
putEnv(BBN_DECAY_SHIFT_ENV, "2")
|
||||||
|
putEnv(BBN_MINOBS_ENV, "7")
|
||||||
|
var g = initBitbrainNetGun()
|
||||||
|
check "knob: input width", g.inputWidth == 33
|
||||||
|
check "knob: nClasses (output resolution)", g.nClasses == 17
|
||||||
|
check "knob: nAde per AD", g.nAde == 128
|
||||||
|
check "knob: clause widths (one AD per width)", g.widths == @[3, 5, 7, 9]
|
||||||
|
check "knob: class half-range", g.maxDeg == 25.0
|
||||||
|
check "knob: SBC mode", g.mode == smCounted
|
||||||
|
check "knob: counted-mode decay knobs",
|
||||||
|
g.decayEvery == 11 and g.decayShift == 2
|
||||||
|
check "knob: minObs", g.minObs == 7
|
||||||
|
putEnv(BBN_MODE_ENV, "bitset")
|
||||||
|
check "knob: bitset mode", initBitbrainNetGun().mode == smBitset
|
||||||
|
clearEnv()
|
||||||
|
|
||||||
|
# ── the input is a CONFIGURED SET OF BLOCKS ──────────────────────────────────
|
||||||
|
|
||||||
|
proc testFeatureBlocks() =
|
||||||
|
clearEnv()
|
||||||
|
armNet()
|
||||||
|
delEnv(BBN_INPUT_ENV)
|
||||||
|
var g = initBitbrainNetGun()
|
||||||
|
check "features: the shipped block set is 47 slots",
|
||||||
|
derivedSlots(g.blockWidths) == BB_DEFAULT_SLOTS and
|
||||||
|
g.inputWidth == BB_DEFAULT_SLOTS
|
||||||
|
check "features: every shipped block is enabled by default",
|
||||||
|
g.blockWidths.allIt(it > 0)
|
||||||
|
# disable two blocks -> the input shrinks by exactly their widths
|
||||||
|
delEnv(BBN_INPUT_ENV)
|
||||||
|
putEnv(BBN_FEATURES_ENV, "epos:0,bull:0")
|
||||||
|
var g2 = initBitbrainNetGun()
|
||||||
|
check "features: a disabled block removes its slots",
|
||||||
|
g2.inputWidth == BB_DEFAULT_SLOTS - 8 - 4
|
||||||
|
# widen one block -> the input grows by exactly the extra slots
|
||||||
|
putEnv(BBN_FEATURES_ENV, "dist:9")
|
||||||
|
var g3 = initBitbrainNetGun()
|
||||||
|
check "features: a wider block adds its slots",
|
||||||
|
g3.inputWidth == BB_DEFAULT_SLOTS - 10 + 18
|
||||||
|
# TR_BITBRAIN_INPUT pins the width whatever the blocks say
|
||||||
|
putEnv(BBN_FEATURES_ENV, "dist:9")
|
||||||
|
putEnv(BBN_INPUT_ENV, "64")
|
||||||
|
var g4 = initBitbrainNetGun()
|
||||||
|
check "features: TR_BITBRAIN_INPUT pins the width",
|
||||||
|
g4.inputWidth == 64
|
||||||
|
putEnv(BBN_INPUT_ENV, "9")
|
||||||
|
var g5 = initBitbrainNetGun()
|
||||||
|
check "features: TR_BITBRAIN_INPUT can shrink below the block total",
|
||||||
|
g5.inputWidth == 9
|
||||||
|
# the unknown-block warning path must not change the widths
|
||||||
|
putEnv(BBN_FEATURES_ENV, "banana,hzn:3")
|
||||||
|
var g6 = initBitbrainNetGun()
|
||||||
|
check "features: an unknown block is ignored, the rest still apply",
|
||||||
|
g6.blockWidths[blockNameIndex("hzn")] == 3 and
|
||||||
|
g6.blockWidths[blockNameIndex("epos")] == 4
|
||||||
|
clearEnv()
|
||||||
|
|
||||||
|
# ── the network really engages ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc testNetworkEngages() =
|
||||||
|
clearEnv()
|
||||||
|
armNet()
|
||||||
|
var g = initBitbrainNetGun()
|
||||||
|
let st = mkState(1, 600, 300, 100, 300, 0)
|
||||||
|
var input = buildInput(g, st)
|
||||||
|
check "engage: the input vector is exactly inputWidth long",
|
||||||
|
input.len == g.inputWidth
|
||||||
|
check "engage: the input is a 0/255 thermometer, not a dead constant",
|
||||||
|
input.allIt(it == 0'u8 or it == BBN_SBC_VALUE) and
|
||||||
|
input.anyIt(it == BBN_SBC_VALUE)
|
||||||
|
check "engage: no network is built before the gun is used",
|
||||||
|
g.networkBytes() == 0
|
||||||
|
discard predict(g, st, 11.0)
|
||||||
|
check "engage: the gun builds the network on first use",
|
||||||
|
g.networkBytes() > 0
|
||||||
|
# two different inputs -> different class outputs
|
||||||
|
g.buildNet()
|
||||||
|
let a = buildInput(g, mkState(1, 600, 300, 100, 300, 0))
|
||||||
|
let b = buildInput(g, mkState(1, 200, 500, 700, 100, 90))
|
||||||
|
check "engage: two different inputs give DIFFERENT bit vectors", a != b
|
||||||
|
# An untrained SBC has no evidence, so the argmax is trivially 0. The real
|
||||||
|
# engagement test is: teach it a rule, then check the readout separates.
|
||||||
|
for i in 0 ..< 300:
|
||||||
|
let half = (i mod 2 == 0)
|
||||||
|
let x = buildInput(g, mkState(i, if half: 200.0 else: 700.0,
|
||||||
|
150.0 + float(i mod 7), 100, 500, float(i mod 3)))
|
||||||
|
g.learnSample(x, if half: 0 else: 7)
|
||||||
|
var labels = newSeq[int](100)
|
||||||
|
for i in 0 ..< 100:
|
||||||
|
let half = (i mod 2 == 0)
|
||||||
|
let x = buildInput(g, mkState(i, if half: 200.0 else: 700.0,
|
||||||
|
150.0 + float(i mod 7), 100, 500, float(i mod 3)))
|
||||||
|
labels[i] = g.inferClass(x)
|
||||||
|
var differ = false
|
||||||
|
for i in 1 ..< labels.len:
|
||||||
|
if labels[i] != labels[0]: differ = true
|
||||||
|
check "engage: a TRAINED ADE+SBC head returns DIFFERENT classes for different states",
|
||||||
|
differ
|
||||||
|
var agree = 0
|
||||||
|
for i in 0 ..< labels.len:
|
||||||
|
if (labels[i] == 0) == (i mod 2 == 0): inc agree
|
||||||
|
check "engage: the readout separates the two taught populations",
|
||||||
|
agree >= 80
|
||||||
|
# learning changes the memory
|
||||||
|
g.resetLearning()
|
||||||
|
let before = g.sbcBytesOf()
|
||||||
|
let beforeOcc = g.sbcsOf()[0].occupancy()
|
||||||
|
g.learnSample(a, 3)
|
||||||
|
check "engage: learning one sample WRITES the SBC memory",
|
||||||
|
g.sbcsOf()[0].occupancy() > beforeOcc
|
||||||
|
check "engage: memoryBytes is unchanged by a learn (no realloc)",
|
||||||
|
g.sbcBytesOf() == before
|
||||||
|
# bitset learn is idempotent
|
||||||
|
clearEnv()
|
||||||
|
armNet()
|
||||||
|
putEnv(BBN_MODE_ENV, "bitset")
|
||||||
|
putEnv(BBN_WIDTHS_ENV, "2,3,4") # denser clauses so coincidences actually fire
|
||||||
|
var gb = initBitbrainNetGun()
|
||||||
|
gb.buildNet()
|
||||||
|
gb.learnSample(a, 2)
|
||||||
|
let occ1 = gb.net.sbcs[0].occupancy()
|
||||||
|
for _ in 0 ..< 49: gb.learnSample(a, 2)
|
||||||
|
check "engage: a bitset learn really writes the memory", occ1 > 0.0
|
||||||
|
check "engage: bitset learn is idempotent (50x learn == 1x)",
|
||||||
|
abs(gb.net.sbcs[0].occupancy() - occ1) < 1e-12
|
||||||
|
# counted learn is monotone: the same cell keeps gaining evidence
|
||||||
|
putEnv(BBN_MODE_ENV, "counted")
|
||||||
|
putEnv(BBN_WIDTHS_ENV, "2,3,4")
|
||||||
|
var gc = initBitbrainNetGun()
|
||||||
|
gc.buildNet()
|
||||||
|
gc.learnSample(a, 2)
|
||||||
|
let ev2 = gc.evidenceFor(a, 2)
|
||||||
|
for _ in 0 ..< 4: gc.learnSample(a, 2)
|
||||||
|
check "engage: counted learn is MONOTONE (5 learns beat 1)",
|
||||||
|
ev2 > 0 and gc.evidenceFor(a, 2) > ev2
|
||||||
|
check "engage: the counted mode really holds saturating counters",
|
||||||
|
gc.sbcsOf()[0].mode == smCounted
|
||||||
|
clearEnv()
|
||||||
|
|
||||||
|
# ── the readout is a fine-grained correction, and it is gated ────────────────
|
||||||
|
|
||||||
|
proc testReadout() =
|
||||||
|
clearEnv()
|
||||||
|
armNet()
|
||||||
|
putEnv(BBN_MINOBS_ENV, "1000")
|
||||||
|
var g = initBitbrainNetGun()
|
||||||
|
let st = mkState(1, 600, 300, 100, 300, 0)
|
||||||
|
discard predict(g, st, 11.0)
|
||||||
|
check "readout: below minObs the correction is exactly zero",
|
||||||
|
g.lastShiftDeg == 0.0 and not isWarmedUp(g)
|
||||||
|
putEnv(BBN_MINOBS_ENV, "1")
|
||||||
|
var g2 = initBitbrainNetGun()
|
||||||
|
# feed a stream so labels resolve and the learner engages
|
||||||
|
var t = 0
|
||||||
|
for _ in 0 ..< 400:
|
||||||
|
let ft = float(t)
|
||||||
|
let st = mkState(t, 100.0 + 2.0 * ft, 200.0 + 1.1 * ft, 400, 300,
|
||||||
|
20.0 * sin(ft * 0.2))
|
||||||
|
discard predict(g2, st, 11.0)
|
||||||
|
inc t
|
||||||
|
check "readout: a resolved stream trains the net", g2.trained > 100
|
||||||
|
check "readout: the net becomes warmed up past minObs", isWarmedUp(g2)
|
||||||
|
check "readout: the correction is a bounded, fine-grained angle",
|
||||||
|
g2.lastShiftDeg >= -g2.maxDeg - 1e-9 and
|
||||||
|
g2.lastShiftDeg <= g2.maxDeg + 1e-9
|
||||||
|
check "readout: the gun's output is a POINT (Pattern base + correction)",
|
||||||
|
predict(g2, mkState(400, 500, 300, 400, 300, 0), 11.0).x != 0.0
|
||||||
|
check "readout: ADE threshold adaptation ran (online calibration)",
|
||||||
|
g2.adapts > 0
|
||||||
|
clearEnv()
|
||||||
|
|
||||||
|
# ── RNG parity ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc testRngClean() =
|
||||||
|
clearEnv()
|
||||||
|
randomize(1234)
|
||||||
|
let a = rand(1_000_000)
|
||||||
|
randomize(1234)
|
||||||
|
armNet()
|
||||||
|
var g = initBitbrainNetGun()
|
||||||
|
g.buildNet()
|
||||||
|
discard predict(g, mkState(1, 300, 300, 100, 100, 0), 11.0)
|
||||||
|
let b = rand(1_000_000)
|
||||||
|
check "parity: building + running the net does not perturb the global RNG",
|
||||||
|
a == b
|
||||||
|
clearEnv()
|
||||||
|
|
||||||
|
testRegistration()
|
||||||
|
testRackEnv()
|
||||||
|
testKnobs()
|
||||||
|
testFeatureBlocks()
|
||||||
|
testNetworkEngages()
|
||||||
|
testReadout()
|
||||||
|
testRngClean()
|
||||||
|
|
||||||
|
if failures > 0:
|
||||||
|
echo "\n", failures, " check(s) FAILED"
|
||||||
|
quit(1)
|
||||||
|
echo "\nAll BITBRAIN (ADE+SBC) engagement checks passed."
|
||||||
@@ -73,7 +73,7 @@ proc testKnownNames() =
|
|||||||
check "known set has no duplicates", known.len == s.len
|
check "known set has no duplicates", known.len == s.len
|
||||||
check "known set only holds TR_*/GUN_* names",
|
check "known set only holds TR_*/GUN_* names",
|
||||||
known.allIt(it.startsWith("TR_") or it.startsWith("GUN_"))
|
known.allIt(it.startsWith("TR_") or it.startsWith("GUN_"))
|
||||||
check "known set covers all 16 rack names",
|
check "known set covers all 18 rack names",
|
||||||
RackGunNames.allIt((RackEnvPrefix & it) in s)
|
RackGunNames.allIt((RackEnvPrefix & it) in s)
|
||||||
for name in ["TR_MOVEMENT", "TR_POWER_ENERGY_MIN", "TR_TMHORIZON_NSTATES",
|
for name in ["TR_MOVEMENT", "TR_POWER_ENERGY_MIN", "TR_TMHORIZON_NSTATES",
|
||||||
"GUN_VBULLET_METRIC", "TR_ENV_REPORT", "GUN_SELECTOR_SEED",
|
"GUN_VBULLET_METRIC", "TR_ENV_REPORT", "GUN_SELECTOR_SEED",
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ proc testRackAlias() =
|
|||||||
putEnv(LG_NET_SWITCH_ENV, "1")
|
putEnv(LG_NET_SWITCH_ENV, "1")
|
||||||
let m2 = loadRackMembership()
|
let m2 = loadRackMembership()
|
||||||
check "rack: TR_BITBRAIN_NET=1 hands the name to the new ADE+SBC gun (id 17)",
|
check "rack: TR_BITBRAIN_NET=1 hands the name to the new ADE+SBC gun (id 17)",
|
||||||
m2[LeadGainId] == rmOff
|
m2[LeadGainId] == rmOff and m2.len == 18
|
||||||
clearEnv()
|
clearEnv()
|
||||||
putEnv("TR_RACK_LEADGAIN", "both")
|
putEnv("TR_RACK_LEADGAIN", "both")
|
||||||
putEnv(LegacyRackEnvName, "off")
|
putEnv(LegacyRackEnvName, "off")
|
||||||
@@ -155,8 +155,8 @@ proc testDefaultParity() =
|
|||||||
let want = if i == 5: rmBoth else: rmOff
|
let want = if i == 5: rmBoth else: rmOff
|
||||||
if m[i] != want: onlyPattern = false
|
if m[i] != want: onlyPattern = false
|
||||||
check "parity: a clean env still loads the shipped onlyPattern rack", onlyPattern
|
check "parity: a clean env still loads the shipped onlyPattern rack", onlyPattern
|
||||||
check "parity: the rack is still 17 guns at id 16 = LEADGAIN",
|
check "parity: the rack is still 18 guns at id 16 = LEADGAIN",
|
||||||
RackGunNames.len == 17 and RackGunNames[LeadGainId] == "LEADGAIN"
|
RackGunNames.len == 18 and RackGunNames[LeadGainId] == "LEADGAIN"
|
||||||
var g = initLeadGainGun()
|
var g = initLeadGainGun()
|
||||||
check "parity: a clean env resolves the shipped candidate set",
|
check "parity: a clean env resolves the shipped candidate set",
|
||||||
g.cands == @[0.0, 0.25, 0.5, 0.75, 1.0] and g.memMode == lgPerRound
|
g.cands == @[0.0, 0.25, 0.5, 0.75, 1.0] and g.memMode == lgPerRound
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ proc testTable() =
|
|||||||
if DefaultRackMembership[i] != want: onlyPattern = false
|
if DefaultRackMembership[i] != want: onlyPattern = false
|
||||||
check "rack: the shipped default is still the onlyPattern rack", onlyPattern
|
check "rack: the shipped default is still the onlyPattern rack", onlyPattern
|
||||||
check "rack: the default rack admits only Pattern (1v1)",
|
check "rack: the default rack admits only Pattern (1v1)",
|
||||||
admittedGuns(17, rm1v1, DefaultRackMembership) == @[PatternId]
|
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||||
check "gate: LEADGAIN is NOT spawned under the default rack",
|
check "gate: LEADGAIN is NOT spawned under the default rack",
|
||||||
not vBulletAdmitted(LeadGainId, rm1v1, DefaultRackMembership, true)
|
not vBulletAdmitted(LeadGainId, rm1v1, DefaultRackMembership, true)
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ proc testEnvOverride() =
|
|||||||
m[LeadGainId] == rmBoth and
|
m[LeadGainId] == rmBoth and
|
||||||
vBulletAdmitted(LeadGainId, rm1v1, m, true)
|
vBulletAdmitted(LeadGainId, rm1v1, m, true)
|
||||||
check "env: admitting LEADGAIN leaves Pattern as the only other member",
|
check "env: admitting LEADGAIN leaves Pattern as the only other member",
|
||||||
admittedGuns(17, rm1v1, m) == @[PatternId, LeadGainId]
|
admittedGuns(18, rm1v1, m) == @[PatternId, LeadGainId]
|
||||||
clearRackEnv()
|
clearRackEnv()
|
||||||
|
|
||||||
proc testLazyAndRngClean() =
|
proc testLazyAndRngClean() =
|
||||||
|
|||||||
@@ -158,14 +158,14 @@ proc testDefaultsOnlyPattern() =
|
|||||||
DefaultRackMembership[12] == rmOff and DefaultRackMembership[13] == rmOff and
|
DefaultRackMembership[12] == rmOff and DefaultRackMembership[13] == rmOff and
|
||||||
DefaultRackMembership[14] == rmOff
|
DefaultRackMembership[14] == rmOff
|
||||||
check "defaults: the default rack admits ONLY Pattern in 1v1",
|
check "defaults: the default rack admits ONLY Pattern in 1v1",
|
||||||
admittedGuns(17, rm1v1, DefaultRackMembership) == @[PatternId]
|
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||||
check "defaults: the default rack admits ONLY Pattern in melee",
|
check "defaults: the default rack admits ONLY Pattern in melee",
|
||||||
admittedGuns(17, rmMelee, DefaultRackMembership) == @[PatternId]
|
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||||
let loaded = loadRackMembership()
|
let loaded = loadRackMembership()
|
||||||
check "defaults: with a clean environment loadRackMembership() == shipped table",
|
check "defaults: with a clean environment loadRackMembership() == shipped table",
|
||||||
loaded == DefaultRackMembership
|
loaded == DefaultRackMembership
|
||||||
check "defaults: RackGunNames covers the shipped 17-gun rack",
|
check "defaults: RackGunNames covers the shipped 18-gun rack",
|
||||||
RackGunNames.len == 17 and DefaultRackMembership.len == 17
|
RackGunNames.len == 18 and DefaultRackMembership.len == 18
|
||||||
|
|
||||||
proc testFloorRespectsAdmission() =
|
proc testFloorRespectsAdmission() =
|
||||||
## The FLOOR path (`bestRate <= 0` or below the floor rate) historically fell
|
## The FLOOR path (`bestRate <= 0` or below the floor rate) historically fell
|
||||||
@@ -213,7 +213,7 @@ proc testRevertOverrideRestoresFullRack() =
|
|||||||
if m[i] != want: full = false
|
if m[i] != want: full = false
|
||||||
check "revert: the documented one-liner restores the all-`both` full rack", full
|
check "revert: the documented one-liner restores the all-`both` full rack", full
|
||||||
check "revert: 1v1 rack admits every gun again (TMPATTERN excluded)",
|
check "revert: 1v1 rack admits every gun again (TMPATTERN excluded)",
|
||||||
admittedGuns(17, rm1v1, m) == @[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16]
|
admittedGuns(18, rm1v1, m) == @[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17]
|
||||||
clearRackEnv()
|
clearRackEnv()
|
||||||
|
|
||||||
proc testEnvOverrides() =
|
proc testEnvOverrides() =
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ proc seedOldRack(t: var VirtualTracker, targetId: int) =
|
|||||||
# ── registration table ────────────────────────────────────────────────────────
|
# ── registration table ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
proc testTable() =
|
proc testTable() =
|
||||||
check "rack: RackGunNames has 17 entries", RackGunNames.len == 17
|
check "rack: RackGunNames has 18 entries", RackGunNames.len == 18
|
||||||
check "rack: the new gun is named TMPATTERN at id 14",
|
check "rack: the new gun is named TMPATTERN at id 14",
|
||||||
RackGunNames[TmPatternId] == "TMPATTERN"
|
RackGunNames[TmPatternId] == "TMPATTERN"
|
||||||
check "rack: the new gun defaults to `off`",
|
check "rack: the new gun defaults to `off`",
|
||||||
@@ -82,9 +82,9 @@ proc oldRackMembership(): array[15, RackMembership] =
|
|||||||
|
|
||||||
proc testDefaultAdmitsOnlyPattern() =
|
proc testDefaultAdmitsOnlyPattern() =
|
||||||
check "default membership admits only PATTERN (1v1)",
|
check "default membership admits only PATTERN (1v1)",
|
||||||
admittedGuns(17, rm1v1, DefaultRackMembership) == @[PatternId]
|
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||||
check "default membership admits only PATTERN (melee)",
|
check "default membership admits only PATTERN (melee)",
|
||||||
admittedGuns(17, rmMelee, DefaultRackMembership) == @[PatternId]
|
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||||
|
|
||||||
proc testEnvOverride() =
|
proc testEnvOverride() =
|
||||||
for name in RackGunNames: delEnv("TR_RACK_" & name)
|
for name in RackGunNames: delEnv("TR_RACK_" & name)
|
||||||
|
|||||||
Reference in New Issue
Block a user