fix(SNNBot): hold aim when error is within dead zone
Adds aim-lock mechanism to prevent re-running forward() pass during DECIDE when the error is already within 1.5° dead zone. Lock is set in EVALUATE when error < 1.5°, reusing the previous targetAngle in DECIDE. Lock releases on fresh enemy scan or when error exceeds threshold, eliminating the ±0.9° wobble from input pattern drift. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -215,6 +215,7 @@ type
|
||||
velSpeed: float64 # speed (units/tick) from last scan delta
|
||||
lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE
|
||||
lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE
|
||||
aimLocked: bool # when true, skip forward() and reuse targetAngle
|
||||
|
||||
# ── aimTo helper ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -254,6 +255,7 @@ method onScannedBot*(bot: SNNBot, e: ScannedBotEvent) =
|
||||
bot.enemyBearing = directionTo(bx, by, e.x, e.y)
|
||||
bot.enemyDist = distanceTo(bx, by, e.x, e.y)
|
||||
bot.hasContact = true
|
||||
bot.aimLocked = false # release lock on fresh enemy scan
|
||||
if bot.hasLastPos:
|
||||
let dx = e.x - bot.lastEnemyX
|
||||
let dy = e.y - bot.lastEnemyY
|
||||
@@ -273,6 +275,7 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
|
||||
bot.velSpeed = 0.0
|
||||
bot.phase = DECIDE
|
||||
bot.tick = 0
|
||||
bot.aimLocked = false
|
||||
setTargetSpeed(0.0)
|
||||
setTurnRate(0.0)
|
||||
|
||||
@@ -335,48 +338,49 @@ method run*(bot: SNNBot) =
|
||||
|
||||
case bot.phase
|
||||
of DECIDE:
|
||||
let relBearing = normalizeRelativeAngle(bot.enemyBearing - gunDir)
|
||||
bot.lastRelBearing = relBearing
|
||||
when USE_RESERVOIR:
|
||||
let binInput = toBinaryInput(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
||||
let aimRaw = bot.res.forward(binInput) # [0,360) or -1.0 sentinel
|
||||
bot.lastBinInput = binInput
|
||||
bot.lastDecideGunDir = gunDir
|
||||
if aimRaw < 0.0:
|
||||
# cold start: use raw bearing directly
|
||||
bot.targetAngle = bot.enemyBearing
|
||||
echo "RES tick=" & $bot.tick & " cold-start aim=" & formatFloat(bot.enemyBearing, ffDecimal, 1)
|
||||
if not bot.aimLocked:
|
||||
let relBearing = normalizeRelativeAngle(bot.enemyBearing - gunDir)
|
||||
bot.lastRelBearing = relBearing
|
||||
when USE_RESERVOIR:
|
||||
let binInput = toBinaryInput(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
||||
let aimRaw = bot.res.forward(binInput) # [0,360) or -1.0 sentinel
|
||||
bot.lastBinInput = binInput
|
||||
bot.lastDecideGunDir = gunDir
|
||||
if aimRaw < 0.0:
|
||||
# cold start: use raw bearing directly
|
||||
bot.targetAngle = bot.enemyBearing
|
||||
echo "RES tick=" & $bot.tick & " cold-start aim=" & formatFloat(bot.enemyBearing, ffDecimal, 1)
|
||||
else:
|
||||
# aimRaw is absolute angle [0,360); convert to -180..180 relative then add gunDir
|
||||
let aimRel = normalizeRelativeAngle(aimRaw - 180.0) # exemplars stored as 0-360, relative bearing is -180..180
|
||||
bot.targetAngle = gunDir + aimRel
|
||||
echo "RES tick=" & $bot.tick & " aim=" & formatFloat(aimRaw, ffDecimal, 1) & " rel=" & formatFloat(aimRel, ffDecimal, 1)
|
||||
else:
|
||||
# aimRaw is absolute angle [0,360); convert to -180..180 relative then add gunDir
|
||||
let aimRel = normalizeRelativeAngle(aimRaw - 180.0) # exemplars stored as 0-360, relative bearing is -180..180
|
||||
bot.targetAngle = gunDir + aimRel
|
||||
echo "RES tick=" & $bot.tick & " aim=" & formatFloat(aimRaw, ffDecimal, 1) & " rel=" & formatFloat(aimRel, ffDecimal, 1)
|
||||
else:
|
||||
let inputs = encodeInputFull(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
||||
# Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks
|
||||
var totalSin = 0.0; var totalCos = 0.0
|
||||
var tickSpikes: array[N_HID, float]
|
||||
var tickVSnap: array[N_HID, float]
|
||||
for h in 0 ..< N_HID: bot.lastSpikes[h] = 0.0
|
||||
for _ in 0 ..< N_INFER:
|
||||
var sinT, cosT: float
|
||||
bot.snn.forward(inputs, tickSpikes, tickVSnap, sinT, cosT)
|
||||
totalSin += sinT; totalCos += cosT
|
||||
let inputs = encodeInputFull(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
||||
# Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks
|
||||
var totalSin = 0.0; var totalCos = 0.0
|
||||
var tickSpikes: array[N_HID, float]
|
||||
var tickVSnap: array[N_HID, float]
|
||||
for h in 0 ..< N_HID: bot.lastSpikes[h] = 0.0
|
||||
for _ in 0 ..< N_INFER:
|
||||
var sinT, cosT: float
|
||||
bot.snn.forward(inputs, tickSpikes, tickVSnap, sinT, cosT)
|
||||
totalSin += sinT; totalCos += cosT
|
||||
for h in 0 ..< N_HID:
|
||||
bot.lastSpikes[h] += tickSpikes[h] # accumulate counts
|
||||
# Store last-tick voltages for learning
|
||||
bot.lastVSnap = tickVSnap
|
||||
bot.snn.lastSinOut = totalSin; bot.snn.lastCosOut = totalCos
|
||||
bot.snn.lastSnnAngle = arctan2(totalSin, totalCos) * 180.0 / PI
|
||||
bot.targetAngle = gunDir + bot.snn.lastSnnAngle
|
||||
bot.lastDecideGunDir = gunDir
|
||||
# Log total spike count across inference window
|
||||
var spikeCount = 0
|
||||
var maxV = 0.0
|
||||
for h in 0 ..< N_HID:
|
||||
bot.lastSpikes[h] += tickSpikes[h] # accumulate counts
|
||||
# Store last-tick voltages for learning
|
||||
bot.lastVSnap = tickVSnap
|
||||
bot.snn.lastSinOut = totalSin; bot.snn.lastCosOut = totalCos
|
||||
bot.snn.lastSnnAngle = arctan2(totalSin, totalCos) * 180.0 / PI
|
||||
bot.targetAngle = gunDir + bot.snn.lastSnnAngle
|
||||
bot.lastDecideGunDir = gunDir
|
||||
# Log total spike count across inference window
|
||||
var spikeCount = 0
|
||||
var maxV = 0.0
|
||||
for h in 0 ..< N_HID:
|
||||
spikeCount += int(bot.lastSpikes[h])
|
||||
maxV = max(maxV, bot.snn.vHid[h])
|
||||
echo "tick=" & $bot.tick & " infer_spk=" & $spikeCount & "/" & $(N_INFER * N_HID) & " maxV=" & formatFloat(maxV, ffDecimal, 3)
|
||||
spikeCount += int(bot.lastSpikes[h])
|
||||
maxV = max(maxV, bot.snn.vHid[h])
|
||||
echo "tick=" & $bot.tick & " infer_spk=" & $spikeCount & "/" & $(N_INFER * N_HID) & " maxV=" & formatFloat(maxV, ffDecimal, 3)
|
||||
bot.phase = WAITING
|
||||
|
||||
of WAITING:
|
||||
@@ -404,18 +408,28 @@ method run*(bot: SNNBot) =
|
||||
let aimErr = abs(normalizeRelativeAngle(aimRaw - correctAngle))
|
||||
if aimErr > 1.5:
|
||||
bot.res.learn(bot.lastBinInput, correctAngle)
|
||||
bot.aimLocked = false # error still large, keep recomputing
|
||||
else:
|
||||
bot.aimLocked = true # error within dead zone, freeze aim
|
||||
else:
|
||||
# Cold start: always learn
|
||||
bot.res.learn(bot.lastBinInput, correctAngle)
|
||||
bot.aimLocked = false
|
||||
let aimErr = if aimRaw >= 0.0: abs(normalizeRelativeAngle(aimRaw - correctAngle)) else: -1.0
|
||||
echo "RES tick=" & $bot.tick &
|
||||
" aim=" & (if aimRaw >= 0.0: formatFloat(aimRaw, ffDecimal, 1) else: "cold") &
|
||||
" correct=" & formatFloat(correctAngle, ffDecimal, 1) &
|
||||
" err=" & (if aimRaw >= 0.0: formatFloat(aimErr, ffDecimal, 1) else: "n/a") &
|
||||
" exemplars=" & $bot.res.count
|
||||
" exemplars=" & $bot.res.count &
|
||||
" lock=" & (if bot.aimLocked: "ON" else: "OFF")
|
||||
else:
|
||||
let err = abs(normalizeRelativeAngle(gunDir - bot.enemyBearing))
|
||||
bot.snn.superSpikeUpdate(bot.lastSpikes, bot.lastVSnap, bot.snn.preTrace, targetRel)
|
||||
# Aim lock: freeze when error within 1.5° threshold
|
||||
if err < 1.5:
|
||||
bot.aimLocked = true
|
||||
else:
|
||||
bot.aimLocked = false
|
||||
# Compute verbose logging metrics
|
||||
var spikeCount = 0
|
||||
var maxV = 0.0
|
||||
@@ -432,7 +446,7 @@ method run*(bot: SNNBot) =
|
||||
for w in bot.snn.wCos:
|
||||
meanWout += abs(w)
|
||||
meanWout /= float(2 * N_HID)
|
||||
echo "tick=" & $bot.tick & " 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 & " 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) & " lock=" & (if bot.aimLocked: "ON" else: "OFF")
|
||||
bot.phase = DECIDE
|
||||
|
||||
# Radar lock
|
||||
|
||||
Reference in New Issue
Block a user