From 2963917072dee7cfbcda400a2c325ae739252f28 Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Mon, 14 Sep 2026 23:08:06 +0200 Subject: [PATCH] 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. --- SNNBot_garage/src/reservoir.nim | 47 +++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/SNNBot_garage/src/reservoir.nim b/SNNBot_garage/src/reservoir.nim index f78f5e6..c7a0c86 100644 --- a/SNNBot_garage/src/reservoir.nim +++ b/SNNBot_garage/src/reservoir.nim @@ -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 ────────────────────────────────────────────────────────────────────── @@ -19,13 +21,12 @@ type BitVec* = array[16, uint64] # 1024 bits 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] - rngState: uint64 + wIn: array[RESERVOIR_SIZE, BitVec80] + wRec: array[RESERVOIR_SIZE, BitVec] + state: BitVec + readout: array[N_BINS, BitVec] + scores*: array[N_BINS, int] + rngState: uint64 # ── PRNG ─────────────────────────────────────────────────────────────────────── @@ -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