TM radial gun: registered (default OFF) + label-bias fix that removes the bias but
retracts its own earlier learning claim === TASK 1: REGISTERED AS GUN 14, DEFAULT `off` === The radial TM gun is now a first-class rack member (`TMPATTERN`, id 14), forceable alone with `TR_RACK_TMPATTERN=both` plus every other `TR_RACK_*=off`. DEFAULT IS `off`, and the justification matters: `both` would let it compete for selection AND (because the shared VirtualTracker ring is order-sensitive) shift every other gun's learning order, so it CANNOT leave the default path unchanged. With `off` its predict and spawnBullets are additionally GATED on rack admission (the only gun wired that way), so the shipped default never spawns it at all: zero cost, zero ring perturbation. Live proof: 1-round battle with only TMPATTERN racked -> `gun 14 (TMPattern): vShots=400 selected=104 other-gun selections=0`. Default-path-unchanged proof: parity checks that the 15-gun default bestGun/ selectGun equals the old 14-gun rack RNG-draw-for-RNG-draw, that gun 14 is never selected by default, and acceptance 12/12. Cost: 0.36 ms/tick (predict 0.30 + onResult 0.05) ~= 3% of the 13.16 ms budget. Tsetlin in the same harness is 1.62 ms/tick, so the new gun is ~4.5x cheaper. === TASK 2: THE LABEL-BIAS FIX - AND A RETRACTION === Root cause confirmed: under bmPoint a SHORT radial correction resolves the virtual bullet BEFORE the base arrival tick, so the label was dropped (labelMisses). Fix: defer the label in a pending queue and flush it once the arrival tick is recorded; labels still come from the BASE arrival tick. labelMisses 4,281,695 -> 0 training samples 1,071,824 -> 5,345,847 (x5) radial head acc 48.8% -> 57.0% (shuffled control 20.0%) bmPoint hit rate 9.4/5.8% -> 9.1/5.7% (unchanged, within noise) So the fix IMPROVES LEARNING but NOT the metric. **RETRACTION OF THE PREVIOUS JOB'S CLAIM.** It reported the radial head's 48.8% against a 36.7% majority baseline and concluded "conditional learning, not a constant bias". With the bias removed, the correctly-measured majority baseline is **58.2%** - so the head at 57.0% is AT/BELOW majority. The earlier apparent conditional learning was PARTLY AN ARTEFACT OF THE BIASED SAMPLE. The bmPoint metric win is real (TMRadial > Linear early 16/2 p=0.0013, overall 18/0 p<0.0001; > shuffled 18/0 p<0.0001) but it comes from a NET-POSITIVE AVERAGE RADIAL SHIFT, not from beating a majority classifier. Recorded plainly rather than left standing. Guards: test_tm_pattern_registration 20 (new), test_tm_pattern_rack_live 4 (new), test_gun_harness 39, test_vbullet_metric 11, test_power_selection 3 (the SIGSEGV is gone - the knn_gun rewrite is now committed), test_adaptive_radar 41, test_tfil_ring_weights 24, test_power_policy 26, test_ram_decision 28, test_rack_membership 38, test_selector_tiebreak 19, test_tm_pattern_learning 3, acceptance_offline_vs_online 12/12. ModularBot compiles (release). Note: `common_libs/tests/range_guns.nim` still builds 14 offline drivers (the offline sweep constructs TmPatternGun directly and acceptance only inspects ids 0..13), so nothing breaks - but a future job wanting it in the offline rack must add a 15th driver and mirror the live admission gating. gun_stats.jsonl now emits 15 rows; downstream tooling should ignore id 14.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
## MEASUREMENT ONLY — per-tick cost of the rack-registered TM pattern gun
|
||||
## (id 14, radial mode) in isolation, following the `runPerGun` pattern of
|
||||
## `measure_parallel_vbullet_cost.nim`.
|
||||
##
|
||||
## Drives ONE gun through a real DrussGT fixture with a one-gun VirtualTracker,
|
||||
## timing `predict` (4 power bins/tick) and `onResult` (one call per resolved
|
||||
## virtual bullet) separately, then reports ms/tick against the ~13.16 ms live
|
||||
## budget (76 ticks/s). Tsetlin is measured the same way as the reference the
|
||||
## brief quotes (~5.3 ms/tick).
|
||||
##
|
||||
## Usage:
|
||||
## nim c -r -d:release common_libs/tests/measure_tm_pattern_cost.nim
|
||||
|
||||
import std/[os, strformat, tables, math, times, monotimes, random]
|
||||
import gun_harness/offline_range
|
||||
import gun_harness/gun_interface
|
||||
import gun_harness/virtual_bullets as vb
|
||||
import guns/tm_pattern
|
||||
import guns/tsetlin
|
||||
|
||||
const
|
||||
RepoRoot = currentSourcePath().parentDir.parentDir.parentDir
|
||||
FixturePath = RepoRoot / "tools" / "fixtures" / "tr_drussgt_vs_crazy.jsonl"
|
||||
|
||||
type
|
||||
CostResult = object
|
||||
predictNs: int64
|
||||
onResultNs: int64
|
||||
ticks: int
|
||||
predictCalls: int
|
||||
onCalls: int
|
||||
|
||||
proc measureGun(fx: Fixture, predictCb: proc(state: WorldState, bulletSpeed: float): GunPrediction {.closure.},
|
||||
resultCb: proc(e: FeedbackEvent) {.closure.},
|
||||
metric: BulletMetric, warmup, measure: int): CostResult =
|
||||
var tracker = vb.initTracker(1, metric)
|
||||
let acc = new(CostResult)
|
||||
let ticks = fx.states.len
|
||||
for si in 0..<ticks:
|
||||
let state = fx.states[si]
|
||||
var preds: array[len(vb.PowerBins), GunPrediction]
|
||||
let t0 = getMonoTime()
|
||||
for b in 0..<len(vb.PowerBins):
|
||||
preds[b] = predictCb(state, bulletSpeed(vb.PowerBins[b]))
|
||||
let t1 = getMonoTime()
|
||||
tracker.spawnBullets(0, preds, state, fx.enemyId)
|
||||
|
||||
var enemyPositions: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
||||
enemyPositions[fx.enemyId] = (x: state.enemyX, y: state.enemyY,
|
||||
lastSeenTick: state.tick, alive: true)
|
||||
tracker.tickBullets(state, enemyPositions,
|
||||
proc(gunId: GunId, binIdx: int, e: FeedbackEvent) =
|
||||
let s0 = getMonoTime()
|
||||
resultCb(e)
|
||||
let s1 = getMonoTime()
|
||||
if si >= warmup and si < warmup + measure:
|
||||
acc.onResultNs += (s1 - s0).inNanoseconds
|
||||
inc acc.onCalls)
|
||||
if si >= warmup and si < warmup + measure:
|
||||
inc acc.ticks
|
||||
acc.predictNs += (t1 - t0).inNanoseconds
|
||||
inc acc.predictCalls, len(vb.PowerBins)
|
||||
result = acc[]
|
||||
|
||||
proc makeTmRadial(fx: Fixture, metric: BulletMetric, warmup, measure: int): CostResult =
|
||||
let g = new(TmPatternGun)
|
||||
g[] = initTmRadialGun()
|
||||
randomize(1)
|
||||
result = measureGun(fx,
|
||||
proc(state: WorldState, bulletSpeed: float): GunPrediction = g[].predict(state, bulletSpeed),
|
||||
proc(e: FeedbackEvent) = g[].onResult(e),
|
||||
metric, warmup, measure)
|
||||
echo fmt" TMPattern(radial): obs={g[].totalObs} labelMiss={g[].labelMisses} " &
|
||||
fmt"traceMiss={g[].traceMisses} radAcc={g[].radCorrect}/{g[].radTotal}"
|
||||
var lab = ""
|
||||
var maj = 0
|
||||
var tot = 0
|
||||
for c in 0..<TM_CLASSES:
|
||||
lab.add $g[].radLabelHist[c] & " "
|
||||
maj = max(maj, g[].radLabelHist[c])
|
||||
tot += g[].radLabelHist[c]
|
||||
echo fmt" radial label hist [{lab}] majority={maj.float / max(1, tot).float * 100:.1f}% pendingDropped={g[].pendingDropped}"
|
||||
|
||||
proc makeTsetlin(fx: Fixture, metric: BulletMetric, warmup, measure: int): CostResult =
|
||||
let g = new(TsetlinGun)
|
||||
g[] = initTsetlinGun()
|
||||
randomize(1)
|
||||
result = measureGun(fx,
|
||||
proc(state: WorldState, bulletSpeed: float): GunPrediction = g[].predict(state, bulletSpeed),
|
||||
proc(e: FeedbackEvent) = g[].onResult(e),
|
||||
metric, warmup, measure)
|
||||
|
||||
proc main() =
|
||||
if not fileExists(FixturePath):
|
||||
echo "fixture missing: ", FixturePath
|
||||
quit(1)
|
||||
let fx = loadFixture(FixturePath)
|
||||
echo fmt"fixture: {fx.meta.adversary} ticks={fx.states.len} metric=point"
|
||||
|
||||
const Warmup = 150
|
||||
const Measure = 800
|
||||
const BudgetMs = 1000.0 / 76.0 # ~76 ticks/s measured bridge throughput
|
||||
echo fmt"warmup={Warmup} measured={Measure} ticks; budget={BudgetMs:.2f} ms/tick"
|
||||
echo ""
|
||||
|
||||
let tm = makeTmRadial(fx, bmPoint, Warmup, Measure)
|
||||
let ts = makeTsetlin(fx, bmPoint, Warmup, Measure)
|
||||
|
||||
proc report(name: string, r: CostResult) =
|
||||
let pMs = r.predictNs.float / 1.0e6 / max(1, r.ticks).float
|
||||
let oMs = r.onResultNs.float / 1.0e6 / max(1, r.ticks).float
|
||||
let tot = pMs + oMs
|
||||
echo fmt"{name:<18} predict {pMs:6.2f} ms/tick onResult {oMs:6.2f} ms/tick " &
|
||||
fmt"total {tot:6.2f} ms/tick ({tot / BudgetMs * 100:5.0f}% budget) " &
|
||||
fmt"onCalls/tick={r.onCalls.float / max(1, r.ticks).float:.1f}"
|
||||
|
||||
echo "=================== PER-TICK COST (single gun, bmPoint) ==================="
|
||||
report("TMPattern(radial)", tm)
|
||||
report("Tsetlin (reference)", ts)
|
||||
|
||||
# ── pooled radial-head learning stats over the real fixture set ─────────────
|
||||
const RealFixtures = ["drussgt_vs_crazy", "drussgt_vs_spinbot",
|
||||
"drussgt_vs_drussgt", "tr_drussgt_vs_crazy", "tr_drussgt_vs_spinbot",
|
||||
"tr_drussgt_vs_modularbot"]
|
||||
echo ""
|
||||
echo "=================== POOLED RADIAL HEAD (real fixtures, bmPoint) ==================="
|
||||
var realHist: array[TM_CLASSES, int]
|
||||
var realChosen: array[TM_CLASSES, int]
|
||||
var realCorrect, realTotal = 0
|
||||
var shufHist: array[TM_CLASSES, int]
|
||||
var shufChosen: array[TM_CLASSES, int]
|
||||
var shufCorrect, shufTotal = 0
|
||||
for name in RealFixtures:
|
||||
let p = RepoRoot / "tools" / "fixtures" / (name & ".jsonl")
|
||||
if not fileExists(p): continue
|
||||
let f = loadFixture(p)
|
||||
block:
|
||||
let g = new(TmPatternGun)
|
||||
g[] = initTmRadialGun()
|
||||
randomize(1)
|
||||
discard measureGun(f,
|
||||
proc(state: WorldState, bulletSpeed: float): GunPrediction = g[].predict(state, bulletSpeed),
|
||||
proc(e: FeedbackEvent) = g[].onResult(e), bmPoint, 0, 0)
|
||||
realCorrect += g[].radCorrect
|
||||
realTotal += g[].radTotal
|
||||
for c in 0..<TM_CLASSES: realHist[c] += g[].radLabelHist[c]
|
||||
for c in 0..<TM_CLASSES: realChosen[c] += g[].radChosenHist[c]
|
||||
block:
|
||||
let g = new(TmPatternGun)
|
||||
g[] = initTmRadialGun()
|
||||
g[].shuffleLabels = true
|
||||
randomize(1)
|
||||
discard measureGun(f,
|
||||
proc(state: WorldState, bulletSpeed: float): GunPrediction = g[].predict(state, bulletSpeed),
|
||||
proc(e: FeedbackEvent) = g[].onResult(e), bmPoint, 0, 0)
|
||||
shufCorrect += g[].radCorrect
|
||||
shufTotal += g[].radTotal
|
||||
for c in 0..<TM_CLASSES: shufHist[c] += g[].radLabelHist[c]
|
||||
for c in 0..<TM_CLASSES: shufChosen[c] += g[].radChosenHist[c]
|
||||
var realMaj, shufMaj, realN, shufN = 0
|
||||
var rs, ss, rc, sc = ""
|
||||
for c in 0..<TM_CLASSES:
|
||||
realMaj = max(realMaj, realHist[c]); realN += realHist[c]
|
||||
shufMaj = max(shufMaj, shufHist[c]); shufN += shufHist[c]
|
||||
rs.add $realHist[c] & " "
|
||||
ss.add $shufHist[c] & " "
|
||||
rc.add $realChosen[c] & " "
|
||||
sc.add $shufChosen[c] & " "
|
||||
echo fmt" real: radAcc={realCorrect}/{realTotal} = {realCorrect.float/max(1,realTotal).float*100:.1f}% " &
|
||||
fmt"majority={realMaj.float/max(1,realN).float*100:.1f}% labels=[{rs}] chosen=[{rc}]"
|
||||
echo fmt" shuffled: radAcc={shufCorrect}/{shufTotal} = {shufCorrect.float/max(1,shufTotal).float*100:.1f}% " &
|
||||
fmt"majority={shufMaj.float/max(1,shufN).float*100:.1f}% labels=[{ss}] chosen=[{sc}]"
|
||||
|
||||
when isMainModule:
|
||||
main()
|
||||
Reference in New Issue
Block a user