mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
eval(patch-probe): billing grid + OCR phase-sweep findings
Probe 1: both fable-5 and sonnet-5 bill vision on a 28x28 patch grid (image_tokens = 3 + ceil(W/28)*ceil(H/28)); snap rows≡6 (mod 7), cols≡4 (mod 28) to avoid stranding paid patch area. Probe 2: patch-boundary straddling does NOT affect OCR accuracy (cols: 4.47% vs 4.60%; rows: z=-0.45, 7-offset paired sweep with line fixed effects). Real misread drivers: high-entropy runs (bimodal derailment on base64 blobs), 5x8 confusables (w/W, 8/0), line wraps. Harnesses: count-tokens-sweep, accuracy-phase-probe (line-DP-aligned scoring), rescore-sweep (offline paired analysis).
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Patch-grid probe findings
|
||||
|
||||
## Probe 1 — billing staircase (count_tokens, free) ✅
|
||||
|
||||
- Both `claude-fable-5` and `claude-sonnet-5` bill vision on a **28×28 px patch
|
||||
grid**. The "Sonnet uses 32×32" rumor is false: image_tokens steps occur
|
||||
exactly when W or H crosses a multiple of 28, on both axes, both models
|
||||
(CSVs in this dir).
|
||||
- Formula: `image_tokens = 3 + ceil(W/28) * ceil(H/28)` (fixed +3 per image).
|
||||
Spot check at H=28: W=53..56 → 5 tokens (2 patches), W=57..60 → 6 (3 patches).
|
||||
- The docs' `(W×H)/750` is just an approximation of 784 px²/patch (28²) plus
|
||||
the constant.
|
||||
- No server-side resample at ≤1568 px long edge: a 1.73 MP image bills the full
|
||||
grid, so there is no ~1.15 MP cap kicking in — what we render is exactly what
|
||||
the model sees.
|
||||
|
||||
## Geometry consequences for pxpipe (CELL 5×8, PAD_X = PAD_Y = 4)
|
||||
|
||||
- Height = 8 + 8·rows → hits a 28-multiple iff **rows ≡ 6 (mod 7)**
|
||||
(every 7 rows = 56 px = exactly 2 patch rows).
|
||||
`MAX_HEIGHT_PX = 728` at 90 rows = 26 patch rows, a perfect fit.
|
||||
- Width = 8 + 5·cols (+ atlas slack) → hits a 28-multiple iff
|
||||
**cols ≡ 4 (mod 28)** (every 28 cols = 140 px = 5 patch cols).
|
||||
312 cols → 1568 px = 56 patch cols, also a perfect fit at the width cap.
|
||||
- Full-cap page: 312 × 90 = 28,080 chars for 3 + 56·26 = **1459 tokens ≈ 19.2
|
||||
chars/token** — the density ceiling for this geometry.
|
||||
- Snapping rule: pick rows ≡ 6 (mod 7) and cols ≡ 4 (mod 28); anything else
|
||||
strands already-paid patch area (up to 27 px per axis, ~1–3% of the bill).
|
||||
- Phase structure: gcd(5,28)=1 → all 28 horizontal glyph↔patch phases occur in
|
||||
every image; gcd(8,28)=4 → only 7 distinct vertical phases.
|
||||
|
||||
## Probe 2 — accuracy vs phase ✅ (verdict: phase alignment does NOT matter)
|
||||
|
||||
- Question: do glyphs straddling a patch boundary misread more? A 5 px glyph
|
||||
straddles when `x mod 28 ≥ 24`; an 8 px glyph row straddles when
|
||||
`y mod 28 ≥ 21`. If straddle phases dominate errors, phase-locked pitch
|
||||
(fractional 5.6 / 9.33 px advances, ≈24% density cost) could pay for itself;
|
||||
if flat, keep packed 5×8 and close the alignment theory.
|
||||
- Method notes (pitfalls that produced false signals first):
|
||||
- Pure-random char grids trip the safety layer ("looks like credentials") —
|
||||
use real repo source as content.
|
||||
- `temperature` is rejected by fable/sonnet-5 → sampling is stochastic;
|
||||
single runs are NOT repeatable (same image scored 169 vs 237 errs).
|
||||
- Models wrap/merge lines (sonnet emitted 101 lines for 90); positional
|
||||
line pairing turns one slip into a phase-flat ~27% error smear. The
|
||||
harness now does banded line-level DP alignment before char scoring
|
||||
(sonnet went 72.54% → 99.87% on the identical response).
|
||||
- Within one image, row phase aliases content line-type every 7 lines
|
||||
(8·7 = 56 = 2 patches) → row buckets are confounded. Controlled sweep:
|
||||
prepend k = 0..6 blank lines (shifts content by 8k px through all 7 row
|
||||
phases, content identical), then score with per-line fixed effects
|
||||
(`rescore-sweep.mjs`, offline, from dumped responses).
|
||||
- **Results (claude-fable-5, 312×90 production geometry):**
|
||||
- Columns (within-image, content-controlled by construction): straddle
|
||||
4.47% vs aligned 4.60% on 3,691 chars — null, per-phase table flat.
|
||||
- Rows (7-offset paired sweep, 439 line×run cells): straddle excess
|
||||
−0.16%±0.28 vs aligned +0.03%±0.29, **z = −0.45** — null.
|
||||
- Real code at full page: fable 99.96% (1/2,276), sonnet 99.87% (3/2,276).
|
||||
28,080 chars for 1,459 image tokens ≈ 19.2 chars/token with ~0.1% CER.
|
||||
- **What actually causes misreads** (in error-yield order):
|
||||
1. Long high-entropy runs (base64/hex blobs): bimodal derailment — the
|
||||
same line on the same pixels scored 4% and 73% error across runs. The
|
||||
decoder loses lock mid-run with no language prior to recover; this, not
|
||||
geometry, is the production misread mechanism.
|
||||
2. 5×8 confusables: `w→W` (11× in one page), `s→S`, `c→C`, `K→H`, `M→N`,
|
||||
`8→0`, `(→O`, `:→.` — legibility floor, context-corrected in real code.
|
||||
3. Line wraps on long lines — harmless after alignment, but consumers that
|
||||
trust exact line numbers must re-align.
|
||||
- Recommendations: keep packed 5×8 (fractional-advance idea rejected); snap
|
||||
dims per Probe 1 for billing only; route/flag high-entropy lines (e.g.
|
||||
>64 chars of base64-ish content) as literal text instead of pixels.
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env node
|
||||
// Probe 2: OCR accuracy vs glyph↔patch phase, using the PRODUCTION renderer.
|
||||
// gcd(CELL_W=5,28)=1 → one image contains all 28 horizontal phases;
|
||||
// gcd(CELL_H=8,28)=4 → 7 vertical phases. Content is gibberish lowercase
|
||||
// 4-letter groups (no language prior, not credential-shaped → avoids the
|
||||
// refusal classifier that fires on mixed-case alnum strings). Every 5th cell
|
||||
// is a space; cols must be a multiple of lcm(5,28)=140 so each phase sees an
|
||||
// equal number of letter cells (4 letters + 1 space per phase per 140 cols).
|
||||
//
|
||||
// Usage: CC_OAUTH_TOKEN=... node accuracy-phase-probe.mjs [model] [cols] [rows] [seed]
|
||||
// cols: multiple of 140, rows: multiple of 7.
|
||||
// NOTE: real inference — costs output tokens.
|
||||
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { renderTextToPngs, PAD_X, PAD_Y, CELL_W, CELL_H } from '../../dist/core/render.js';
|
||||
|
||||
const model = process.argv[2] ?? 'claude-fable-5';
|
||||
const COLS = +(process.argv[3] ?? 140);
|
||||
const ROWS = +(process.argv[4] ?? 21);
|
||||
const seedArg = process.argv[5] ?? '1';
|
||||
const PADL = +(process.argv[6] ?? 0); // blank lines prepended to the IMAGE only: shifts row phase by PADL*CELL_H px, content identical
|
||||
const fileMode = !/^\d+$/.test(seedArg); // non-numeric 5th arg = path to a real text file
|
||||
const seed = fileMode ? 1 : +seedArg;
|
||||
const P = 28;
|
||||
if (!fileMode && (COLS % 140 || ROWS % 7)) console.error(`warn: cols%140=${COLS % 140} rows%7=${ROWS % 7} — phases unbalanced`);
|
||||
|
||||
let s = (seed >>> 0) || 1;
|
||||
const rnd = () => (s ^= s << 13, s ^= s >>> 17, s ^= s << 5, (s >>> 0) / 2 ** 32);
|
||||
// Random-order REAL dictionary words: refusal-safe, production-representative.
|
||||
// Language prior is phase-independent, so straddle effects survive as relative
|
||||
// error-rate differences. Phase balance is statistical, not exact (per-phase
|
||||
// totals are tracked, so unevenness is handled in the rates).
|
||||
const { readFileSync } = await import('node:fs');
|
||||
let grid;
|
||||
if (fileMode) {
|
||||
// Production-representative content: real source text, pre-wrapped to COLS.
|
||||
grid = readFileSync(seedArg, 'utf8').split('\n')
|
||||
.map(l => l.replace(/\t/g, ' ').replace(/[^\x20-\x7e]/g, '?').trimEnd().slice(0, COLS))
|
||||
.filter(l => l.trim().length >= 8)
|
||||
.slice(0, ROWS);
|
||||
if (grid.length < ROWS) console.error(`warn: file only yielded ${grid.length} lines`);
|
||||
} else {
|
||||
const WORDS = readFileSync('/usr/share/dict/words', 'utf8').split('\n')
|
||||
.filter(w => /^[a-z]{3,7}$/.test(w));
|
||||
grid = Array.from({ length: ROWS }, () => {
|
||||
let line = '';
|
||||
for (;;) {
|
||||
const w = WORDS[(rnd() * WORDS.length) | 0];
|
||||
if (line.length + w.length + (line ? 1 : 0) > COLS) break;
|
||||
line += (line ? ' ' : '') + w;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
}
|
||||
|
||||
const imgs = await renderTextToPngs('\n'.repeat(PADL) + grid.join('\n'), COLS);
|
||||
if (PADL) console.error(`padLines=${PADL} (row shift ${PADL * CELL_H}px, phase +${(PADL * CELL_H) % 28} mod 28)`);
|
||||
if (imgs.length !== 1) throw new Error(`expected 1 image, got ${imgs.length}`);
|
||||
const img = imgs[0];
|
||||
const png = img.png ?? img.data ?? img.buffer ?? Object.values(img).find(v => v instanceof Uint8Array);
|
||||
if (!png) throw new Error('no png buffer; keys=' + Object.keys(img).join(','));
|
||||
console.error(`image: ${img.width ?? '?'}x${img.height ?? '?'}px ${png.length}B; predicted image_tokens=` +
|
||||
(img.width && img.height ? 3 + Math.ceil(img.width / P) * Math.ceil(img.height / P) : '?'));
|
||||
|
||||
const BASE = process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com';
|
||||
const res = await fetch(`${BASE}/v1/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'authorization': `Bearer ${process.env.CC_OAUTH_TOKEN}`,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'anthropic-beta': 'oauth-2025-04-20',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
max_tokens: ROWS * (COLS + 2) + 1500,
|
||||
system: "You are Claude Code, Anthropic's official CLI for Claude.",
|
||||
messages: [{
|
||||
role: 'user', content: [
|
||||
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: Buffer.from(png).toString('base64') } },
|
||||
{ type: 'text', text: `OCR this image to plain text. Preserve line breaks. Output only the text.` },
|
||||
],
|
||||
}],
|
||||
}),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!res.ok) { console.error('API error:', JSON.stringify(j)); process.exit(1); }
|
||||
await writeFile(`/tmp/phase-probe-${model}-${Date.now()}.json`, JSON.stringify({ model, cols: COLS, rows: ROWS, seedArg, padLines: PADL, grid, resp: j }, null, 2));
|
||||
const out = (j.content?.find(b => b.type === 'text')?.text ?? '')
|
||||
.replace(/```[a-z]*\n?/g, '').split('\n').map(l => l.trimEnd()).filter(l => l.length);
|
||||
console.error(`usage=${JSON.stringify(j.usage)} stop=${j.stop_reason} lines=${out.length}/${ROWS}`);
|
||||
if (out.length < ROWS) console.error('rawTextHead: ' + JSON.stringify((j.content?.find(b => b.type === 'text')?.text ?? '').slice(0, 200)));
|
||||
|
||||
// Alignment-based scoring: Levenshtein backtrace marks which truth positions
|
||||
// matched exactly; everything else (sub or indel) is an error at its truth pos.
|
||||
function alignOk(truth, got, subs) {
|
||||
const T = truth.length, G = got.length;
|
||||
const dp = Array.from({ length: T + 1 }, () => new Array(G + 1).fill(0));
|
||||
for (let i = 0; i <= T; i++) dp[i][0] = i;
|
||||
for (let jj = 0; jj <= G; jj++) dp[0][jj] = jj;
|
||||
for (let i = 1; i <= T; i++) for (let jj = 1; jj <= G; jj++)
|
||||
dp[i][jj] = Math.min(dp[i - 1][jj - 1] + (truth[i - 1] === got[jj - 1] ? 0 : 1), dp[i - 1][jj] + 1, dp[i][jj - 1] + 1);
|
||||
const ok = new Array(T).fill(false);
|
||||
let i = T, jj = G;
|
||||
while (i > 0 && jj > 0) {
|
||||
if (dp[i][jj] === dp[i - 1][jj - 1] + (truth[i - 1] === got[jj - 1] ? 0 : 1)) {
|
||||
if (truth[i - 1] === got[jj - 1]) ok[i - 1] = true;
|
||||
else subs.set(`${truth[i - 1]}->${got[jj - 1]}`, (subs.get(`${truth[i - 1]}->${got[jj - 1]}`) ?? 0) + 1);
|
||||
i--; jj--;
|
||||
} else if (dp[i][jj] === dp[i - 1][jj] + 1) i--;
|
||||
else jj--;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Line-level alignment BEFORE char scoring: the model may emit preamble lines,
|
||||
// wrap long lines (1 truth : 2 out), or drop lines. Positional r->r pairing
|
||||
// turns one such slip into a phase-flat error smear. Banded DP, merge-aware.
|
||||
function lev(a, b) {
|
||||
const m = b.length;
|
||||
let prev = Array.from({ length: m + 1 }, (_, k) => k), cur = new Array(m + 1);
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
cur[0] = i;
|
||||
for (let k = 1; k <= m; k++)
|
||||
cur[k] = Math.min(prev[k - 1] + (a[i - 1] === b[k - 1] ? 0 : 1), prev[k] + 1, cur[k - 1] + 1);
|
||||
[prev, cur] = [cur, prev];
|
||||
}
|
||||
return prev[m];
|
||||
}
|
||||
function alignLines(truth, got) {
|
||||
const T = truth.length, G = got.length, BAND = 25, INF = 1e9;
|
||||
const dp = Array.from({ length: T + 1 }, () => new Array(G + 1).fill(INF));
|
||||
const bt = Array.from({ length: T + 1 }, () => new Array(G + 1).fill(null));
|
||||
dp[0][0] = 0;
|
||||
for (let jj = 1; jj <= G; jj++) { dp[0][jj] = dp[0][jj - 1] + 2; bt[0][jj] = ['spur']; }
|
||||
for (let i = 1; i <= T; i++) {
|
||||
dp[i][0] = dp[i - 1][0] + truth[i - 1].length; bt[i][0] = ['miss'];
|
||||
for (let jj = Math.max(1, i - BAND); jj <= Math.min(G, i + BAND); jj++) {
|
||||
let c = dp[i - 1][jj - 1] + lev(truth[i - 1], got[jj - 1]), b = ['m11'];
|
||||
if (jj >= 2 && dp[i - 1][jj - 2] < INF) {
|
||||
const merged = [got[jj - 2] + ' ' + got[jj - 1], got[jj - 2] + got[jj - 1]];
|
||||
for (const mtxt of merged) {
|
||||
const c2 = dp[i - 1][jj - 2] + lev(truth[i - 1], mtxt);
|
||||
if (c2 < c) { c = c2; b = ['m12', mtxt]; }
|
||||
}
|
||||
}
|
||||
if (dp[i][jj - 1] + 2 < c) { c = dp[i][jj - 1] + 2; b = ['spur']; }
|
||||
if (dp[i - 1][jj] + truth[i - 1].length < c) { c = dp[i - 1][jj] + truth[i - 1].length; b = ['miss']; }
|
||||
dp[i][jj] = c; bt[i][jj] = b;
|
||||
}
|
||||
}
|
||||
const matched = new Array(T).fill(null);
|
||||
let i = T, jj = G;
|
||||
while ((i > 0 || jj > 0) && bt[i][jj]) {
|
||||
const b = bt[i][jj];
|
||||
if (b[0] === 'spur') jj--;
|
||||
else if (b[0] === 'miss') i--;
|
||||
else if (b[0] === 'm12') { matched[i - 1] = b[1]; i--; jj -= 2; }
|
||||
else { matched[i - 1] = got[jj - 1]; i--; jj--; }
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
const mk = () => ({ err: new Array(P).fill(0), tot: new Array(P).fill(0) });
|
||||
const col = mk(), row = mk();
|
||||
let errs = 0, tot = 0, missedLines = 0;
|
||||
const subs = new Map();
|
||||
const matched = alignLines(grid, out);
|
||||
for (let r = 0; r < ROWS && r < grid.length; r++) {
|
||||
const truth = grid[r];
|
||||
if (matched[r] == null) { missedLines++; continue; } // structural, not phase-scorable
|
||||
const ok = alignOk(truth, matched[r], subs);
|
||||
for (let c = 0; c < truth.length; c++) {
|
||||
if (truth[c] === ' ') continue; // letters only
|
||||
const cp = (PAD_X + c * CELL_W) % P, rp = (PAD_Y + (r + PADL) * CELL_H) % P;
|
||||
col.tot[cp]++; row.tot[rp]++; tot++;
|
||||
if (!ok[c]) { errs++; col.err[cp]++; row.err[rp]++; }
|
||||
}
|
||||
}
|
||||
const agg = (m, pred) => {
|
||||
let e = 0, t = 0;
|
||||
for (let p = 0; p < P; p++) if (m.tot[p] && pred(p)) { e += m.err[p]; t += m.tot[p]; }
|
||||
return t ? `${(100 * e / t).toFixed(2)}% (${e}/${t})` : 'n/a';
|
||||
};
|
||||
console.log(`model=${model} cols=${COLS} rows=${ROWS} seed=${seed}`);
|
||||
console.log(`overall acc=${(100 * (1 - errs / tot)).toFixed(2)}% errs=${errs}/${tot} scoredLines=${grid.length - missedLines}/${ROWS} missedLines=${missedLines} rawOutLines=${out.length}`);
|
||||
console.log(`colStraddle(x%28>=24): ${agg(col, p => p >= 24)} vs aligned: ${agg(col, p => p < 24)}`);
|
||||
console.log(`rowStraddle(y%28>=21): ${agg(row, p => p >= 21)} vs aligned: ${agg(row, p => p < 21)}`);
|
||||
console.log('axis,phase,err,tot,errPct');
|
||||
for (let p = 0; p < P; p++) if (col.tot[p]) console.log(`col,${p},${col.err[p]},${col.tot[p]},${(100 * col.err[p] / col.tot[p]).toFixed(1)}`);
|
||||
for (let p = 0; p < P; p++) if (row.tot[p]) console.log(`row,${p},${row.err[p]},${row.tot[p]},${(100 * row.err[p] / row.tot[p]).toFixed(1)}`);
|
||||
console.log('topConfusions: ' + [...subs.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).map(([k, n]) => `${k}×${n}`).join(' '));
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Probe 1: billing-staircase sweep against /v1/messages/count_tokens (free, unbilled).
|
||||
*
|
||||
* Sends blank PNGs of swept dimensions and records input_tokens. If image cost
|
||||
* quantizes as ceil(W/P)*ceil(H/P)*k, step positions in the W (or H) direction
|
||||
* reveal the vision patch size P (28 vs 32 hypothesis). If cost is smooth
|
||||
* ~(W*H)/750, billing is decoupled from the encoder grid and this channel is silent.
|
||||
*
|
||||
* Usage:
|
||||
* CC_OAUTH_TOKEN=... node count-tokens-sweep.mjs <model> <axis W|H> <fixed> <from> <to>
|
||||
* Output: CSV w,h,input_tokens,image_tokens (image_tokens = delta vs no-image baseline)
|
||||
*/
|
||||
import sharp from 'sharp';
|
||||
|
||||
const TOKEN = process.env.CC_OAUTH_TOKEN;
|
||||
if (!TOKEN) {
|
||||
console.error('CC_OAUTH_TOKEN not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const API = 'https://api.anthropic.com/v1/messages/count_tokens';
|
||||
|
||||
async function count(model, content) {
|
||||
const body = {
|
||||
model,
|
||||
// Constant across all calls -> cancels in the baseline delta.
|
||||
system: [{ type: 'text', text: "You are Claude Code, Anthropic's official CLI for Claude." }],
|
||||
messages: [{ role: 'user', content }],
|
||||
};
|
||||
const res = await fetch(API, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
authorization: `Bearer ${TOKEN}`,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'anthropic-beta': 'oauth-2025-04-20',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
||||
return (await res.json()).input_tokens;
|
||||
}
|
||||
|
||||
const blankPng = (w, h) =>
|
||||
sharp({ create: { width: w, height: h, channels: 3, background: { r: 255, g: 255, b: 255 } } })
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const model = process.argv[2] ?? 'claude-fable-5';
|
||||
const axis = (process.argv[3] ?? 'W').toUpperCase();
|
||||
const fixed = parseInt(process.argv[4] ?? '56', 10);
|
||||
const from = parseInt(process.argv[5] ?? '20', 10);
|
||||
const to = parseInt(process.argv[6] ?? '100', 10);
|
||||
|
||||
const baseline = await count(model, [{ type: 'text', text: 'x' }]);
|
||||
console.log(`# model=${model} axis=${axis} fixed=${fixed} baseline=${baseline}`);
|
||||
console.log('w,h,input_tokens,image_tokens');
|
||||
|
||||
for (let v = from; v <= to; v++) {
|
||||
const [w, h] = axis === 'W' ? [v, fixed] : [fixed, v];
|
||||
const data = (await blankPng(w, h)).toString('base64');
|
||||
let t;
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
t = await count(model, [
|
||||
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data } },
|
||||
{ type: 'text', text: 'x' },
|
||||
]);
|
||||
break;
|
||||
} catch (e) {
|
||||
if (attempt >= 3) throw e;
|
||||
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1))); // ride out RPM 429s
|
||||
}
|
||||
}
|
||||
console.log(`${w},${h},${t},${t - baseline}`);
|
||||
await new Promise((r) => setTimeout(r, 60));
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env node
|
||||
// Offline paired re-analysis of the row-phase sweep. Loads dumped responses
|
||||
// (/tmp/phase-probe-claude-fable-5-*.json), maps each to its padLines k via the
|
||||
// usage token counts in /tmp/phase-sweep-k*.txt, then measures the phase effect
|
||||
// with per-line fixed effects: excess(ℓ,k) = errRate(ℓ,k) − mean_k errRate(ℓ,·).
|
||||
// Content cancels; only geometry remains. No API calls.
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { PAD_Y, CELL_H } from '../../dist/core/render.js';
|
||||
|
||||
const P = 28;
|
||||
|
||||
function lev(a, b) {
|
||||
const m = b.length;
|
||||
let prev = Array.from({ length: m + 1 }, (_, k) => k), cur = new Array(m + 1);
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
cur[0] = i;
|
||||
for (let k = 1; k <= m; k++)
|
||||
cur[k] = Math.min(prev[k - 1] + (a[i - 1] === b[k - 1] ? 0 : 1), prev[k] + 1, cur[k - 1] + 1);
|
||||
[prev, cur] = [cur, prev];
|
||||
}
|
||||
return prev[m];
|
||||
}
|
||||
function alignLines(truth, got) {
|
||||
const T = truth.length, G = got.length, BAND = 25, INF = 1e9;
|
||||
const dp = Array.from({ length: T + 1 }, () => new Array(G + 1).fill(INF));
|
||||
const bt = Array.from({ length: T + 1 }, () => new Array(G + 1).fill(null));
|
||||
dp[0][0] = 0;
|
||||
for (let jj = 1; jj <= G; jj++) { dp[0][jj] = dp[0][jj - 1] + 2; bt[0][jj] = ['spur']; }
|
||||
for (let i = 1; i <= T; i++) {
|
||||
dp[i][0] = dp[i - 1][0] + truth[i - 1].length; bt[i][0] = ['miss'];
|
||||
for (let jj = Math.max(1, i - BAND); jj <= Math.min(G, i + BAND); jj++) {
|
||||
let c = dp[i - 1][jj - 1] + lev(truth[i - 1], got[jj - 1]), b = ['m11'];
|
||||
if (jj >= 2 && dp[i - 1][jj - 2] < INF) {
|
||||
for (const mtxt of [got[jj - 2] + ' ' + got[jj - 1], got[jj - 2] + got[jj - 1]]) {
|
||||
const c2 = dp[i - 1][jj - 2] + lev(truth[i - 1], mtxt);
|
||||
if (c2 < c) { c = c2; b = ['m12', mtxt]; }
|
||||
}
|
||||
}
|
||||
if (dp[i][jj - 1] + 2 < c) { c = dp[i][jj - 1] + 2; b = ['spur']; }
|
||||
if (dp[i - 1][jj] + truth[i - 1].length < c) { c = dp[i - 1][jj] + truth[i - 1].length; b = ['miss']; }
|
||||
dp[i][jj] = c; bt[i][jj] = b;
|
||||
}
|
||||
}
|
||||
const matched = new Array(T).fill(null);
|
||||
let i = T, jj = G;
|
||||
while ((i > 0 || jj > 0) && bt[i][jj]) {
|
||||
const b = bt[i][jj];
|
||||
if (b[0] === 'spur') jj--;
|
||||
else if (b[0] === 'miss') i--;
|
||||
else if (b[0] === 'm12') { matched[i - 1] = b[1]; i--; jj -= 2; }
|
||||
else { matched[i - 1] = got[jj - 1]; i--; jj--; }
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
// --- map dumps to k via usage token counts in sweep stdout files ---
|
||||
const keyOf = u => `${u.input_tokens}/${u.output_tokens}`;
|
||||
const kByKey = new Map();
|
||||
for (const f of readdirSync('/tmp').filter(f => /^phase-sweep-k\d\.txt$/.test(f))) {
|
||||
const txt = readFileSync('/tmp/' + f, 'utf8');
|
||||
const m = txt.match(/usage=(\{.*?\}) stop=/s);
|
||||
if (m) kByKey.set(keyOf(JSON.parse(m[1])), +f.match(/k(\d)/)[1]);
|
||||
}
|
||||
|
||||
const runs = [];
|
||||
for (const f of readdirSync('/tmp').filter(f => f.startsWith('phase-probe-claude-fable-5-') && f.endsWith('.json'))) {
|
||||
const d = JSON.parse(readFileSync('/tmp/' + f, 'utf8'));
|
||||
if (!(d.seedArg ?? '').includes('atlas')) continue;
|
||||
const k = kByKey.get(keyOf(d.resp.usage));
|
||||
runs.push({ file: f, k: k ?? 0, replicate: k == null, grid: d.grid, resp: d.resp });
|
||||
}
|
||||
console.log(`runs: ${runs.map(r => `k=${r.k}${r.replicate ? '(rep)' : ''}`).join(' ')}`);
|
||||
|
||||
// --- per-line error rates ---
|
||||
const L = runs[0].grid.length;
|
||||
const cells = []; // {l, k, rate, phase, straddle}
|
||||
for (const run of runs) {
|
||||
const out = (run.resp.content?.find(b => b.type === 'text')?.text ?? '')
|
||||
.replace(/```[a-z]*\n?/g, '').split('\n').map(l => l.trimEnd()).filter(l => l.length);
|
||||
const matched = alignLines(run.grid, out);
|
||||
for (let l = 0; l < L; l++) {
|
||||
if (matched[l] == null) continue;
|
||||
const phase = (PAD_Y + (l + run.k) * CELL_H) % P;
|
||||
cells.push({ l, k: run.k, rate: lev(run.grid[l], matched[l]) / run.grid[l].length, phase, straddle: phase >= 21 });
|
||||
}
|
||||
}
|
||||
|
||||
// per-line means (fixed effect)
|
||||
const byLine = new Map();
|
||||
for (const c of cells) (byLine.get(c.l) ?? byLine.set(c.l, []).get(c.l)).push(c.rate);
|
||||
const lineMean = new Map([...byLine].map(([l, rs]) => [l, rs.reduce((a, b) => a + b, 0) / rs.length]));
|
||||
for (const c of cells) c.excess = c.rate - lineMean.get(c.l);
|
||||
|
||||
const bucket = (sel) => {
|
||||
const xs = cells.filter(sel).map(c => c.excess);
|
||||
const n = xs.length, mean = xs.reduce((a, b) => a + b, 0) / n;
|
||||
const sd = Math.sqrt(xs.reduce((a, b) => a + (b - mean) ** 2, 0) / (n - 1));
|
||||
return { n, mean, se: sd / Math.sqrt(n) };
|
||||
};
|
||||
console.log('\nphase, n(line×run), excessErr%, ±SE%');
|
||||
for (const p of [...new Set(cells.map(c => c.phase))].sort((a, b) => a - b)) {
|
||||
const { n, mean, se } = bucket(c => c.phase === p);
|
||||
console.log(`${String(p).padStart(2)}${p >= 21 ? '*' : ' '} , ${n}, ${(100 * mean).toFixed(2)}, ±${(100 * se).toFixed(2)}`);
|
||||
}
|
||||
const s = bucket(c => c.straddle), a = bucket(c => !c.straddle);
|
||||
console.log(`\nstraddle(≥21): ${(100 * s.mean).toFixed(2)}%±${(100 * s.se).toFixed(2)} (n=${s.n}) aligned: ${(100 * a.mean).toFixed(2)}%±${(100 * a.se).toFixed(2)} (n=${a.n}) z=${((s.mean - a.mean) / Math.hypot(s.se, a.se)).toFixed(2)}`);
|
||||
|
||||
// hardest lines: err across k to eyeball content-vs-geometry
|
||||
const hard = [...lineMean].sort((x, y) => y[1] - x[1]).slice(0, 6);
|
||||
console.log('\nhardest lines (idx, meanErr%, per-run rate% by k, head):');
|
||||
for (const [l, m] of hard) {
|
||||
const per = cells.filter(c => c.l === l).sort((x, y) => x.k - y.k).map(c => `k${c.k}${c.straddle ? '*' : ''}:${(100 * c.rate).toFixed(0)}`);
|
||||
console.log(`#${l} ${(100 * m).toFixed(1)}% [${per.join(' ')}] ${JSON.stringify(runs[0].grid[l].slice(0, 48))}`);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
# model=claude-fable-5 axis=H fixed=56 baseline=31
|
||||
w,h,input_tokens,image_tokens
|
||||
56,16,36,5
|
||||
56,17,36,5
|
||||
56,18,36,5
|
||||
56,19,36,5
|
||||
56,20,36,5
|
||||
56,21,36,5
|
||||
56,22,36,5
|
||||
56,23,36,5
|
||||
56,24,36,5
|
||||
56,25,36,5
|
||||
56,26,36,5
|
||||
56,27,36,5
|
||||
56,28,36,5
|
||||
56,29,38,7
|
||||
56,30,38,7
|
||||
56,31,38,7
|
||||
56,32,38,7
|
||||
56,33,38,7
|
||||
56,34,38,7
|
||||
56,35,38,7
|
||||
56,36,38,7
|
||||
56,37,38,7
|
||||
56,38,38,7
|
||||
56,39,38,7
|
||||
56,40,38,7
|
||||
56,41,38,7
|
||||
56,42,38,7
|
||||
56,43,38,7
|
||||
56,44,38,7
|
||||
56,45,38,7
|
||||
56,46,38,7
|
||||
56,47,38,7
|
||||
56,48,38,7
|
||||
56,49,38,7
|
||||
56,50,38,7
|
||||
56,51,38,7
|
||||
56,52,38,7
|
||||
56,53,38,7
|
||||
56,54,38,7
|
||||
56,55,38,7
|
||||
56,56,38,7
|
||||
56,57,40,9
|
||||
56,58,40,9
|
||||
56,59,40,9
|
||||
56,60,40,9
|
||||
56,61,40,9
|
||||
56,62,40,9
|
||||
56,63,40,9
|
||||
56,64,40,9
|
||||
56,65,40,9
|
||||
56,66,40,9
|
||||
56,67,40,9
|
||||
56,68,40,9
|
||||
56,69,40,9
|
||||
56,70,40,9
|
||||
56,71,40,9
|
||||
56,72,40,9
|
||||
56,73,40,9
|
||||
56,74,40,9
|
||||
56,75,40,9
|
||||
56,76,40,9
|
||||
56,77,40,9
|
||||
56,78,40,9
|
||||
56,79,40,9
|
||||
56,80,40,9
|
||||
56,81,40,9
|
||||
56,82,40,9
|
||||
56,83,40,9
|
||||
56,84,40,9
|
||||
56,85,42,11
|
||||
56,86,42,11
|
||||
56,87,42,11
|
||||
56,88,42,11
|
||||
56,89,42,11
|
||||
56,90,42,11
|
||||
56,91,42,11
|
||||
56,92,42,11
|
||||
56,93,42,11
|
||||
56,94,42,11
|
||||
56,95,42,11
|
||||
56,96,42,11
|
||||
56,97,42,11
|
||||
56,98,42,11
|
||||
56,99,42,11
|
||||
56,100,42,11
|
||||
|
@@ -0,0 +1,87 @@
|
||||
# model=claude-fable-5 axis=W fixed=56 baseline=31
|
||||
w,h,input_tokens,image_tokens
|
||||
16,56,36,5
|
||||
17,56,36,5
|
||||
18,56,36,5
|
||||
19,56,36,5
|
||||
20,56,36,5
|
||||
21,56,36,5
|
||||
22,56,36,5
|
||||
23,56,36,5
|
||||
24,56,36,5
|
||||
25,56,36,5
|
||||
26,56,36,5
|
||||
27,56,36,5
|
||||
28,56,36,5
|
||||
29,56,38,7
|
||||
30,56,38,7
|
||||
31,56,38,7
|
||||
32,56,38,7
|
||||
33,56,38,7
|
||||
34,56,38,7
|
||||
35,56,38,7
|
||||
36,56,38,7
|
||||
37,56,38,7
|
||||
38,56,38,7
|
||||
39,56,38,7
|
||||
40,56,38,7
|
||||
41,56,38,7
|
||||
42,56,38,7
|
||||
43,56,38,7
|
||||
44,56,38,7
|
||||
45,56,38,7
|
||||
46,56,38,7
|
||||
47,56,38,7
|
||||
48,56,38,7
|
||||
49,56,38,7
|
||||
50,56,38,7
|
||||
51,56,38,7
|
||||
52,56,38,7
|
||||
53,56,38,7
|
||||
54,56,38,7
|
||||
55,56,38,7
|
||||
56,56,38,7
|
||||
57,56,40,9
|
||||
58,56,40,9
|
||||
59,56,40,9
|
||||
60,56,40,9
|
||||
61,56,40,9
|
||||
62,56,40,9
|
||||
63,56,40,9
|
||||
64,56,40,9
|
||||
65,56,40,9
|
||||
66,56,40,9
|
||||
67,56,40,9
|
||||
68,56,40,9
|
||||
69,56,40,9
|
||||
70,56,40,9
|
||||
71,56,40,9
|
||||
72,56,40,9
|
||||
73,56,40,9
|
||||
74,56,40,9
|
||||
75,56,40,9
|
||||
76,56,40,9
|
||||
77,56,40,9
|
||||
78,56,40,9
|
||||
79,56,40,9
|
||||
80,56,40,9
|
||||
81,56,40,9
|
||||
82,56,40,9
|
||||
83,56,40,9
|
||||
84,56,40,9
|
||||
85,56,42,11
|
||||
86,56,42,11
|
||||
87,56,42,11
|
||||
88,56,42,11
|
||||
89,56,42,11
|
||||
90,56,42,11
|
||||
91,56,42,11
|
||||
92,56,42,11
|
||||
93,56,42,11
|
||||
94,56,42,11
|
||||
95,56,42,11
|
||||
96,56,42,11
|
||||
97,56,42,11
|
||||
98,56,42,11
|
||||
99,56,42,11
|
||||
100,56,42,11
|
||||
|
@@ -0,0 +1,87 @@
|
||||
# model=claude-sonnet-5 axis=H fixed=56 baseline=31
|
||||
w,h,input_tokens,image_tokens
|
||||
56,16,36,5
|
||||
56,17,36,5
|
||||
56,18,36,5
|
||||
56,19,36,5
|
||||
56,20,36,5
|
||||
56,21,36,5
|
||||
56,22,36,5
|
||||
56,23,36,5
|
||||
56,24,36,5
|
||||
56,25,36,5
|
||||
56,26,36,5
|
||||
56,27,36,5
|
||||
56,28,36,5
|
||||
56,29,38,7
|
||||
56,30,38,7
|
||||
56,31,38,7
|
||||
56,32,38,7
|
||||
56,33,38,7
|
||||
56,34,38,7
|
||||
56,35,38,7
|
||||
56,36,38,7
|
||||
56,37,38,7
|
||||
56,38,38,7
|
||||
56,39,38,7
|
||||
56,40,38,7
|
||||
56,41,38,7
|
||||
56,42,38,7
|
||||
56,43,38,7
|
||||
56,44,38,7
|
||||
56,45,38,7
|
||||
56,46,38,7
|
||||
56,47,38,7
|
||||
56,48,38,7
|
||||
56,49,38,7
|
||||
56,50,38,7
|
||||
56,51,38,7
|
||||
56,52,38,7
|
||||
56,53,38,7
|
||||
56,54,38,7
|
||||
56,55,38,7
|
||||
56,56,38,7
|
||||
56,57,40,9
|
||||
56,58,40,9
|
||||
56,59,40,9
|
||||
56,60,40,9
|
||||
56,61,40,9
|
||||
56,62,40,9
|
||||
56,63,40,9
|
||||
56,64,40,9
|
||||
56,65,40,9
|
||||
56,66,40,9
|
||||
56,67,40,9
|
||||
56,68,40,9
|
||||
56,69,40,9
|
||||
56,70,40,9
|
||||
56,71,40,9
|
||||
56,72,40,9
|
||||
56,73,40,9
|
||||
56,74,40,9
|
||||
56,75,40,9
|
||||
56,76,40,9
|
||||
56,77,40,9
|
||||
56,78,40,9
|
||||
56,79,40,9
|
||||
56,80,40,9
|
||||
56,81,40,9
|
||||
56,82,40,9
|
||||
56,83,40,9
|
||||
56,84,40,9
|
||||
56,85,42,11
|
||||
56,86,42,11
|
||||
56,87,42,11
|
||||
56,88,42,11
|
||||
56,89,42,11
|
||||
56,90,42,11
|
||||
56,91,42,11
|
||||
56,92,42,11
|
||||
56,93,42,11
|
||||
56,94,42,11
|
||||
56,95,42,11
|
||||
56,96,42,11
|
||||
56,97,42,11
|
||||
56,98,42,11
|
||||
56,99,42,11
|
||||
56,100,42,11
|
||||
|
@@ -0,0 +1,87 @@
|
||||
# model=claude-sonnet-5 axis=W fixed=56 baseline=31
|
||||
w,h,input_tokens,image_tokens
|
||||
16,56,36,5
|
||||
17,56,36,5
|
||||
18,56,36,5
|
||||
19,56,36,5
|
||||
20,56,36,5
|
||||
21,56,36,5
|
||||
22,56,36,5
|
||||
23,56,36,5
|
||||
24,56,36,5
|
||||
25,56,36,5
|
||||
26,56,36,5
|
||||
27,56,36,5
|
||||
28,56,36,5
|
||||
29,56,38,7
|
||||
30,56,38,7
|
||||
31,56,38,7
|
||||
32,56,38,7
|
||||
33,56,38,7
|
||||
34,56,38,7
|
||||
35,56,38,7
|
||||
36,56,38,7
|
||||
37,56,38,7
|
||||
38,56,38,7
|
||||
39,56,38,7
|
||||
40,56,38,7
|
||||
41,56,38,7
|
||||
42,56,38,7
|
||||
43,56,38,7
|
||||
44,56,38,7
|
||||
45,56,38,7
|
||||
46,56,38,7
|
||||
47,56,38,7
|
||||
48,56,38,7
|
||||
49,56,38,7
|
||||
50,56,38,7
|
||||
51,56,38,7
|
||||
52,56,38,7
|
||||
53,56,38,7
|
||||
54,56,38,7
|
||||
55,56,38,7
|
||||
56,56,38,7
|
||||
57,56,40,9
|
||||
58,56,40,9
|
||||
59,56,40,9
|
||||
60,56,40,9
|
||||
61,56,40,9
|
||||
62,56,40,9
|
||||
63,56,40,9
|
||||
64,56,40,9
|
||||
65,56,40,9
|
||||
66,56,40,9
|
||||
67,56,40,9
|
||||
68,56,40,9
|
||||
69,56,40,9
|
||||
70,56,40,9
|
||||
71,56,40,9
|
||||
72,56,40,9
|
||||
73,56,40,9
|
||||
74,56,40,9
|
||||
75,56,40,9
|
||||
76,56,40,9
|
||||
77,56,40,9
|
||||
78,56,40,9
|
||||
79,56,40,9
|
||||
80,56,40,9
|
||||
81,56,40,9
|
||||
82,56,40,9
|
||||
83,56,40,9
|
||||
84,56,40,9
|
||||
85,56,42,11
|
||||
86,56,42,11
|
||||
87,56,42,11
|
||||
88,56,42,11
|
||||
89,56,42,11
|
||||
90,56,42,11
|
||||
91,56,42,11
|
||||
92,56,42,11
|
||||
93,56,42,11
|
||||
94,56,42,11
|
||||
95,56,42,11
|
||||
96,56,42,11
|
||||
97,56,42,11
|
||||
98,56,42,11
|
||||
99,56,42,11
|
||||
100,56,42,11
|
||||
|
Reference in New Issue
Block a user