port: fit-trajectory -- position carries the clock, alpha carries almost nothing
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.
This commit is contained in:
@@ -193,3 +193,60 @@ been burned before — and the port would be changing a renderer rule on it.
|
||||
`screen_view.gd` keeps its current behaviour until the row is identified by
|
||||
travel direction. **This is not scepticism about the conclusion**, which the
|
||||
port's own flagged limit predicted would fall this way; it is about which row.
|
||||
|
||||
---
|
||||
|
||||
# 📌 What was actually holding this up: we both picked the wrong observable
|
||||
|
||||
The human asked what was blocking us and whether the approach needed revising.
|
||||
**It did, and the diagnosis is short: 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 capture reports the sweep's x-centre running `−1.690 → +0.500`.
|
||||
Read as NDC (`x_ndc = 2·x_px/W − 1`, **an assumption, stated as one**):
|
||||
|
||||
| | ndc | centre px | implied leaf t |
|
||||
|---|---|---|---|
|
||||
| first sample | −1.690 | −441.6 | **−0.5** |
|
||||
| last sample | +0.500 | +960.0 | 349.9 |
|
||||
| **declared at leaf t=0** | **−1.687** | **−439.5** | — |
|
||||
|
||||
**A 2 px agreement on a 2 160 px travel.** So the game's sweep begins travelling
|
||||
at leaf t≈0, from off-screen left — **the same as the port.**
|
||||
|
||||
🔴 **That contradicts my own earlier framing on this page**, which attributed
|
||||
~135 units of the earliness to "the leaf clock starting at title t=0 with no
|
||||
offset". If the game's leaf clock also starts at 0, that is not a defect and F6
|
||||
is a **visibility** question — alpha, or draw order, or something not yet named —
|
||||
rather than a clock question. I am flagging it rather than rewriting the section:
|
||||
this rests on two numbers relayed in a message, which is exactly the thing that
|
||||
should be read from the repository instead.
|
||||
|
||||
## Why alpha was the wrong tool, stated generally
|
||||
|
||||
| | alpha | position |
|
||||
|---|---|---|
|
||||
| dynamic range | 8 bits, quantised | **2 160 px** |
|
||||
| shape | non-monotone, ramps and holds | **monotone** |
|
||||
| failure mode that bit us | a 14-sample tail out of 1754 looks like signal | a wrong shape raises the residual |
|
||||
| yields the clock? | no | **origin AND rate together** |
|
||||
|
||||
**When something moves, its position carries the clock and its alpha carries
|
||||
almost nothing.** Neither of us reached for a trajectory comparison because
|
||||
neither of us had one.
|
||||
|
||||
## So: `tools/port/fit-trajectory`
|
||||
|
||||
Solves `x_measured(frame) ≈ declared(t0 + rate·frame)` for the pair, and reports
|
||||
the **residual**, which is the part that matters: it says whether the model was
|
||||
right at all, where a value-at-an-instant never can.
|
||||
|
||||
Its `--selftest` runs both directions — recovers a known clock from a synthesised
|
||||
series to 0.09 px, and **rejects** a wrong-shape series at 81.9 px against a 20 px
|
||||
bar — because a fit that cannot fail is a curve-fitter, not a measurement. Wired
|
||||
into `check-all`.
|
||||
|
||||
⚠️ It fits a **constant** rate. A stalling guest clock or uneven capture drops
|
||||
raise the residual rather than being absorbed, which is deliberate.
|
||||
|
||||
@@ -149,6 +149,7 @@ step decisions-index must-pass tools/port/index-decisions --check
|
||||
# monorepo move and the `export/` rename. Only that class fails; a citation that
|
||||
# is merely on a peer's unmerged branch is reported, because the fix is a merge
|
||||
# and nobody in this container can make it.
|
||||
step trajectory-fit must-pass tools/port/fit-trajectory --selftest
|
||||
step doc-citations must-pass tools/port/check-citations
|
||||
step citations-control must-pass tools/port/check-citations --selftest
|
||||
# A refuted claim asserted outside its correction is a lie the corpus tells a
|
||||
|
||||
147
tools/port/fit-trajectory
Executable file
147
tools/port/fit-trajectory
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user