SharpMeow commited on
Commit
dd7161c
·
verified ·
1 Parent(s): 3ba468f

Voxel exact tab with the first frozen result, from Scrolls main 1dcab57 (PR #255)

Browse files
Files changed (8) hide show
  1. README.md +5 -1
  2. app.js +76 -3
  3. index.html +117 -1
  4. style.css +103 -0
  5. vx.js +382 -0
  6. vx/fixture/show/SYNTH-A/synthetic-sheet.json +0 -0
  7. vx/fixture/summary.json +1042 -0
  8. vx/summary.json +1049 -0
README.md CHANGED
@@ -26,8 +26,12 @@ On fresh test sites, 91% of snapped PHerc0139 vertices sit within one voxel of t
26
  1.129 µm scan, against 24% for the published labels.
27
 
28
  - **Viewer**: pick a segment and a z plane; orange is the published label, blue the snapped one, gray strokes join
29
- matching points. Arrow keys step along the label and through z; `?seg=SEGMENT&z=Z` links to a view.
 
30
  - **All segments**: every segment with its share snapped, median move and held-back count.
 
 
 
31
  - **How it works**: the two methods, how they were measured, the files in each segment folder, and the limits.
32
  - **Connect your tool**: tested snippets for the dataset, a mesh and the CT.
33
 
 
26
  1.129 µm scan, against 24% for the published labels.
27
 
28
  - **Viewer**: pick a segment and a z plane; orange is the published label, blue the snapped one, gray strokes join
29
+ matching points; Animate snap moves each vertex from its published position to its snapped one, slowly. Arrow keys
30
+ step along the label and through z; `?seg=SEGMENT&z=Z` links to a view.
31
  - **All segments**: every segment with its share snapped, median move and held-back count.
32
+ - **Voxel exact** (shown once results are published): per scroll, Measured, Certified or Not shown, with the share
33
+ within 0.5, 1 and 2 voxels, the abstain strata, residual histograms and the controls. `?vx=fixture` previews it on
34
+ made-up data under a banner.
35
  - **How it works**: the two methods, how they were measured, the files in each segment folder, and the limits.
36
  - **Connect your tool**: tested snippets for the dataset, a mesh and the CT.
37
 
app.js CHANGED
@@ -10,7 +10,8 @@ const $ = (id) => document.getElementById(id);
10
  const fmt = (n) => (n == null ? 'n/a' : Number(n).toLocaleString('en-US'));
11
  const pct = (a, b) => (b ? `${(100 * a / b).toFixed(1)}%` : 'n/a');
12
 
13
- const S = { index: [], seg: null, view: null, rows: null, plane: 0, cx: 0, cy: 0, win: 192, order: [], at: 0, abort: null };
 
14
 
15
  async function getJSON(url) { const r = await fetch(url, { credentials: 'omit' }); if (!r.ok) throw new Error(`${r.status} ${url}`); return r.json(); }
16
 
@@ -126,6 +127,7 @@ async function draw() {
126
  const X = (x) => (x - x0 + 0.5) * k, Y = (y) => (y - y0 + 0.5) * k;
127
  const css = getComputedStyle(document.documentElement);
128
  const lw = Math.max(1.5, Math.min(3, k * 0.4));
 
129
  if ($('l-move').checked) {
130
  // gray strokes from the published line to the snapped one, at each piece's midpoint
131
  cx2d.strokeStyle = css.getPropertyValue('--move').trim() || 'rgba(200,200,200,.6)';
@@ -144,11 +146,77 @@ async function draw() {
144
  };
145
  if ($('l-pub').checked) line(0, css.getPropertyValue('--pub').trim() || '#f08a24');
146
  if ($('l-snap').checked) line(4, css.getPropertyValue('--snap').trim() || '#2f7bf6');
 
 
 
 
 
 
 
147
  // scale bar: 10 voxels
148
  const um = S.view.voxel_um || null;
149
  cx2d.fillStyle = '#fff'; cx2d.fillRect(16, W - 22, 10 * k, 4);
150
  cx2d.font = '13px system-ui, sans-serif'; cx2d.fillText(um ? `10 voxels (${Math.round(10 * um)} µm)` : '10 voxels', 16, W - 30);
151
- history.replaceState(null, '', `?${new URLSearchParams({ ...(params.get('data') ? { data: params.get('data') } : {}), seg: S.seg.segment, z: String(z) })}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
153
 
154
  function msg(t) { const e = $('cv-msg'); e.textContent = t; e.hidden = !t; }
@@ -223,15 +291,18 @@ function listSegs() {
223
 
224
  function table() {
225
  const tb = document.querySelector('#tbl tbody'); tb.innerHTML = '';
 
226
  for (const s of S.index) {
227
  const tr = document.createElement('tr');
228
- tr.innerHTML = `<td>${s.scroll}</td><td>${s.segment}</td><td>${s.method}</td><td class="n">${fmt(s.vertices_released)}</td><td class="n">${pct(s.snapped, s.vertices_released)}</td><td class="n">${s.move_voxels && s.move_voxels.median != null ? s.move_voxels.median.toFixed(2) : 'n/a'}</td><td class="n">${fmt(s.vertices_held_back)}</td>`;
 
229
  tr.onclick = () => { tab('view'); showStage(); openSeg(s).catch((e) => msg(e.message)); };
230
  tb.appendChild(tr);
231
  }
232
  }
233
 
234
  function tab(name) {
 
235
  document.querySelectorAll('.tabs button').forEach((b) => b.setAttribute('aria-selected', String(b.dataset.tab === name)));
236
  document.querySelectorAll('[data-pane]').forEach((p) => { p.hidden = p.dataset.pane !== name; });
237
  }
@@ -244,6 +315,7 @@ $('f-find').oninput = listSegs;
244
  ['l-pub', 'l-snap', 'l-move'].forEach((id) => { $(id).onchange = () => draw(); });
245
  $('z-range').oninput = () => { S.plane = Number($('z-range').value); ringOrder(); const r = planeRows(); if (r.length) { let best = Infinity, bi = 0; for (let i = 0; i < r.length / R; i++) { const d = (r[R * i] - S.cx) ** 2 + (r[R * i + 1] - S.cy) ** 2; if (d < best) { best = d; bi = i; } } S.at = Math.max(0, S.order.indexOf(bi)); } draw(); };
246
  $('win').onchange = () => { S.win = Number($('win').value); draw(); };
 
247
  $('b-next').onclick = () => { centerOn(S.at + Math.max(1, Math.round(S.order.length / 24))); draw(); };
248
  $('b-prev').onclick = () => { centerOn(S.at - Math.max(1, Math.round(S.order.length / 24))); draw(); };
249
  document.addEventListener('keydown', (e) => {
@@ -263,6 +335,7 @@ window.snappedLabels = {
263
  segments: () => S.index.slice(),
264
  open: (segment, z) => { const s = S.index.find((x) => x.segment === segment); if (!s) throw new Error(`no segment ${segment}`); return openSeg(s, z); },
265
  view: () => (S.view ? { ...S.view, plane: S.view.planes[S.plane], center: [S.cx, S.cy] } : null),
 
266
  };
267
 
268
  (async function init() {
 
10
  const fmt = (n) => (n == null ? 'n/a' : Number(n).toLocaleString('en-US'));
11
  const pct = (a, b) => (b ? `${(100 * a / b).toFixed(1)}%` : 'n/a');
12
 
13
+ const S = { index: [], seg: null, view: null, rows: null, plane: 0, cx: 0, cy: 0, win: 192, order: [], at: 0, abort: null,
14
+ tween: null }; // tween: null, or 0 (published) to 1 (snapped) while the snap animation plays
15
 
16
  async function getJSON(url) { const r = await fetch(url, { credentials: 'omit' }); if (!r.ok) throw new Error(`${r.status} ${url}`); return r.json(); }
17
 
 
127
  const X = (x) => (x - x0 + 0.5) * k, Y = (y) => (y - y0 + 0.5) * k;
128
  const css = getComputedStyle(document.documentElement);
129
  const lw = Math.max(1.5, Math.min(3, k * 0.4));
130
+ if (S.tween != null) { tweenLine(r, m, X, Y, lw, css); scaleBar(W, k); return; }
131
  if ($('l-move').checked) {
132
  // gray strokes from the published line to the snapped one, at each piece's midpoint
133
  cx2d.strokeStyle = css.getPropertyValue('--move').trim() || 'rgba(200,200,200,.6)';
 
146
  };
147
  if ($('l-pub').checked) line(0, css.getPropertyValue('--pub').trim() || '#f08a24');
148
  if ($('l-snap').checked) line(4, css.getPropertyValue('--snap').trim() || '#2f7bf6');
149
+ scaleBar(W, k);
150
+ const keep = {};
151
+ for (const k of ['data', 'vx', 'tab']) if (params.get(k)) keep[k] = params.get(k);
152
+ history.replaceState(null, '', `?${new URLSearchParams({ ...keep, seg: S.seg.segment, z: String(z) })}`);
153
+ }
154
+
155
+ function scaleBar(W, k) {
156
  // scale bar: 10 voxels
157
  const um = S.view.voxel_um || null;
158
  cx2d.fillStyle = '#fff'; cx2d.fillRect(16, W - 22, 10 * k, 4);
159
  cx2d.font = '13px system-ui, sans-serif'; cx2d.fillText(um ? `10 voxels (${Math.round(10 * um)} µm)` : '10 voxels', 16, W - 30);
160
+ }
161
+
162
+ // ---------------------------------------------------------------- snap animation
163
+
164
+ // Each line piece moves in a straight line from its published position to its snapped one, its color going from the
165
+ // published orange to the snapped blue. Filled and left-in-place vertices are drawn at the published position in both
166
+ // (the legend's rule), so they do not move. One loop: hold 1.2 s on the published line, 2.4 s to the snapped line,
167
+ // hold 1.6 s, 2.4 s back. With prefers-reduced-motion it never plays by itself: each click steps published, halfway,
168
+ // snapped.
169
+ const TW = { hold0: 1200, move: 2400, hold1: 1600 };
170
+ const anim = { on: false, t0: 0, raf: 0, step: -1 };
171
+ const mix = (a, b, t) => { const h = (s) => { const m = s.trim().replace('#', ''); return [0, 2, 4].map((i) => parseInt(m.slice(i, i + 2), 16)); }; const A = h(a), B = h(b); return `rgb(${A.map((v, i) => Math.round(v + (B[i] - v) * t)).join(',')})`; };
172
+
173
+ function tweenLine(r, m, X, Y, lw, css) {
174
+ const t = S.tween, pub = css.getPropertyValue('--pub').trim() || '#f08a24', snap = css.getPropertyValue('--snap').trim() || '#2f7bf6';
175
+ // the published line stays, faint, so the motion reads against it
176
+ cx2d.globalAlpha = 0.35; cx2d.strokeStyle = pub; cx2d.lineWidth = lw; cx2d.lineCap = 'round'; cx2d.beginPath();
177
+ for (let i = 0; i < m; i++) { const a = R * i; cx2d.moveTo(X(r[a]), Y(r[a + 1])); cx2d.lineTo(X(r[a + 2]), Y(r[a + 3])); }
178
+ cx2d.stroke(); cx2d.globalAlpha = 1;
179
+ cx2d.strokeStyle = mix(pub, snap, t); cx2d.lineWidth = lw; cx2d.beginPath();
180
+ for (let i = 0; i < m; i++) {
181
+ const a = R * i, L = (p, q) => p + (q - p) * t;
182
+ cx2d.moveTo(X(L(r[a], r[a + 4])), Y(L(r[a + 1], r[a + 5]))); cx2d.lineTo(X(L(r[a + 2], r[a + 6])), Y(L(r[a + 3], r[a + 7])));
183
+ }
184
+ cx2d.stroke();
185
+ const W = cv.width, word = t <= 0.001 ? 'Published label' : t >= 0.999 ? 'Snapped label' : 'Snapping along the normal';
186
+ cx2d.font = '600 15px system-ui, sans-serif'; cx2d.fillStyle = 'rgba(0,0,0,.55)'; cx2d.fillRect(12, 12, cx2d.measureText(word).width + 20, 28);
187
+ cx2d.fillStyle = '#fff'; cx2d.fillText(word, 22, 31);
188
+ void W;
189
+ }
190
+
191
+ function tweenAt(ms) {
192
+ const cyc = 2 * TW.move + TW.hold0 + TW.hold1, u = ms % cyc;
193
+ if (u < TW.hold0) return 0;
194
+ if (u < TW.hold0 + TW.move) return (u - TW.hold0) / TW.move;
195
+ if (u < TW.hold0 + TW.move + TW.hold1) return 1;
196
+ return 1 - (u - TW.hold0 - TW.move - TW.hold1) / TW.move;
197
+ }
198
+
199
+ const reduced = () => window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
200
+
201
+ function animate(on) {
202
+ if (reduced() && on !== false) {
203
+ // three still frames, one per click
204
+ anim.step = (anim.step + 1) % 4;
205
+ S.tween = anim.step === 3 ? null : [0, 0.5, 1][anim.step];
206
+ $('b-anim').textContent = anim.step === 3 ? 'Step the snap' : ['Published (1 of 3)', 'Halfway (2 of 3)', 'Snapped (3 of 3)'][anim.step];
207
+ $('b-anim').setAttribute('aria-pressed', String(S.tween != null));
208
+ draw();
209
+ return S.tween != null;
210
+ }
211
+ anim.on = on == null ? !anim.on : !!on;
212
+ cancelAnimationFrame(anim.raf);
213
+ $('b-anim').setAttribute('aria-pressed', String(anim.on));
214
+ $('b-anim').textContent = anim.on ? 'Stop' : 'Animate snap';
215
+ if (!anim.on) { S.tween = null; draw(); return false; }
216
+ anim.t0 = performance.now();
217
+ const tick = (now) => { if (!anim.on) return; S.tween = tweenAt(now - anim.t0); draw(); anim.raf = requestAnimationFrame(tick); };
218
+ anim.raf = requestAnimationFrame(tick);
219
+ return true;
220
  }
221
 
222
  function msg(t) { const e = $('cv-msg'); e.textContent = t; e.hidden = !t; }
 
291
 
292
  function table() {
293
  const tb = document.querySelector('#tbl tbody'); tb.innerHTML = '';
294
+ $('th-claim').hidden = !(window.vxClaim && window.vxClaim('') != null);
295
  for (const s of S.index) {
296
  const tr = document.createElement('tr');
297
+ const claim = window.vxClaim ? window.vxClaim(s.scroll) : null;
298
+ tr.innerHTML = `<td>${s.scroll}</td><td>${s.segment}</td><td>${s.method}</td><td class="n">${fmt(s.vertices_released)}</td><td class="n">${pct(s.snapped, s.vertices_released)}</td><td class="n">${s.move_voxels && s.move_voxels.median != null ? s.move_voxels.median.toFixed(2) : 'n/a'}</td><td class="n">${fmt(s.vertices_held_back)}</td>${claim ? `<td>${claim}</td>` : ''}`;
299
  tr.onclick = () => { tab('view'); showStage(); openSeg(s).catch((e) => msg(e.message)); };
300
  tb.appendChild(tr);
301
  }
302
  }
303
 
304
  function tab(name) {
305
+ if (name !== 'view' && anim.on) animate(false);
306
  document.querySelectorAll('.tabs button').forEach((b) => b.setAttribute('aria-selected', String(b.dataset.tab === name)));
307
  document.querySelectorAll('[data-pane]').forEach((p) => { p.hidden = p.dataset.pane !== name; });
308
  }
 
315
  ['l-pub', 'l-snap', 'l-move'].forEach((id) => { $(id).onchange = () => draw(); });
316
  $('z-range').oninput = () => { S.plane = Number($('z-range').value); ringOrder(); const r = planeRows(); if (r.length) { let best = Infinity, bi = 0; for (let i = 0; i < r.length / R; i++) { const d = (r[R * i] - S.cx) ** 2 + (r[R * i + 1] - S.cy) ** 2; if (d < best) { best = d; bi = i; } } S.at = Math.max(0, S.order.indexOf(bi)); } draw(); };
317
  $('win').onchange = () => { S.win = Number($('win').value); draw(); };
318
+ $('b-anim').onclick = () => animate();
319
  $('b-next').onclick = () => { centerOn(S.at + Math.max(1, Math.round(S.order.length / 24))); draw(); };
320
  $('b-prev').onclick = () => { centerOn(S.at - Math.max(1, Math.round(S.order.length / 24))); draw(); };
321
  document.addEventListener('keydown', (e) => {
 
335
  segments: () => S.index.slice(),
336
  open: (segment, z) => { const s = S.index.find((x) => x.segment === segment); if (!s) throw new Error(`no segment ${segment}`); return openSeg(s, z); },
337
  view: () => (S.view ? { ...S.view, plane: S.view.planes[S.plane], center: [S.cx, S.cy] } : null),
338
+ animate: (on) => animate(on),
339
  };
340
 
341
  (async function init() {
index.html CHANGED
@@ -70,6 +70,7 @@
70
  <!-- space-onescroll:end -->
71
  </head>
72
  <body>
 
73
  <header class="top">
74
  <div class="brand">
75
  <h1>Snapped Labels</h1>
@@ -78,6 +79,7 @@
78
  <nav class="tabs" role="tablist">
79
  <button data-tab="view" role="tab" aria-selected="true">Viewer</button>
80
  <button data-tab="all" role="tab">All segments</button>
 
81
  <button data-tab="how" role="tab">How it works</button>
82
  <button data-tab="connect" role="tab">Connect your tool</button>
83
  </nav>
@@ -106,6 +108,7 @@
106
  <label class="tog"><input type="checkbox" id="l-pub" checked><i class="dot pub"></i>Published</label>
107
  <label class="tog"><input type="checkbox" id="l-snap" checked><i class="dot snap"></i>Snapped</label>
108
  <label class="tog"><input type="checkbox" id="l-move" checked><i class="dot move"></i>Moves</label>
 
109
  </div>
110
  <div class="canvaswrap">
111
  <canvas id="cv" width="720" height="720" aria-label="CT slice with the published and snapped labels"></canvas>
@@ -124,10 +127,122 @@
124
 
125
  <section data-pane="all" hidden>
126
  <div class="tablewrap"><table id="tbl"><thead><tr>
127
- <th>Scroll</th><th>Segment</th><th>Method</th><th class="n">Vertices</th><th class="n">Snapped</th><th class="n">Median move</th><th class="n">Held back</th>
128
  </tr></thead><tbody></tbody></table></div>
129
  </section>
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  <section data-pane="how" hidden class="prose">
132
  <figure class="fig"><img src="https://huggingface.co/datasets/SharpMeow/vesuvius-snapped-labels/resolve/main/snapped-0139.gif" width="600" height="600" alt="PHerc0139 segment w054 on CT at z 8048: the published label, then the snapped label, then both with the moves, at four stops along the label" loading="lazy"><figcaption>PHerc0139 w054, z 8048: published (orange), snapped (blue), both.</figcaption></figure>
133
  <h2>What a snap does</h2>
@@ -245,5 +360,6 @@ Page source: https://github.com/ChaseHendrick/Scrolls/tree/main/spaces/snapped-l
245
  </section>
246
  </main>
247
  <script src="app.js"></script>
 
248
  </body>
249
  </html>
 
70
  <!-- space-onescroll:end -->
71
  </head>
72
  <body>
73
+ <div class="vx-fixture vx-banner" role="alert" hidden><b>Synthetic fixture.</b> Every number, scroll and line in the Voxel exact tab is made up, to test the page. No real results are shown.</div>
74
  <header class="top">
75
  <div class="brand">
76
  <h1>Snapped Labels</h1>
 
79
  <nav class="tabs" role="tablist">
80
  <button data-tab="view" role="tab" aria-selected="true">Viewer</button>
81
  <button data-tab="all" role="tab">All segments</button>
82
+ <button data-tab="exact" role="tab" id="vx-tabbtn" hidden>Voxel exact</button>
83
  <button data-tab="how" role="tab">How it works</button>
84
  <button data-tab="connect" role="tab">Connect your tool</button>
85
  </nav>
 
108
  <label class="tog"><input type="checkbox" id="l-pub" checked><i class="dot pub"></i>Published</label>
109
  <label class="tog"><input type="checkbox" id="l-snap" checked><i class="dot snap"></i>Snapped</label>
110
  <label class="tog"><input type="checkbox" id="l-move" checked><i class="dot move"></i>Moves</label>
111
+ <button id="b-anim" class="anim" type="button" aria-pressed="false" title="Move each vertex from its published position to its snapped one, slowly, on a loop">Animate snap</button>
112
  </div>
113
  <div class="canvaswrap">
114
  <canvas id="cv" width="720" height="720" aria-label="CT slice with the published and snapped labels"></canvas>
 
127
 
128
  <section data-pane="all" hidden>
129
  <div class="tablewrap"><table id="tbl"><thead><tr>
130
+ <th>Scroll</th><th>Segment</th><th>Method</th><th class="n">Vertices</th><th class="n">Snapped</th><th class="n">Median move</th><th class="n">Held back</th><th id="th-claim" hidden>Claim</th>
131
  </tr></thead><tbody></tbody></table></div>
132
  </section>
133
 
134
+ <section data-pane="exact" hidden class="vx">
135
+ <div class="vx-fixture vx-banner inline" hidden><b>Synthetic fixture.</b> The scrolls SYNTH-A, SYNTH-B and SYNTH-C and every number below are made up, to test the page.</div>
136
+ <p id="vx-error" class="vx-error" hidden></p>
137
+ <div class="vx-intro">
138
+ <h2>Is each snapped vertex within one voxel of the papyrus face?</h2>
139
+ <p>Per scroll, one of three claim levels. <b>Measured</b>: scored against the face seen in an independent, finer scan of the same scroll, and at least one class cleared the bar. <b>Certified</b>: no finer scan, so a lower bound from a truth-free certificate whose assumptions are listed. <b>Not shown</b>: neither holds, and the card says why; where a test ran, the card still shows what it measured, which makes no claim. Every number here is model output: the snapped meshes come from a band finder, and the truth face is itself found by a rule.</p>
140
+ </div>
141
+ <section class="stats vx-tiles" id="vx-tiles" aria-label="Scrolls by claim level"></section>
142
+ <div id="vx-cards" class="vx-cards"></div>
143
+
144
+ <section class="vx-showwrap" aria-labelledby="vx-show-h">
145
+ <h2 id="vx-show-h">Per-vertex residuals</h2>
146
+ <p id="vx-show-none" class="hint">No public residual view yet. Drawing the truth face at every vertex would publish where the face is, densely, and spend that region for every later test, so a region appears here only once it is named as spent.</p>
147
+ <div id="vx-show" hidden>
148
+ <div class="bar">
149
+ <select id="vx-show-pick" aria-label="Showcase segment"></select>
150
+ <span id="vx-show-title" class="vt"></span>
151
+ <span class="sp"></span>
152
+ <fieldset class="vx-modes"><legend class="lbl">Color</legend>
153
+ <label><input type="radio" name="vx-mode" value="signed" checked> signed residual</label>
154
+ <label><input type="radio" name="vx-mode" value="0.5"> within 0.5</label>
155
+ <label><input type="radio" name="vx-mode" value="1"> within 1</label>
156
+ <label><input type="radio" name="vx-mode" value="2"> within 2</label>
157
+ </fieldset>
158
+ </div>
159
+ <div class="vx-showgrid">
160
+ <div class="vx-heatwrap">
161
+ <canvas id="vx-heat" width="600" height="150" aria-label="Signed residual at each vertex of the label surface, rows by columns; click a cell to see it on the CT"></canvas>
162
+ <div id="vx-tip" class="vx-tip" hidden></div>
163
+ <div id="vx-heat-legend" class="legend vx-heatlegend"></div>
164
+ <p class="hint">Each cell is a vertex of the label surface (grid rows and columns). Blue: the snapped point lies inward of the truth face; orange: outward. Click a cell to center the CT on it. The outlined row is the z plane shown.</p>
165
+ </div>
166
+ <div>
167
+ <div class="bar">
168
+ <label class="tog"><input type="checkbox" id="vx-l-pub" checked><i class="dot pub"></i>Published</label>
169
+ <label class="tog"><input type="checkbox" id="vx-l-snap" checked><i class="dot snap"></i>Snapped</label>
170
+ <label class="tog"><input type="checkbox" id="vx-l-truth" checked><i class="dot truth"></i>Truth face</label>
171
+ <span class="sp"></span><span id="vx-z" class="hint"></span>
172
+ <select id="vx-win" aria-label="Window"><option value="96">96 voxels</option><option value="192" selected>192 voxels</option><option value="384">384 voxels</option></select>
173
+ </div>
174
+ <div class="canvaswrap"><canvas id="vx-cv" width="576" height="576" aria-label="CT slice with the published, snapped and truth lines"></canvas></div>
175
+ <p id="vx-truth-note" class="hint"></p>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </section>
180
+
181
+ <section class="prose vx-how">
182
+ <h2>How it is measured</h2>
183
+ <figure class="fig">
184
+ <svg class="vx-diagram" viewBox="0 0 680 312" role="img" aria-labelledby="vx-d1t vx-d1d">
185
+ <title id="vx-d1t">A cross section through two wraps of papyrus</title>
186
+ <desc id="vx-d1d">The published label sits beside the sheet. The snap moves it along the mesh normal, toward the scroll's center line side, onto the sheet's recto face. The truth face, found in a finer scan, is drawn dashed, with a band of one label voxel on each side. The verso face, on the far side of the sheet, is a control.</desc>
187
+ <defs><marker id="vx-ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" fill="currentColor"/></marker></defs>
188
+ <path class="d-band" d="M262,20 C250,110 250,190 262,280 L318,280 C306,190 306,110 318,20 Z"/>
189
+ <path class="d-band" d="M622,20 C610,110 610,190 622,280 L672,280 C660,190 660,110 672,20 Z"/>
190
+ <rect class="d-tol" x="241" y="20" width="26" height="260" rx="3"/>
191
+ <path class="d-truth" d="M256,20 C244,110 244,190 256,280"/>
192
+ <path class="d-recto" d="M262,20 C250,110 250,190 262,280"/>
193
+ <path class="d-verso" d="M318,20 C306,110 306,190 318,280"/>
194
+ <line class="d-arrow" x1="72" y1="150" x2="22" y2="150" marker-end="url(#vx-ah)"/>
195
+ <text x="22" y="132" class="d-t">toward the scroll's</text><text x="22" y="176" class="d-t">center line</text>
196
+ <line class="d-normal" x1="196" y1="150" x2="245" y2="150" marker-end="url(#vx-ah)"/>
197
+ <circle class="d-pub" cx="190" cy="150" r="7"/>
198
+ <circle class="d-snap" cx="253" cy="150" r="7"/>
199
+ <text x="190" y="128" class="d-t" text-anchor="middle">published</text>
200
+ <text x="212" y="172" class="d-t muted" text-anchor="middle">normal</text>
201
+ <line class="d-lead" x1="250" y1="160" x2="200" y2="236"/>
202
+ <text x="200" y="252" class="d-t" text-anchor="middle">snapped, on the recto face</text>
203
+ <line class="d-lead" x1="248" y1="62" x2="352" y2="56"/>
204
+ <text x="358" y="52" class="d-t">truth face (dashed), from the finer scan</text>
205
+ <text x="358" y="70" class="d-t muted">shaded: ±1 label voxel around it</text>
206
+ <line class="d-lead" x1="312" y1="208" x2="352" y2="208"/>
207
+ <text x="358" y="212" class="d-t">verso face: a control</text>
208
+ <text x="642" y="306" class="d-t muted" text-anchor="middle">next wrap</text>
209
+ <text x="290" y="306" class="d-t muted" text-anchor="middle">one wrap of papyrus</text>
210
+ <text x="8" y="306" class="d-t muted">not to scale</text>
211
+ </svg>
212
+ <figcaption>One vertex, seen in cross section: published (orange), snapped along its normal onto the recto face (blue), and the truth face (dashed) with its one-voxel band. Not to scale; a label voxel is about 8 to 9 µm.</figcaption>
213
+ </figure>
214
+ <ol class="vx-flow" aria-label="How a test runs, in order">
215
+ <li><b>Release 1.0.1</b>, pinned by its revision, so the vertices scored are the ones you download.</li>
216
+ <li><b>Split by <code>abstain.tif</code></b>: C0 snapped, C2 filled, C1 left in place. Each is scored on its own.</li>
217
+ <li><b>Test sites</b> drawn after the rule was frozen, in regions never used before.</li>
218
+ <li><b>Truth face</b> found in the finer scan (1.129 µm for PHerc0139); a 2.4 µm scan, where one exists, is the positive control.</li>
219
+ <li><b>Negative controls</b>: the published label, offsets of 1.5 and 3 voxels planted on purpose, and the verso face. Each must fail the bar.</li>
220
+ <li><b>Verdict</b> per stratum: PASS, FAIL, NOT SHOWN or UNANSWERABLE; or VOID for the whole test when a control or the integrity check breaks.</li>
221
+ </ol>
222
+ <h2>The three claim levels</h2>
223
+ <p><b>Measured.</b> A finer scan of the same scroll exists. At test sites drawn after the rule was fixed, the face is found in it, registered into the label scan, and each snapped vertex's distance to it is measured along the normal. The share within 1 voxel and its one-sided 95% lower bound are reported; the claim holds when the bound clears the bar. The rule was fixed before any test site was looked at; its preregistration digest is <code id="vx-prereg"></code>.</p>
224
+ <p><b>Certified.</b> No finer scan. A certificate computed from the label scan alone gives a lower bound on the share within 1 voxel, valid only under the assumptions the card lists. It was fixed before any test site was looked at too, under the same preregistration.</p>
225
+ <p><b>Not shown.</b> Neither a measurement nor a certificate holds for that scroll, or a control failed. The card says why. Where a test ran, the card still shows its shares, intervals and controls, so you can see how far it is from the bar; nothing on it is a claim.</p>
226
+ <h2>Limits</h2>
227
+ <ul>
228
+ <li>A snap moves along the normal only. It cannot fix a label that follows the wrong wrap, and where two sheets touch the face it finds can belong to the neighbor.</li>
229
+ <li>Only scored strata carry a claim, and a share is a share of test sites, not of every vertex.</li>
230
+ <li>The truth face is itself found by a rule on the finer scan, so it is model output too.</li>
231
+ <li>A certificate rests on its stated assumptions. The certificate code and the snapping code are not public.</li>
232
+ <li>Only the scrolls in the dataset appear here. The meshes are geometry: they say nothing about ink or text.</li>
233
+ </ul>
234
+ <h2>What each number is</h2>
235
+ <div class="vx-tw"><table class="vx-labels"><thead><tr><th>On this tab</th><th>Claim label</th></tr></thead><tbody>
236
+ <tr><td>Shares, bounds, histograms</td><td>Model output, measured</td></tr>
237
+ <tr><td>A certificate's bound</td><td>Model output, certified</td></tr>
238
+ <tr><td>Snapped and truth lines</td><td>Model output</td></tr>
239
+ <tr><td>Which scrolls are prize-eligible</td><td id="vx-elig">Sourced fact</td></tr>
240
+ </tbody></table></div>
241
+ <p class="hint">Source: <span id="vx-src"></span>.</p>
242
+ <p class="credit">Scans and source meshes: Vesuvius Challenge open data, CC BY-NC 4.0; the snapped meshes are shared under the same terms. Page code Apache 2.0. Made by Chase Hendrick.</p>
243
+ </section>
244
+ </section>
245
+
246
  <section data-pane="how" hidden class="prose">
247
  <figure class="fig"><img src="https://huggingface.co/datasets/SharpMeow/vesuvius-snapped-labels/resolve/main/snapped-0139.gif" width="600" height="600" alt="PHerc0139 segment w054 on CT at z 8048: the published label, then the snapped label, then both with the moves, at four stops along the label" loading="lazy"><figcaption>PHerc0139 w054, z 8048: published (orange), snapped (blue), both.</figcaption></figure>
248
  <h2>What a snap does</h2>
 
360
  </section>
361
  </main>
362
  <script src="app.js"></script>
363
+ <script src="vx.js"></script>
364
  </body>
365
  </html>
style.css CHANGED
@@ -79,3 +79,106 @@ tbody tr { cursor: pointer; } tbody tr:hover { background: var(--bg); }
79
  .side { order: 1; } /* on a phone the viewer comes first and the list follows it */
80
  .zl input[type="range"] { width: 160px; }
81
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  .side { order: 1; } /* on a phone the viewer comes first and the list follows it */
80
  .zl input[type="range"] { width: 160px; }
81
  }
82
+
83
+ /* ---------------------------------------------------------------- Voxel exact tab */
84
+ :root { --band: #d9c6a3; --neutral: #c9ccd6; --neg: #2f7bf6; --pos: #d9661a; --heldcell: #ece6ee; --tol: rgba(47, 123, 246, .14);
85
+ --ok: #1f8a5b; --bad: #c4352b; --warnbg: #fff4d6; --warnink: #6b4a00; --warnline: #e8b931; }
86
+ @media (prefers-color-scheme: dark) {
87
+ :root { --band: #6d5d43; --neutral: #3d4256; --heldcell: #2a2433; --tol: rgba(110, 160, 255, .18); --ok: #4cc38a; --bad: #ff7a6e;
88
+ --warnbg: #3a2f10; --warnink: #ffe39a; --warnline: #c99a1c; }
89
+ }
90
+ .vx-banner { background: repeating-linear-gradient(135deg, var(--warnbg) 0 14px, color-mix(in srgb, var(--warnbg) 70%, var(--warnline)) 14px 28px);
91
+ color: var(--warnink); border-bottom: 2px solid var(--warnline); padding: 8px 24px; font-size: .95em; }
92
+ .vx-banner.inline { border: 2px solid var(--warnline); border-radius: 10px; margin: 0 0 12px; }
93
+ .vx-error { background: var(--panel); border: 2px solid var(--bad); border-radius: 10px; padding: 8px 12px; }
94
+ .vx { max-width: 1100px; }
95
+ .vx-intro h2, .vx-showwrap h2 { font-size: 1.15em; margin: .2em 0 .3em; }
96
+ .vx-intro p { color: var(--mute); max-width: 60em; margin: 0 0 8px; }
97
+ .vx-tiles { padding: 4px 0 12px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
98
+ .vx-t-measured b { color: var(--snap) !important; } .vx-t-certified b { color: var(--accent) !important; } .vx-t-not_shown b { color: var(--mute) !important; }
99
+ .vx-cards { display: grid; gap: 14px; }
100
+ .vx-card { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px 16px; }
101
+ .vx-card.c-measured { border-left: 4px solid var(--snap); } .vx-card.c-certified { border-left: 4px solid var(--accent); }
102
+ .vx-card.c-not_shown { border-left: 4px dashed var(--mute); }
103
+ .vx-head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 6px 16px; }
104
+ .vx-head h3 { margin: 0; font-size: 1.2em; }
105
+ .vx-head small { color: var(--mute); }
106
+ .vx-badge { font-size: .82em; color: var(--mute); border: 1px solid var(--line); border-radius: 99px; padding: 2px 10px; }
107
+ .vx-badge b { color: var(--ink); margin-right: 4px; }
108
+ .vx-headline { font-size: 1.05em; margin: 8px 0 0; }
109
+ .vx-headline b { font-size: 1.35em; }
110
+ .vx-card h4 { font-size: .9em; margin: 12px 0 6px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
111
+ .vx-meter { margin-top: 10px; gap: 2px; background: transparent; }
112
+ .vx-meter i:first-child { border-radius: 99px 0 0 99px; } .vx-meter i:last-child { border-radius: 0 99px 99px 0; }
113
+ .vx-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 2px; }
114
+ .vx-chip { font: inherit; font-size: .85em; border: 1px solid var(--line); background: var(--bg); color: var(--ink); border-radius: 8px; padding: 4px 10px; cursor: pointer; }
115
+ .vx-chip.sub { margin-left: 18px; }
116
+ .vx-chip[aria-pressed="true"] { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
117
+ .vx-chip .vx-v { font-weight: 600; margin-left: 4px; }
118
+ .vx-chip.v-pass .vx-v { color: var(--ok); } .vx-chip.v-fail .vx-v { color: var(--bad); }
119
+ .vx-chip.v-not-shown .vx-v, .vx-chip.v-unanswerable .vx-v, .vx-chip.v-void .vx-v { color: var(--mute); }
120
+ .vx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 22px; }
121
+ .vx-tol { display: grid; gap: 8px; }
122
+ .vx-tolrow { display: grid; grid-template-columns: 70px 1fr 150px; align-items: center; gap: 10px; font-size: .9em; }
123
+ .vx-toll { color: var(--mute); }
124
+ .vx-track { position: relative; height: 14px; border-radius: 4px; background: color-mix(in srgb, var(--line) 70%, transparent); }
125
+ .vx-track.na { background: repeating-linear-gradient(135deg, transparent 0 5px, var(--line) 5px 6px); display: flex; align-items: center; padding-left: 8px; }
126
+ .vx-track.na em { font-size: .8em; color: var(--mute); }
127
+ .vx-fill { position: absolute; left: 0; top: 2px; bottom: 2px; border-radius: 0 4px 4px 0; background: var(--snap); }
128
+ .vx-whisk { position: absolute; top: 50%; height: 0; border-top: 2px solid var(--ink); }
129
+ .vx-whisk::before, .vx-whisk::after { content: ""; position: absolute; top: -6px; height: 10px; border-left: 2px solid var(--ink); }
130
+ .vx-whisk::before { left: 0; } .vx-whisk::after { right: 0; }
131
+ .vx-barline { position: absolute; top: -4px; bottom: -4px; border-left: 2px dashed var(--ink); }
132
+ .vx-tolv b { font-variant-numeric: tabular-nums; } .vx-tolv small { color: var(--mute); display: block; font-size: .78em; }
133
+ .vx-hist { width: 100%; height: auto; display: block; color: var(--ink); }
134
+ .vx-hist .vx-band { fill: var(--tol); }
135
+ .vx-hist .vx-hbar { fill: var(--snap); }
136
+ .vx-hist .vx-hpub { fill: none; stroke: var(--pub); stroke-width: 2; stroke-linejoin: round; }
137
+ .vx-hist .vx-base { stroke: var(--line); stroke-width: 1; }
138
+ .vx-hist .vx-zero { stroke: var(--mute); stroke-width: 1; }
139
+ .vx-hist .vx-tick { stroke: var(--mute); } .vx-hist .vx-tickt { fill: var(--mute); font-size: 10px; }
140
+ .vx-pubtog { font-weight: 400; font-size: .95em; }
141
+ .vx-ctl { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2px 18px; font-size: .86em; }
142
+ .vx-ctl li.yes span { color: var(--ok); font-weight: 700; } .vx-ctl li.no span { color: var(--bad); font-weight: 700; }
143
+ .vx-not { margin-top: 10px; }
144
+ .vx-assume { margin: 4px 0 8px; font-size: .9em; }
145
+ .vx-num summary { cursor: pointer; color: var(--accent); margin-top: 6px; }
146
+ .vx-num table { font-size: .82em; }
147
+ .vx-showwrap { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px 16px; margin: 16px 0; }
148
+ .vx-showgrid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 576px); gap: 16px; align-items: start; }
149
+ .vx-heatwrap { position: relative; min-width: 0; }
150
+ #vx-heat { width: 100%; height: auto; image-rendering: pixelated; border-radius: 8px; border: 1px solid var(--line); cursor: pointer; }
151
+ .vx-tip { position: absolute; background: var(--ink); color: var(--bg); font-size: .8em; padding: 3px 8px; border-radius: 6px; pointer-events: none; white-space: nowrap; }
152
+ .vx-heatlegend { margin-top: 6px; align-items: center; }
153
+ .vx-grad { display: inline-block; width: 120px; height: 10px; border-radius: 3px; background: linear-gradient(90deg, var(--neg), var(--neutral), var(--pos)); }
154
+ .legend i.hatch { background: repeating-linear-gradient(135deg, transparent 0 2px, var(--mute) 2px 3px); border: 1px solid var(--line); }
155
+ .vx-modes { border: 0; margin: 0; padding: 0; display: inline-flex; gap: 10px; align-items: center; flex-wrap: wrap; font-size: .88em; color: var(--mute); }
156
+ .vx-modes legend { float: left; margin-right: 6px; }
157
+ .dot.truth { background: transparent; border: 2px dashed var(--ink); width: 12px; height: 12px; }
158
+ #vx-cv { cursor: default; }
159
+ .vx-how { max-width: none; margin-top: 16px; }
160
+ .vx-diagram { width: 100%; max-width: 680px; height: auto; display: block; color: var(--ink); font: 13px system-ui, sans-serif; }
161
+ .vx-diagram .d-band { fill: var(--band); }
162
+ .vx-diagram .d-tol { fill: var(--tol); }
163
+ .vx-diagram .d-truth { fill: none; stroke: var(--ink); stroke-width: 2; stroke-dasharray: 6 5; }
164
+ .vx-diagram .d-recto { fill: none; stroke: var(--snap); stroke-width: 2.5; }
165
+ .vx-diagram .d-verso { fill: none; stroke: var(--mute); stroke-width: 2; stroke-dasharray: 2 4; }
166
+ .vx-diagram .d-arrow, .vx-diagram .d-normal { stroke: currentColor; stroke-width: 2; }
167
+ .vx-diagram .d-lead { stroke: var(--mute); stroke-width: 1; }
168
+ .vx-diagram .d-pub { fill: var(--pub); stroke: var(--panel); stroke-width: 2; }
169
+ .vx-diagram .d-snap { fill: var(--snap); stroke: var(--panel); stroke-width: 2; }
170
+ .vx-diagram .d-t { fill: currentColor; } .vx-diagram .muted { fill: var(--mute); }
171
+ .vx-flow { list-style: none; counter-reset: f; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; }
172
+ .vx-flow li { counter-increment: f; position: relative; background: var(--bg); border: 1px solid var(--line); border-radius: 10px; padding: 26px 10px 10px; font-size: .86em; }
173
+ .vx-flow li::before { content: counter(f); position: absolute; top: 6px; left: 10px; font-weight: 700; color: var(--accent); }
174
+ .vx-tw { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; }
175
+ .vx-tw th, .vx-tw td { white-space: normal; overflow-wrap: anywhere; padding: 5px 8px; }
176
+ #vx-prereg { overflow-wrap: anywhere; word-break: break-all; }
177
+ button.anim { font: inherit; font-size: .9em; border: 1px solid var(--line); background: var(--panel); color: var(--ink); border-radius: 99px; padding: 3px 12px; cursor: pointer; }
178
+ button.anim[aria-pressed="true"] { border-color: var(--snap); box-shadow: inset 0 0 0 1px var(--snap); }
179
+ @media (max-width: 760px) {
180
+ .vx-banner { padding-left: 16px; padding-right: 16px; }
181
+ .vx-grid, .vx-showgrid { grid-template-columns: 1fr; }
182
+ .vx-tolrow { grid-template-columns: 60px 1fr 110px; }
183
+ .vx-ctl { grid-template-columns: 1fr; }
184
+ }
vx.js ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Snapped Labels, "Voxel exact" tab: per-scroll claim levels (Measured, Certified, Not shown), tolerance bars, abstain
2
+ // strata, residual histograms and controls, read from vx/summary.json (schema snapped-labels/voxel-exact@1, checked by
3
+ // python -m kit vxsummary check). ?vx=URL reads another summary; ?vx=fixture reads the synthetic fixture, always under a
4
+ // banner. The tab stays hidden until a summary loads and validates. Loaded after app.js and uses its helpers.
5
+ 'use strict';
6
+
7
+ const VX = { doc: null, url: null, fixture: false, show: null, showIdx: 0, mode: 'signed', pick: {} };
8
+ const VX_SCHEMA = 'snapped-labels/voxel-exact@1';
9
+ const VX_CLAIMS = { measured: 'Measured', certified: 'Certified', not_shown: 'Not shown' };
10
+ const VX_ORDER = { measured: 0, certified: 1, not_shown: 2 };
11
+ const VX_TOLS = ['0.5', '1', '2'];
12
+ // a verdict is text plus a shape, never color alone
13
+ const VX_ICON = { PASS: '✓', FAIL: '✕', 'NOT SHOWN': '○', UNANSWERABLE: '?', VOID: '⊘' };
14
+ const VX_STRATUM_NAME = { C0: 'snapped (code 0)', C0L: 'snapped, placed in the label scan (code 0)', C2: 'filled (code 2)', C1: 'left in place (code 1)' };
15
+
16
+ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
17
+ const p1 = (x) => (x == null || !Number.isFinite(x) ? 'n/a' : `${(100 * x).toFixed(1)}%`);
18
+ const sgn = (x) => (x > 0 ? `+${x.toFixed(2)}` : x.toFixed(2));
19
+
20
+ function vxURL() {
21
+ const q = params.get('vx');
22
+ if (q === 'fixture') return new URL('vx/fixture/summary.json', location.href).href;
23
+ return new URL(q || 'vx/summary.json', location.href).href;
24
+ }
25
+
26
+ // The page's own check, a subset of kit/vxsummary.py: enough that a broken file never draws as numbers.
27
+ function vxValid(d) {
28
+ const bad = [];
29
+ if (!d || d.schema !== VX_SCHEMA) bad.push(`schema is not ${VX_SCHEMA}`);
30
+ if (!d || !Array.isArray(d.scrolls) || !d.scrolls.length) bad.push('no scrolls');
31
+ for (const s of (d && d.scrolls) || []) {
32
+ if (!(s.claim in VX_CLAIMS)) bad.push(`${s.scroll}: claim ${s.claim}`);
33
+ for (const st of s.strata || []) for (const t of VX_TOLS) {
34
+ const w = st.within && st.within[t];
35
+ if (w && !(w.n >= w.k && w.k >= 0 && w.share >= 0 && w.share <= 1)) bad.push(`${s.scroll} ${st.id} within ${t}`);
36
+ }
37
+ }
38
+ if (d && typeof d.fixture !== 'boolean') bad.push('fixture flag missing');
39
+ return bad;
40
+ }
41
+
42
+ // ---------------------------------------------------------------- small charts
43
+
44
+ function tolBars(st, rule) {
45
+ const bar = rule && rule.bar != null ? rule.bar : 0.9;
46
+ return `<div class="vx-tol" role="list">${VX_TOLS.map((t) => {
47
+ const w = st.within[t];
48
+ const label = `${t === '1' ? '1 voxel' : `${t} voxels`}`;
49
+ if (!w) return `<div class="vx-tolrow" role="listitem"><span class="vx-toll">${label}</span><span class="vx-track na"><em>not measured</em></span><span class="vx-tolv">n/a</span></div>`;
50
+ const lo = w.lower != null ? w.lower : null, hi = w.upper != null ? w.upper : null;
51
+ const whisk = lo != null ? `<i class="vx-whisk" style="left:${100 * lo}%;width:${100 * ((hi != null ? hi : w.share) - lo)}%"></i>` : '';
52
+ const line = t === '1' ? `<i class="vx-barline" style="left:${100 * bar}%" title="the bar, ${p1(bar)}"></i>` : '';
53
+ return `<div class="vx-tolrow" role="listitem"><span class="vx-toll">${label}</span>
54
+ <span class="vx-track" title="${w.k} of ${w.n} sites (${w.basis === 'itt' ? 'every selected site' : 'scorable sites'})${lo != null ? `; one-sided 95% interval ${p1(lo)} to ${p1(hi)}` : ''}">
55
+ <i class="vx-fill" style="width:${100 * w.share}%"></i>${whisk}${line}</span>
56
+ <span class="vx-tolv"><b>${p1(w.share)}</b> <small>${w.k} of ${w.n}${w.basis === 'itt' ? ', every selected site' : ', scorable sites'}</small></span></div>`;
57
+ }).join('')}</div>`;
58
+ }
59
+
60
+ function hist(st, showPub) {
61
+ const h = st.residual_hist, ph = st.published_hist;
62
+ if (!h || !h.edges || h.edges.length < 2) return '<p class="hint">No residual distribution for this stratum.</p>';
63
+ const W = 360, H = 130, L = 8, B = 22, T = 10;
64
+ const e = h.edges, lo = e[0], hi = e[e.length - 1];
65
+ const X = (v) => L + (W - 2 * L) * (v - lo) / (hi - lo);
66
+ const all = [...h.counts, ...(showPub && ph && ph.counts ? ph.counts : [])];
67
+ const max = Math.max(1, ...all);
68
+ const Y = (c) => H - B - (H - B - T) * c / max;
69
+ let bars = '';
70
+ h.counts.forEach((c, i) => {
71
+ if (!c) return;
72
+ const x0 = X(e[i]) + 1, x1 = X(e[i + 1]) - 1;
73
+ bars += `<rect class="vx-hbar" x="${x0.toFixed(1)}" y="${Y(c).toFixed(1)}" width="${Math.max(1, x1 - x0).toFixed(1)}" height="${(H - B - Y(c)).toFixed(1)}" rx="1.5"><title>${e[i]} to ${e[i + 1]} voxels: ${c} sites</title></rect>`;
74
+ });
75
+ let pub = '';
76
+ if (showPub && ph && ph.counts && ph.edges && ph.edges.length === ph.counts.length + 1) {
77
+ let d = `M${X(ph.edges[0]).toFixed(1)},${Y(0).toFixed(1)}`;
78
+ ph.counts.forEach((c, i) => { d += `L${X(ph.edges[i]).toFixed(1)},${Y(c).toFixed(1)}L${X(ph.edges[i + 1]).toFixed(1)},${Y(c).toFixed(1)}`; });
79
+ d += `L${X(ph.edges[ph.edges.length - 1]).toFixed(1)},${Y(0).toFixed(1)}`;
80
+ pub = `<path class="vx-hpub" d="${d}"><title>published label</title></path>`;
81
+ }
82
+ const ticks = [-4, -2, -1, 0, 1, 2, 4].filter((v) => v >= lo && v <= hi)
83
+ .map((v) => `<line class="vx-tick" x1="${X(v)}" x2="${X(v)}" y1="${H - B}" y2="${H - B + 4}"/><text class="vx-tickt" x="${X(v)}" y="${H - 6}" text-anchor="middle">${v > 0 ? '+' + v : v}</text>`).join('');
84
+ const out = (h.under || h.over) ? `<text class="vx-tickt" x="${W - L}" y="${T + 2}" text-anchor="end">${h.under} below ${lo}, ${h.over} above +${hi}</text>` : '';
85
+ return `<svg class="vx-hist" viewBox="0 0 ${W} ${H}" role="img" aria-label="Signed residual of the snapped vertices, in label voxels, ${h.counts.reduce((a, b) => a + b, 0)} sites${showPub ? ', with the published label' : ''}">
86
+ <rect class="vx-band" x="${X(-1)}" y="${T}" width="${X(1) - X(-1)}" height="${H - B - T}"><title>within 1 voxel</title></rect>
87
+ <line class="vx-base" x1="${L}" x2="${W - L}" y1="${H - B}" y2="${H - B}"/>
88
+ ${bars}${pub}
89
+ <line class="vx-zero" x1="${X(0)}" x2="${X(0)}" y1="${T}" y2="${H - B}"/>
90
+ ${ticks}${out}</svg>`;
91
+ }
92
+
93
+ function meter(s) {
94
+ const v = s.vertices, by = v.by_code, tot = Math.max(1, v.released + v.held_back);
95
+ const part = (n, color) => (n ? `<i style="width:calc(${100 * n / tot}% - 2px);background:${color}"></i>` : '');
96
+ return `<div class="meter vx-meter">${part(by['0'], 'var(--snap)')}${part(by['2'], 'var(--fill)')}${part(by['1'], 'var(--pub)')}${part(v.held_back, 'var(--held)')}</div>
97
+ <div class="legend"><span><i style="background:var(--snap)"></i>snapped ${p1(by['0'] / tot)}</span><span><i style="background:var(--fill)"></i>filled ${p1(by['2'] / tot)}</span><span><i style="background:var(--pub)"></i>left in place ${p1(by['1'] / tot)}</span><span><i style="background:var(--held)"></i>held back ${p1(v.held_back / tot)}</span></div>`;
98
+ }
99
+
100
+ function chip(st, on, scroll) {
101
+ const icon = VX_ICON[st.verdict] || '';
102
+ return `<button class="vx-chip v-${st.verdict.replace(' ', '-').toLowerCase()}${st.id === 'C0L' ? ' sub' : ''}" data-scroll="${esc(scroll)}" data-stratum="${esc(st.id)}" aria-pressed="${on}" title="${esc(st.why)}">
103
+ <b>${esc(st.id)}</b> ${esc(VX_STRATUM_NAME[st.id] || '')} <span class="vx-v"><span aria-hidden="true">${icon}</span> ${esc(st.verdict)}</span></button>`;
104
+ }
105
+
106
+ function controls(st) {
107
+ if (!st.controls || !st.controls.length) return '';
108
+ return `<ul class="vx-ctl">${st.controls.map((c) => {
109
+ const state = c.holds == null ? 'not measured' : c.holds ? 'holds' : 'does not hold';
110
+ return `<li class="${c.holds === false ? 'no' : c.holds ? 'yes' : ''}"><span aria-hidden="true">${c.holds == null ? '·' : c.holds ? '✓' : '✕'}</span> ${esc(c.name)}: ${p1(c.observed)} within 1 voxel, required ${esc(c.require)} ${p1(c.limit)}: <b>${state}</b></li>`;
111
+ }).join('')}</ul>`;
112
+ }
113
+
114
+ function numbers(s) {
115
+ const rows = [];
116
+ for (const st of s.strata || []) {
117
+ for (const t of VX_TOLS) {
118
+ const w = st.within[t];
119
+ rows.push(`<tr><td>${esc(st.id)}</td><td>${esc(st.verdict)}</td><td>${t}</td><td class="n">${w ? w.k : ''}</td><td class="n">${w ? w.n : ''}</td><td class="n">${w ? p1(w.share) : 'not measured'}</td><td class="n">${w && w.lower != null ? p1(w.lower) : ''}</td><td class="n">${w && w.upper != null ? p1(w.upper) : ''}</td><td>${w ? esc(w.basis) : ''}</td></tr>`);
120
+ }
121
+ }
122
+ const v = s.vertices;
123
+ return `<details class="vx-num"><summary>Show numbers</summary>
124
+ <p class="hint">Vertices released ${fmt(v.released)} (code 0: ${fmt(v.by_code['0'])}, code 2: ${fmt(v.by_code['2'])}, code 1: ${fmt(v.by_code['1'])}), held back ${fmt(v.held_back)}. Label scan ${esc(s.label_scan.voxel_um)} µm${s.truth ? `; truth ${esc(s.truth.voxel_um)} µm scan` : ''}${s.oracle ? `; positive control ${esc(s.oracle.voxel_um)} µm scan` : ''}.${s.void && s.void.length ? ` Void: ${esc(s.void.join('; '))}.` : ''}</p>
125
+ ${rows.length ? `<div class="vx-tw"><table><thead><tr><th>Stratum</th><th>Verdict</th><th>Voxels</th><th class="n">k</th><th class="n">n</th><th class="n">Share</th><th class="n">Lower</th><th class="n">Upper</th><th>Basis</th></tr></thead><tbody>${rows.join('')}</tbody></table></div>` : ''}
126
+ ${s.certificate ? `<p class="hint">Certificate: ${esc(s.certificate.method)}, lower bound ${p1(s.certificate.lower)} at ${p1(s.certificate.level)} over ${fmt(s.certificate.n)} vertices.</p>` : ''}</details>`;
127
+ }
128
+
129
+ // ---------------------------------------------------------------- cards
130
+
131
+ function stratumOrder(strata) {
132
+ const rank = { C0: 0, C0L: 1, C2: 2, C1: 3 };
133
+ return strata.slice().sort((a, b) => (rank[a.id] ?? 9) - (rank[b.id] ?? 9));
134
+ }
135
+
136
+ function strataBody(s, rule) {
137
+ const strata = stratumOrder(s.strata || []);
138
+ const prim = strata.find((x) => x.primary) || strata[0];
139
+ const cur = strata.find((x) => x.id === VX.pick[s.scroll]) || prim;
140
+ const one = prim && prim.within['1'];
141
+ const pubc = prim && (prim.controls || []).find((c) => /^published/.test(c.name));
142
+ const head2 = one ? `<p class="vx-headline"><b>${p1(one.share)}</b> within 1 voxel (${esc(prim.id)}, <span aria-hidden="true">${VX_ICON[prim.verdict] || ''}</span> ${esc(prim.verdict)}). One-sided 95% lower bound ${p1(one.lower)} (bar ${p1(rule.bar)}).</p>
143
+ <p class="hint">${pubc ? `${esc(pubc.name.replace(/^published label/, 'Published label'))}: ${p1(pubc.observed)}.` : ''}${s.void && s.void.length ? ` The test is void: ${esc(s.void.join('; '))}.` : ''}</p>` : '';
144
+ const pubOn = !!VX.pick[`${s.scroll}:pub`];
145
+ return `${head2}
146
+ ${meter(s)}
147
+ <div class="vx-chips" role="group" aria-label="Strata">${strata.map((x) => chip(x, x.id === cur.id, s.scroll)).join('')}</div>
148
+ <div class="vx-grid">
149
+ <div><h4>${esc(cur.id)}: share within each tolerance</h4>${tolBars(cur, rule)}
150
+ <p class="hint">${esc(cur.verdict)}: ${esc(cur.why)}${cur.instrument_limited ? ' (limited by the instrument)' : ''}. ${cur.n_selected} sites selected, ${cur.n_scorable} scorable. Whiskers: one-sided 95% interval. Dashed line: the bar.</p></div>
151
+ <div><h4>${esc(cur.id)}: signed residual, voxels <label class="tog vx-pubtog"><input type="checkbox" data-pub="${esc(s.scroll)}"${pubOn ? ' checked' : ''}><i class="dot pub"></i>published label</label></h4>
152
+ ${hist(cur, pubOn)}
153
+ <p class="hint">${esc(cur.residual_hist && cur.residual_hist.sign ? cur.residual_hist.sign : '')}. Shaded: within 1 voxel.${cur.median_abs != null ? ` Median distance ${cur.median_abs.toFixed(2)} voxels.` : ''}</p></div>
154
+ </div>
155
+ <h4>Controls (each must be able to fail)</h4>${controls(cur)}
156
+ <p class="hint vx-not">What this does not show: labels that follow the wrong wrap, sheets that touch with no gap, and any scroll, segment or region not scored here.</p>
157
+ ${numbers(s)}`;
158
+ }
159
+
160
+ function scrollCard(s) {
161
+ const rule = VX.doc.rule || {};
162
+ const badge = { measured: 'Model output, measured against an independent finer scan', certified: 'Model output, truth-free certificate', not_shown: (s.strata || []).length ? 'tested; no claim' : 'no claim' }[s.claim];
163
+ const head = `<header class="vx-head"><div><h3>${esc(s.scroll)}</h3><small>label scan ${esc(s.label_scan.voxel_um)} µm voxels${s.truth ? `, truth from a ${esc(s.truth.voxel_um)} µm scan` : ''}</small></div>
164
+ <span class="vx-badge c-${s.claim}"><b>${VX_CLAIMS[s.claim]}</b> ${badge}</span></header>`;
165
+ if (s.claim === 'not_shown') {
166
+ const tested = (s.strata || []).length > 0;
167
+ return `<article class="vx-card c-${s.claim}" data-scroll="${esc(s.scroll)}">${head}<p>Not shown: ${esc(s.why_not_shown)}.${tested ? ' The numbers below are what the test measured (model output); they make no claim.' : ''}</p>${tested ? strataBody(s, rule) : `${meter(s)}${numbers(s)}`}</article>`;
168
+ }
169
+ if (s.claim === 'certified') {
170
+ const c = s.certificate;
171
+ return `<article class="vx-card c-${s.claim}" data-scroll="${esc(s.scroll)}">${head}
172
+ <p class="vx-headline">One-sided ${p1(c.level)} lower bound <b>${p1(c.lower)}</b> within 1 voxel (bar ${p1(rule.bar)}).</p>
173
+ <p class="hint">No truth scan; lower bound from the certificate, over ${fmt(c.n)} vertices. It rests on these assumptions:</p>
174
+ <ul class="vx-assume">${c.assumptions.map((a) => `<li>${esc(a)}</li>`).join('')}</ul>
175
+ ${meter(s)}${numbers(s)}</article>`;
176
+ }
177
+ return `<article class="vx-card c-${s.claim}" data-scroll="${esc(s.scroll)}">${head}${strataBody(s, rule)}</article>`;
178
+ }
179
+
180
+ function renderCards() {
181
+ const d = VX.doc;
182
+ const sc = d.scrolls.slice().sort((a, b) => VX_ORDER[a.claim] - VX_ORDER[b.claim]);
183
+ const count = (c) => d.scrolls.filter((s) => s.claim === c).length;
184
+ $('vx-tiles').innerHTML = ['measured', 'certified', 'not_shown'].map((c) => `<div class="stat vx-t-${c}"><b>${count(c)}</b><span>scrolls ${VX_CLAIMS[c].toLowerCase()}</span></div>`).join('');
185
+ $('vx-cards').innerHTML = sc.map(scrollCard).join('');
186
+ $('vx-cards').querySelectorAll('.vx-chip').forEach((b) => { b.onclick = () => { VX.pick[b.dataset.scroll] = b.dataset.stratum; renderCards(); }; });
187
+ $('vx-cards').querySelectorAll('input[data-pub]').forEach((c) => { c.onchange = () => { VX.pick[`${c.dataset.pub}:pub`] = c.checked; renderCards(); }; });
188
+ const pre = d.prereg || {};
189
+ $('vx-prereg').textContent = `${pre.sha256 || 'n/a'}, frozen ${pre.frozen || 'n/a'}`;
190
+ const pr = d.prizes_read || {};
191
+ $('vx-elig').innerHTML = d.fixture ? 'The fixture\'s scrolls are made up.' : `Sourced fact: none of these scrolls is on the prize lists at <a href="${esc(/^https:\/\//.test(String(pr.url)) ? pr.url : 'https://scrollprize.org/prizes')}" target="_blank" rel="noopener">scrollprize.org/prizes</a> (read ${esc(pr.date)}).`;
192
+ $('vx-src').textContent = `${d.dataset.repo} ${d.dataset.release} at ${d.dataset.revision.slice(0, 12)}, summary ${d.generated}${d.fixture ? ', synthetic fixture' : ''}`;
193
+ }
194
+
195
+ // ---------------------------------------------------------------- showcase: residual heatmap and CT with the truth face
196
+
197
+ const RS = 12; // floats per showcase line piece: published, snapped, truth (ax ay bx by each)
198
+
199
+ async function openShowcase(segment) {
200
+ const list = (VX.doc && VX.doc.showcase) || [];
201
+ const e = list.find((x) => x.segment === segment) || list[0];
202
+ if (!e) throw new Error('no showcase segment');
203
+ const base = new URL(e.file, VX.url).href;
204
+ const meta = await getJSON(base);
205
+ // the grids are plain number lists (null: no value)
206
+ const res = Float32Array.from(meta.residual, (v) => (v == null ? NaN : v)), state = Uint8Array.from(meta.state);
207
+ const xyz = Float32Array.from(meta.xyz), rows = Float32Array.from(meta.rows);
208
+ VX.show = { meta, res, state, xyz, rows, plane: 0, cx: 0, cy: 0, win: Number($('vx-win').value) || 192, ct: null,
209
+ mode: (document.querySelector('input[name="vx-mode"]:checked') || {}).value || 'signed' };
210
+ $('vx-show-title').textContent = `${meta.scroll} · ${meta.segment}`;
211
+ $('vx-truth-note').textContent = meta.truth_note;
212
+ const mid = Math.floor(meta.planes.length / 2);
213
+ pickPlane(mid);
214
+ heat();
215
+ await showDraw();
216
+ }
217
+
218
+ function pickPlane(i) {
219
+ // center on a piece a quarter of the way along the plane's line
220
+ const V = VX.show, p = V.meta.planes[i];
221
+ V.plane = i;
222
+ if (!p || !p.count) return;
223
+ const q = RS * (p.first + Math.floor(p.count / 4));
224
+ V.cx = V.rows[q + 4]; V.cy = V.rows[q + 5];
225
+ }
226
+
227
+ function divColor(v, css) {
228
+ // diverging: blue (negative) to a neutral gray to orange (positive), clamped at +-2 voxels
229
+ const t = Math.max(-1, Math.min(1, v / 2));
230
+ const hex = (s) => { const m = s.trim().replace('#', ''); return [0, 2, 4].map((i) => parseInt(m.slice(i, i + 2), 16)); };
231
+ const neg = hex(css.getPropertyValue('--neg') || '#2f7bf6'), pos = hex(css.getPropertyValue('--pos') || '#e0701a'), mid = hex(css.getPropertyValue('--neutral') || '#c9ccd6');
232
+ const a = t < 0 ? neg : pos, k = Math.abs(t);
233
+ return `rgb(${mid.map((m, i) => Math.round(m + (a[i] - m) * k)).join(',')})`;
234
+ }
235
+
236
+ function heat() {
237
+ const V = VX.show; if (!V) return;
238
+ const [h, w] = V.meta.shape, cv2 = $('vx-heat'), g = cv2.getContext('2d');
239
+ const cw = Math.max(2, Math.floor(Math.min(900, cv2.parentNode.clientWidth || 600) / w));
240
+ cv2.width = w * cw; cv2.height = h * cw;
241
+ const css = getComputedStyle(document.documentElement);
242
+ const surf = css.getPropertyValue('--panel').trim() || '#fff', line = css.getPropertyValue('--line').trim() || '#ccc';
243
+ const tol = V.mode === 'signed' ? null : Number(V.mode);
244
+ g.fillStyle = surf; g.fillRect(0, 0, cv2.width, cv2.height);
245
+ for (let i = 0; i < h; i++) for (let j = 0; j < w; j++) {
246
+ const k = i * w + j, s = V.state[k], x = j * cw, y = i * cw;
247
+ if (s === 2) { g.fillStyle = css.getPropertyValue('--heldcell').trim() || '#ddd'; g.fillRect(x, y, cw, cw); continue; } // held back: never drawn
248
+ if (s === 1 || !Number.isFinite(V.res[k])) { // no value: hatched
249
+ g.strokeStyle = line; g.lineWidth = 1; g.beginPath(); g.moveTo(x, y + cw); g.lineTo(x + cw, y); g.stroke(); continue;
250
+ }
251
+ const v = V.res[k];
252
+ g.fillStyle = tol == null ? divColor(v, css) : (Math.abs(v) <= tol ? css.getPropertyValue('--snap').trim() : css.getPropertyValue('--pub').trim());
253
+ g.fillRect(x, y, cw, cw);
254
+ }
255
+ const p = V.meta.planes[V.plane];
256
+ if (p) { const i = V.meta.planes.indexOf(p); g.strokeStyle = css.getPropertyValue('--ink').trim(); g.lineWidth = 2; g.strokeRect(1, i * cw + 1, cv2.width - 2, cw - 2); }
257
+ V.cw = cw;
258
+ $('vx-heat-legend').innerHTML = tol == null
259
+ ? `<span class="vx-grad" aria-hidden="true"></span><span>-2</span><span>0</span><span>+2 voxels (clamped)</span><span><i class="hatch"></i>no value</span><span><i style="background:var(--heldcell)"></i>held back for evaluation</span>`
260
+ : `<span><i style="background:var(--snap)"></i>within ${tol} voxel${tol === 1 ? '' : 's'}</span><span><i style="background:var(--pub)"></i>not within</span><span><i class="hatch"></i>no value</span><span><i style="background:var(--heldcell)"></i>held back for evaluation</span>`;
261
+ }
262
+
263
+ async function showDraw() {
264
+ const V = VX.show; if (!V) return;
265
+ const cvs = $('vx-cv'), g = cvs.getContext('2d'), W = cvs.width, n = V.win, k = W / n;
266
+ const p = V.meta.planes[V.plane], z = p ? p.z : 0;
267
+ const x0 = Math.round(V.cx - n / 2), y0 = Math.round(V.cy - n / 2);
268
+ $('vx-z').textContent = `z ${z}`;
269
+ if (V.meta.volume) {
270
+ const key = `${z}|${y0}|${x0}|${n}`;
271
+ if (!V.ct || V.ct.key !== key) {
272
+ const buf = await ctWindow(V.meta.volume, z, y0, x0, n);
273
+ const img = new ImageData(n, n);
274
+ for (let i = 0; i < n * n; i++) { img.data[4 * i] = img.data[4 * i + 1] = img.data[4 * i + 2] = buf[i]; img.data[4 * i + 3] = 255; }
275
+ const off = document.createElement('canvas'); off.width = off.height = n; off.getContext('2d').putImageData(img, 0, 0);
276
+ V.ct = { key, img: off };
277
+ }
278
+ g.imageSmoothingEnabled = false; g.drawImage(V.ct.img, 0, 0, W, W);
279
+ } else {
280
+ g.fillStyle = '#15171f'; g.fillRect(0, 0, W, W);
281
+ g.fillStyle = 'rgba(255,255,255,.55)'; g.font = '14px system-ui, sans-serif'; g.fillText('Synthetic fixture: no CT', 14, 24);
282
+ }
283
+ const X = (x) => (x - x0 + 0.5) * k, Y = (y) => (y - y0 + 0.5) * k;
284
+ const css = getComputedStyle(document.documentElement);
285
+ const piece = (o, color, width, dash) => {
286
+ g.strokeStyle = color; g.lineWidth = width; g.setLineDash(dash || []); g.lineCap = 'round'; g.beginPath();
287
+ for (let q = p.first; q < p.first + p.count; q++) { const a = RS * q + o; g.moveTo(X(V.rows[a]), Y(V.rows[a + 1])); g.lineTo(X(V.rows[a + 2]), Y(V.rows[a + 3])); }
288
+ g.stroke(); g.setLineDash([]);
289
+ };
290
+ if (p && p.count && !V.meta.volume) {
291
+ // no CT in the fixture: a made-up sheet, drawn as a band along the made-up face
292
+ piece(8, css.getPropertyValue('--band').trim() || '#d9c6a3', 7 * k);
293
+ }
294
+ if (p && p.count) {
295
+ if ($('vx-l-pub').checked) piece(0, css.getPropertyValue('--pub').trim(), 2.5);
296
+ if ($('vx-l-snap').checked) piece(4, css.getPropertyValue('--snap').trim(), 2.5);
297
+ if ($('vx-l-truth').checked) { piece(8, '#11131c', 4.5, [7, 5]); piece(8, '#ffffff', 2, [7, 5]); }
298
+ }
299
+ g.fillStyle = '#fff'; g.fillRect(16, W - 22, 10 * k, 4);
300
+ g.font = '13px system-ui, sans-serif'; g.fillText(V.meta.voxel_um ? `10 voxels (${Math.round(10 * V.meta.voxel_um)} µm)` : '10 voxels', 16, W - 30);
301
+ }
302
+
303
+ function heatCell(ev) {
304
+ const V = VX.show; if (!V) return null;
305
+ const b = $('vx-heat').getBoundingClientRect(), [h, w] = V.meta.shape;
306
+ const j = Math.floor((ev.clientX - b.left) / b.width * w), i = Math.floor((ev.clientY - b.top) / b.height * h);
307
+ if (i < 0 || j < 0 || i >= h || j >= w) return null;
308
+ return { i, j, k: i * w + j };
309
+ }
310
+
311
+ // ---------------------------------------------------------------- wiring
312
+
313
+ function wireShowcase() {
314
+ const heatCv = $('vx-heat'), tip = $('vx-tip');
315
+ heatCv.addEventListener('mousemove', (ev) => {
316
+ const c = heatCell(ev), V = VX.show; if (!c) { tip.hidden = true; return; }
317
+ const s = V.state[c.k], v = V.res[c.k];
318
+ tip.textContent = s === 2 ? 'held back for evaluation' : (s === 1 || !Number.isFinite(v)) ? 'no value here' : `${sgn(v)} voxels (row ${c.i}, column ${c.j})`;
319
+ tip.hidden = false; tip.style.left = `${ev.offsetX + 14}px`; tip.style.top = `${ev.offsetY + 10}px`;
320
+ });
321
+ heatCv.addEventListener('mouseleave', () => { tip.hidden = true; });
322
+ heatCv.addEventListener('click', (ev) => {
323
+ const c = heatCell(ev), V = VX.show; if (!c || V.state[c.k] === 2) return;
324
+ const x = V.xyz[3 * c.k], y = V.xyz[3 * c.k + 1], z = V.xyz[3 * c.k + 2];
325
+ let best = Infinity, bi = 0;
326
+ V.meta.planes.forEach((p, i) => { const d = Math.abs(p.z - z); if (d < best) { best = d; bi = i; } });
327
+ V.plane = bi; V.cx = x; V.cy = y; heat(); showDraw();
328
+ });
329
+ document.querySelectorAll('input[name="vx-mode"]').forEach((r) => { r.onchange = () => { VX.show.mode = r.value; heat(); }; });
330
+ ['vx-l-pub', 'vx-l-snap', 'vx-l-truth'].forEach((id) => { $(id).onchange = () => showDraw(); });
331
+ $('vx-win').onchange = () => { VX.show.win = Number($('vx-win').value); showDraw(); };
332
+ window.addEventListener('resize', () => heat());
333
+ }
334
+
335
+ async function vxInit() {
336
+ VX.url = vxURL();
337
+ let d;
338
+ try {
339
+ const r = await fetch(VX.url, { credentials: 'omit' });
340
+ if (!r.ok) { if (params.get('vx')) throw new Error(`${r.status} ${VX.url}`); return; } // no results yet: the tab stays hidden
341
+ d = await r.json();
342
+ } catch (e) {
343
+ if (!params.get('vx')) return;
344
+ $('vx-tabbtn').hidden = false; $('vx-error').hidden = false; $('vx-error').textContent = `Could not load the voxel-exact summary: ${e.message}`; return;
345
+ }
346
+ const bad = vxValid(d);
347
+ if (bad.length) {
348
+ if (!params.get('vx')) return;
349
+ $('vx-tabbtn').hidden = false; $('vx-error').hidden = false; $('vx-error').textContent = `The voxel-exact summary does not validate: ${bad.slice(0, 3).join('; ')}`; return;
350
+ }
351
+ VX.doc = d; VX.fixture = d.fixture === true;
352
+ $('vx-tabbtn').hidden = false;
353
+ // a summary from ?vx=URL is someone else's file: say so, so a shared link cannot pass as this Space's results
354
+ if (params.get('vx') && params.get('vx') !== 'fixture') { $('vx-error').hidden = false; $('vx-error').textContent = `Loaded from ${VX.url} (the vx parameter), not this Space's own results.`; }
355
+ document.querySelectorAll('.vx-fixture').forEach((b) => { b.hidden = !VX.fixture; });
356
+ document.body.classList.toggle('vx-fx', VX.fixture);
357
+ renderCards();
358
+ if (typeof table === 'function') table();
359
+ const sc = d.showcase || [];
360
+ $('vx-show-none').hidden = sc.length > 0; $('vx-show').hidden = !sc.length;
361
+ if (sc.length) {
362
+ $('vx-show-pick').innerHTML = sc.map((s) => `<option value="${esc(s.segment)}">${esc(s.scroll)} · ${esc(s.segment)}</option>`).join('');
363
+ $('vx-show-pick').onchange = () => openShowcase($('vx-show-pick').value).catch((e) => { $('vx-show-title').textContent = e.message; });
364
+ wireShowcase();
365
+ await openShowcase(sc[0].segment).catch((e) => { $('vx-show-title').textContent = e.message; });
366
+ }
367
+ if (params.get('tab') === 'exact') tab('exact');
368
+ }
369
+
370
+ // the scroll's claim level for the All segments table (app.js)
371
+ window.vxClaim = (scroll) => {
372
+ if (!VX.doc) return null;
373
+ const s = VX.doc.scrolls.find((x) => x.scroll === scroll);
374
+ return s ? VX_CLAIMS[s.claim] : 'Not scored';
375
+ };
376
+
377
+ Object.assign(window.snappedLabels, {
378
+ voxelExact: () => (VX.doc ? JSON.parse(JSON.stringify(VX.doc)) : null),
379
+ openShowcase: (segment) => openShowcase(segment),
380
+ });
381
+
382
+ window.vxReady = vxInit();
vx/fixture/show/SYNTH-A/synthetic-sheet.json ADDED
The diff for this file is too large to render. See raw diff
 
vx/fixture/summary.json ADDED
@@ -0,0 +1,1042 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema": "snapped-labels/voxel-exact@1",
3
+ "fixture": true,
4
+ "generated": "2026-10-28T00:00:00Z",
5
+ "dataset": {
6
+ "repo": "SharpMeow/vesuvius-snapped-labels",
7
+ "release": "1.0.1",
8
+ "revision": "95432ec570d6f096d754e6ac16cafdeb83ca8a04"
9
+ },
10
+ "rule": {
11
+ "primary_tolerance": 1.0,
12
+ "tolerances": [
13
+ 0.5,
14
+ 1.0,
15
+ 2.0
16
+ ],
17
+ "bar": 0.9,
18
+ "interval": {
19
+ "method": "clopper-pearson",
20
+ "sided": "one",
21
+ "level": 0.95
22
+ },
23
+ "max_move": 16.0
24
+ },
25
+ "prereg": {
26
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
27
+ "frozen": "2026-10-28T00:00:00Z"
28
+ },
29
+ "prizes_read": {
30
+ "url": "https://scrollprize.org/prizes",
31
+ "date": "2026-10-28"
32
+ },
33
+ "scrolls": [
34
+ {
35
+ "scroll": "SYNTH-A",
36
+ "claim": "measured",
37
+ "why_not_shown": null,
38
+ "label_scan": {
39
+ "volume_id": "synthetic-label-scan",
40
+ "voxel_um": 9.0
41
+ },
42
+ "truth": {
43
+ "kind": "finer_scan",
44
+ "volume_id": "synthetic-fine-scan",
45
+ "voxel_um": 1.1
46
+ },
47
+ "oracle": {
48
+ "kind": "finer_scan",
49
+ "volume_id": "synthetic-mid-scan",
50
+ "voxel_um": 2.4
51
+ },
52
+ "certificate": null,
53
+ "vertices": {
54
+ "released": 120000,
55
+ "held_back": 18000,
56
+ "by_code": {
57
+ "0": 80000,
58
+ "2": 38800,
59
+ "1": 1200
60
+ }
61
+ },
62
+ "void": [],
63
+ "strata": [
64
+ {
65
+ "id": "C0",
66
+ "code": 0,
67
+ "primary": true,
68
+ "verdict": "PASS",
69
+ "instrument_limited": false,
70
+ "why": "every leg holds",
71
+ "n_selected": 60,
72
+ "n_scorable": 58,
73
+ "within": {
74
+ "0.5": {
75
+ "basis": "scorable",
76
+ "k": 41,
77
+ "n": 58,
78
+ "share": 0.706897,
79
+ "lower": null,
80
+ "upper": null
81
+ },
82
+ "1": {
83
+ "basis": "itt",
84
+ "k": 56,
85
+ "n": 60,
86
+ "share": 0.933333,
87
+ "lower": 0.856,
88
+ "upper": 0.9933
89
+ },
90
+ "2": {
91
+ "basis": "scorable",
92
+ "k": 58,
93
+ "n": 58,
94
+ "share": 1.0,
95
+ "lower": null,
96
+ "upper": null
97
+ }
98
+ },
99
+ "median_abs": 0.404,
100
+ "residual_hist": {
101
+ "edges": [
102
+ -4.0,
103
+ -3.75,
104
+ -3.5,
105
+ -3.25,
106
+ -3.0,
107
+ -2.75,
108
+ -2.5,
109
+ -2.25,
110
+ -2.0,
111
+ -1.75,
112
+ -1.5,
113
+ -1.25,
114
+ -1.0,
115
+ -0.75,
116
+ -0.5,
117
+ -0.25,
118
+ 0.0,
119
+ 0.25,
120
+ 0.5,
121
+ 0.75,
122
+ 1.0,
123
+ 1.25,
124
+ 1.5,
125
+ 1.75,
126
+ 2.0,
127
+ 2.25,
128
+ 2.5,
129
+ 2.75,
130
+ 3.0,
131
+ 3.25,
132
+ 3.5,
133
+ 3.75,
134
+ 4.0
135
+ ],
136
+ "counts": [
137
+ 0,
138
+ 0,
139
+ 0,
140
+ 0,
141
+ 0,
142
+ 0,
143
+ 0,
144
+ 0,
145
+ 0,
146
+ 0,
147
+ 0,
148
+ 0,
149
+ 2,
150
+ 1,
151
+ 6,
152
+ 12,
153
+ 12,
154
+ 9,
155
+ 10,
156
+ 4,
157
+ 2,
158
+ 0,
159
+ 0,
160
+ 0,
161
+ 0,
162
+ 0,
163
+ 0,
164
+ 0,
165
+ 0,
166
+ 0,
167
+ 0,
168
+ 0
169
+ ],
170
+ "under": 0,
171
+ "over": 0,
172
+ "sign": "positive: the snapped point lies outward of the truth face, away from the center line"
173
+ },
174
+ "published_hist": {
175
+ "edges": [
176
+ -4.0,
177
+ -3.75,
178
+ -3.5,
179
+ -3.25,
180
+ -3.0,
181
+ -2.75,
182
+ -2.5,
183
+ -2.25,
184
+ -2.0,
185
+ -1.75,
186
+ -1.5,
187
+ -1.25,
188
+ -1.0,
189
+ -0.75,
190
+ -0.5,
191
+ -0.25,
192
+ 0.0,
193
+ 0.25,
194
+ 0.5,
195
+ 0.75,
196
+ 1.0,
197
+ 1.25,
198
+ 1.5,
199
+ 1.75,
200
+ 2.0,
201
+ 2.25,
202
+ 2.5,
203
+ 2.75,
204
+ 3.0,
205
+ 3.25,
206
+ 3.5,
207
+ 3.75,
208
+ 4.0
209
+ ],
210
+ "counts": [
211
+ 0,
212
+ 0,
213
+ 0,
214
+ 0,
215
+ 0,
216
+ 0,
217
+ 0,
218
+ 0,
219
+ 0,
220
+ 0,
221
+ 0,
222
+ 1,
223
+ 0,
224
+ 1,
225
+ 2,
226
+ 1,
227
+ 3,
228
+ 2,
229
+ 0,
230
+ 3,
231
+ 1,
232
+ 3,
233
+ 4,
234
+ 7,
235
+ 5,
236
+ 5,
237
+ 3,
238
+ 3,
239
+ 5,
240
+ 2,
241
+ 1,
242
+ 2
243
+ ],
244
+ "under": 0,
245
+ "over": 4
246
+ },
247
+ "controls": [
248
+ {
249
+ "name": "published label",
250
+ "observed": 0.22,
251
+ "require": "below",
252
+ "limit": 0.6,
253
+ "holds": true
254
+ },
255
+ {
256
+ "name": "planted +1.5 voxels",
257
+ "observed": 0.12,
258
+ "require": "below",
259
+ "limit": 0.5,
260
+ "holds": true
261
+ },
262
+ {
263
+ "name": "planted -1.5 voxels",
264
+ "observed": 0.15,
265
+ "require": "below",
266
+ "limit": 0.5,
267
+ "holds": true
268
+ },
269
+ {
270
+ "name": "planted +3 voxels",
271
+ "observed": 0.02,
272
+ "require": "below",
273
+ "limit": 0.2,
274
+ "holds": true
275
+ },
276
+ {
277
+ "name": "verso face",
278
+ "observed": 0.05,
279
+ "require": "below",
280
+ "limit": 0.3,
281
+ "holds": true
282
+ },
283
+ {
284
+ "name": "oracle face (2.4 um scan)",
285
+ "observed": 0.97,
286
+ "require": "above",
287
+ "limit": 0.8,
288
+ "holds": true
289
+ }
290
+ ]
291
+ },
292
+ {
293
+ "id": "C2",
294
+ "code": 2,
295
+ "primary": false,
296
+ "verdict": "NOT SHOWN",
297
+ "instrument_limited": false,
298
+ "why": "the lower bound is under the bar",
299
+ "n_selected": 60,
300
+ "n_scorable": 55,
301
+ "within": {
302
+ "0.5": {
303
+ "basis": "scorable",
304
+ "k": 22,
305
+ "n": 55,
306
+ "share": 0.4,
307
+ "lower": null,
308
+ "upper": null
309
+ },
310
+ "1": {
311
+ "basis": "itt",
312
+ "k": 44,
313
+ "n": 60,
314
+ "share": 0.733333,
315
+ "lower": 0.632,
316
+ "upper": 0.7933
317
+ },
318
+ "2": {
319
+ "basis": "scorable",
320
+ "k": 52,
321
+ "n": 55,
322
+ "share": 0.945455,
323
+ "lower": null,
324
+ "upper": null
325
+ }
326
+ },
327
+ "median_abs": 0.907,
328
+ "residual_hist": {
329
+ "edges": [
330
+ -4.0,
331
+ -3.75,
332
+ -3.5,
333
+ -3.25,
334
+ -3.0,
335
+ -2.75,
336
+ -2.5,
337
+ -2.25,
338
+ -2.0,
339
+ -1.75,
340
+ -1.5,
341
+ -1.25,
342
+ -1.0,
343
+ -0.75,
344
+ -0.5,
345
+ -0.25,
346
+ 0.0,
347
+ 0.25,
348
+ 0.5,
349
+ 0.75,
350
+ 1.0,
351
+ 1.25,
352
+ 1.5,
353
+ 1.75,
354
+ 2.0,
355
+ 2.25,
356
+ 2.5,
357
+ 2.75,
358
+ 3.0,
359
+ 3.25,
360
+ 3.5,
361
+ 3.75,
362
+ 4.0
363
+ ],
364
+ "counts": [
365
+ 0,
366
+ 0,
367
+ 0,
368
+ 0,
369
+ 0,
370
+ 0,
371
+ 0,
372
+ 0,
373
+ 0,
374
+ 0,
375
+ 2,
376
+ 3,
377
+ 3,
378
+ 5,
379
+ 2,
380
+ 6,
381
+ 4,
382
+ 2,
383
+ 5,
384
+ 5,
385
+ 6,
386
+ 4,
387
+ 2,
388
+ 4,
389
+ 1,
390
+ 0,
391
+ 1,
392
+ 0,
393
+ 0,
394
+ 0,
395
+ 0,
396
+ 0
397
+ ],
398
+ "under": 0,
399
+ "over": 0,
400
+ "sign": "positive: the snapped point lies outward of the truth face, away from the center line"
401
+ },
402
+ "published_hist": {
403
+ "edges": [
404
+ -4.0,
405
+ -3.75,
406
+ -3.5,
407
+ -3.25,
408
+ -3.0,
409
+ -2.75,
410
+ -2.5,
411
+ -2.25,
412
+ -2.0,
413
+ -1.75,
414
+ -1.5,
415
+ -1.25,
416
+ -1.0,
417
+ -0.75,
418
+ -0.5,
419
+ -0.25,
420
+ 0.0,
421
+ 0.25,
422
+ 0.5,
423
+ 0.75,
424
+ 1.0,
425
+ 1.25,
426
+ 1.5,
427
+ 1.75,
428
+ 2.0,
429
+ 2.25,
430
+ 2.5,
431
+ 2.75,
432
+ 3.0,
433
+ 3.25,
434
+ 3.5,
435
+ 3.75,
436
+ 4.0
437
+ ],
438
+ "counts": [
439
+ 0,
440
+ 0,
441
+ 0,
442
+ 0,
443
+ 0,
444
+ 1,
445
+ 0,
446
+ 0,
447
+ 0,
448
+ 0,
449
+ 0,
450
+ 0,
451
+ 0,
452
+ 2,
453
+ 1,
454
+ 2,
455
+ 3,
456
+ 1,
457
+ 4,
458
+ 4,
459
+ 3,
460
+ 4,
461
+ 3,
462
+ 3,
463
+ 8,
464
+ 2,
465
+ 3,
466
+ 5,
467
+ 1,
468
+ 0,
469
+ 2,
470
+ 0
471
+ ],
472
+ "under": 0,
473
+ "over": 3
474
+ },
475
+ "controls": [
476
+ {
477
+ "name": "published label",
478
+ "observed": 0.25,
479
+ "require": "below",
480
+ "limit": 0.6,
481
+ "holds": true
482
+ },
483
+ {
484
+ "name": "planted +1.5 voxels",
485
+ "observed": 0.12,
486
+ "require": "below",
487
+ "limit": 0.5,
488
+ "holds": true
489
+ },
490
+ {
491
+ "name": "planted -1.5 voxels",
492
+ "observed": 0.15,
493
+ "require": "below",
494
+ "limit": 0.5,
495
+ "holds": true
496
+ },
497
+ {
498
+ "name": "planted +3 voxels",
499
+ "observed": 0.02,
500
+ "require": "below",
501
+ "limit": 0.2,
502
+ "holds": true
503
+ },
504
+ {
505
+ "name": "verso face",
506
+ "observed": 0.05,
507
+ "require": "below",
508
+ "limit": 0.3,
509
+ "holds": true
510
+ },
511
+ {
512
+ "name": "oracle face (2.4 um scan)",
513
+ "observed": 0.97,
514
+ "require": "above",
515
+ "limit": 0.8,
516
+ "holds": true
517
+ }
518
+ ]
519
+ },
520
+ {
521
+ "id": "C1",
522
+ "code": 1,
523
+ "primary": false,
524
+ "verdict": "UNANSWERABLE",
525
+ "instrument_limited": false,
526
+ "why": "12 selected sites, under the minimum of 30",
527
+ "n_selected": 12,
528
+ "n_scorable": 12,
529
+ "within": {
530
+ "0.5": {
531
+ "basis": "scorable",
532
+ "k": 2,
533
+ "n": 12,
534
+ "share": 0.166667,
535
+ "lower": null,
536
+ "upper": null
537
+ },
538
+ "1": {
539
+ "basis": "itt",
540
+ "k": 5,
541
+ "n": 12,
542
+ "share": 0.416667,
543
+ "lower": null,
544
+ "upper": 0.4767
545
+ },
546
+ "2": {
547
+ "basis": "scorable",
548
+ "k": 9,
549
+ "n": 12,
550
+ "share": 0.75,
551
+ "lower": null,
552
+ "upper": null
553
+ }
554
+ },
555
+ "median_abs": 1.979,
556
+ "residual_hist": {
557
+ "edges": [
558
+ -4.0,
559
+ -3.75,
560
+ -3.5,
561
+ -3.25,
562
+ -3.0,
563
+ -2.75,
564
+ -2.5,
565
+ -2.25,
566
+ -2.0,
567
+ -1.75,
568
+ -1.5,
569
+ -1.25,
570
+ -1.0,
571
+ -0.75,
572
+ -0.5,
573
+ -0.25,
574
+ 0.0,
575
+ 0.25,
576
+ 0.5,
577
+ 0.75,
578
+ 1.0,
579
+ 1.25,
580
+ 1.5,
581
+ 1.75,
582
+ 2.0,
583
+ 2.25,
584
+ 2.5,
585
+ 2.75,
586
+ 3.0,
587
+ 3.25,
588
+ 3.5,
589
+ 3.75,
590
+ 4.0
591
+ ],
592
+ "counts": [
593
+ 0,
594
+ 0,
595
+ 0,
596
+ 0,
597
+ 0,
598
+ 0,
599
+ 0,
600
+ 0,
601
+ 0,
602
+ 0,
603
+ 0,
604
+ 1,
605
+ 0,
606
+ 0,
607
+ 0,
608
+ 1,
609
+ 1,
610
+ 0,
611
+ 1,
612
+ 0,
613
+ 2,
614
+ 0,
615
+ 0,
616
+ 1,
617
+ 0,
618
+ 2,
619
+ 0,
620
+ 0,
621
+ 0,
622
+ 1,
623
+ 0,
624
+ 0
625
+ ],
626
+ "under": 0,
627
+ "over": 2,
628
+ "sign": "positive: the snapped point lies outward of the truth face, away from the center line"
629
+ },
630
+ "published_hist": {
631
+ "edges": [
632
+ -4.0,
633
+ -3.75,
634
+ -3.5,
635
+ -3.25,
636
+ -3.0,
637
+ -2.75,
638
+ -2.5,
639
+ -2.25,
640
+ -2.0,
641
+ -1.75,
642
+ -1.5,
643
+ -1.25,
644
+ -1.0,
645
+ -0.75,
646
+ -0.5,
647
+ -0.25,
648
+ 0.0,
649
+ 0.25,
650
+ 0.5,
651
+ 0.75,
652
+ 1.0,
653
+ 1.25,
654
+ 1.5,
655
+ 1.75,
656
+ 2.0,
657
+ 2.25,
658
+ 2.5,
659
+ 2.75,
660
+ 3.0,
661
+ 3.25,
662
+ 3.5,
663
+ 3.75,
664
+ 4.0
665
+ ],
666
+ "counts": [
667
+ 0,
668
+ 0,
669
+ 0,
670
+ 0,
671
+ 0,
672
+ 0,
673
+ 0,
674
+ 0,
675
+ 0,
676
+ 0,
677
+ 0,
678
+ 0,
679
+ 2,
680
+ 0,
681
+ 0,
682
+ 0,
683
+ 0,
684
+ 1,
685
+ 0,
686
+ 2,
687
+ 0,
688
+ 1,
689
+ 0,
690
+ 2,
691
+ 1,
692
+ 0,
693
+ 0,
694
+ 1,
695
+ 0,
696
+ 1,
697
+ 0,
698
+ 0
699
+ ],
700
+ "under": 0,
701
+ "over": 1
702
+ },
703
+ "controls": [
704
+ {
705
+ "name": "published label",
706
+ "observed": 0.21,
707
+ "require": "below",
708
+ "limit": 0.6,
709
+ "holds": true
710
+ },
711
+ {
712
+ "name": "planted +1.5 voxels",
713
+ "observed": 0.12,
714
+ "require": "below",
715
+ "limit": 0.5,
716
+ "holds": true
717
+ },
718
+ {
719
+ "name": "planted -1.5 voxels",
720
+ "observed": 0.15,
721
+ "require": "below",
722
+ "limit": 0.5,
723
+ "holds": true
724
+ },
725
+ {
726
+ "name": "planted +3 voxels",
727
+ "observed": 0.02,
728
+ "require": "below",
729
+ "limit": 0.2,
730
+ "holds": true
731
+ },
732
+ {
733
+ "name": "verso face",
734
+ "observed": 0.05,
735
+ "require": "below",
736
+ "limit": 0.3,
737
+ "holds": true
738
+ },
739
+ {
740
+ "name": "oracle face (2.4 um scan)",
741
+ "observed": 0.97,
742
+ "require": "above",
743
+ "limit": 0.8,
744
+ "holds": true
745
+ }
746
+ ]
747
+ },
748
+ {
749
+ "id": "C0L",
750
+ "code": 0,
751
+ "primary": false,
752
+ "verdict": "PASS",
753
+ "instrument_limited": false,
754
+ "why": "every leg holds",
755
+ "n_selected": 60,
756
+ "n_scorable": 59,
757
+ "within": {
758
+ "0.5": {
759
+ "basis": "scorable",
760
+ "k": 38,
761
+ "n": 59,
762
+ "share": 0.644068,
763
+ "lower": null,
764
+ "upper": null
765
+ },
766
+ "1": {
767
+ "basis": "itt",
768
+ "k": 55,
769
+ "n": 60,
770
+ "share": 0.916667,
771
+ "lower": 0.835,
772
+ "upper": 0.9767
773
+ },
774
+ "2": {
775
+ "basis": "scorable",
776
+ "k": 59,
777
+ "n": 59,
778
+ "share": 1.0,
779
+ "lower": null,
780
+ "upper": null
781
+ }
782
+ },
783
+ "median_abs": 0.571,
784
+ "residual_hist": {
785
+ "edges": [
786
+ -4.0,
787
+ -3.75,
788
+ -3.5,
789
+ -3.25,
790
+ -3.0,
791
+ -2.75,
792
+ -2.5,
793
+ -2.25,
794
+ -2.0,
795
+ -1.75,
796
+ -1.5,
797
+ -1.25,
798
+ -1.0,
799
+ -0.75,
800
+ -0.5,
801
+ -0.25,
802
+ 0.0,
803
+ 0.25,
804
+ 0.5,
805
+ 0.75,
806
+ 1.0,
807
+ 1.25,
808
+ 1.5,
809
+ 1.75,
810
+ 2.0,
811
+ 2.25,
812
+ 2.5,
813
+ 2.75,
814
+ 3.0,
815
+ 3.25,
816
+ 3.5,
817
+ 3.75,
818
+ 4.0
819
+ ],
820
+ "counts": [
821
+ 0,
822
+ 0,
823
+ 0,
824
+ 0,
825
+ 0,
826
+ 0,
827
+ 0,
828
+ 0,
829
+ 0,
830
+ 0,
831
+ 0,
832
+ 1,
833
+ 0,
834
+ 5,
835
+ 4,
836
+ 11,
837
+ 9,
838
+ 10,
839
+ 7,
840
+ 7,
841
+ 1,
842
+ 1,
843
+ 3,
844
+ 0,
845
+ 0,
846
+ 0,
847
+ 0,
848
+ 0,
849
+ 0,
850
+ 0,
851
+ 0,
852
+ 0
853
+ ],
854
+ "under": 0,
855
+ "over": 0,
856
+ "sign": "positive: the snapped point lies outward of the truth face, away from the center line"
857
+ },
858
+ "published_hist": {
859
+ "edges": [
860
+ -4.0,
861
+ -3.75,
862
+ -3.5,
863
+ -3.25,
864
+ -3.0,
865
+ -2.75,
866
+ -2.5,
867
+ -2.25,
868
+ -2.0,
869
+ -1.75,
870
+ -1.5,
871
+ -1.25,
872
+ -1.0,
873
+ -0.75,
874
+ -0.5,
875
+ -0.25,
876
+ 0.0,
877
+ 0.25,
878
+ 0.5,
879
+ 0.75,
880
+ 1.0,
881
+ 1.25,
882
+ 1.5,
883
+ 1.75,
884
+ 2.0,
885
+ 2.25,
886
+ 2.5,
887
+ 2.75,
888
+ 3.0,
889
+ 3.25,
890
+ 3.5,
891
+ 3.75,
892
+ 4.0
893
+ ],
894
+ "counts": [
895
+ 0,
896
+ 0,
897
+ 0,
898
+ 0,
899
+ 0,
900
+ 0,
901
+ 0,
902
+ 0,
903
+ 0,
904
+ 0,
905
+ 0,
906
+ 0,
907
+ 1,
908
+ 0,
909
+ 0,
910
+ 0,
911
+ 5,
912
+ 2,
913
+ 4,
914
+ 2,
915
+ 5,
916
+ 3,
917
+ 7,
918
+ 2,
919
+ 5,
920
+ 1,
921
+ 0,
922
+ 1,
923
+ 5,
924
+ 6,
925
+ 4,
926
+ 2
927
+ ],
928
+ "under": 0,
929
+ "over": 4
930
+ },
931
+ "controls": [
932
+ {
933
+ "name": "published label",
934
+ "observed": 0.24,
935
+ "require": "below",
936
+ "limit": 0.6,
937
+ "holds": true
938
+ },
939
+ {
940
+ "name": "planted +1.5 voxels",
941
+ "observed": 0.12,
942
+ "require": "below",
943
+ "limit": 0.5,
944
+ "holds": true
945
+ },
946
+ {
947
+ "name": "planted -1.5 voxels",
948
+ "observed": 0.15,
949
+ "require": "below",
950
+ "limit": 0.5,
951
+ "holds": true
952
+ },
953
+ {
954
+ "name": "planted +3 voxels",
955
+ "observed": 0.02,
956
+ "require": "below",
957
+ "limit": 0.2,
958
+ "holds": true
959
+ },
960
+ {
961
+ "name": "verso face",
962
+ "observed": 0.05,
963
+ "require": "below",
964
+ "limit": 0.3,
965
+ "holds": true
966
+ },
967
+ {
968
+ "name": "oracle face (2.4 um scan)",
969
+ "observed": 0.97,
970
+ "require": "above",
971
+ "limit": 0.8,
972
+ "holds": true
973
+ }
974
+ ]
975
+ }
976
+ ]
977
+ },
978
+ {
979
+ "scroll": "SYNTH-B",
980
+ "claim": "certified",
981
+ "why_not_shown": null,
982
+ "label_scan": {
983
+ "volume_id": "synthetic-label-scan-b",
984
+ "voxel_um": 7.9
985
+ },
986
+ "truth": null,
987
+ "oracle": null,
988
+ "certificate": {
989
+ "method": "synthetic two-face agreement bound",
990
+ "lower": 0.81,
991
+ "level": 0.95,
992
+ "n": 5000,
993
+ "assumptions": [
994
+ "Made-up assumption one: the two faces err independently.",
995
+ "Made-up assumption two: the sampled vertices represent the scroll."
996
+ ]
997
+ },
998
+ "vertices": {
999
+ "released": 90000,
1000
+ "held_back": 9000,
1001
+ "by_code": {
1002
+ "0": 70000,
1003
+ "2": 19000,
1004
+ "1": 1000
1005
+ }
1006
+ },
1007
+ "void": [],
1008
+ "strata": []
1009
+ },
1010
+ {
1011
+ "scroll": "SYNTH-C",
1012
+ "claim": "not_shown",
1013
+ "why_not_shown": "no finer scan and the certificate's assumptions did not hold (synthetic example)",
1014
+ "label_scan": {
1015
+ "volume_id": "synthetic-label-scan-c",
1016
+ "voxel_um": 9.0
1017
+ },
1018
+ "truth": null,
1019
+ "oracle": null,
1020
+ "certificate": null,
1021
+ "vertices": {
1022
+ "released": 40000,
1023
+ "held_back": 4000,
1024
+ "by_code": {
1025
+ "0": 22000,
1026
+ "2": 16000,
1027
+ "1": 2000
1028
+ }
1029
+ },
1030
+ "void": [],
1031
+ "strata": []
1032
+ }
1033
+ ],
1034
+ "showcase": [
1035
+ {
1036
+ "scroll": "SYNTH-A",
1037
+ "segment": "synthetic-sheet",
1038
+ "file": "show/SYNTH-A/synthetic-sheet.json",
1039
+ "spent": true
1040
+ }
1041
+ ]
1042
+ }
vx/summary.json ADDED
@@ -0,0 +1,1049 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema": "snapped-labels/voxel-exact@1",
3
+ "fixture": false,
4
+ "generated": "2026-10-28T07:10:00Z",
5
+ "dataset": {
6
+ "repo": "SharpMeow/vesuvius-snapped-labels",
7
+ "release": "1.0.1",
8
+ "revision": "95432ec570d6f096d754e6ac16cafdeb83ca8a04"
9
+ },
10
+ "rule": {
11
+ "primary_tolerance": 1.0,
12
+ "tolerances": [
13
+ 0.5,
14
+ 1.0,
15
+ 2.0
16
+ ],
17
+ "bar": 0.9,
18
+ "interval": {
19
+ "method": "clopper-pearson",
20
+ "sided": "one",
21
+ "level": 0.95
22
+ },
23
+ "max_move": 16.0
24
+ },
25
+ "prereg": {
26
+ "sha256": "bcac8e074d03c75ece811bee91688ce8682b5aad8c820e2b0e77003207c1d52a",
27
+ "frozen": "2026-10-28T02:40:03Z"
28
+ },
29
+ "prizes_read": {
30
+ "url": "https://scrollprize.org/prizes",
31
+ "date": "2026-10-28"
32
+ },
33
+ "scrolls": [
34
+ {
35
+ "scroll": "PHerc0139",
36
+ "claim": "not_shown",
37
+ "why_not_shown": "the frozen test gave NOT SHOWN for every class, so no class clears the bar, and the finer reference scan does not clear it either, so the instrument limits this test",
38
+ "label_scan": {
39
+ "volume_id": "20250728140407",
40
+ "voxel_um": 9.362
41
+ },
42
+ "truth": {
43
+ "kind": "finer_scan",
44
+ "volume_id": "20260413113053",
45
+ "voxel_um": 1.129
46
+ },
47
+ "oracle": {
48
+ "kind": "finer_scan",
49
+ "volume_id": "20260102150214",
50
+ "voxel_um": 2.399
51
+ },
52
+ "certificate": null,
53
+ "vertices": {
54
+ "released": 3147195,
55
+ "held_back": 872310,
56
+ "by_code": {
57
+ "0": 2758538,
58
+ "2": 373639,
59
+ "1": 15018
60
+ }
61
+ },
62
+ "void": [],
63
+ "strata": [
64
+ {
65
+ "id": "C0",
66
+ "code": 0,
67
+ "primary": true,
68
+ "verdict": "NOT SHOWN",
69
+ "instrument_limited": true,
70
+ "why": "no PASS, since the lower bound 70.5% is under the 90% bar, and no FAIL, since 10 sites are certain misses where a FAIL needs 28; 152 of 200 sites within 1 voxel counting each unscorable site as a miss, 18 scorable sites missed and 30 not scorable; the 2.399 µm scan's own face reaches a lower bound of 68.4% at the same sites; sites in z 4680 to 5184 of the label scan, one vertex each, drawn after the rule was frozen",
71
+ "n_selected": 200,
72
+ "n_scorable": 170,
73
+ "within": {
74
+ "0.5": null,
75
+ "1": {
76
+ "basis": "itt",
77
+ "k": 152,
78
+ "n": 200,
79
+ "share": 0.76,
80
+ "lower": 0.7051,
81
+ "upper": 0.809029
82
+ },
83
+ "2": {
84
+ "basis": "scorable",
85
+ "k": 159,
86
+ "n": 170,
87
+ "share": 0.935294,
88
+ "lower": null,
89
+ "upper": null
90
+ }
91
+ },
92
+ "median_abs": 0.26771,
93
+ "residual_hist": {
94
+ "edges": [
95
+ -4.0,
96
+ -3.75,
97
+ -3.5,
98
+ -3.25,
99
+ -3.0,
100
+ -2.75,
101
+ -2.5,
102
+ -2.25,
103
+ -2.0,
104
+ -1.75,
105
+ -1.5,
106
+ -1.25,
107
+ -1.0,
108
+ -0.75,
109
+ -0.5,
110
+ -0.25,
111
+ 0.0,
112
+ 0.25,
113
+ 0.5,
114
+ 0.75,
115
+ 1.0,
116
+ 1.25,
117
+ 1.5,
118
+ 1.75,
119
+ 2.0,
120
+ 2.25,
121
+ 2.5,
122
+ 2.75,
123
+ 3.0,
124
+ 3.25,
125
+ 3.5,
126
+ 3.75,
127
+ 4.0
128
+ ],
129
+ "counts": [
130
+ 0,
131
+ 0,
132
+ 0,
133
+ 0,
134
+ 0,
135
+ 0,
136
+ 0,
137
+ 0,
138
+ 0,
139
+ 0,
140
+ 0,
141
+ 1,
142
+ 4,
143
+ 5,
144
+ 20,
145
+ 29,
146
+ 52,
147
+ 27,
148
+ 11,
149
+ 4,
150
+ 1,
151
+ 5,
152
+ 0,
153
+ 0,
154
+ 0,
155
+ 0,
156
+ 2,
157
+ 0,
158
+ 0,
159
+ 0,
160
+ 0,
161
+ 0
162
+ ],
163
+ "under": 2,
164
+ "over": 7,
165
+ "sign": "positive: the snapped point lies outward of the truth face, away from the center line"
166
+ },
167
+ "published_hist": {
168
+ "edges": [
169
+ -4.0,
170
+ -3.75,
171
+ -3.5,
172
+ -3.25,
173
+ -3.0,
174
+ -2.75,
175
+ -2.5,
176
+ -2.25,
177
+ -2.0,
178
+ -1.75,
179
+ -1.5,
180
+ -1.25,
181
+ -1.0,
182
+ -0.75,
183
+ -0.5,
184
+ -0.25,
185
+ 0.0,
186
+ 0.25,
187
+ 0.5,
188
+ 0.75,
189
+ 1.0,
190
+ 1.25,
191
+ 1.5,
192
+ 1.75,
193
+ 2.0,
194
+ 2.25,
195
+ 2.5,
196
+ 2.75,
197
+ 3.0,
198
+ 3.25,
199
+ 3.5,
200
+ 3.75,
201
+ 4.0
202
+ ],
203
+ "counts": [
204
+ 0,
205
+ 0,
206
+ 0,
207
+ 0,
208
+ 1,
209
+ 1,
210
+ 0,
211
+ 3,
212
+ 1,
213
+ 4,
214
+ 3,
215
+ 2,
216
+ 4,
217
+ 8,
218
+ 5,
219
+ 1,
220
+ 6,
221
+ 7,
222
+ 10,
223
+ 7,
224
+ 9,
225
+ 8,
226
+ 12,
227
+ 5,
228
+ 8,
229
+ 5,
230
+ 6,
231
+ 0,
232
+ 8,
233
+ 6,
234
+ 1,
235
+ 0
236
+ ],
237
+ "under": 1,
238
+ "over": 38
239
+ },
240
+ "controls": [
241
+ {
242
+ "name": "published label, C0 test gate (170 scorable sites)",
243
+ "observed": 0.282353,
244
+ "require": "below",
245
+ "limit": 0.6,
246
+ "holds": true
247
+ },
248
+ {
249
+ "name": "2.399 µm face moved +1.5 voxels, C0 test gate (161 sites)",
250
+ "observed": 0.049689,
251
+ "require": "below",
252
+ "limit": 0.5,
253
+ "holds": true
254
+ },
255
+ {
256
+ "name": "2.399 µm face moved -1.5 voxels, C0 test gate (161 sites)",
257
+ "observed": 0.111801,
258
+ "require": "below",
259
+ "limit": 0.5,
260
+ "holds": true
261
+ },
262
+ {
263
+ "name": "2.399 µm face moved +3 voxels, C0 test gate (161 sites)",
264
+ "observed": 0.0,
265
+ "require": "below",
266
+ "limit": 0.2,
267
+ "holds": true
268
+ },
269
+ {
270
+ "name": "2.399 µm face moved -3 voxels, C0 test gate (161 sites)",
271
+ "observed": 0.0,
272
+ "require": "below",
273
+ "limit": 0.2,
274
+ "holds": true
275
+ },
276
+ {
277
+ "name": "verso face, C0 test gate (170 scorable sites)",
278
+ "observed": 0.0,
279
+ "require": "below",
280
+ "limit": 0.5,
281
+ "holds": true
282
+ },
283
+ {
284
+ "name": "positive control: the 2.399 µm scan's own face, C0 test gate (170 scorable sites)",
285
+ "observed": 0.870588,
286
+ "require": "above",
287
+ "limit": 0.75,
288
+ "holds": true
289
+ }
290
+ ]
291
+ },
292
+ {
293
+ "id": "C0L",
294
+ "code": 0,
295
+ "primary": false,
296
+ "verdict": "NOT SHOWN",
297
+ "instrument_limited": true,
298
+ "why": "no PASS, since the lower bound 15.1% is under the 90% bar, and no FAIL, since 10 sites are certain misses where a FAIL needs 14; 18 of 80 sites within 1 voxel counting each unscorable site as a miss, 13 scorable sites missed and 49 not scorable; the 2.399 µm scan's own face reaches a lower bound of 0.4% at the same sites; sites in z 4680 to 5184 of the label scan, one vertex each, drawn after the rule was frozen",
299
+ "n_selected": 80,
300
+ "n_scorable": 31,
301
+ "within": {
302
+ "0.5": null,
303
+ "1": {
304
+ "basis": "itt",
305
+ "k": 18,
306
+ "n": 80,
307
+ "share": 0.225,
308
+ "lower": 0.15076,
309
+ "upper": 0.315169
310
+ },
311
+ "2": {
312
+ "basis": "scorable",
313
+ "k": 21,
314
+ "n": 31,
315
+ "share": 0.677419,
316
+ "lower": null,
317
+ "upper": null
318
+ }
319
+ },
320
+ "median_abs": 0.579733,
321
+ "residual_hist": {
322
+ "edges": [
323
+ -4.0,
324
+ -3.75,
325
+ -3.5,
326
+ -3.25,
327
+ -3.0,
328
+ -2.75,
329
+ -2.5,
330
+ -2.25,
331
+ -2.0,
332
+ -1.75,
333
+ -1.5,
334
+ -1.25,
335
+ -1.0,
336
+ -0.75,
337
+ -0.5,
338
+ -0.25,
339
+ 0.0,
340
+ 0.25,
341
+ 0.5,
342
+ 0.75,
343
+ 1.0,
344
+ 1.25,
345
+ 1.5,
346
+ 1.75,
347
+ 2.0,
348
+ 2.25,
349
+ 2.5,
350
+ 2.75,
351
+ 3.0,
352
+ 3.25,
353
+ 3.5,
354
+ 3.75,
355
+ 4.0
356
+ ],
357
+ "counts": [
358
+ 0,
359
+ 0,
360
+ 0,
361
+ 0,
362
+ 0,
363
+ 0,
364
+ 0,
365
+ 1,
366
+ 0,
367
+ 0,
368
+ 0,
369
+ 0,
370
+ 0,
371
+ 2,
372
+ 4,
373
+ 4,
374
+ 3,
375
+ 2,
376
+ 1,
377
+ 2,
378
+ 0,
379
+ 1,
380
+ 1,
381
+ 1,
382
+ 2,
383
+ 1,
384
+ 1,
385
+ 0,
386
+ 0,
387
+ 1,
388
+ 0,
389
+ 0
390
+ ],
391
+ "under": 0,
392
+ "over": 4,
393
+ "sign": "positive: the snapped point lies outward of the truth face, away from the center line"
394
+ },
395
+ "published_hist": {
396
+ "edges": [
397
+ -4.0,
398
+ -3.75,
399
+ -3.5,
400
+ -3.25,
401
+ -3.0,
402
+ -2.75,
403
+ -2.5,
404
+ -2.25,
405
+ -2.0,
406
+ -1.75,
407
+ -1.5,
408
+ -1.25,
409
+ -1.0,
410
+ -0.75,
411
+ -0.5,
412
+ -0.25,
413
+ 0.0,
414
+ 0.25,
415
+ 0.5,
416
+ 0.75,
417
+ 1.0,
418
+ 1.25,
419
+ 1.5,
420
+ 1.75,
421
+ 2.0,
422
+ 2.25,
423
+ 2.5,
424
+ 2.75,
425
+ 3.0,
426
+ 3.25,
427
+ 3.5,
428
+ 3.75,
429
+ 4.0
430
+ ],
431
+ "counts": [
432
+ 0,
433
+ 0,
434
+ 0,
435
+ 0,
436
+ 0,
437
+ 0,
438
+ 0,
439
+ 1,
440
+ 0,
441
+ 1,
442
+ 2,
443
+ 0,
444
+ 0,
445
+ 0,
446
+ 0,
447
+ 2,
448
+ 0,
449
+ 2,
450
+ 0,
451
+ 1,
452
+ 1,
453
+ 0,
454
+ 0,
455
+ 0,
456
+ 0,
457
+ 1,
458
+ 0,
459
+ 1,
460
+ 1,
461
+ 1,
462
+ 0,
463
+ 0
464
+ ],
465
+ "under": 0,
466
+ "over": 17
467
+ },
468
+ "controls": [
469
+ {
470
+ "name": "published label, C0 test gate (170 scorable sites)",
471
+ "observed": 0.282353,
472
+ "require": "below",
473
+ "limit": 0.6,
474
+ "holds": true
475
+ },
476
+ {
477
+ "name": "2.399 µm face moved +1.5 voxels, C0 test gate (161 sites)",
478
+ "observed": 0.049689,
479
+ "require": "below",
480
+ "limit": 0.5,
481
+ "holds": true
482
+ },
483
+ {
484
+ "name": "2.399 µm face moved -1.5 voxels, C0 test gate (161 sites)",
485
+ "observed": 0.111801,
486
+ "require": "below",
487
+ "limit": 0.5,
488
+ "holds": true
489
+ },
490
+ {
491
+ "name": "2.399 µm face moved +3 voxels, C0 test gate (161 sites)",
492
+ "observed": 0.0,
493
+ "require": "below",
494
+ "limit": 0.2,
495
+ "holds": true
496
+ },
497
+ {
498
+ "name": "2.399 µm face moved -3 voxels, C0 test gate (161 sites)",
499
+ "observed": 0.0,
500
+ "require": "below",
501
+ "limit": 0.2,
502
+ "holds": true
503
+ },
504
+ {
505
+ "name": "verso face, C0 test gate (170 scorable sites)",
506
+ "observed": 0.0,
507
+ "require": "below",
508
+ "limit": 0.5,
509
+ "holds": true
510
+ },
511
+ {
512
+ "name": "positive control: the 2.399 µm scan's own face, C0 test gate (170 scorable sites)",
513
+ "observed": 0.870588,
514
+ "require": "above",
515
+ "limit": 0.75,
516
+ "holds": true
517
+ }
518
+ ]
519
+ },
520
+ {
521
+ "id": "C2",
522
+ "code": 2,
523
+ "primary": true,
524
+ "verdict": "NOT SHOWN",
525
+ "instrument_limited": true,
526
+ "why": "no PASS, since the lower bound 0.2% is under the 90% bar, and no FAIL, since 22 sites are certain misses where a FAIL needs 28; 2 of 200 sites within 1 voxel counting each unscorable site as a miss, 38 scorable sites missed and 160 not scorable; the 2.399 µm scan's own face reaches a lower bound of 0.0% at the same sites; sites in z 4680 to 5184 of the label scan, one vertex each, drawn after the rule was frozen",
527
+ "n_selected": 200,
528
+ "n_scorable": 40,
529
+ "within": {
530
+ "0.5": null,
531
+ "1": {
532
+ "basis": "itt",
533
+ "k": 2,
534
+ "n": 200,
535
+ "share": 0.01,
536
+ "lower": 0.00178,
537
+ "upper": 0.031143
538
+ },
539
+ "2": {
540
+ "basis": "scorable",
541
+ "k": 11,
542
+ "n": 40,
543
+ "share": 0.275,
544
+ "lower": null,
545
+ "upper": null
546
+ }
547
+ },
548
+ "median_abs": 4.643408,
549
+ "residual_hist": {
550
+ "edges": [
551
+ -4.0,
552
+ -3.75,
553
+ -3.5,
554
+ -3.25,
555
+ -3.0,
556
+ -2.75,
557
+ -2.5,
558
+ -2.25,
559
+ -2.0,
560
+ -1.75,
561
+ -1.5,
562
+ -1.25,
563
+ -1.0,
564
+ -0.75,
565
+ -0.5,
566
+ -0.25,
567
+ 0.0,
568
+ 0.25,
569
+ 0.5,
570
+ 0.75,
571
+ 1.0,
572
+ 1.25,
573
+ 1.5,
574
+ 1.75,
575
+ 2.0,
576
+ 2.25,
577
+ 2.5,
578
+ 2.75,
579
+ 3.0,
580
+ 3.25,
581
+ 3.5,
582
+ 3.75,
583
+ 4.0
584
+ ],
585
+ "counts": [
586
+ 0,
587
+ 0,
588
+ 1,
589
+ 0,
590
+ 0,
591
+ 0,
592
+ 1,
593
+ 1,
594
+ 1,
595
+ 2,
596
+ 1,
597
+ 1,
598
+ 0,
599
+ 0,
600
+ 0,
601
+ 1,
602
+ 0,
603
+ 0,
604
+ 0,
605
+ 1,
606
+ 1,
607
+ 0,
608
+ 1,
609
+ 2,
610
+ 0,
611
+ 0,
612
+ 0,
613
+ 1,
614
+ 2,
615
+ 0,
616
+ 1,
617
+ 1
618
+ ],
619
+ "under": 6,
620
+ "over": 15,
621
+ "sign": "positive: the snapped point lies outward of the truth face, away from the center line"
622
+ },
623
+ "published_hist": {
624
+ "edges": [
625
+ -4.0,
626
+ -3.75,
627
+ -3.5,
628
+ -3.25,
629
+ -3.0,
630
+ -2.75,
631
+ -2.5,
632
+ -2.25,
633
+ -2.0,
634
+ -1.75,
635
+ -1.5,
636
+ -1.25,
637
+ -1.0,
638
+ -0.75,
639
+ -0.5,
640
+ -0.25,
641
+ 0.0,
642
+ 0.25,
643
+ 0.5,
644
+ 0.75,
645
+ 1.0,
646
+ 1.25,
647
+ 1.5,
648
+ 1.75,
649
+ 2.0,
650
+ 2.25,
651
+ 2.5,
652
+ 2.75,
653
+ 3.0,
654
+ 3.25,
655
+ 3.5,
656
+ 3.75,
657
+ 4.0
658
+ ],
659
+ "counts": [
660
+ 1,
661
+ 0,
662
+ 0,
663
+ 0,
664
+ 0,
665
+ 2,
666
+ 0,
667
+ 1,
668
+ 2,
669
+ 0,
670
+ 1,
671
+ 0,
672
+ 0,
673
+ 0,
674
+ 0,
675
+ 1,
676
+ 0,
677
+ 0,
678
+ 2,
679
+ 1,
680
+ 0,
681
+ 0,
682
+ 0,
683
+ 0,
684
+ 0,
685
+ 1,
686
+ 0,
687
+ 0,
688
+ 0,
689
+ 1,
690
+ 0,
691
+ 1
692
+ ],
693
+ "under": 1,
694
+ "over": 25
695
+ },
696
+ "controls": [
697
+ {
698
+ "name": "published label, C0 test gate (170 scorable sites)",
699
+ "observed": 0.282353,
700
+ "require": "below",
701
+ "limit": 0.6,
702
+ "holds": true
703
+ },
704
+ {
705
+ "name": "2.399 µm face moved +1.5 voxels, C0 test gate (161 sites)",
706
+ "observed": 0.049689,
707
+ "require": "below",
708
+ "limit": 0.5,
709
+ "holds": true
710
+ },
711
+ {
712
+ "name": "2.399 µm face moved -1.5 voxels, C0 test gate (161 sites)",
713
+ "observed": 0.111801,
714
+ "require": "below",
715
+ "limit": 0.5,
716
+ "holds": true
717
+ },
718
+ {
719
+ "name": "2.399 µm face moved +3 voxels, C0 test gate (161 sites)",
720
+ "observed": 0.0,
721
+ "require": "below",
722
+ "limit": 0.2,
723
+ "holds": true
724
+ },
725
+ {
726
+ "name": "2.399 µm face moved -3 voxels, C0 test gate (161 sites)",
727
+ "observed": 0.0,
728
+ "require": "below",
729
+ "limit": 0.2,
730
+ "holds": true
731
+ },
732
+ {
733
+ "name": "verso face, C0 test gate (170 scorable sites)",
734
+ "observed": 0.0,
735
+ "require": "below",
736
+ "limit": 0.5,
737
+ "holds": true
738
+ },
739
+ {
740
+ "name": "positive control: the 2.399 µm scan's own face, C0 test gate (170 scorable sites)",
741
+ "observed": 0.870588,
742
+ "require": "above",
743
+ "limit": 0.75,
744
+ "holds": true
745
+ }
746
+ ]
747
+ },
748
+ {
749
+ "id": "C1",
750
+ "code": 1,
751
+ "primary": true,
752
+ "verdict": "NOT SHOWN",
753
+ "instrument_limited": true,
754
+ "why": "no PASS, since the lower bound 0.0% is under the 90% bar, and no FAIL, since 0 sites are certain misses where a FAIL needs 28; 0 of 200 sites within 1 voxel counting each unscorable site as a miss, 2 scorable sites missed and 198 not scorable; the 2.399 µm scan's own face reaches a lower bound of 0.0% at the same sites; sites in z 4680 to 5184 of the label scan, one vertex each, drawn after the rule was frozen",
755
+ "n_selected": 200,
756
+ "n_scorable": 2,
757
+ "within": {
758
+ "0.5": null,
759
+ "1": {
760
+ "basis": "itt",
761
+ "k": 0,
762
+ "n": 200,
763
+ "share": 0.0,
764
+ "lower": 0.0,
765
+ "upper": 0.014867
766
+ },
767
+ "2": {
768
+ "basis": "scorable",
769
+ "k": 0,
770
+ "n": 2,
771
+ "share": 0.0,
772
+ "lower": null,
773
+ "upper": null
774
+ }
775
+ },
776
+ "median_abs": 13.756861,
777
+ "residual_hist": {
778
+ "edges": [
779
+ -4.0,
780
+ -3.75,
781
+ -3.5,
782
+ -3.25,
783
+ -3.0,
784
+ -2.75,
785
+ -2.5,
786
+ -2.25,
787
+ -2.0,
788
+ -1.75,
789
+ -1.5,
790
+ -1.25,
791
+ -1.0,
792
+ -0.75,
793
+ -0.5,
794
+ -0.25,
795
+ 0.0,
796
+ 0.25,
797
+ 0.5,
798
+ 0.75,
799
+ 1.0,
800
+ 1.25,
801
+ 1.5,
802
+ 1.75,
803
+ 2.0,
804
+ 2.25,
805
+ 2.5,
806
+ 2.75,
807
+ 3.0,
808
+ 3.25,
809
+ 3.5,
810
+ 3.75,
811
+ 4.0
812
+ ],
813
+ "counts": [
814
+ 0,
815
+ 0,
816
+ 0,
817
+ 0,
818
+ 0,
819
+ 0,
820
+ 0,
821
+ 0,
822
+ 0,
823
+ 0,
824
+ 0,
825
+ 0,
826
+ 0,
827
+ 0,
828
+ 0,
829
+ 0,
830
+ 0,
831
+ 0,
832
+ 0,
833
+ 0,
834
+ 0,
835
+ 0,
836
+ 0,
837
+ 0,
838
+ 0,
839
+ 0,
840
+ 0,
841
+ 0,
842
+ 0,
843
+ 0,
844
+ 0,
845
+ 0
846
+ ],
847
+ "under": 0,
848
+ "over": 2,
849
+ "sign": "positive: the snapped point lies outward of the truth face, away from the center line"
850
+ },
851
+ "published_hist": {
852
+ "edges": [
853
+ -4.0,
854
+ -3.75,
855
+ -3.5,
856
+ -3.25,
857
+ -3.0,
858
+ -2.75,
859
+ -2.5,
860
+ -2.25,
861
+ -2.0,
862
+ -1.75,
863
+ -1.5,
864
+ -1.25,
865
+ -1.0,
866
+ -0.75,
867
+ -0.5,
868
+ -0.25,
869
+ 0.0,
870
+ 0.25,
871
+ 0.5,
872
+ 0.75,
873
+ 1.0,
874
+ 1.25,
875
+ 1.5,
876
+ 1.75,
877
+ 2.0,
878
+ 2.25,
879
+ 2.5,
880
+ 2.75,
881
+ 3.0,
882
+ 3.25,
883
+ 3.5,
884
+ 3.75,
885
+ 4.0
886
+ ],
887
+ "counts": [
888
+ 0,
889
+ 0,
890
+ 0,
891
+ 0,
892
+ 0,
893
+ 0,
894
+ 0,
895
+ 0,
896
+ 0,
897
+ 0,
898
+ 0,
899
+ 0,
900
+ 0,
901
+ 0,
902
+ 0,
903
+ 0,
904
+ 0,
905
+ 0,
906
+ 0,
907
+ 0,
908
+ 0,
909
+ 0,
910
+ 0,
911
+ 0,
912
+ 0,
913
+ 0,
914
+ 0,
915
+ 0,
916
+ 0,
917
+ 0,
918
+ 0,
919
+ 0
920
+ ],
921
+ "under": 0,
922
+ "over": 2
923
+ },
924
+ "controls": [
925
+ {
926
+ "name": "published label, C0 test gate (170 scorable sites)",
927
+ "observed": 0.282353,
928
+ "require": "below",
929
+ "limit": 0.6,
930
+ "holds": true
931
+ },
932
+ {
933
+ "name": "2.399 µm face moved +1.5 voxels, C0 test gate (161 sites)",
934
+ "observed": 0.049689,
935
+ "require": "below",
936
+ "limit": 0.5,
937
+ "holds": true
938
+ },
939
+ {
940
+ "name": "2.399 µm face moved -1.5 voxels, C0 test gate (161 sites)",
941
+ "observed": 0.111801,
942
+ "require": "below",
943
+ "limit": 0.5,
944
+ "holds": true
945
+ },
946
+ {
947
+ "name": "2.399 µm face moved +3 voxels, C0 test gate (161 sites)",
948
+ "observed": 0.0,
949
+ "require": "below",
950
+ "limit": 0.2,
951
+ "holds": true
952
+ },
953
+ {
954
+ "name": "2.399 µm face moved -3 voxels, C0 test gate (161 sites)",
955
+ "observed": 0.0,
956
+ "require": "below",
957
+ "limit": 0.2,
958
+ "holds": true
959
+ },
960
+ {
961
+ "name": "verso face, C0 test gate (170 scorable sites)",
962
+ "observed": 0.0,
963
+ "require": "below",
964
+ "limit": 0.5,
965
+ "holds": true
966
+ },
967
+ {
968
+ "name": "positive control: the 2.399 µm scan's own face, C0 test gate (170 scorable sites)",
969
+ "observed": 0.870588,
970
+ "require": "above",
971
+ "limit": 0.75,
972
+ "holds": true
973
+ }
974
+ ]
975
+ }
976
+ ]
977
+ },
978
+ {
979
+ "scroll": "PHerc0814",
980
+ "claim": "not_shown",
981
+ "why_not_shown": "not tested yet: no frozen voxel-exact test has run on this scroll",
982
+ "label_scan": {
983
+ "volume_id": "20250804134230",
984
+ "voxel_um": 9.362
985
+ },
986
+ "truth": null,
987
+ "oracle": null,
988
+ "certificate": null,
989
+ "vertices": {
990
+ "released": 1143812,
991
+ "held_back": 226239,
992
+ "by_code": {
993
+ "0": 956569,
994
+ "2": 179773,
995
+ "1": 7470
996
+ }
997
+ },
998
+ "void": [],
999
+ "strata": []
1000
+ },
1001
+ {
1002
+ "scroll": "PHerc0841",
1003
+ "claim": "not_shown",
1004
+ "why_not_shown": "not tested yet: no frozen voxel-exact test has run on this scroll",
1005
+ "label_scan": {
1006
+ "volume_id": "20250821151531",
1007
+ "voxel_um": 9.366
1008
+ },
1009
+ "truth": null,
1010
+ "oracle": null,
1011
+ "certificate": null,
1012
+ "vertices": {
1013
+ "released": 73856,
1014
+ "held_back": 0,
1015
+ "by_code": {
1016
+ "0": 58582,
1017
+ "2": 15106,
1018
+ "1": 168
1019
+ }
1020
+ },
1021
+ "void": [],
1022
+ "strata": []
1023
+ },
1024
+ {
1025
+ "scroll": "PHerc0172",
1026
+ "claim": "not_shown",
1027
+ "why_not_shown": "not tested yet: no frozen voxel-exact test has run on this scroll, which was snapped in its own label scan",
1028
+ "label_scan": {
1029
+ "volume_id": "20241024131838",
1030
+ "voxel_um": 7.91
1031
+ },
1032
+ "truth": null,
1033
+ "oracle": null,
1034
+ "certificate": null,
1035
+ "vertices": {
1036
+ "released": 28579231,
1037
+ "held_back": 0,
1038
+ "by_code": {
1039
+ "0": 17954644,
1040
+ "2": 10317309,
1041
+ "1": 307278
1042
+ }
1043
+ },
1044
+ "void": [],
1045
+ "strata": []
1046
+ }
1047
+ ],
1048
+ "showcase": []
1049
+ }