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).
|
||||
# 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).
|
||||
# Binary reservoir alternative toggled via USE_RESERVOIR const (issue #159).
|
||||
#
|
||||
@@ -333,45 +333,39 @@ method onBulletHit*(bot: SNNBot, e: BulletHitBotEvent) =
|
||||
|
||||
# ── Reservoir helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
proc toBinaryInput(bearing: float, velDir: float, velSpeed: float, hasVel: bool): BitVec80 =
|
||||
## Binary-native encoding using relative velocity direction for cross-round generalization.
|
||||
## Bits 0-35: bearing (absolute, for range estimate)
|
||||
## Bits 36-71: relative velocity direction = (velDir - bearing) mod 360
|
||||
## This encodes lateral vs. radial motion regardless of which wall Walls is on.
|
||||
## Bits 72-79: speed bands
|
||||
## ponytail: relative-vel encoding; revert to absolute if aimer convergence regresses
|
||||
proc toBinaryInput(bearing: float, velDir: float, velSpeed: float, distance: float, hasVel: bool): BitVec80 =
|
||||
## Physics-based encoding for lead generalization (issue #152).
|
||||
## Raw bearing removed — irrelevant to lead offset.
|
||||
## Bits 0-35: relVelDir = (velDir - bearing) normalized to [-180,180), 36 bins × 10°, 5 active (center ± 2 neighbors).
|
||||
## Bits 36-38: speed level (3 bits, center ± 1 neighbor).
|
||||
## Bits 39-46: distance thermometer, 8 bands of ~125px in [0,1000].
|
||||
## 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]
|
||||
|
||||
# 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:
|
||||
# Relative velocity direction: (velDir - bearing + 360) mod 360
|
||||
# Encodes lateral/radial motion direction independent of which wall Walls is on
|
||||
let relVel = ((velDir - bearing) + 360.0) mod 360.0
|
||||
let vNorm = relVel / 10.0
|
||||
# Relative velocity direction: how the target crosses our line of fire
|
||||
var relVelDir = (velDir - bearing)
|
||||
while relVelDir >= 180.0: relVelDir -= 360.0
|
||||
while relVelDir < -180.0: relVelDir += 360.0
|
||||
let vNorm = (relVelDir + 180.0) / 10.0 # 0..36
|
||||
let vCenter = int(vNorm) mod 36
|
||||
for offset in -2 .. 2:
|
||||
let idx = 36 + (vCenter + offset + 36) mod 36
|
||||
let word = idx div 64
|
||||
let bit = idx mod 64
|
||||
result[word] = result[word] or (1'u64 shl bit)
|
||||
let idx = (vCenter + offset + 36) mod 36 # bits 0-35
|
||||
result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64))
|
||||
|
||||
# Speed: bits 72-79 (8 bits, 1 unit/tick bands)
|
||||
# Activate center + 1 neighbor on each side = 3 bits
|
||||
let sCenter = clamp(int(velSpeed), 0, 7)
|
||||
# Speed: bits 36-38, center ± 1 neighbor clamped to [0,2]
|
||||
let sCenter = clamp(int(velSpeed), 0, 2)
|
||||
for offset in -1 .. 1:
|
||||
let idx = 72 + clamp(sCenter + offset, 0, 7)
|
||||
let word = idx div 64
|
||||
let bit = idx mod 64
|
||||
result[word] = result[word] or (1'u64 shl bit)
|
||||
let idx = 36 + clamp(sCenter + offset, 0, 2)
|
||||
result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64))
|
||||
|
||||
# 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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -396,7 +390,7 @@ method run*(bot: SNNBot) =
|
||||
let absBearing = bot.enemyBearing
|
||||
bot.lastRelBearing = normalizeRelativeAngle(absBearing - gunDir)
|
||||
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
|
||||
bot.lastBinInput = binInput
|
||||
bot.lastDecideGunDir = gunDir
|
||||
|
||||
@@ -5,7 +5,7 @@ import std/[bitops, math]
|
||||
const
|
||||
INPUT_BITS* = 80
|
||||
WORDS_IN* = 2 # 80 bits → 2 × uint64 (128 bits, only 80 used)
|
||||
MAX_K* = 128
|
||||
MAX_K* = 64
|
||||
MIN_SIM* = 3
|
||||
|
||||
type
|
||||
|
||||
Reference in New Issue
Block a user