TMComposites gate: per-gun confidence faithful for 3 guns; no pair composes
Adds a per-sample intrinsic-confidence field (GunPrediction.confidence, threaded through FeedbackEvent/VirtualBullet, populated by Pattern, DecayGF, KNN, GuessFactor, Tsetlin, TMHorizon) and an offline recorder + analyzer that reproduce the paper's Figure 2 per gun and its Eq-8 composite. Measured on 3 held-out tr-bridge DrussGT battles (33k ticks, ~133k samples/gun): - FAITHFUL: DecayGF (rho +0.133), KNN (+0.090), Pattern (+0.064, weak). - GuessFactor is ANTI-faithful (rho -0.067); Tsetlin c_max is useless (0.001). - No pair of guns specialises complementarily: the same gun dominates both high-confidence slices in every pair. - Eq-8 alpha-normalised confidence-weighted composite: 18.41% vs Pattern 20.45% (McNemar p=3.1e-126). Faithful-only variant 18.68%, still loses. Shuffle control passes weakly (composite > shuffle, p=4e-14) so ~0.7pp of competence is real but ~2pp short. Offline veto: design is dead. See docs/tmcomposites_gate.md.
This commit is contained in:
@@ -113,6 +113,7 @@ proc predict*(g: var DecayGFGun, state: WorldState, bulletSpeed: float): GunPred
|
||||
GunPrediction(
|
||||
x: clamp(px, BotRadius, state.arenaWidth - BotRadius),
|
||||
y: clamp(py, BotRadius, state.arenaHeight - BotRadius),
|
||||
confidence: g.bins[peak], # class-sum max over GF bins (see guess_factor.nim)
|
||||
)
|
||||
|
||||
proc onResult*(g: var DecayGFGun, e: FeedbackEvent) =
|
||||
|
||||
@@ -141,6 +141,9 @@ proc predict*(g: var GFGun, state: WorldState, bulletSpeed: float): GunPredictio
|
||||
GunPrediction(
|
||||
x: clamp(px, BotRadius, state.arenaWidth - BotRadius),
|
||||
y: clamp(py, BotRadius, state.arenaHeight - BotRadius),
|
||||
# TMComposites Eq 4 analogue: the GF histogram is the class distribution over
|
||||
# GF bins, so the class-sum max c_max is the peak bin's accumulated weight.
|
||||
confidence: g.bins[peak],
|
||||
)
|
||||
|
||||
proc onResult*(g: var GFGun, e: FeedbackEvent) =
|
||||
|
||||
@@ -299,6 +299,10 @@ proc predict*(g: var KNNGun, state: WorldState, bulletSpd: float): GunPrediction
|
||||
GunPrediction(
|
||||
x: clamp(px, BotRadius, state.arenaWidth - BotRadius),
|
||||
y: clamp(py, BotRadius, state.arenaHeight - BotRadius),
|
||||
# TMComposites Eq 4 analogue: the KNN Gaussian density over GF candidates is
|
||||
# the class distribution; bestScore is the class-sum max. Cold-start returns
|
||||
# (no data / < 5 neighbours) leave the default 0.0 = no vote.
|
||||
confidence: bestScore,
|
||||
)
|
||||
|
||||
proc onResult*(g: var KNNGun, e: FeedbackEvent) =
|
||||
|
||||
@@ -50,6 +50,10 @@ type
|
||||
cacheValid: bool
|
||||
cacheTick: int
|
||||
bestMatch: int ## -1 = no usable match (linear fallback)
|
||||
lastMatchScore*: float ## best pattern-match cost (lower = better);
|
||||
## set by findBestMatch, exposed as the gun's
|
||||
## intrinsic per-sample confidence
|
||||
## (TMComposites gate, docs/tmcomposites_gate.md)
|
||||
playStart: int
|
||||
playAvail: int
|
||||
pathX: array[HistorySize + 1, float]
|
||||
@@ -87,9 +91,11 @@ proc linearPredict(state: WorldState, bulletSpeed: float): (float, float) =
|
||||
|
||||
# --- pattern search + play-forward ---
|
||||
|
||||
proc findBestMatch(g: PatternMatcherGun): int =
|
||||
proc findBestMatch(g: var PatternMatcherGun): int =
|
||||
## Speed-independent history search. Returns the start index of the best
|
||||
## matching pattern, or -1 when there is not enough history.
|
||||
## matching pattern, or -1 when there is not enough history. Stores the best
|
||||
## match cost in `g.lastMatchScore` for the confidence readout.
|
||||
g.lastMatchScore = Inf
|
||||
if g.count < PatternLen * 2:
|
||||
return -1
|
||||
|
||||
@@ -111,6 +117,7 @@ proc findBestMatch(g: PatternMatcherGun): int =
|
||||
if score < bestScore:
|
||||
bestScore = score
|
||||
bestMatch = i
|
||||
g.lastMatchScore = bestScore
|
||||
bestMatch
|
||||
|
||||
proc buildPath(g: var PatternMatcherGun, state: WorldState, bestMatch: int) =
|
||||
@@ -222,7 +229,10 @@ proc predict*(g: var PatternMatcherGun, state: WorldState,
|
||||
return g.applyRadial(state, px, py)
|
||||
|
||||
let (px, py) = g.projectFromPath(state, bulletSpeed)
|
||||
g.applyRadial(state, px, py)
|
||||
result = g.applyRadial(state, px, py)
|
||||
# Match quality as a confidence: a perfect historical match (cost 0) gives 1.0,
|
||||
# a worse match decays toward 0. Deterministic and per-sample.
|
||||
result.confidence = 1.0 / (1.0 + max(0.0, g.lastMatchScore))
|
||||
|
||||
proc onResult*(g: var PatternMatcherGun, e: FeedbackEvent) =
|
||||
discard # pattern matcher learns from movement observation, not feedback
|
||||
|
||||
@@ -1091,13 +1091,16 @@ proc predict*(g: var TmHorizonGun, state: WorldState,
|
||||
let sgn = if side == 1: 1.0 else: -1.0
|
||||
shift = sgn * g.shiftDeg * magScale
|
||||
|
||||
let pred =
|
||||
if shift == 0.0: base
|
||||
else: tmhApplyShift(state.selfX, state.selfY, base.x, base.y, shift)
|
||||
# TMComposites Eq 4 analogue: the side-machine's class-sum margin (normalised
|
||||
# by the clause half-count) is the per-sample confidence in WHICH WAY to shift.
|
||||
var predOut = base
|
||||
if shift != 0.0:
|
||||
predOut = tmhApplyShift(state.selfX, state.selfY, base.x, base.y, shift)
|
||||
if warm: predOut.confidence = ev.sideConf
|
||||
|
||||
let aimDeg = radToDeg(arctan2(pred.y - state.selfY, pred.x - state.selfX))
|
||||
let aimDeg = radToDeg(arctan2(predOut.y - state.selfY, predOut.x - state.selfX))
|
||||
g.tmhLog(state, bulletSpeed, h, side, mag, ev.sideConf, shift, aimDeg)
|
||||
pred
|
||||
predOut
|
||||
|
||||
proc onResult*(g: var TmHorizonGun, e: FeedbackEvent) =
|
||||
## The label comes from our own observation ring, not from virtual-bullet
|
||||
|
||||
@@ -413,7 +413,14 @@ proc predict*(g: var TsetlinGun, state: WorldState, bulletSpeed: float): GunPred
|
||||
alive: true,
|
||||
)
|
||||
|
||||
GunPrediction(x: predX, y: predY)
|
||||
GunPrediction(
|
||||
x: predX,
|
||||
y: predY,
|
||||
# TMComposites Eq 4: the two output teams' clamped clause sums are (vx, vy);
|
||||
# their magnitude is how hard the machine is voting to move the correction.
|
||||
# Warm-up fallback (window not full) leaves the default 0.0 = no vote.
|
||||
confidence: hypot(vx, vy),
|
||||
)
|
||||
|
||||
proc onResult*(g: var TsetlinGun, e: FeedbackEvent) =
|
||||
inc g.shotCount
|
||||
|
||||
Reference in New Issue
Block a user