feat(SNNBot): physics-based input encoding for lead generalization
Replace (bearing, velDir, speed) with (relVelDir, speed, distance). Raw bearing is irrelevant to lead offset — the correction depends on how the target crosses the line of fire, not where it is. This lets exemplars from one position generalize to all positions with similar geometry.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# SNNBot — SNN core + aiming loop prototype (issue #152).
|
# SNNBot — SNN core + aiming loop prototype (issue #152).
|
||||||
# 80-input population-coded → 12 LIF hidden → polar-coded (sin/cos) output decoded via atan2.
|
# 80-input population-coded → 12 LIF hidden → polar-coded (sin/cos) output decoded via atan2.
|
||||||
# Inputs: bearing [0-35], velocity direction [36-71], speed [72-79].
|
# Inputs: relVelDir [0-35], speed [36-38], distance thermometer [39-46]. Raw bearing removed (irrelevant to lead).
|
||||||
# No movement, no firing. SuperSpike three-factor rule on all weights (issue #158).
|
# No movement, no firing. SuperSpike three-factor rule on all weights (issue #158).
|
||||||
# Binary reservoir alternative toggled via USE_RESERVOIR const (issue #159).
|
# Binary reservoir alternative toggled via USE_RESERVOIR const (issue #159).
|
||||||
#
|
#
|
||||||
@@ -333,45 +333,39 @@ method onBulletHit*(bot: SNNBot, e: BulletHitBotEvent) =
|
|||||||
|
|
||||||
# ── Reservoir helpers ─────────────────────────────────────────────────────────
|
# ── Reservoir helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
proc toBinaryInput(bearing: float, velDir: float, velSpeed: float, hasVel: bool): BitVec80 =
|
proc toBinaryInput(bearing: float, velDir: float, velSpeed: float, distance: float, hasVel: bool): BitVec80 =
|
||||||
## Binary-native encoding using relative velocity direction for cross-round generalization.
|
## Physics-based encoding for lead generalization (issue #152).
|
||||||
## Bits 0-35: bearing (absolute, for range estimate)
|
## Raw bearing removed — irrelevant to lead offset.
|
||||||
## Bits 36-71: relative velocity direction = (velDir - bearing) mod 360
|
## Bits 0-35: relVelDir = (velDir - bearing) normalized to [-180,180), 36 bins × 10°, 5 active (center ± 2 neighbors).
|
||||||
## This encodes lateral vs. radial motion regardless of which wall Walls is on.
|
## Bits 36-38: speed level (3 bits, center ± 1 neighbor).
|
||||||
## Bits 72-79: speed bands
|
## Bits 39-46: distance thermometer, 8 bands of ~125px in [0,1000].
|
||||||
## ponytail: relative-vel encoding; revert to absolute if aimer convergence regresses
|
## Bits 47-79: unused (sparse is fine for popcount similarity).
|
||||||
|
## ponytail: physics-based; revert to absolute bearing if convergence regresses
|
||||||
result = [0'u64, 0'u64]
|
result = [0'u64, 0'u64]
|
||||||
|
|
||||||
# Bearing: bits 0-35 (36 bits, 10° bands)
|
|
||||||
# Activate center band + 2 neighbors on each side = 5 bits
|
|
||||||
let bNorm = (bearing + 180.0) / 10.0 # 0..36
|
|
||||||
let bCenter = int(bNorm) mod 36
|
|
||||||
for offset in -2 .. 2:
|
|
||||||
let idx = (bCenter + offset + 36) mod 36
|
|
||||||
let word = idx div 64
|
|
||||||
let bit = idx mod 64
|
|
||||||
result[word] = result[word] or (1'u64 shl bit)
|
|
||||||
|
|
||||||
if hasVel:
|
if hasVel:
|
||||||
# Relative velocity direction: (velDir - bearing + 360) mod 360
|
# Relative velocity direction: how the target crosses our line of fire
|
||||||
# Encodes lateral/radial motion direction independent of which wall Walls is on
|
var relVelDir = (velDir - bearing)
|
||||||
let relVel = ((velDir - bearing) + 360.0) mod 360.0
|
while relVelDir >= 180.0: relVelDir -= 360.0
|
||||||
let vNorm = relVel / 10.0
|
while relVelDir < -180.0: relVelDir += 360.0
|
||||||
|
let vNorm = (relVelDir + 180.0) / 10.0 # 0..36
|
||||||
let vCenter = int(vNorm) mod 36
|
let vCenter = int(vNorm) mod 36
|
||||||
for offset in -2 .. 2:
|
for offset in -2 .. 2:
|
||||||
let idx = 36 + (vCenter + offset + 36) mod 36
|
let idx = (vCenter + offset + 36) mod 36 # bits 0-35
|
||||||
let word = idx div 64
|
result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64))
|
||||||
let bit = idx mod 64
|
|
||||||
result[word] = result[word] or (1'u64 shl bit)
|
|
||||||
|
|
||||||
# Speed: bits 72-79 (8 bits, 1 unit/tick bands)
|
# Speed: bits 36-38, center ± 1 neighbor clamped to [0,2]
|
||||||
# Activate center + 1 neighbor on each side = 3 bits
|
let sCenter = clamp(int(velSpeed), 0, 2)
|
||||||
let sCenter = clamp(int(velSpeed), 0, 7)
|
|
||||||
for offset in -1 .. 1:
|
for offset in -1 .. 1:
|
||||||
let idx = 72 + clamp(sCenter + offset, 0, 7)
|
let idx = 36 + clamp(sCenter + offset, 0, 2)
|
||||||
let word = idx div 64
|
result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64))
|
||||||
let bit = idx mod 64
|
|
||||||
result[word] = result[word] or (1'u64 shl bit)
|
# Distance thermometer: bits 39-46, 8 bands of ~125px over [0,1000]
|
||||||
|
# All bits up to the current band set (inclusive)
|
||||||
|
let band = clamp(int(distance / 125.0), 0, 7)
|
||||||
|
for b in 0 .. band:
|
||||||
|
let idx = 39 + b
|
||||||
|
result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64))
|
||||||
|
|
||||||
# ── Main loop ─────────────────────────────────────────────────────────────────
|
# ── Main loop ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -396,7 +390,7 @@ method run*(bot: SNNBot) =
|
|||||||
let absBearing = bot.enemyBearing
|
let absBearing = bot.enemyBearing
|
||||||
bot.lastRelBearing = normalizeRelativeAngle(absBearing - gunDir)
|
bot.lastRelBearing = normalizeRelativeAngle(absBearing - gunDir)
|
||||||
when USE_RESERVOIR:
|
when USE_RESERVOIR:
|
||||||
let binInput = toBinaryInput(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
let binInput = toBinaryInput(absBearing, bot.velDirDeg, bot.velSpeed, bot.enemyDist, bot.hasLastPos)
|
||||||
let aimOffset = bot.res.forward(binInput) # lead correction offset or -999.0 sentinel
|
let aimOffset = bot.res.forward(binInput) # lead correction offset or -999.0 sentinel
|
||||||
bot.lastBinInput = binInput
|
bot.lastBinInput = binInput
|
||||||
bot.lastDecideGunDir = gunDir
|
bot.lastDecideGunDir = gunDir
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import std/[bitops, math]
|
|||||||
const
|
const
|
||||||
INPUT_BITS* = 80
|
INPUT_BITS* = 80
|
||||||
WORDS_IN* = 2 # 80 bits → 2 × uint64 (128 bits, only 80 used)
|
WORDS_IN* = 2 # 80 bits → 2 × uint64 (128 bits, only 80 used)
|
||||||
MAX_K* = 128
|
MAX_K* = 64
|
||||||
MIN_SIM* = 3
|
MIN_SIM* = 3
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|||||||
Reference in New Issue
Block a user