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
|
||||
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)
|
||||
AIM_TOL = 2.0 # arrive tolerance (degrees)
|
||||
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)
|
||||
# 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
|
||||
lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE
|
||||
lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE
|
||||
lastError: float # last aiming error (for jump detection)
|
||||
|
||||
# ── aimTo helper ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -274,7 +272,6 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
|
||||
bot.velSpeed = 0.0
|
||||
bot.phase = DECIDE
|
||||
bot.tick = 0
|
||||
bot.lastError = -1.0 # sentinel: no error yet
|
||||
setTargetSpeed(0.0)
|
||||
setTurnRate(0.0)
|
||||
|
||||
@@ -385,21 +382,18 @@ method run*(bot: SNNBot) =
|
||||
aimTo(bot.targetAngle, gunDir)
|
||||
let err = abs(normalizeRelativeAngle(bot.targetAngle - gunDir))
|
||||
|
||||
# Bidirectional fire gate: fire only when error within tolerance
|
||||
if err < AIM_TOL:
|
||||
# Adaptive dead zone: scale learning threshold by enemy velocity
|
||||
# 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)
|
||||
bot.phase = EVALUATE
|
||||
else:
|
||||
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:
|
||||
# Predictive error signal: extrapolate enemy position at bullet impact time.
|
||||
if bot.hasLastPos:
|
||||
@@ -409,12 +403,17 @@ method run*(bot: SNNBot) =
|
||||
let futureY = bot.lastEnemyY + sin(velRad) * bot.velSpeed * travelTime
|
||||
# correctAngle: absolute world-frame bearing [0,360) to predicted enemy position
|
||||
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:
|
||||
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:
|
||||
let aimErr = abs(normalizeRelativeAngle(aimRaw - correctAngle))
|
||||
if aimErr > 1.5:
|
||||
if aimErr > adaptiveDeadZone:
|
||||
bot.res.learn(bot.lastBinInput, correctAngle)
|
||||
else:
|
||||
# Cold start: always learn
|
||||
|
||||
Reference in New Issue
Block a user