Files
Sylpheed/tools/re-capture/check_labels.py
sylph-decoder bbe339d856 re: pre-register F6's numbers for an out-of-sample boot
Every F5/F6 figure comes from captures taken for other purposes and
re-analysed after the reader and three labels were corrected. None has been
tested out of sample. Six predictions registered before the capture, five of
them checkable by check_labels.py, which now takes --cap NAME=path.

Prediction 6 is the one already got wrong once (0.057 against ptcopyright
instead of 0.14 against the real plate), and is stated so a landing near
0.057 would show the correction itself was wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-04 13:17:26 +00:00

165 lines
7.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Re-derive every element LABEL in the F5/F6 findings from captures + disc.
⚠️ WHY THIS EXISTS. Three of my errors were the label, not the measurement:
`0x3003` read as a different role from `0x3002`; `ptcopyright` called "the plate"
in the clock-conflict page; and `ptcopyright` called "the plate" AGAIN in the F6
page, written after that correction. Every number was right. What it was pointed
at was not, and a correction in one document did not reach the next.
sylpheed-port has `check-authored-vs-declared` for values with a declared
counterpart. It cannot cover a value that exists only in a capture and names an
element -- that one rests entirely on my label. This is that case.
Each identification below was originally MADE by matching a declared quantity, so
each is re-derivable. If a label drifts, the agreement it was built on breaks.
python3 check_labels.py # check
python3 check_labels.py --selftest # prove it can fail
"""
import sys, collections
sys.path.insert(0, __file__.rsplit('/', 1)[0])
from read_draws import read
# ptbtn00f.rat, GP_TITLE entries 2/3 -- read off the disc with
# `cargo run -p sylpheed-formats --example leaf_keyframes -- GP_TITLE ptbtn00f.rat 2`
# Declared loop 120 units. This is the FOCUS record; ptbtn00.rat is the leaf and
# is flat 255. Looking only at the leaf is how I wrongly called alpha 80
# undeclared -- an absence claim from a search that did not cover the space.
PTBTN00F = [(0,0),(6,6),(29,74),(35,80),(50,80),(58,74),(97,6),(105,0),(120,0)]
def declared_alpha(t):
t = t % 120
for i in range(len(PTBTN00F)-1):
(t0,a0),(t1,a1) = PTBTN00F[i], PTBTN00F[i+1]
if t0 <= t <= t1:
return a0 if t1==t0 else a0 + (a1-a0)*(t-t0)/(t1-t0)
return 0
CAPS = {'f6': '/sylph-home/re/f6/xenia_re_ui_draws_01.log',
'f6b': '/sylph-home/re/f6b/xenia_re_ui_draws_01.log'}
# `--cap NAME=/path/to/log` replaces the set, for testing a fresh capture
# out-of-sample against predictions registered before it was taken.
_ov = [a for a in sys.argv if a.startswith('--cap')]
if _ov:
i = sys.argv.index(_ov[0])
spec = sys.argv[i+1] if _ov[0] == '--cap' else _ov[0].split('=',1)[1]
k, v = spec.split('=', 1)
CAPS = {k: v}
SWEEP_PAGE = '8154'
PTCOPY_X = (-0.54, 0.54, 0.54, -0.54)
def features(path, swap_plate=False):
fr = read(path); fs = sorted(fr)
f = {}
pairs = []
for fm in fs:
adds = [q for q in fr[fm] if q.blend == '0x01010101' and q.page.startswith(SWEEP_PAGE)]
if len(adds) >= 2: pairs.append((fm, adds[0], adds[1]))
def cycles(idx, sign):
out = []
for i in range(1, len(pairs)):
d = pairs[i][idx].cx - pairs[i-1][idx].cx
if (sign > 0 and d < -1.0) or (sign < 0 and d > 1.0): out.append(pairs[i][0])
return out
ca, cb = cycles(1, +1), cycles(2, -1)
f['pteff03_period'] = ca[1] - ca[0]
f['pteff03a_period'] = cb[1] - cb[0]
import math
def length(q):
pts = [(x*640, y*360) for x, y in q.verts]
return max(math.dist(pts[i], pts[j]) for i in range(4) for j in range(i+1, 4))
f['pteff03_len'] = sum(length(p[1]) for p in pairs) / len(pairs)
f['pteff03a_len'] = sum(length(p[2]) for p in pairs) / len(pairs)
# parent ramp: sweep quad A's alpha climbing to full from its cycle start
start = ca[0]
seq = [(fm, a.alpha) for fm, a, _ in pairs if fm >= start]
f['parent_ramp'] = next(fm for fm, al in seq if al >= 239) - start
# ptcopyright: fade-in length of the -0.54..0.54 quad
cop = [(fm, q.alpha) for fm in fs for q in fr[fm]
if tuple(round(v[0], 2) for v in q.verts) == PTCOPY_X]
f['ptcopyright_ramp'] = next(fm for fm, a in cop if a >= 255) - cop[0][0]
# ptbtn00f: the slot that comes and goes
slots = collections.defaultdict(list)
for fm in fs:
for q in fr[fm]: slots[(q.page[:4], round(q.cx, 2))].append(fm)
cands = []
for k, v in slots.items():
if len(v) < 200: continue
gaps = sum(1 for i in range(1, len(v)) if v[i]-v[i-1] > 1)
if gaps >= 8: cands.append((gaps, k, v))
cands.sort(reverse=True)
if swap_plate and len(cands) > 0: # selftest: point the label at ptcopyright instead
f['pulse_period'] = f['ptcopyright_ramp']
else:
v = cands[0][2]
ons = [v[0]] + [v[i] for i in range(1, len(v)) if v[i]-v[i-1] > 1]
per = sorted(ons[i]-ons[i-1] for i in range(1, len(ons)))
f['pulse_period'] = per[len(per)//2]
# AMPLITUDE: predict the drawn alpha from ptbtn00f.rat's declared curve
key = cands[0][1]
upf = 120.0 / f['pulse_period'] # title units per frame, from the period
start = ons[1] if len(ons) > 1 else ons[0]
obs = []
for fm in range(start, start + f['pulse_period']):
got = [q.alpha for q in fr.get(fm, []) if (q.page[:4], round(q.cx,2)) == key]
if got: obs.append((fm-start, got[0]))
# ⚠️ ALIGN BY CONTENT, not by assuming the onset frame is t=0. The 6->74
# segment climbs ~6 alpha levels per FRAME, so half a frame of phase error
# alone produces ~3 levels of mean error. Search the lag; the lag is a
# measurement, not an error (TEMPORAL-VERIFICATION.md).
best = (99.0, None)
lag = 0.0
while lag < 4.0:
e = sum(abs(a - declared_alpha((k+lag)*upf)) for k, a in obs)/len(obs) if obs else 99.0
if e < best[0]: best = (e, lag)
lag += 0.05
f['pulse_amp_err'], f['pulse_lag'] = best
if swap_plate:
f['pulse_amp_err'] = 99.0
return f
# label -> (derived ratio, declared value, tolerance, what the label asserts)
def checks(f):
return [
("pteff03a is the 720-unit leaf (not a second copy of the 600)",
f['pteff03a_period']/f['pteff03_period'], 720/600, 0.05),
("pteff03a is the sy=800 strip, pteff03 the sy=600",
f['pteff03a_len']/f['pteff03_len'], 800/600, 0.06),
("the pulsing slot is ptbtn00f (120-unit loop vs the sweep's 600 leaf units)",
f['pulse_period']/f['pteff03_period'], 0.1, 0.05),
("the -0.54 quad is ptcopyright (22-unit ramp vs the parent's 30)",
f['ptcopyright_ramp']/f['parent_ramp'], 22/30, 0.08),
("the pulse AMPLITUDE matches ptbtn00f.rat's declared 8-key curve (peak 80)",
f['pulse_amp_err'], 0.0, None),
]
def run(swap=False):
bad = 0
for name, path in CAPS.items():
f = features(path, swap_plate=swap)
print(f" {name}:")
for label, got, want, tol in checks(f):
if tol is None: # absolute: mean |alpha| error, <=3 levels
err, ok = got, got <= 3.0
print(f" [{'PASS' if ok else 'FAIL'}] mean |alpha| error {got:5.2f} levels (tol 3.00, best lag {f.get('pulse_lag',0):.2f} fr) {label}")
bad += not ok
continue
err = abs(got-want)/want
ok = err <= tol
bad += not ok
print(f" [{'PASS' if ok else 'FAIL'}] {got:.4f} vs {want:.4f} ({err*100:4.1f}%, tol {tol*100:.0f}%) {label}")
return bad
if __name__ == '__main__':
if '--selftest' in sys.argv:
print("SELFTEST — the plate label deliberately pointed at ptcopyright.")
print("A check that cannot fail here would not have caught the real error.\n")
bad = run(swap=True)
print(f"\n{'OK: mislabel detected' if bad else 'BROKEN: mislabel NOT detected'} ({bad} failures)")
sys.exit(0 if bad else 1)
print("Element labels in the F5/F6 findings, re-derived from captures + disc:\n")
bad = run()
print(f"\n{'all labels agree' if not bad else str(bad)+' LABEL(S) DRIFTED'}")
sys.exit(1 if bad else 0)