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:
@@ -9,7 +9,7 @@
|
|||||||
# WAITING → aimTo() each tick; when error < 2° → EVALUATE
|
# WAITING → aimTo() each tick; when error < 2° → EVALUATE
|
||||||
# EVALUATE → measure error, compute SuperSpike/reservoir update, log, → DECIDE
|
# EVALUATE → measure error, compute SuperSpike/reservoir update, log, → DECIDE
|
||||||
|
|
||||||
import std/[math, random, os, strutils, bitops]
|
import std/[math, random, os, strutils]
|
||||||
import robocode_tankroyale_botapi
|
import robocode_tankroyale_botapi
|
||||||
import radar_lock/radar_lock as radar_lock
|
import radar_lock/radar_lock as radar_lock
|
||||||
import reservoir
|
import reservoir
|
||||||
@@ -201,7 +201,7 @@ type
|
|||||||
|
|
||||||
SNNBot = ref object of Bot
|
SNNBot = ref object of Bot
|
||||||
snn: SNN
|
snn: SNN
|
||||||
res: BinaryAimer
|
res: LeadGrid
|
||||||
phase: Phase
|
phase: Phase
|
||||||
targetAngle: float # SNN output (absolute bearing)
|
targetAngle: float # SNN output (absolute bearing)
|
||||||
enemyBearing: float # last known enemy bearing
|
enemyBearing: float # last known enemy bearing
|
||||||
@@ -217,8 +217,7 @@ type
|
|||||||
velDirDeg: float64 # velocity direction (degrees) from last scan delta
|
velDirDeg: float64 # velocity direction (degrees) from last scan delta
|
||||||
velSpeed: float64 # speed (units/tick) from last scan delta
|
velSpeed: float64 # speed (units/tick) from last scan delta
|
||||||
lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE
|
lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE
|
||||||
lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE
|
lastVPerp: float # DECIDE-time vPerp, reused in EVALUATE
|
||||||
lastInput: BitVec80 # previous EVALUATE-time input (for change detection)
|
|
||||||
lastAbsBearing: float # absolute bearing to enemy captured at DECIDE time
|
lastAbsBearing: float # absolute bearing to enemy captured at DECIDE time
|
||||||
roundTick: int # ticks elapsed in current round (reset each round)
|
roundTick: int # ticks elapsed in current round (reset each round)
|
||||||
bulletsFired: int # count of bullets fired this round
|
bulletsFired: int # count of bullets fired this round
|
||||||
@@ -301,13 +300,12 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
|
|||||||
bot.lastAbsBearing = 0.0
|
bot.lastAbsBearing = 0.0
|
||||||
bot.lastDecideGunDir = 0.0
|
bot.lastDecideGunDir = 0.0
|
||||||
bot.targetAngle = 0.0
|
bot.targetAngle = 0.0
|
||||||
bot.lastInput = [0'u64, 0'u64]
|
|
||||||
setTargetSpeed(0.0)
|
setTargetSpeed(0.0)
|
||||||
setTurnRate(0.0)
|
setTurnRate(0.0)
|
||||||
|
|
||||||
method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) =
|
method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) =
|
||||||
initSNN(bot.snn)
|
initSNN(bot.snn)
|
||||||
bot.res = initBinaryAimer()
|
bot.res = initLeadGrid()
|
||||||
|
|
||||||
|
|
||||||
method onBulletFired*(bot: SNNBot, e: BulletFiredEvent) =
|
method onBulletFired*(bot: SNNBot, e: BulletFiredEvent) =
|
||||||
@@ -319,50 +317,6 @@ method onBulletHit*(bot: SNNBot, e: BulletHitBotEvent) =
|
|||||||
inc bot.bulletsHit
|
inc bot.bulletsHit
|
||||||
bot.hitRateEMA = bot.hitRateEMA * 0.85 + 1.0 * 0.15
|
bot.hitRateEMA = bot.hitRateEMA * 0.85 + 1.0 * 0.15
|
||||||
|
|
||||||
# ── Reservoir helpers ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
proc toBinaryInput(relVelDir: float, velSpeed: float, distance: float, hasVel: bool): BitVec80 =
|
|
||||||
## Physics-based encoding for lead generalization (issue #152).
|
|
||||||
## Bits 0-15: v_perp signed thermometer — velSpeed*sin(relVelDir) in [-8,+8].
|
|
||||||
## Positive (target moves right): bits 0-7; negative (left): bits 8-15.
|
|
||||||
## +3 sets bits 0,1,2; -3 sets bits 8,9,10. Zero overlap between signs.
|
|
||||||
## Bits 16-31: v_parallel signed thermometer — velSpeed*cos(relVelDir) in [-8,+8].
|
|
||||||
## Positive = approaching (less lead needed), negative = receding.
|
|
||||||
## Bits 32-39: distance thermometer, 8 bands of ~125px in [0,1000].
|
|
||||||
## Bits 40-47: total speed thermometer, 8 bands of 1 unit/tick.
|
|
||||||
## Bits 48-79: unused.
|
|
||||||
result = [0'u64, 0'u64]
|
|
||||||
|
|
||||||
template setThermo(baseIdx: int, value: float) =
|
|
||||||
## Signed thermometer: positive → bits baseIdx..baseIdx+7, negative → bits baseIdx+8..baseIdx+15.
|
|
||||||
let mag = min(int(abs(value)), 8)
|
|
||||||
if value >= 0.0:
|
|
||||||
for b in 0 ..< mag:
|
|
||||||
let idx = baseIdx + b
|
|
||||||
result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64))
|
|
||||||
else:
|
|
||||||
for b in 0 ..< mag:
|
|
||||||
let idx = baseIdx + 8 + b
|
|
||||||
result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64))
|
|
||||||
|
|
||||||
if hasVel:
|
|
||||||
let vPerp = velSpeed * sin(degToRad(relVelDir)) # lateral speed, range [-8,+8]
|
|
||||||
let vPar = velSpeed * cos(degToRad(relVelDir)) # approach speed, range [-8,+8]
|
|
||||||
setThermo(0, vPerp)
|
|
||||||
setThermo(16, vPar)
|
|
||||||
|
|
||||||
# Total speed thermometer: bits 40-47
|
|
||||||
let speedInt = clamp(int(velSpeed + 0.5), 0, 8)
|
|
||||||
for b in 0 ..< speedInt:
|
|
||||||
let idx = 40 + b
|
|
||||||
result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64))
|
|
||||||
|
|
||||||
# Distance thermometer: bits 32-39, 8 bands of ~125px over [0,1000]
|
|
||||||
let band = clamp(int(distance / 125.0), 0, 7)
|
|
||||||
for b in 0 .. band:
|
|
||||||
let idx = 32 + b
|
|
||||||
result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64))
|
|
||||||
|
|
||||||
# ── Main loop ─────────────────────────────────────────────────────────────────
|
# ── Main loop ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
method run*(bot: SNNBot) =
|
method run*(bot: SNNBot) =
|
||||||
@@ -390,20 +344,18 @@ method run*(bot: SNNBot) =
|
|||||||
var relVelDir = bot.velDirDeg - absBearing
|
var relVelDir = bot.velDirDeg - absBearing
|
||||||
while relVelDir >= 180.0: relVelDir -= 360.0
|
while relVelDir >= 180.0: relVelDir -= 360.0
|
||||||
while relVelDir < -180.0: relVelDir += 360.0
|
while relVelDir < -180.0: relVelDir += 360.0
|
||||||
let binInput = toBinaryInput(relVelDir, bot.velSpeed, bot.enemyDist, bot.hasLastPos)
|
let vPerp = bot.velSpeed * sin(degToRad(relVelDir))
|
||||||
let aimOffset = bot.res.forward(binInput) # lead correction offset or -999.0 sentinel
|
let aimOffset = bot.res.forward(vPerp, bot.enemyDist)
|
||||||
bot.lastBinInput = binInput
|
bot.lastVPerp = vPerp
|
||||||
bot.lastDecideGunDir = gunDir
|
bot.lastDecideGunDir = gunDir
|
||||||
bot.lastAbsBearing = absBearing
|
bot.lastAbsBearing = absBearing
|
||||||
let rate = if bot.bulletsFired > 0: float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0 else: 0.0
|
let rate = if bot.bulletsFired > 0: float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0 else: 0.0
|
||||||
if aimOffset <= -999.0:
|
if aimOffset <= -999.0:
|
||||||
# cold start: aim directly at enemy (no lead)
|
|
||||||
bot.targetAngle = absBearing
|
bot.targetAngle = absBearing
|
||||||
echo "RES tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% cold-start aim=" & formatFloat(absBearing, ffDecimal, 1) & " fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0)
|
echo "RES tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% cold-start aim=" & formatFloat(absBearing, ffDecimal, 1) & " fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0) & " cells=" & $bot.res.totalCount
|
||||||
else:
|
else:
|
||||||
# apply lead correction offset to current bearing
|
|
||||||
bot.targetAngle = (absBearing + aimOffset + 360.0) mod 360.0
|
bot.targetAngle = (absBearing + aimOffset + 360.0) mod 360.0
|
||||||
echo "RES tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% offset=" & formatFloat(aimOffset, ffDecimal, 1) & " aim=" & formatFloat(bot.targetAngle, ffDecimal, 1) & " fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0)
|
echo "RES tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% offset=" & formatFloat(aimOffset, ffDecimal, 1) & " aim=" & formatFloat(bot.targetAngle, ffDecimal, 1) & " fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0) & " cells=" & $bot.res.totalCount
|
||||||
else:
|
else:
|
||||||
let inputs = encodeInputFull(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
let inputs = encodeInputFull(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
||||||
# Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks
|
# Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks
|
||||||
@@ -462,35 +414,29 @@ method run*(bot: SNNBot) =
|
|||||||
let adaptiveDeadZone = 0.5 + (0.3 - 0.5) * t # lerp(0.5, 0.3, t)
|
let adaptiveDeadZone = 0.5 + (0.3 - 0.5) * t # lerp(0.5, 0.3, t)
|
||||||
|
|
||||||
when USE_RESERVOIR:
|
when USE_RESERVOIR:
|
||||||
# correctOffset: how much lead to apply from current bearing
|
|
||||||
let correctOffset = normalizeRelativeAngle(correctAngle - bot.lastAbsBearing)
|
let correctOffset = normalizeRelativeAngle(correctAngle - bot.lastAbsBearing)
|
||||||
let producedOffset = bot.res.forward(bot.lastBinInput)
|
let producedOffset = bot.res.forward(bot.lastVPerp, bot.enemyDist)
|
||||||
# Detect significant input change via Hamming distance
|
|
||||||
let hammingDist = popcount(bot.lastBinInput[0] xor bot.lastInput[0]) + popcount(bot.lastBinInput[1] xor bot.lastInput[1])
|
|
||||||
let inputChanged = hammingDist > 2 # 3+ bits flipped = situation changed
|
|
||||||
|
|
||||||
# Learn if: error > dead zone OR input pattern changed significantly
|
# Learn if: cold start OR error > dead zone
|
||||||
if producedOffset > -999.0:
|
if producedOffset > -999.0:
|
||||||
let aimErr = abs(normalizeRelativeAngle(producedOffset - correctOffset))
|
let aimErr = abs(normalizeRelativeAngle(producedOffset - correctOffset))
|
||||||
if aimErr > adaptiveDeadZone or inputChanged:
|
if aimErr > adaptiveDeadZone:
|
||||||
bot.res.learn(bot.lastBinInput, correctOffset)
|
bot.res.learn(bot.lastVPerp, bot.enemyDist, correctOffset)
|
||||||
else:
|
else:
|
||||||
# Cold start: always learn
|
bot.res.learn(bot.lastVPerp, bot.enemyDist, correctOffset)
|
||||||
bot.res.learn(bot.lastBinInput, correctOffset)
|
|
||||||
let aimErr = if producedOffset > -999.0: abs(normalizeRelativeAngle(producedOffset - correctOffset)) else: -1.0
|
let aimErr = if producedOffset > -999.0: abs(normalizeRelativeAngle(producedOffset - correctOffset)) else: -1.0
|
||||||
let rate = if bot.bulletsFired > 0: (float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0) else: 0.0
|
let rate = if bot.bulletsFired > 0: (float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0) else: 0.0
|
||||||
echo "RES tick=" & $bot.tick &
|
echo "RES tick=" & $bot.tick &
|
||||||
" pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) &
|
" pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) &
|
||||||
" hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "%" &
|
" hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "%" &
|
||||||
" fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit &
|
" fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit &
|
||||||
" rate=" & (if bot.bulletsFired > 0: formatFloat(float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0, ffDecimal, 1) else: "0.0") & "%" &
|
" rate=" & (if bot.bulletsFired > 0: formatFloat(rate, ffDecimal, 1) else: "0.0") & "%" &
|
||||||
" energy=" & formatFloat(getEnergy(), ffDecimal, 0) &
|
" energy=" & formatFloat(getEnergy(), ffDecimal, 0) &
|
||||||
" offset=" & (if producedOffset > -999.0: formatFloat(producedOffset, ffDecimal, 1) else: "cold") &
|
" offset=" & (if producedOffset > -999.0: formatFloat(producedOffset, ffDecimal, 1) else: "cold") &
|
||||||
" correctOffset=" & formatFloat(correctOffset, ffDecimal, 1) &
|
" correctOffset=" & formatFloat(correctOffset, ffDecimal, 1) &
|
||||||
" err=" & (if producedOffset > -999.0: formatFloat(aimErr, ffDecimal, 1) else: "n/a") &
|
" err=" & (if producedOffset > -999.0: formatFloat(aimErr, ffDecimal, 1) else: "n/a") &
|
||||||
" hammingDist=" & $hammingDist &
|
" vPerp=" & formatFloat(bot.lastVPerp, ffDecimal, 2) &
|
||||||
" inputChanged=" & $inputChanged &
|
" cells=" & $bot.res.totalCount
|
||||||
" exemplars=" & $bot.res.count
|
|
||||||
else:
|
else:
|
||||||
let targetRel = normalizeRelativeAngle(correctAngle - bot.lastDecideGunDir)
|
let targetRel = normalizeRelativeAngle(correctAngle - bot.lastDecideGunDir)
|
||||||
bot.snn.superSpikeUpdate(bot.lastSpikes, bot.lastVSnap, bot.snn.preTrace, targetRel)
|
bot.snn.superSpikeUpdate(bot.lastSpikes, bot.lastVSnap, bot.snn.preTrace, targetRel)
|
||||||
@@ -514,8 +460,6 @@ method run*(bot: SNNBot) =
|
|||||||
let rate = if bot.bulletsFired > 0: (float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0) else: 0.0
|
let rate = if bot.bulletsFired > 0: (float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0) else: 0.0
|
||||||
echo "tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0) & " err=" & formatFloat(err, ffDecimal, 1) & "° infer_spk=" & $spikeCount & "/" & $(N_INFER * N_HID) & " maxV=" & formatFloat(maxV, ffDecimal, 3) & " |wih|=" & formatFloat(meanWih, ffDecimal, 4) & " |wOut|=" & formatFloat(meanWout, ffDecimal, 4) & " sin=" & formatFloat(bot.snn.lastSinOut, ffDecimal, 3) & " cos=" & formatFloat(bot.snn.lastCosOut, ffDecimal, 3) & " snnAngle=" & formatFloat(bot.snn.lastSnnAngle, ffDecimal, 1)
|
echo "tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0) & " err=" & formatFloat(err, ffDecimal, 1) & "° infer_spk=" & $spikeCount & "/" & $(N_INFER * N_HID) & " maxV=" & formatFloat(maxV, ffDecimal, 3) & " |wih|=" & formatFloat(meanWih, ffDecimal, 4) & " |wOut|=" & formatFloat(meanWout, ffDecimal, 4) & " sin=" & formatFloat(bot.snn.lastSinOut, ffDecimal, 3) & " cos=" & formatFloat(bot.snn.lastCosOut, ffDecimal, 3) & " snnAngle=" & formatFloat(bot.snn.lastSnnAngle, ffDecimal, 1)
|
||||||
|
|
||||||
# Update lastInput for next EVALUATE cycle's change detection
|
|
||||||
bot.lastInput = bot.lastBinInput
|
|
||||||
bot.phase = DECIDE
|
bot.phase = DECIDE
|
||||||
|
|
||||||
# Radar lock
|
# Radar lock
|
||||||
|
|||||||
@@ -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
|
const
|
||||||
INPUT_BITS* = 80
|
VPERP_BINS* = 17 # -8 to +8 inclusive (integer speed units)
|
||||||
WORDS_IN* = 2 # 80 bits → 2 × uint64 (128 bits, only 80 used)
|
DIST_BINS* = 8 # distance bands
|
||||||
MAX_K* = 128
|
DIST_BAND* = 125.0 # pixels per band
|
||||||
MIN_SIM* = 3
|
|
||||||
|
|
||||||
type
|
type
|
||||||
BitVec80* = array[WORDS_IN, uint64]
|
GridCell = object
|
||||||
|
sumSin: float
|
||||||
|
sumCos: float
|
||||||
|
count: int
|
||||||
|
|
||||||
Exemplar* = object
|
LeadGrid* = object
|
||||||
pattern*: BitVec80
|
cells: array[VPERP_BINS * DIST_BINS, GridCell] # 17 × 8 = 136 cells
|
||||||
offset*: float # lead correction in degrees, typically [-30, +30]
|
|
||||||
active*: bool
|
|
||||||
|
|
||||||
BinaryAimer* = object
|
proc initLeadGrid*(): LeadGrid =
|
||||||
exemplars*: array[MAX_K, Exemplar]
|
result = LeadGrid()
|
||||||
count*: int
|
|
||||||
nextSlot*: int
|
|
||||||
|
|
||||||
proc initBinaryAimer*(): BinaryAimer =
|
proc cellIndex(vPerp: float, distance: float): int {.inline.} =
|
||||||
result = BinaryAimer()
|
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 =
|
proc forward*(grid: var LeadGrid, vPerp, distance: float): float =
|
||||||
## Kernel regression: weighted circular mean over active exemplars.
|
## Returns circular mean offset in degrees, or -999.0 when no data.
|
||||||
## Weights by similarity^2 and exponential recency decay (0.95^age).
|
let idx = cellIndex(vPerp, distance)
|
||||||
## Returns offset (lead correction) in degrees, or -999.0 sentinel when cold (no weight).
|
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 sinSum = 0.0
|
||||||
var cosSum = 0.0
|
var cosSum = 0.0
|
||||||
var totalW = 0.0
|
var totalW = 0.0
|
||||||
for i in 0 ..< MAX_K:
|
for dv in -1 .. 1:
|
||||||
if not aimer.exemplars[i].active: continue
|
for dd in -1 .. 1:
|
||||||
var sim = 0
|
let vb = vBin + dv
|
||||||
for w in 0 ..< WORDS_IN:
|
let db = dBin + dd
|
||||||
sim += popcount(input[w] and aimer.exemplars[i].pattern[w]).int
|
if vb < 0 or vb >= VPERP_BINS or db < 0 or db >= DIST_BINS: continue
|
||||||
let excess = sim - MIN_SIM
|
let c = grid.cells[vb * DIST_BINS + db]
|
||||||
if excess <= 0: continue
|
if c.count == 0: continue
|
||||||
let age = (aimer.nextSlot - 1 - i + MAX_K) mod MAX_K
|
let w = if dv == 0 and dd == 0: 1.0
|
||||||
let recency = pow(0.95, age.float)
|
elif dv == 0 or dd == 0: 0.5
|
||||||
let weight = float(excess * excess) * recency
|
else: 0.25
|
||||||
let rad = degToRad(aimer.exemplars[i].offset)
|
sinSum += w * c.sumSin / float(c.count)
|
||||||
sinSum += weight * sin(rad)
|
cosSum += w * c.sumCos / float(c.count)
|
||||||
cosSum += weight * cos(rad)
|
totalW += w
|
||||||
totalW += weight
|
|
||||||
if totalW == 0.0:
|
if totalW == 0.0:
|
||||||
return -999.0
|
return -999.0
|
||||||
result = radToDeg(arctan2(sinSum, cosSum))
|
result = radToDeg(arctan2(sinSum, cosSum))
|
||||||
|
|
||||||
proc learn*(aimer: var BinaryAimer, input: BitVec80, offset: float) =
|
proc learn*(grid: var LeadGrid, vPerp, distance, correctOffset: float) =
|
||||||
## Ring-buffer store: write exemplar at nextSlot, advance mod MAX_K.
|
let idx = cellIndex(vPerp, distance)
|
||||||
aimer.exemplars[aimer.nextSlot] = Exemplar(pattern: input, offset: offset, active: true)
|
grid.cells[idx].sumSin += sin(degToRad(correctOffset))
|
||||||
aimer.nextSlot = (aimer.nextSlot + 1) mod MAX_K
|
grid.cells[idx].sumCos += cos(degToRad(correctOffset))
|
||||||
if aimer.count < MAX_K: inc aimer.count
|
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