From 642341b12b3adb269d552aec1df24dd09a8cb129 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sun, 30 Aug 2026 12:38:18 +0000 Subject: [PATCH] re: rest_plateau() picks the wrong plateau -- and it is the whole residual rest_plateau() selects the LONGEST run of identical adjacent poses, which need not be the run covering the screen's settle instant. rest_vs_settle left a 21.9 % disagreement that I recorded as ambiguous by construction. It is not. CONTROL exactly one plateau, covering the settle instant: 3 072 / 3 072 agree (100.0 %) TEST more than one plateau, at least one covering: 1 622 elements, agree on 586 (36.1 %) of the 1 036 disagreements, rest() landed on a run NOT covering the settle instant: 1 036 -- all of them, no exceptions Both poses are genuinely held in these cases -- they are plateau cases, not transients -- so this is rest() returning a pose the screen has ALREADY LEFT by the time it settles. This corrects my own METHOD entry of two iterations ago, which said a candidate cannot be adjudicated against the incumbent it replaces. Too strong. The bare comparison cannot; the comparison plus a structural property that independently says which side is wrong in each disagreement can. What I lacked was not an oracle but a discriminator. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v --- .../examples/plateau_choice.rs | 90 +++++++++++++++++++ docs/re/data/plateau-choice.txt | 36 ++++++++ docs/re/structures/ui-resting-pose.md | 31 +++++-- 3 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 crates/sylpheed-formats/examples/plateau_choice.rs create mode 100644 docs/re/data/plateau-choice.txt diff --git a/crates/sylpheed-formats/examples/plateau_choice.rs b/crates/sylpheed-formats/examples/plateau_choice.rs new file mode 100644 index 00000000..3234fa2e --- /dev/null +++ b/crates/sylpheed-formats/examples/plateau_choice.rs @@ -0,0 +1,90 @@ +//! When an element has MORE THAN ONE plateau, does `rest_plateau()` pick the +//! wrong one — and is that the 21.9 % residual? +//! +//! `rest_vs_settle` found that among elements holding a pose ACROSS the screen's +//! settle instant, `pose_at(settle)` and `rest()` still disagree 21.9 % of the +//! time. I hypothesised that `rest_plateau()` picks the **longest** run (it does — +//! `len >= any_len`), which need not be the run covering the settle instant. +//! +//! ⚠️ **Control**: on elements with exactly ONE plateau that covers the settle +//! instant, the two MUST agree. If they do not, the hypothesis is not the +//! explanation and something else is wrong. +//! +//! cargo run -p sylpheed-formats --example plateau_choice +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::path::PathBuf; + +fn main() { + let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); + let mut paks: Vec = std::fs::read_dir(root.join("dat")).expect("dat/") + .filter_map(|e| e.ok().map(|e| e.path())) + .filter(|p| p.extension().is_some_and(|x| x == "pak")).collect(); + paks.sort(); + let (mut one_cov, mut one_agree) = (0usize, 0usize); // control + let (mut multi_cov, mut multi_agree, mut multi_wrongrun) = (0usize, 0usize, 0usize); + for pak in &paks { + let Ok(ar) = PakArchive::open(pak) else { continue }; + for e in ar.entries() { + let Ok(by) = ar.read(e) else { continue }; + let Some(b) = ui_layout::parse_build(&by) else { continue }; + let Some((lo, hi)) = b.settle_window() else { continue }; + if hi - lo < 10 { continue } + let st = lo + (hi - lo) / 2; + for el in &b.elements { + let k = &el.keyframes; + if k.len() < 2 { continue } + let same = |a: &ui_layout::Keyframe, c: &ui_layout::Keyframe| { + a.fade == c.fade && a.scale_x == c.scale_x && a.scale_y == c.scale_y + && a.tint == c.tint && a.x == c.x && a.y == c.y + }; + // enumerate maximal runs of length >= 2, with their time spans + let mut runs: Vec<(usize, usize)> = Vec::new(); + let mut i = 0usize; + while i < k.len() { + let mut j = i; + while j + 1 < k.len() && same(&k[j], &k[j + 1]) { j += 1 } + if j - i + 1 >= 2 { runs.push((i, j)) } + i = j + 1; + } + if runs.is_empty() { continue } + let covers = |&(a, c): &(usize, usize)| match (k[a].time, k[c].time) { + (Some(t0), Some(t1)) => t0 <= st && st <= t1, + _ => false, + }; + let covering: Vec<_> = runs.iter().filter(|r| covers(r)).collect(); + if covering.is_empty() { continue } + let (Some(r), Some(s)) = (el.rest(), el.pose_at(st)) else { continue }; + let agree = r.fade == s.fade && r.x == s.x && r.y == s.y + && r.scale_x == s.scale_x && r.scale_y == s.scale_y; + if runs.len() == 1 { + one_cov += 1; + if agree { one_agree += 1 } + } else { + multi_cov += 1; + if agree { multi_agree += 1 } + else { + // did rest() land on a run that does NOT cover settle? + let on_covering = covering.iter().any(|&&(a, c)| { + (a..=c).any(|idx| { + let kk = &k[idx]; + kk.fade == r.fade && kk.x == r.x && kk.y == r.y + && kk.scale_x == r.scale_x && kk.scale_y == r.scale_y + }) + }); + if !on_covering { multi_wrongrun += 1 } + } + } + } + } + } + println!("CONTROL — exactly ONE plateau, and it covers the settle instant:"); + println!(" {one_cov} elements, rest() and pose_at(settle) agree on {one_agree} ({:.1} %)", + 100.0 * one_agree as f64 / one_cov.max(1) as f64); + println!("\nTEST — MORE THAN ONE plateau, at least one covering the settle instant:"); + println!(" {multi_cov} elements, agree on {multi_agree} ({:.1} %)", + 100.0 * multi_agree as f64 / multi_cov.max(1) as f64); + println!(" of the {} disagreements, rest() landed on a run that does NOT cover", + multi_cov - multi_agree); + println!(" the settle instant: {multi_wrongrun}"); + println!("\n--- END (if this line is missing, the run did not finish) ---"); +} diff --git a/docs/re/data/plateau-choice.txt b/docs/re/data/plateau-choice.txt new file mode 100644 index 00000000..8d4b11ce --- /dev/null +++ b/docs/re/data/plateau-choice.txt @@ -0,0 +1,36 @@ +# Does rest_plateau() pick the WRONG plateau? Yes -- and it accounts for the +# whole residual. 2026-08-30, examples/plateau_choice.rs +# +# rest_plateau() selects the LONGEST run of identical adjacent poses +# ('len >= any_len'), which need not be the run covering the screen's settle +# instant. rest_vs_settle left a 21.9 % disagreement unexplained and I flagged +# it as ambiguous by construction. It is not ambiguous. +# +CONTROL — exactly ONE plateau, and it covers the settle instant: + 3072 elements, rest() and pose_at(settle) agree on 3072 (100.0 %) + +TEST — MORE THAN ONE plateau, at least one covering the settle instant: + 1622 elements, agree on 586 (36.1 %) + of the 1036 disagreements, rest() landed on a run that does NOT cover + the settle instant: 1036 + +--- END (if this line is missing, the run did not finish) --- +# +# ✅ THE CONTROL IS EXACT. Where an element has exactly ONE plateau and it +# covers the settle instant, rest() and pose_at(settle) agree 3 072 / 3 072. +# The comparison is sound; the disagreements are not noise. +# +# ✅ AND EVERY DISAGREEMENT IS ATTRIBUTABLE. In all 1 036 of them rest() +# returned a pose from a run that does NOT contain the settle instant, while +# pose_at(settle) sat on one that does. Both poses are genuinely HELD -- these +# are plateau cases -- so this is not 'a held pose versus a transient'. It is +# rest() returning a pose the screen has ALREADY LEFT by the time it settles. +# +# 1036 of 1036, no exceptions. The 21.9 % residual is the incumbent's. +# +# 🔴 THIS CORRECTS MY OWN METHOD ENTRY of two iterations ago, which said a +# candidate cannot be adjudicated against the incumbent it replaces. Too +# strong. The bare comparison cannot -- but the comparison PLUS a structural +# property that independently says which side is wrong in each disagreement +# CAN, and 'does the chosen run contain the settle instant' is such a +# property. What I lacked was not an oracle; it was a discriminator. diff --git a/docs/re/structures/ui-resting-pose.md b/docs/re/structures/ui-resting-pose.md index ef66ab34..3a2f1a99 100644 --- a/docs/re/structures/ui-resting-pose.md +++ b/docs/re/structures/ui-resting-pose.md @@ -342,15 +342,30 @@ The naive one was misspecified, caught by asking what 46.6 % means physically: `rest()` finds *a* held pose, and many elements hold one during the build-in then move on. Different questions; disagreement proves nothing. -⚠️ **And the fair control's 21.9 % residual is ambiguous by construction.** -`rest_plateau()` picks one plateau; an element with two, whose settle instant falls -in the *other*, disagrees — and there `pose_at(settle)` is **right** and `rest()` -wrong. The control cannot separate *"the candidate is wrong"* from *"the incumbent -is wrong"*. +⚠️ **The fair control's 21.9 % residual looked ambiguous by construction** — +`rest_plateau()` picks one plateau, and an element with two whose settle instant +falls in the *other* disagrees. -🔴 **Comparing a candidate to the incumbent cannot adjudicate when the incumbent is -the thing under suspicion.** No care with this control fixes that; it is the wrong -shape of experiment. +✅ **RESOLVED (2026-08-30, later): it is not ambiguous, and the residual is entirely +the incumbent's** ([`../data/plateau-choice.txt`](../data/plateau-choice.txt)). + +| | | +|---|---| +| **control** — exactly one plateau, covering the settle instant | **3 072 / 3 072 agree (100.0 %)** | +| **test** — more than one plateau, at least one covering | 1 622 elements, agree on 586 (36.1 %) | +| of the **1 036** disagreements, `rest()` landed on a run **not covering** the settle instant | **1 036 — all of them** | + +`rest_plateau()` selects the **longest** run (`len >= any_len`), which need not be +the one the screen is actually sitting in. **Both poses are genuinely held** — these +are plateau cases, not transients — so this is `rest()` returning a pose the screen +has **already left** by the time it settles. + +🔴 ~~**Comparing a candidate to the incumbent cannot adjudicate when the incumbent is +the thing under suspicion.**~~ **Too strong — corrected the same day.** The *bare* +comparison cannot. The comparison **plus a structural property that independently +says which side is wrong in each disagreement** can, and *"does the chosen run +contain the settle instant"* is such a property: it attributes **1 036 of 1 036**. +What was missing was not an oracle but a **discriminator**. ### ✅ Closing the gap: `settle_time()` itself, against the game