feat(SNNBot): adaptive learning dead zone scaled by enemy velocity
Stationary targets get large dead zone (3°) for stable aim. Fast movers get small dead zone (0.5°) for rapid adaptation. Replaces fixed threshold and jump-reset hack. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,6 @@ const
|
|||||||
LEAK = 0.9 # LIF membrane leak factor
|
LEAK = 0.9 # LIF membrane leak factor
|
||||||
THRESH = 0.08 # ponytail: THRESH=0.08 tuned for 80-input layer; raise if firing saturates
|
THRESH = 0.08 # ponytail: THRESH=0.08 tuned for 80-input layer; raise if firing saturates
|
||||||
MAX_GUN_TURN = 20.0 # max gun turn per tick (degrees)
|
MAX_GUN_TURN = 20.0 # max gun turn per tick (degrees)
|
||||||
AIM_TOL = 2.0 # arrive tolerance (degrees)
|
|
||||||
ETA = 0.1 # SuperSpike learning rate for hidden→output weights (r_0 from paper, bumped 2x)
|
ETA = 0.1 # SuperSpike learning rate for hidden→output weights (r_0 from paper, bumped 2x)
|
||||||
ETA_IH = 0.02 # ponytail: ETA_IH=0.02 bumped 4x for input→hidden (was 0.005, too conservative)
|
ETA_IH = 0.02 # ponytail: ETA_IH=0.02 bumped 4x for input→hidden (was 0.005, too conservative)
|
||||||
# ponytail: separate input→hidden rate; add RMaxProp optimizer if convergence still unstable
|
# ponytail: separate input→hidden rate; add RMaxProp optimizer if convergence still unstable
|
||||||
@@ -215,7 +214,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
|
||||||
lastError: float # last aiming error (for jump detection)
|
|
||||||
|
|
||||||
# ── aimTo helper ──────────────────────────────────────────────────────────────
|
# ── aimTo helper ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -274,7 +272,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.lastError = -1.0 # sentinel: no error yet
|
|
||||||
setTargetSpeed(0.0)
|
setTargetSpeed(0.0)
|
||||||
setTurnRate(0.0)
|
setTurnRate(0.0)
|
||||||
|
|
||||||
@@ -385,21 +382,18 @@ method run*(bot: SNNBot) =
|
|||||||
aimTo(bot.targetAngle, gunDir)
|
aimTo(bot.targetAngle, gunDir)
|
||||||
let err = abs(normalizeRelativeAngle(bot.targetAngle - gunDir))
|
let err = abs(normalizeRelativeAngle(bot.targetAngle - gunDir))
|
||||||
|
|
||||||
# Bidirectional fire gate: fire only when error within tolerance
|
# Adaptive dead zone: scale learning threshold by enemy velocity
|
||||||
if err < AIM_TOL:
|
# Stationary → 3° (stable), full speed (8 units/tick) → 0.5° (fast tracking)
|
||||||
|
let t = (bot.velSpeed / 8.0).clamp(0.0, 1.0)
|
||||||
|
let adaptiveDeadZone = 3.0 + (0.5 - 3.0) * t # lerp(3.0, 0.5, t)
|
||||||
|
|
||||||
|
# Fire gate: only fire when error within adaptive dead zone
|
||||||
|
if err < adaptiveDeadZone:
|
||||||
discard setFire(FIRE_POWER)
|
discard setFire(FIRE_POWER)
|
||||||
bot.phase = EVALUATE
|
bot.phase = EVALUATE
|
||||||
else:
|
else:
|
||||||
discard setFire(0.0)
|
discard setFire(0.0)
|
||||||
|
|
||||||
# Fast recovery: detect error jump (regime change) and reset ring buffer
|
|
||||||
# ponytail: >5° jump triggers reset; tune threshold if target moves frequently
|
|
||||||
if bot.lastError >= 0.0 and err - bot.lastError > 5.0:
|
|
||||||
bot.res.count = 0
|
|
||||||
bot.res.nextSlot = 0
|
|
||||||
|
|
||||||
bot.lastError = err
|
|
||||||
|
|
||||||
of EVALUATE:
|
of EVALUATE:
|
||||||
# Predictive error signal: extrapolate enemy position at bullet impact time.
|
# Predictive error signal: extrapolate enemy position at bullet impact time.
|
||||||
if bot.hasLastPos:
|
if bot.hasLastPos:
|
||||||
@@ -409,12 +403,17 @@ method run*(bot: SNNBot) =
|
|||||||
let futureY = bot.lastEnemyY + sin(velRad) * bot.velSpeed * travelTime
|
let futureY = bot.lastEnemyY + sin(velRad) * bot.velSpeed * travelTime
|
||||||
# correctAngle: absolute world-frame bearing [0,360) to predicted enemy position
|
# correctAngle: absolute world-frame bearing [0,360) to predicted enemy position
|
||||||
let correctAngle = directionTo(myX, myY, futureX, futureY)
|
let correctAngle = directionTo(myX, myY, futureX, futureY)
|
||||||
|
|
||||||
|
# Adaptive learning dead zone: scale by enemy velocity (same as fire gate)
|
||||||
|
let t = (bot.velSpeed / 8.0).clamp(0.0, 1.0)
|
||||||
|
let adaptiveDeadZone = 3.0 + (0.5 - 3.0) * t # lerp(3.0, 0.5, t)
|
||||||
|
|
||||||
when USE_RESERVOIR:
|
when USE_RESERVOIR:
|
||||||
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)
|
# Only learn if aim error exceeds adaptive dead zone
|
||||||
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 > adaptiveDeadZone:
|
||||||
bot.res.learn(bot.lastBinInput, correctAngle)
|
bot.res.learn(bot.lastBinInput, correctAngle)
|
||||||
else:
|
else:
|
||||||
# Cold start: always learn
|
# Cold start: always learn
|
||||||
|
|||||||
Reference in New Issue
Block a user