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:
2026-09-25 22:02:28 +02:00
parent d0750ab020
commit f58d65d2e8
12 changed files with 2666 additions and 11 deletions
+8 -5
View File
@@ -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