fix(SNNBot): enforce sparse reservoir state via k-Winners-Take-All

Dense firing (~50%) caused all readout bins to saturate identically.
Replace threshold-based firing with k-WTA: only top 50 neurons fire
per tick (~5% sparsity). Sparse patterns give low inter-angle overlap
-> readout bins can discriminate between different bearings.
This commit is contained in:
2026-09-14 23:08:06 +02:00
parent 45b167f2b3
commit 2963917072
+21 -14
View File
@@ -1,6 +1,6 @@
# ponytail: binary reservoir aimer — prototype; if reservoir projection is poor, scale RESERVOIR_SIZE to 4096
import std/[bitops, math]
import std/[algorithm, bitops, math]
# ── Constants ──────────────────────────────────────────────────────────────────
@@ -11,6 +11,8 @@ const
INPUT_BITS* = 80
SPARSITY_IN = 0.1
SPARSITY_REC = 0.05
K_ACTIVE = 50 # ~5% of RESERVOIR_SIZE fire per tick
# ponytail: K_ACTIVE=50 gives ~5% sparsity; increase if readout can't discriminate, decrease if patterns overlap too much
# ── Types ──────────────────────────────────────────────────────────────────────
@@ -21,7 +23,6 @@ type
Reservoir* = object
wIn: array[RESERVOIR_SIZE, BitVec80]
wRec: array[RESERVOIR_SIZE, BitVec]
threshold: array[RESERVOIR_SIZE, int]
state: BitVec
readout: array[N_BINS, BitVec]
scores*: array[N_BINS, int]
@@ -61,27 +62,33 @@ proc initReservoir*(seed: int): Reservoir =
for w in 0 ..< 16:
result.wRec[i][w] = sparseBits(result, SPARSITY_REC)
# ponytail: threshold=1 gives ~50% firing; raise if reservoir saturates (all neurons fire every tick)
result.threshold[i] = 1
# readout and state are zero-initialized by default
# ── Forward ────────────────────────────────────────────────────────────────────
proc forward*(r: var Reservoir, input: BitVec80): int =
var newState: BitVec
# Compute activation scores for all neurons
var activations: array[RESERVOIR_SIZE, int]
for i in 0 ..< RESERVOIR_SIZE:
let inScore = popcount(input[0] and r.wIn[i][0]) +
popcount(input[1] and r.wIn[i][1])
let inScore = popcount(input[0] and r.wIn[i][0]).int +
popcount(input[1] and r.wIn[i][1]).int
var recScore = 0
for w in 0 ..< 16:
recScore += popcount(r.state[w] and r.wRec[i][w])
recScore += popcount(r.state[w] and r.wRec[i][w]).int
activations[i] = inScore + recScore
if inScore + recScore > r.threshold[i]:
let wordIdx = i shr 6 # i div 64
let bitIdx = i and 63 # i mod 64
newState[wordIdx] = newState[wordIdx] or (1'u64 shl bitIdx)
# k-WTA: find K-th highest activation via sort on a copy
var sorted = activations
sort(sorted, order = SortOrder.Descending)
let kThreshold = sorted[min(K_ACTIVE - 1, RESERVOIR_SIZE - 1)]
# Fire exactly K_ACTIVE neurons (tie-break: first in index order)
var newState: BitVec
var count = 0
for i in 0 ..< RESERVOIR_SIZE:
if activations[i] >= kThreshold and count < K_ACTIVE:
newState[i shr 6] = newState[i shr 6] or (1'u64 shl (i and 63))
inc count
r.state = newState