From 42b6cebd8e32a497d1078692ab1a4166c94620c0 Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Mon, 14 Sep 2026 22:28:41 +0200 Subject: [PATCH] fix(SNNBot): average matured snapshot targets instead of multiple updates Multiple superSpikeUpdate calls per EVALUATE caused oscillation by effectively multiplying the learning rate. Now: accumulate retroTarget via circular mean across all matured snapshots, apply one update with the averaged target and most recent snapshot's SNN state. --- SNNBot_garage/src/SNNBot.nim | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/SNNBot_garage/src/SNNBot.nim b/SNNBot_garage/src/SNNBot.nim index 65ae93c..73d2b03 100644 --- a/SNNBot_garage/src/SNNBot.nim +++ b/SNNBot_garage/src/SNNBot.nim @@ -464,9 +464,16 @@ method run*(bot: SNNBot) = # Retroactive would-have-hit error signal: find the most recently matured snapshot. # A snapshot matures when currentTick >= snapshotTick + ceil(distance / BULLET_SPEED). # Look up enemy position at impact tick from ring buffer (offset from most-recent write). - var retroTarget = bot.lastRelBearing # fallback; overwritten by each matured snapshot (last one used for logging) + var retroTarget = bot.lastRelBearing # fallback; overwritten after averaging (used for logging) var hasMatured = false var keepIdx = 0 # first non-matured snapshot to keep + # Accumulate retroTarget circular components across all matured snapshots. + var sumSin = 0.0; var sumCos = 0.0 + var matureCount = 0 + # Most-recent matured snapshot's SNN state (most relevant for weight update). + var bestSpikes: array[N_HID, float] + var bestVSnap: array[N_HID, float] + var bestPreTrace: array[N_IN, float] for i in 0 ..< bot.snapshots.len: let snap = bot.snapshots[i] let travelTicks = int(ceil(snap.distance / BULLET_SPEED)) @@ -483,13 +490,23 @@ method run*(bot: SNNBot) = let ex = bot.posBuf[foundSlot].x let ey = bot.posBuf[foundSlot].y let absBearing = directionTo(snap.botX, snap.botY, ex, ey) - retroTarget = normalizeRelativeAngle(absBearing - snap.gunHeading) - # Update weights for every matured snapshot immediately; earlier ones were previously discarded. - bot.snn.superSpikeUpdate(snap.spikes, snap.vSnap, snap.preTrace, retroTarget) + let rt = normalizeRelativeAngle(absBearing - snap.gunHeading) + # Accumulate circular mean components + sumSin += sin(degToRad(rt)) + sumCos += cos(degToRad(rt)) + inc matureCount + # Keep most-recent (highest index) snapshot's SNN state + bestSpikes = snap.spikes + bestVSnap = snap.vSnap + bestPreTrace = snap.preTrace hasMatured = true keepIdx = i + 1 # discard matured snapshots up to and including this one else: break # snapshots are in order; stop at first non-matured + # One update per EVALUATE using circular-mean target — stable learning rate regardless of snapshot count. + if matureCount > 0: + retroTarget = arctan2(sumSin, sumCos) * 180.0 / PI + bot.snn.superSpikeUpdate(bestSpikes, bestVSnap, bestPreTrace, retroTarget) # Discard matured snapshots; immature ones are preserved automatically (keepIdx stays 0 # or points past the last matured entry; the rest of bot.snapshots is kept intact). if keepIdx > 0: