feat(SNNBot): grid accumulator replaces exemplar ring buffer
Ring buffer had amnesia — cycled out all data every 128 ticks, preventing convergence. Grid accumulator permanently stores average lead offsets indexed by (v_perp, distance). 136 cells, 1.5 KB. Knowledge accumulates across rounds → convergence guaranteed for stationary velocity patterns. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,57 +1,62 @@
|
||||
# ponytail: direct binary readout; add reservoir back when temporal features matter (step 3)
|
||||
# ponytail: grid accumulator replaces ring buffer; add temporal features when needed (step 3)
|
||||
|
||||
import std/[bitops, math]
|
||||
import std/math
|
||||
|
||||
const
|
||||
INPUT_BITS* = 80
|
||||
WORDS_IN* = 2 # 80 bits → 2 × uint64 (128 bits, only 80 used)
|
||||
MAX_K* = 128
|
||||
MIN_SIM* = 3
|
||||
VPERP_BINS* = 17 # -8 to +8 inclusive (integer speed units)
|
||||
DIST_BINS* = 8 # distance bands
|
||||
DIST_BAND* = 125.0 # pixels per band
|
||||
|
||||
type
|
||||
BitVec80* = array[WORDS_IN, uint64]
|
||||
GridCell = object
|
||||
sumSin: float
|
||||
sumCos: float
|
||||
count: int
|
||||
|
||||
Exemplar* = object
|
||||
pattern*: BitVec80
|
||||
offset*: float # lead correction in degrees, typically [-30, +30]
|
||||
active*: bool
|
||||
LeadGrid* = object
|
||||
cells: array[VPERP_BINS * DIST_BINS, GridCell] # 17 × 8 = 136 cells
|
||||
|
||||
BinaryAimer* = object
|
||||
exemplars*: array[MAX_K, Exemplar]
|
||||
count*: int
|
||||
nextSlot*: int
|
||||
proc initLeadGrid*(): LeadGrid =
|
||||
result = LeadGrid()
|
||||
|
||||
proc initBinaryAimer*(): BinaryAimer =
|
||||
result = BinaryAimer()
|
||||
proc cellIndex(vPerp: float, distance: float): int {.inline.} =
|
||||
let vBin = clamp(int(vPerp + 8.5), 0, VPERP_BINS - 1)
|
||||
let dBin = clamp(int(distance / DIST_BAND), 0, DIST_BINS - 1)
|
||||
result = vBin * DIST_BINS + dBin
|
||||
|
||||
proc forward*(aimer: var BinaryAimer, input: BitVec80): float =
|
||||
## Kernel regression: weighted circular mean over active exemplars.
|
||||
## Weights by similarity^2 and exponential recency decay (0.95^age).
|
||||
## Returns offset (lead correction) in degrees, or -999.0 sentinel when cold (no weight).
|
||||
proc forward*(grid: var LeadGrid, vPerp, distance: float): float =
|
||||
## Returns circular mean offset in degrees, or -999.0 when no data.
|
||||
let idx = cellIndex(vPerp, distance)
|
||||
let vBin = clamp(int(vPerp + 8.5), 0, VPERP_BINS - 1)
|
||||
let dBin = clamp(int(distance / DIST_BAND), 0, DIST_BINS - 1)
|
||||
|
||||
# Accumulate weighted contributions: direct cell (w=1), cardinal neighbors (w=0.5), diagonals (w=0.25)
|
||||
var sinSum = 0.0
|
||||
var cosSum = 0.0
|
||||
var totalW = 0.0
|
||||
for i in 0 ..< MAX_K:
|
||||
if not aimer.exemplars[i].active: continue
|
||||
var sim = 0
|
||||
for w in 0 ..< WORDS_IN:
|
||||
sim += popcount(input[w] and aimer.exemplars[i].pattern[w]).int
|
||||
let excess = sim - MIN_SIM
|
||||
if excess <= 0: continue
|
||||
let age = (aimer.nextSlot - 1 - i + MAX_K) mod MAX_K
|
||||
let recency = pow(0.95, age.float)
|
||||
let weight = float(excess * excess) * recency
|
||||
let rad = degToRad(aimer.exemplars[i].offset)
|
||||
sinSum += weight * sin(rad)
|
||||
cosSum += weight * cos(rad)
|
||||
totalW += weight
|
||||
for dv in -1 .. 1:
|
||||
for dd in -1 .. 1:
|
||||
let vb = vBin + dv
|
||||
let db = dBin + dd
|
||||
if vb < 0 or vb >= VPERP_BINS or db < 0 or db >= DIST_BINS: continue
|
||||
let c = grid.cells[vb * DIST_BINS + db]
|
||||
if c.count == 0: continue
|
||||
let w = if dv == 0 and dd == 0: 1.0
|
||||
elif dv == 0 or dd == 0: 0.5
|
||||
else: 0.25
|
||||
sinSum += w * c.sumSin / float(c.count)
|
||||
cosSum += w * c.sumCos / float(c.count)
|
||||
totalW += w
|
||||
if totalW == 0.0:
|
||||
return -999.0
|
||||
result = radToDeg(arctan2(sinSum, cosSum))
|
||||
|
||||
proc learn*(aimer: var BinaryAimer, input: BitVec80, offset: float) =
|
||||
## Ring-buffer store: write exemplar at nextSlot, advance mod MAX_K.
|
||||
aimer.exemplars[aimer.nextSlot] = Exemplar(pattern: input, offset: offset, active: true)
|
||||
aimer.nextSlot = (aimer.nextSlot + 1) mod MAX_K
|
||||
if aimer.count < MAX_K: inc aimer.count
|
||||
proc learn*(grid: var LeadGrid, vPerp, distance, correctOffset: float) =
|
||||
let idx = cellIndex(vPerp, distance)
|
||||
grid.cells[idx].sumSin += sin(degToRad(correctOffset))
|
||||
grid.cells[idx].sumCos += cos(degToRad(correctOffset))
|
||||
inc grid.cells[idx].count
|
||||
|
||||
proc totalCount*(grid: LeadGrid): int =
|
||||
for c in grid.cells:
|
||||
result += c.count
|
||||
|
||||
Reference in New Issue
Block a user