fix(SNNBot): use absolute bearing to break aim feedback loop
The input encoded relative bearing, which changes when the bot aims, creating a closed-loop oscillation (aim→input→aim). Switching to absolute bearing (world frame) makes the input independent of aim commands, eliminating the feedback-induced jitter.
This commit is contained in:
@@ -215,7 +215,6 @@ type
|
|||||||
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
|
lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE
|
||||||
aimLocked: bool # when true, skip forward() and reuse targetAngle
|
|
||||||
|
|
||||||
# ── aimTo helper ──────────────────────────────────────────────────────────────
|
# ── aimTo helper ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -255,7 +254,6 @@ method onScannedBot*(bot: SNNBot, e: ScannedBotEvent) =
|
|||||||
bot.enemyBearing = directionTo(bx, by, e.x, e.y)
|
bot.enemyBearing = directionTo(bx, by, e.x, e.y)
|
||||||
bot.enemyDist = distanceTo(bx, by, e.x, e.y)
|
bot.enemyDist = distanceTo(bx, by, e.x, e.y)
|
||||||
bot.hasContact = true
|
bot.hasContact = true
|
||||||
bot.aimLocked = false # release lock on fresh enemy scan
|
|
||||||
if bot.hasLastPos:
|
if bot.hasLastPos:
|
||||||
let dx = e.x - bot.lastEnemyX
|
let dx = e.x - bot.lastEnemyX
|
||||||
let dy = e.y - bot.lastEnemyY
|
let dy = e.y - bot.lastEnemyY
|
||||||
@@ -275,7 +273,6 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
|
|||||||
bot.velSpeed = 0.0
|
bot.velSpeed = 0.0
|
||||||
bot.phase = DECIDE
|
bot.phase = DECIDE
|
||||||
bot.tick = 0
|
bot.tick = 0
|
||||||
bot.aimLocked = false
|
|
||||||
setTargetSpeed(0.0)
|
setTargetSpeed(0.0)
|
||||||
setTurnRate(0.0)
|
setTurnRate(0.0)
|
||||||
|
|
||||||
@@ -338,25 +335,24 @@ method run*(bot: SNNBot) =
|
|||||||
|
|
||||||
case bot.phase
|
case bot.phase
|
||||||
of DECIDE:
|
of DECIDE:
|
||||||
if not bot.aimLocked:
|
# absBearing: world-frame bearing [0,360) from directionTo — independent of bot heading
|
||||||
let relBearing = normalizeRelativeAngle(bot.enemyBearing - gunDir)
|
let absBearing = bot.enemyBearing
|
||||||
bot.lastRelBearing = relBearing
|
bot.lastRelBearing = normalizeRelativeAngle(absBearing - gunDir)
|
||||||
when USE_RESERVOIR:
|
when USE_RESERVOIR:
|
||||||
let binInput = toBinaryInput(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
let binInput = toBinaryInput(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
||||||
let aimRaw = bot.res.forward(binInput) # [0,360) or -1.0 sentinel
|
let aimRaw = bot.res.forward(binInput) # [0,360) or -1.0 sentinel
|
||||||
bot.lastBinInput = binInput
|
bot.lastBinInput = binInput
|
||||||
bot.lastDecideGunDir = gunDir
|
bot.lastDecideGunDir = gunDir
|
||||||
if aimRaw < 0.0:
|
if aimRaw < 0.0:
|
||||||
# cold start: use raw bearing directly
|
# cold start: aim directly at enemy
|
||||||
bot.targetAngle = bot.enemyBearing
|
bot.targetAngle = absBearing
|
||||||
echo "RES tick=" & $bot.tick & " cold-start aim=" & formatFloat(bot.enemyBearing, ffDecimal, 1)
|
echo "RES tick=" & $bot.tick & " cold-start aim=" & formatFloat(absBearing, ffDecimal, 1)
|
||||||
else:
|
else:
|
||||||
# aimRaw is absolute angle [0,360); convert to -180..180 relative then add gunDir
|
# aimRaw is absolute [0,360): use directly as target
|
||||||
let aimRel = normalizeRelativeAngle(aimRaw - 180.0) # exemplars stored as 0-360, relative bearing is -180..180
|
bot.targetAngle = aimRaw
|
||||||
bot.targetAngle = gunDir + aimRel
|
echo "RES tick=" & $bot.tick & " aim=" & formatFloat(aimRaw, ffDecimal, 1)
|
||||||
echo "RES tick=" & $bot.tick & " aim=" & formatFloat(aimRaw, ffDecimal, 1) & " rel=" & formatFloat(aimRel, ffDecimal, 1)
|
|
||||||
else:
|
else:
|
||||||
let inputs = encodeInputFull(relBearing, 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
|
||||||
var totalSin = 0.0; var totalCos = 0.0
|
var totalSin = 0.0; var totalCos = 0.0
|
||||||
var tickSpikes: array[N_HID, float]
|
var tickSpikes: array[N_HID, float]
|
||||||
@@ -385,12 +381,9 @@ method run*(bot: SNNBot) =
|
|||||||
|
|
||||||
of WAITING:
|
of WAITING:
|
||||||
aimTo(bot.targetAngle, gunDir)
|
aimTo(bot.targetAngle, gunDir)
|
||||||
if bot.aimLocked:
|
|
||||||
discard setFire(FIRE_POWER)
|
|
||||||
else:
|
|
||||||
discard setFire(0.0)
|
|
||||||
let err = abs(normalizeRelativeAngle(bot.targetAngle - gunDir))
|
let err = abs(normalizeRelativeAngle(bot.targetAngle - gunDir))
|
||||||
if err < AIM_TOL:
|
if err < AIM_TOL:
|
||||||
|
discard setFire(FIRE_POWER)
|
||||||
bot.phase = EVALUATE
|
bot.phase = EVALUATE
|
||||||
|
|
||||||
of EVALUATE:
|
of EVALUATE:
|
||||||
@@ -400,39 +393,28 @@ method run*(bot: SNNBot) =
|
|||||||
let velRad = degToRad(bot.velDirDeg)
|
let velRad = degToRad(bot.velDirDeg)
|
||||||
let futureX = bot.lastEnemyX + cos(velRad) * bot.velSpeed * travelTime
|
let futureX = bot.lastEnemyX + cos(velRad) * bot.velSpeed * travelTime
|
||||||
let futureY = bot.lastEnemyY + sin(velRad) * bot.velSpeed * travelTime
|
let futureY = bot.lastEnemyY + sin(velRad) * bot.velSpeed * travelTime
|
||||||
let absBearing = directionTo(myX, myY, futureX, futureY)
|
# correctAngle: absolute world-frame bearing [0,360) to predicted enemy position
|
||||||
let targetRel = normalizeRelativeAngle(absBearing - bot.lastDecideGunDir)
|
let correctAngle = directionTo(myX, myY, futureX, futureY)
|
||||||
when USE_RESERVOIR:
|
when USE_RESERVOIR:
|
||||||
# correctAngle must be [0,360): targetRel is -180..180, shift it
|
|
||||||
var correctAngle = targetRel + 180.0
|
|
||||||
let aimRaw = bot.res.forward(bot.lastBinInput)
|
let aimRaw = bot.res.forward(bot.lastBinInput)
|
||||||
# Dead zone: only learn if aim error > 1.5° (prevents ring buffer churn)
|
# Dead zone: only learn if aim error > 1.5° (prevents ring buffer churn)
|
||||||
if aimRaw >= 0.0:
|
if aimRaw >= 0.0:
|
||||||
let aimErr = abs(normalizeRelativeAngle(aimRaw - correctAngle))
|
let aimErr = abs(normalizeRelativeAngle(aimRaw - correctAngle))
|
||||||
if aimErr > 1.5:
|
if aimErr > 1.5:
|
||||||
bot.res.learn(bot.lastBinInput, correctAngle)
|
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:
|
else:
|
||||||
# Cold start: always learn
|
# Cold start: always learn
|
||||||
bot.res.learn(bot.lastBinInput, correctAngle)
|
bot.res.learn(bot.lastBinInput, correctAngle)
|
||||||
bot.aimLocked = false
|
|
||||||
let aimErr = if aimRaw >= 0.0: abs(normalizeRelativeAngle(aimRaw - correctAngle)) else: -1.0
|
let aimErr = if aimRaw >= 0.0: abs(normalizeRelativeAngle(aimRaw - correctAngle)) else: -1.0
|
||||||
echo "RES tick=" & $bot.tick &
|
echo "RES tick=" & $bot.tick &
|
||||||
" aim=" & (if aimRaw >= 0.0: formatFloat(aimRaw, ffDecimal, 1) else: "cold") &
|
" aim=" & (if aimRaw >= 0.0: formatFloat(aimRaw, ffDecimal, 1) else: "cold") &
|
||||||
" correct=" & formatFloat(correctAngle, ffDecimal, 1) &
|
" correct=" & formatFloat(correctAngle, ffDecimal, 1) &
|
||||||
" err=" & (if aimRaw >= 0.0: formatFloat(aimErr, ffDecimal, 1) else: "n/a") &
|
" 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:
|
else:
|
||||||
let err = abs(normalizeRelativeAngle(gunDir - bot.enemyBearing))
|
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)
|
||||||
# Aim lock: freeze when error within 1.5° threshold
|
let err = abs(normalizeRelativeAngle(gunDir - bot.enemyBearing))
|
||||||
if err < 1.5:
|
|
||||||
bot.aimLocked = true
|
|
||||||
else:
|
|
||||||
bot.aimLocked = false
|
|
||||||
# Compute verbose logging metrics
|
# Compute verbose logging metrics
|
||||||
var spikeCount = 0
|
var spikeCount = 0
|
||||||
var maxV = 0.0
|
var maxV = 0.0
|
||||||
@@ -449,7 +431,7 @@ method run*(bot: SNNBot) =
|
|||||||
for w in bot.snn.wCos:
|
for w in bot.snn.wCos:
|
||||||
meanWout += abs(w)
|
meanWout += abs(w)
|
||||||
meanWout /= float(2 * N_HID)
|
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) & " lock=" & (if bot.aimLocked: "ON" else: "OFF")
|
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)
|
||||||
bot.phase = DECIDE
|
bot.phase = DECIDE
|
||||||
|
|
||||||
# Radar lock
|
# Radar lock
|
||||||
|
|||||||
Reference in New Issue
Block a user