Files
SirRoboGarage/common_libs/bitbrain/bitbrain.nim
T
SirStone 77e6dace01 BitBrain: generic clean-room ADE + SBC library with MNIST acceptance
Implement the BitBrain (Address Decoder Element + Sparse Binary Coincidence)
classifier as a generic, deterministic Nim library under common_libs/bitbrain/,
written from the published algorithm (Front. Neuroinform. 17:1125844), not from
the GPL-3.0 reference C.

- ade.nim: signed thresholded random projection (scale 64 / centre 127 defaults
  reproduce the reference), multi-width ADs, optional deterministic homeostatic
  threshold adaptation. Hebbian longevity and Metropolis-Hastings sampling are
  described but not implemented.
- sbc.nim: packed class-bit coincidence memory; idempotent learn, counting
  inference.
- bitbrain.nim: container over several ADs and SBCs, online learn/infer, argmax
  readout, memory accounting.
- tests: 32 unit checks (idempotence, planted rule + monotone online curve,
  shuffled-label chance control, unseen input, homeostasis, memory).
- tests/test_bitbrain_mnist.nim: loads the reference pretrained ADs/thresholds
  and MNIST from /tmp, reproduces the reference exactly - 97.210% corrected and
  96.540% bug-compatible - confirming the port.

No gun/wiring integration yet; inputs and outputs to be agreed separately.
2026-09-24 20:52:45 +02:00

137 lines
5.2 KiB
Nim

## BitBrain container — ADs + SBC memories + a counting readout.
##
## Clean-room implementation of the classification pipeline described in
##
## "BitBrain and Sparse Binary Coincidence (SBC) memories",
## Frontiers in Neuroinformatics 17:1125844, 2023.
##
## See `ade.nim` for the clean-room note (the reference C is GPL-3.0 and was not
## copied).
##
## The container holds several Address Decoders (possibly of different widths)
## and several SBC memories, each built from a pair of ADs. `learn` populates the
## SBCs from a labelled sample; `infer` drives every AD, reads every SBC with the
## same coincidence rule and sums the per-class set-bit counts. The class with the
## highest total wins. Everything is per-sample and order-free: the memory is
## monotone, so `learn` and `infer` may be interleaved arbitrarily.
##
## Reference setup used by the paper and the MNIST acceptance harness: 4 ADs of
## 2048 ADEs with widths {6, 8, 10, 12} and 6 cross-AD SBCs. The paper's
## 10-SBC variant adds 4 within-AD ("half-size") SBCs; `withinPairs` builds those
## (this implementation stores them full-size — the half-size packing is a
## separate memory optimisation).
import std/random
import ade, sbc
export ade, sbc
type
SbcSpec* = object
## Which pair of ADs feeds one SBC memory.
row*: int
col*: int
BitBrain* = object
ades*: seq[AddressDecoder]
sbcs*: seq[Sbc]
specs*: seq[SbcSpec]
nClasses*: int
proc crossPairs*(nAdes: int): seq[SbcSpec] =
## All unordered pairs of distinct ADs. For 4 ADs this is the paper's 6 SBCs.
for a in 0 ..< nAdes:
for b in a + 1 ..< nAdes:
result.add SbcSpec(row: a, col: b)
proc withinPairs*(nAdes: int): seq[SbcSpec] =
## The square/within-AD pairs (the paper's "half-size" SBCs).
for a in 0 ..< nAdes:
result.add SbcSpec(row: a, col: a)
proc initBitBrain*(ades: seq[AddressDecoder], specs: seq[SbcSpec],
nClasses: int): BitBrain =
## Build the container. Every AD must have the same number of ADEs because an
## SBC's axes are both `w` long (the paper's setup).
doAssert ades.len > 0, "need at least one AD"
doAssert nClasses > 0, "need at least one class"
let w = ades[0].nAde
for ad in ades:
doAssert ad.nAde == w, "all ADs must have the same number of ADEs"
result.ades = ades
result.nClasses = nClasses
result.specs = specs
result.sbcs = newSeq[Sbc](specs.len)
for s in 0 ..< specs.len:
doAssert specs[s].row >= 0 and specs[s].row < ades.len
doAssert specs[s].col >= 0 and specs[s].col < ades.len
result.sbcs[s] = initSbc(w, nClasses)
proc buildRandomBitBrain*(widths: seq[int], nAde, inputWidth, nClasses: int,
seed: int64,
specs: seq[SbcSpec] = @[]): BitBrain =
## Initialise a whole BitBrain with random ADs. If `specs` is empty the
## paper's 6 cross-AD SBCs are used. Fully deterministic given `seed`.
var rng = initRand(seed)
var ades: seq[AddressDecoder]
for w in widths:
ades.add initRandomAddressDecoder(nAde, w, inputWidth, rng)
let pairs = if specs.len == 0: crossPairs(widths.len) else: specs
result = initBitBrain(ades, pairs, nClasses)
proc nAdes*(bb: BitBrain): int {.inline.} =
bb.ades.len
proc resetLearning*(bb: var BitBrain) =
## Wipe every SBC. The ADs (and their thresholds) are left untouched.
for s in 0 ..< bb.sbcs.len:
bb.sbcs[s].clear()
proc fireInto*[T: SomeInteger](bb: BitBrain, input: openArray[T],
lists: var seq[seq[int32]]) =
## Compute the sparse firing pattern of every AD for `input`. `lists` is resized
## to `nAdes` and each entry is filled with that AD's firing ADE indices.
if lists.len != bb.ades.len:
lists.setLen(bb.ades.len)
for a in 0 ..< bb.ades.len:
bb.ades[a].activeList(input, lists[a])
proc learn*[T: SomeInteger](bb: var BitBrain, input: openArray[T], class: int) =
## One online supervised step: set the `class` bit of every observed
## coincidence. Idempotent — repeating the same sample is a no-op.
var lists: seq[seq[int32]]
bb.fireInto(input, lists)
for s in 0 ..< bb.sbcs.len:
let spec = bb.specs[s]
discard bb.sbcs[s].learn(lists[spec.row], lists[spec.col], class)
proc infer*[T: SomeInteger](bb: BitBrain, input: openArray[T]):
tuple[label: int, counts: seq[int]] =
## Drive every AD, count set class bits in every SBC, and return the argmax
## class plus the aggregated per-class counts. Ties go to the lowest class
## index (matching the reference's `>` scan that keeps the first maximum).
var lists: seq[seq[int32]]
bb.fireInto(input, lists)
result.counts = newSeq[int](bb.nClasses)
for s in 0 ..< bb.sbcs.len:
let spec = bb.specs[s]
bb.sbcs[s].infer(lists[spec.row], lists[spec.col], result.counts)
result.label = 0
var best = result.counts[0]
for k in 1 ..< bb.nClasses:
if result.counts[k] > best:
best = result.counts[k]
result.label = k
proc memoryBytes*(bb: BitBrain): int =
## Total bytes: AD synapse codes + thresholds + counters + SBC bit tensors.
for ad in bb.ades:
result += ad.memoryBytes
for s in bb.sbcs:
result += s.memoryBytes
proc sbcMemoryBytes*(bb: BitBrain): int =
## Just the SBC bit tensors — the dominant term for realistic configurations.
for s in bb.sbcs:
result += s.memoryBytes