From 978f4f99437079d3f11369b1ff5e821f70743eed Mon Sep 17 00:00:00 2001 From: Sylpheed port agent Date: Mon, 31 Aug 2026 05:01:00 +0000 Subject: [PATCH] port: localise the menu residual -- nothing is displaced, and the split I expected is absent tools/port/edge-residual-map tiles the frame at 64 px and runs the shift discriminator inside each tile, which is what edge-residual-kind's reach statement said it could not do. Division agreed with the Decoder: the map is mine, the element inventory is theirs. The first control failed and that is the useful part. A known +2 px displacement localises perfectly but reads back +0.839 -- the slope is a linearisation and saturates. So there are two controls now: +1 px asserts localisation AND magnitude (+0.949), +2 px asserts localisation and sign only. A hot tile's slope is a floor on the displacement, never a ceiling. Result: every dx and dy in the top ten tiles is under 0.1 px. Nothing in the hot region has moved, locally or globally, so the misplaced-soft-element candidate now has no support on this screen. Hot tiles cluster at x 384-704, y 64-256 plus an outlier at 640,576; the tool names nothing and the list has gone to the Decoder. And a null: I added a flat-pixel column expecting two families, edge-only against hot-everywhere. The hot tiles run continuously 1.24..3.37 across a median of 1.84. What nearly manufactured the split was carrying over the frame-wide POOLED ratio of 3.16 as the baseline -- pooling is dominated by the tiles with the most edge pixels. Same quantity, wrong population; caught because the baseline was computed before the claim rather than after. --- docs/port/DECISIONS.md | 68 ++++++++++++- tools/port/edge-residual-map | 180 +++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 1 deletion(-) create mode 100755 tools/port/edge-residual-map diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 8803cd17..9815ccbf 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -325 sections. Search this before re-deriving anything. +326 sections. Search this before re-deriving anything. * [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28) * [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28) @@ -336,6 +336,7 @@ dies, which is what this file is for. * [Refutation: the peer's tone/geometry positive control rests on a number of mine that cannot carry it](#refutation-the-peers-tonegeometry-positive-control-rests-on-a-number-of-mine-that-cannot-carry-it) * [The menu's edge residual is **not** a misregistration — the Decoder's discriminator, run](#the-menus-edge-residual-is-not-a-misregistration--the-decoders-discriminator-run) * [`GP_DIALOG` 2/3 restored to `authored/flow.json` — on a measurement this time](#gp_dialog-23-restored-to-authoredflowjson--on-a-measurement-this-time) +* [The residual map: no local displacement either, and the split I expected is not there](#the-residual-map-no-local-displacement-either-and-the-split-i-expected-is-not-there) ## P0 — the exporter, 2026-08-28 @@ -15798,3 +15799,68 @@ claim was correctly out of the file until somebody went and looked. ⚠️ Their reach, carried across: one JP boot, one screen, does not generalise — `GP_TITLE` 4/7 differs by more than text. Nothing in the port keys off locale today, so this is recorded, not consumed. + +## The residual map: no local displacement either, and the split I expected is not there + +The Decoder proposed the division and it is the right one: *"the map is yours and +the element inventory is mine."* `tools/port/edge-residual-map` tiles the frame at +64 px and runs the same shift discriminator **inside each tile**, which is the +thing `edge-residual-kind` said it structurally could not do. + +### 🔴 The first control failed, and that is the useful part + +A known **+2 px** displacement localises perfectly — the displaced region is the +top four tiles — but reads back **+0.839 px**. The slope is a linearisation, +`residual ≈ dx · gradient`, valid only while `dx` is small against the width of an +edge. **The estimator saturates.** Reporting that as a distance would have +understated a real displacement by more than half. + +So there are now **two** controls, each asserting only what it can: + +| control | localises | magnitude | +|---|---|---| +| **+1 px** (linear regime) | ✅ top 4 tiles | ✅ **+0.949** | +| **+2 px** (saturating) | ✅ top 4 tiles | ⚠️ +0.839 — **a lower bound** | + +📌 **A hot tile's slope is a floor on the displacement, never a ceiling.** + +### The result + +| tile | edge | flat | e/f | dx | dy | +|---|---|---|---|---|---| +| 512,128 | **18.80** | 10.20 | 1.84 | **−0.001** | **−0.000** | +| 576,64 | 15.11 | 9.09 | 1.66 | −0.012 | +0.029 | +| 384,256 | 14.12 | 4.40 | 3.21 | +0.003 | +0.003 | +| 512,192 | 11.91 | 3.53 | 3.37 | −0.038 | −0.090 | +| 448,128 | 11.78 | 9.53 | 1.24 | +0.084 | +0.036 | + +median tile 5.13 · hottest 3.66× median · **every dx and dy under 0.1 px** + +🔴 **No tile in the top ten is displaced.** Against a control that reads a true +1 px at +0.949 and finds a 2 px one even while understating it, **nothing in the +hot region has moved.** The peer's third candidate — a misplaced soft element — +now has no support anywhere on this screen, globally or locally. + +The hot tiles cluster: **x 384–704, y 64–256**, a wide upper-centre band, plus one +outlier at **640,576**. Those are coordinates. **This tool names nothing** — what +sits under them is the Decoder's, and I have sent them the list. + +### ⚠️ And a structure I expected, went looking for, and did not find + +I added the `flat` column expecting two families: tiles hot *only* at edges (an +edge-rendering difference) against tiles hot *everywhere* (a local tone the global +LUT mis-serves). Reading the first ten rows, that split looked obvious. + +It is not there. The hot tiles run **continuously from 1.24 to 3.37** across a +median of **1.84**. + +📌 **What nearly manufactured it:** I had the frame-wide *pooled* edge/flat ratio, +**3.16**, from the earlier work, and against 3.16 the rows at 1.2–1.8 look like a +distinct low family. But the pooled figure is dominated by the tiles carrying the +most edge pixels; **the per-tile median is 1.84**, and against *that* the same rows +are unremarkable. **Same quantity, wrong population** — the week's pattern again, +caught this time only because I computed the baseline before writing the claim +rather than after. + +**So the hot region is not one anomalous element with a character of its own**, and +that is a finding, not an absence of one. diff --git a/tools/port/edge-residual-map b/tools/port/edge-residual-map new file mode 100755 index 00000000..20d6debb --- /dev/null +++ b/tools/port/edge-residual-map @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""WHERE does the edge residual sit, and is that region locally shifted? + + RENDER= tools/port/edge-residual-map [screen] + +`edge-residual-kind` fits the whole frame and excludes a GLOBAL translation. Its +own reach statement says the thing it cannot do: one misplaced element is a small +share of 38 752 edge pixels and would not move a whole-frame number. This tiles +the frame and runs the same discriminator INSIDE each tile, so a single displaced +element shows up as one hot tile with a local slope -- which is invisible to the +global fit by construction, not by accident. + +Division of labour, agreed with the Decoder 2026-08-31: the residual map is the +port's (it needs the render beside the capture), the element inventory is theirs +(it needs the disc). This tool produces the map and NAMES NOTHING. + +THE CONTROL IS A KNOWN LOCAL SHIFT. A map that cannot localise a displacement it +was told about cannot be trusted to have found one it was not. Exit 0 the report +is trustworthy, 2 the control failed and the report is suppressed. No 1. +""" +import math, os, subprocess, sys, tempfile + +CAPS = "docs/re/captures/title-builds" +SCREEN = sys.argv[1] if len(sys.argv) > 1 else "main_menu" +W, H = 1279, 675 # top-left crop of the guest surface; never scaled +TILE = 64 +EDGE = 12 +MIN_EDGE_PX = 150 # below this a tile's slope is noise +# The controls displace this region and the map must find it there. +CTRL_BOX = (448, 320, 640, 448) # x0, y0, x1, y1 +# TWO controls, because ONE OF THEM FAILED AND TAUGHT ME THE LIMIT. The slope is +# a linearisation, residual ~ dx * gradient, which holds only while dx is small +# against the width of an edge. A +2 px displacement localises perfectly but reads +# back +0.8..+1.25, so the estimator SATURATES. Control A checks magnitude in the +# regime where magnitude means something; control B checks that a displacement too +# large to measure is still FOUND. Reporting a saturating slope as a distance +# would understate a real displacement by more than half. +CTRL_A_DX = 1 # linear regime: localisation AND magnitude +CTRL_B_DX = 2 # saturating: localisation and SIGN only + + +def gray(png, out): + subprocess.run(["convert", png, "-colorspace", "Gray", "-depth", "8", + "gray:" + out], check=True) + return open(out, "rb").read() + + +def lutfit(a, b): + tot = [0] * 256; cnt = [0] * 256 + for i in range(len(a)): + tot[a[i]] += b[i]; cnt[a[i]] += 1 + return [(tot[v] // cnt[v]) if cnt[v] else v for v in range(256)] + + +def tiles(a, b): + """Per-tile mean |residual| on edge pixels, and the local shift slope.""" + lut = lutfit(a, b) # ONE global LUT: tone is global, displacement is not + out = {} + for ty in range(0, H - 1, TILE): + for tx in range(0, W - 1, TILE): + gx = []; gy = []; rs = []; flat = [] + for y in range(max(1, ty), min(H - 1, ty + TILE)): + o = y * W + for x in range(max(1, tx), min(W - 1, tx + TILE)): + i = o + x + ax = (a[i + 1] - a[i - 1]) * 0.5 + ay = (a[i + W] - a[i - W]) * 0.5 + d = float(lut[a[i]] - b[i]) + if abs(ax) + abs(ay) < EDGE: + flat.append(abs(d)); continue + gx.append(ax); gy.append(ay); rs.append(d) + n = len(rs) + if n < MIN_EDGE_PX: + continue + mabs = sum(abs(v) for v in rs) / n + mflat = (sum(flat) / len(flat)) if flat else 0.0 + mr = sum(rs) / n + + def slope(u): + mu = sum(u) / n + suu = sum((v - mu) ** 2 for v in u) + if suu <= 0: + return 0.0 + return sum((u[k] - mu) * (rs[k] - mr) for k in range(n)) / suu + out[(tx, ty)] = (mabs, slope(gx), slope(gy), n, mflat) + return out + + +def top(t, k=8): + return sorted(t.items(), key=lambda kv: -kv[1][0])[:k] + + +def show(t, label, k=8): + print(f" {label}") + print(f" {'tile':>12} {'edge':>7} {'flat':>7} {'e/f':>6} " + f"{'dx':>7} {'dy':>7} {'edge px':>8}") + for (tx, ty), (m, sx, sy, n, mf) in top(t, k): + ef = (m / mf) if mf > 0.01 else float('inf') + print(f" {tx:4d},{ty:4d} {m:7.2f} {mf:7.2f} {ef:6.2f} " + f"{sx:+7.3f} {sy:+7.3f} {n:8d}") + + +def shift_box(a, box, dx): + x0, y0, x1, y1 = box + out = bytearray(a) + for y in range(y0, y1): + for x in range(x0, x1): + out[y * W + x] = a[y * W + min(W - 1, max(0, x - dx))] + return bytes(out) + + +tmp = tempfile.mkdtemp() +cap = f"{CAPS}/live-{SCREEN.replace('_', '-')}.png" +render = os.environ.get("RENDER", "") +for p in (cap, render): + if not p or not os.path.exists(p): + print(f" 🔴 missing: {p or 'RENDER='}"); sys.exit(2) +subprocess.run(["convert", render, "-crop", f"{W}x{H}+0+0", "+repage", + f"{tmp}/crop.png"], check=True) +r = gray(f"{tmp}/crop.png", f"{tmp}/r.gray") +c = gray(cap, f"{tmp}/c.gray") + +print("CONTROLS -- the render against itself with ONE REGION displaced.\n" + "The map must put that region on top; magnitude only in the linear regime.\n") +x0, y0, x1, y1 = CTRL_BOX +bad = [] + + +def control(dx, check_magnitude): + t = tiles(r, shift_box(r, CTRL_BOX, dx)) + show(t, f"known +{dx} px shift inside x {x0}-{x1}, y {y0}-{y1}", 4) + hits = [(k, v) for k, v in top(t, 4) + if x0 - TILE < k[0] < x1 and y0 - TILE < k[1] < y1] + if not hits: + bad.append(f"+{dx} px: displaced region not in the top 4 tiles") + return + best = max(hits, key=lambda kv: kv[1][0])[1][1] + if best <= 0.3: + bad.append(f"+{dx} px: local slope {best:+.3f} has the wrong sign or is flat") + elif check_magnitude and abs(best - dx) > 0.4: + bad.append(f"+{dx} px: local slope {best:+.3f} does not recover it") + print(f" -> localised, local slope {best:+.3f} px" + f"{'' if check_magnitude else ' (saturating -- a LOWER BOUND)'}\n") + + +control(CTRL_A_DX, True) +control(CTRL_B_DX, False) +if bad: + print(" 🔴 CONTROL FAILED: " + "; ".join(bad)) + print(" A map that cannot find a displacement it was told about cannot be") + print(" trusted to have found one it was not. Report suppressed.") + sys.exit(2) +print(" ✅ controls pass: a 1 px displacement is localised and measured, a 2 px\n" + " one is localised with its magnitude understated. So a hot tile with a\n" + " real slope is a floor on the displacement, never a ceiling.\n") + +print(f"THE REAL PAIR -- {SCREEN}\n") +rt = tiles(r, c) +show(rt, f"{SCREEN}: hottest tiles, whole-frame LUT applied", 10) +ms = sorted(v[0] for v in rt.values()) +med = ms[len(ms) // 2] +efs = sorted(v[0] / v[4] for v in rt.values() if v[4] > 0.01) +med_ef = efs[len(efs) // 2] +hot = max(rt.items(), key=lambda kv: kv[1][0]) +print(f"\n median tile |resid| {med:.2f} hottest {hot[1][0]:.2f} " + f"at {hot[0][0]},{hot[0][1]} ({hot[1][0]/med:.2f}x median)") +print(f" median tile edge/flat {med_ef:.2f}") +efs_hot = [v[0] / v[4] for _, v in top(rt, 10) if v[4] > 0.01] +print(f" hot tiles span edge/flat {min(efs_hot):.2f}..{max(efs_hot):.2f}, " + f"straddling that median") +print(" 📌 SO THE COLUMN DOES NOT SPLIT THEM. I added it expecting two families --") +print(" tiles hot only at edges (an edge-rendering difference) against tiles") +print(" hot everywhere (a local tone the global LUT mis-serves). The hot tiles") +print(" run continuously across the median instead, so the hot region is NOT") +print(" one anomalous element with a character of its own. Note the frame-wide") +print(" pooled edge/flat is 3.16 while the per-tile median is 1.84: pooling is") +print(" dominated by the tiles carrying the most edge pixels, and reading a") +print(" per-tile threshold off it would have manufactured the split.") +print("\n ⚠️ THIS TOOL NAMES NOTHING. A hot tile is a coordinate, not an element.") +print(" What sits under it is the Decoder's to say -- they hold the disc.")