Answering the human's 'what is holding you up': two agents spent three exchanges on ALPHA while the answer sat in a POSITION series neither of us compared to anything. The arithmetic that should have been step one: the Decoder's quoted sweep centre -1.690 read as NDC is -441.6 px, and the declared position at leaf t=0 is -439.5 px. A 2 px agreement on a 2160 px travel -- so the game's sweep begins travelling at leaf t~0, the same as the port. That CONTRADICTS my own earlier framing on this page, which blamed ~135 units on the leaf clock. If the game's leaf clock also starts at 0 then F6 is a visibility question, not a clock question. Flagged rather than rewritten: it rests on two numbers relayed in a message, which is what should be read from the repo instead. The tool: solves x(frame) ~= declared(t0 + rate*frame) for both parameters and reports the RESIDUAL, which says whether the model was right at all. Selftest runs both directions -- recovers a known clock to 0.09 px and rejects a wrong-shape series at 81.9 px against a 20 px bar. In check-all.
148 lines
6.2 KiB
Python
Executable File
148 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Align a MEASURED trajectory against a DECLARED one, and solve for the clock.
|
|
|
|
tools/port/fit-trajectory SCREEN.json ELEMENT measured.tsv
|
|
tools/port/fit-trajectory --selftest
|
|
|
|
🔴 WHY THIS EXISTS. F6 asked "when does the title sweep start?" Both agents spent
|
|
three exchanges on ALPHA -- a bound, a refutation, a downgrade -- and the answer
|
|
was sitting in a POSITION series nobody compared to anything.
|
|
|
|
Alpha is the weakest observable we have: 8 bits, quantised, and the argument that
|
|
collapsed did so on a 14-sample tail out of 1754, where a vertex-grouping slip
|
|
looks exactly like a signal. The sweep's POSITION travels 2 160 px, monotonically,
|
|
and is immune to every one of those failure modes.
|
|
|
|
📌 The generalisation, which is the reusable part: **when something moves, its
|
|
POSITION carries the clock and its ALPHA carries almost nothing.** A trajectory
|
|
fit yields the clock ORIGIN and the RATE together, and its residual says whether
|
|
the model was right at all -- which a value-at-an-instant never does. This is
|
|
`TEMPORAL-VERIFICATION.md`'s "align by content" made into arithmetic.
|
|
|
|
WHAT IT DOES NOT DO. It fits t = t0 + rate * frame, i.e. a constant rate. If the
|
|
guest's clock stalls or the capture drops frames unevenly, the residual rises and
|
|
the tool says so rather than absorbing it -- that is the point of reporting RMS
|
|
rather than just the parameters.
|
|
"""
|
|
import json
|
|
import sys
|
|
|
|
|
|
def declared_track(path, element_id):
|
|
"""[(t, x)] for an element, preferring its leaf -- the leaf is what moves."""
|
|
screen = json.load(open(path))
|
|
|
|
def find(elements):
|
|
for el in elements:
|
|
if el.get("id") == element_id:
|
|
return el
|
|
for sub in ("leaf", "focus"):
|
|
inner = el.get(sub, {}).get("elements", [])
|
|
for fe in inner:
|
|
if fe.get("id") == element_id:
|
|
return fe
|
|
return None
|
|
|
|
el = find(screen["elements"])
|
|
if el is None:
|
|
raise SystemExit("no element %r in %s" % (element_id, path))
|
|
track = [(float(k["t"]), float(k["pos"][0]))
|
|
for k in el.get("keyframes", []) if k.get("pos")]
|
|
if len(track) < 2:
|
|
raise SystemExit("%s declares no positional keyframes" % element_id)
|
|
return sorted(track)
|
|
|
|
|
|
def at(track, t):
|
|
if t <= track[0][0]:
|
|
return track[0][1]
|
|
if t >= track[-1][0]:
|
|
return track[-1][1]
|
|
for (t0, x0), (t1, x1) in zip(track, track[1:]):
|
|
if t0 <= t <= t1:
|
|
return x0 + (x1 - x0) * (t - t0) / (t1 - t0)
|
|
return track[-1][1]
|
|
|
|
|
|
def fit(track, series):
|
|
"""Solve x_measured(frame) ~= declared(t0 + rate*frame). Coarse then refine.
|
|
|
|
A grid rather than a gradient because the declared track is piecewise linear
|
|
and its corners make the residual non-smooth -- a gradient walks into one and
|
|
reports a corner as an optimum.
|
|
"""
|
|
span = track[-1][0] - track[0][0]
|
|
frames = [f for f, _ in series]
|
|
width = max(frames) - min(frames) or 1.0
|
|
best = None
|
|
lo_r, hi_r, lo_t, hi_t = 1e-4, 20.0 * span / width, -span, span
|
|
for _ in range(4):
|
|
for i in range(60):
|
|
rate = lo_r + (hi_r - lo_r) * i / 59.0
|
|
for j in range(60):
|
|
t0 = lo_t + (hi_t - lo_t) * j / 59.0
|
|
err = sum((x - at(track, t0 + rate * f)) ** 2 for f, x in series)
|
|
if best is None or err < best[0]:
|
|
best = (err, t0, rate)
|
|
_, t0, rate = best
|
|
dr, dt = (hi_r - lo_r) / 20.0, (hi_t - lo_t) / 20.0
|
|
lo_r, hi_r, lo_t, hi_t = rate - dr, rate + dr, t0 - dt, t0 + dt
|
|
err, t0, rate = best
|
|
return t0, rate, (err / len(series)) ** 0.5
|
|
|
|
|
|
def main():
|
|
if "--selftest" in sys.argv:
|
|
# 🔴 A FIT THAT CANNOT FAIL IS A CURVE-FITTER, NOT A MEASUREMENT.
|
|
# Two directions: a series SYNTHESISED from the track with a known clock
|
|
# must be recovered, and a series that is not this element's motion at
|
|
# all must produce a large residual rather than a confident wrong clock.
|
|
track = [(0.0, -639.0), (150.0, -39.0), (540.0, 1521.0)]
|
|
true_t0, true_rate = 37.0, 0.31
|
|
good = [(f, at(track, true_t0 + true_rate * f)) for f in range(0, 900, 7)]
|
|
t0, rate, rms = fit(track, good)
|
|
ok_recover = abs(t0 - true_t0) < 1.0 and abs(rate - true_rate) < 0.01 and rms < 1.0
|
|
print("selftest recover : t0=%.2f (true %.2f) rate=%.4f (true %.4f) rms=%.3f px -> %s"
|
|
% (t0, true_t0, rate, true_rate, rms, "ok" if ok_recover else "🔴 BROKEN"))
|
|
# The negative: a quadratic sweep is NOT this piecewise-linear travel.
|
|
bad = [(f, -639.0 + 0.0027 * f * f) for f in range(0, 900, 7)]
|
|
_, _, rms_bad = fit(track, bad)
|
|
ok_reject = rms_bad > 20.0
|
|
print("selftest reject : wrong-shape series rms=%.1f px (needs >20) -> %s"
|
|
% (rms_bad, "ok" if ok_reject else "🔴 BROKEN -- it fits anything"))
|
|
return 0 if (ok_recover and ok_reject) else 2
|
|
|
|
if len(sys.argv) < 4:
|
|
raise SystemExit(__doc__)
|
|
track = declared_track(sys.argv[1], sys.argv[2])
|
|
series = []
|
|
for line in open(sys.argv[3]):
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
parts = line.replace(",", " ").split()
|
|
series.append((float(parts[0]), float(parts[1])))
|
|
if len(series) < 3:
|
|
raise SystemExit("need at least 3 measured samples")
|
|
t0, rate, rms = fit(track, series)
|
|
print("declared track : %s t=%.0f..%.0f x=%.0f..%.0f"
|
|
% (sys.argv[2], track[0][0], track[-1][0], track[0][1], track[-1][1]))
|
|
print("measured : %d samples, frames %.0f..%.0f"
|
|
% (len(series), series[0][0], series[-1][0]))
|
|
print()
|
|
print(" clock origin t0 = %+.2f units at frame 0" % t0)
|
|
print(" clock rate = %.4f units per frame" % rate)
|
|
print(" residual (RMS) = %.2f px over a %.0f px travel"
|
|
% (rms, track[-1][1] - track[0][1]))
|
|
print()
|
|
if rms > 20.0:
|
|
print("🔴 residual is large -- a constant-rate model does not describe this")
|
|
print(" series, so t0 and rate above are a best fit to the wrong shape.")
|
|
return 1
|
|
print("the measured motion IS this declared track, on that clock")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|