Open the reference comparison in one click
'Compare to reference…' now sits beside the Compare toggle: picking a .h5
opens the study directly — the file's own settings (its recorded niter, its
band, its dt undivided) become the single selected variant, the compare bar
opens showing them, and the study builds paused at the file's exact initial
state, ready to Run. Loading a file over an existing study tears it down and
opens the new one; clearing stays disabled while a study is up.
The niter chips gain the file's recorded count when the standard list lacks
it, and drop any such value again when the file is cleared. Count lines say
'1 variant', since that is now the common case.
5 changed files+79−34
README.mdmodified+13−10View file
@@ -609,16 +609,19 @@ the surface-correction iteration count independent of the file, and
609609 `--tolerance`/`--tolerance-linf` each independently turn their metric into a
610610 pass/fail for CI.
611611
612-The same check runs in the page: **Compare → Reference file…** loads a `.h5`
613-into the convergence study. The file then defines the whole problem — model,
614-parameters, geometry, initial state — so every variant starts from its exact
615-initial condition, runs to its end time, and stops there, measured against
616-one extra static row showing the file's final state on the file's own
617-surface. Watching *where* the variants leave the reference (rather than just
618-reading one number per run) is the point; the lmax choices are floored at the
619-file's own band, since a narrower one could not hold its initial state.
620-Reading the file uses [h5wasm](https://github.com/usnistgov/h5wasm)'s wasm
621-build, loaded lazily on the first file opened.
612+The same check runs in the page: **Compare to reference…** picks a `.h5` and
613+opens the comparison in one step — the file's own settings (its recorded
614+niter, its band, its dt) as the single variant, paused at the file's exact
615+initial state, ready to Run. The file defines the whole problem — model,
616+parameters, geometry, initial state — and the run stops at the file's end
617+time, measured against one extra static row showing its final state on its
618+own surface. Watching *where* a variant leaves the reference (rather than
619+just reading one number per run) is the point. To widen the study, stop
620+comparing, pick more chips, and press Compare — the file stays loaded, with
621+the lmax choices floored at its band, since a narrower one could not hold
622+its initial state. Reading the file uses
623+[h5wasm](https://github.com/usnistgov/h5wasm)'s wasm build, loaded lazily on
624+the first file opened.
622625
623626 ## Development
624627
docs/ellipsoid-reference-spec.mdmodified+9−7View file
@@ -107,13 +107,15 @@ how much that correction term actually matters for a given run. `--tolerance
107107 <n>` and `--tolerance-linf <n>` each independently turn their metric into a
108108 pass/fail (nonzero exit code on failure), for use in CI.
109109
110-The browser demo runs the same check visually: **Compare → Reference file…**
111-loads a reference file into the convergence study, seeds every variant from
112-its exact initial state, runs them side by side to its end time, and shows
113-its final state as one extra static row — on the file's own surface, with
114-each variant's relative-L2 distance to it updating live. Both readers share
115-one parser (`src/compare/referenceCase.ts`), so the layout above is
116-interpreted identically on the CLI and in the page.
110+The browser demo runs the same check visually: **Compare to reference…**
111+picks a reference file and opens the comparison in one step, with the file's
112+own settings as the single variant, paused at its exact initial state. Run
113+takes it to the file's end time and stops; its final state shows as one
114+extra static row — on the file's own surface, with each variant's
115+relative-L2 distance to it updating live — and more variants can be added
116+from the compare bar's chips. Both readers share one parser
117+(`src/compare/referenceCase.ts`), so the layout above is interpreted
118+identically on the CLI and in the page.
117119
118120 Note the files record only the two endpoint states (`initial/`, `final/`) —
119121 no intermediate snapshots — so the comparison is meaningful at the end time;
index.htmlmodified+4−4View file
@@ -281,6 +281,10 @@
281281 <button id="resetview">Reset view</button>
282282 <button id="movietoggle" title="Export the run as an MP4 movie">Export movie</button>
283283 <button id="comparetoggle" title="Run several solver settings side by side on one clock">Compare</button>
284+ <button id="cmp-load"
285+ title="Check this solver against a saved reference run (.h5, the layout in docs/ellipsoid-reference-spec.md): opens a comparison seeded from its exact initial state, with its own settings, that runs to its end time and is measured against its final state.">
286+ Compare to reference…</button>
287+ <input type="file" id="cmp-file" accept=".h5" hidden>
284288 </div>
285289 <div class="controls" id="comparebar" hidden>
286290 <div class="cmp-axes">
@@ -300,10 +304,6 @@
300304 <label title="The run everything else is measured against">reference
301305 <select id="cmp-ref"></select>
302306 </label>
303- <button id="cmp-load"
304- title="Check the variants against a saved reference run (.h5, the layout in docs/ellipsoid-reference-spec.md): every variant starts from its exact initial state, runs to its end time, and is measured against its final state.">
305- Reference file…</button>
306- <input type="file" id="cmp-file" accept=".h5" hidden>
307307 <span id="cmp-fileinfo" class="stats" hidden></span>
308308 <button id="cmp-fileclear" hidden
309309 title="Drop the reference file and compare the variants against each other again">×</button>
src/compare/compareRun.tsmodified+2−1View file
@@ -316,7 +316,8 @@ export class CompareRun {
316316
317317 const solverGrid = sessions.map((s) => `${s.cfg.nlat}×${s.cfg.nphi}`);
318318 const note =
319- `${variants.length} variants · display grid ${nlat}×${nphi}` +
319+ `${variants.length} variant${variants.length === 1 ? '' : 's'} · ` +
320+ `display grid ${nlat}×${nphi}` +
320321 (sessions.some((s) => s.cfg.nlat > nlat)
321322 ? ` (below the finest solver grid ${solverGrid[solverGrid.length - 1]} — display only)`
322323 : '') +
src/main.tsmodified+51−12View file
@@ -1271,12 +1271,32 @@ function refreshVariants(): void {
12711271 : '';
12721272 elCmpCount.textContent = tooMany
12731273 ? `too many: ${tooMany}`
1274- : `${variants.length} variants${refCase ? ' + the file' : ''} × ` +
1274+ : `${variants.length} variant${variants.length === 1 ? '' : 's'}` +
1275+ `${refCase ? ' + the file' : ''} × ` +
12751276 `${cmpModel.species.length} species = ${panels} panels`;
12761277 elCmpCount.style.color = tooMany ? '#b35900' : '';
12771278 elCmpStart.disabled = tooMany !== '' && compareRun === null;
12781279 }
12791280
1281+/**
1282+ * The niter chips on offer. A loaded reference file adds its own recorded
1283+ * iteration count if the standard list lacks it, so the file's settings are
1284+ * always selectable; clearing the file drops any selection outside the
1285+ * standard list again.
1286+ */
1287+function rebuildNiterChips(): void {
1288+ const all = [...elNiter.options].map((o) => Number(o.value));
1289+ let values = all;
1290+ if (refCase && !all.includes(refCase.niter)) {
1291+ values = [...all, refCase.niter].sort((a, b) => a - b);
1292+ }
1293+ if (!refCase) {
1294+ for (const v of [...cmpSelected.niter]) if (!values.includes(v)) cmpSelected.niter.delete(v);
1295+ if (cmpSelected.niter.size === 0) cmpSelected.niter.add(DEFAULT_NITER);
1296+ }
1297+ buildChips(elCmpNiter, values, cmpSelected.niter, String);
1298+}
1299+
12801300 /**
12811301 * The lmax chips on offer. A loaded reference file floors them at its own
12821302 * band: a variant below it could not even hold the file's initial state
@@ -1296,12 +1316,7 @@ function rebuildLmaxChips(): void {
12961316 buildChips(elCmpLmax, values, cmpSelected.lmax, String);
12971317 }
12981318
1299-buildChips(
1300- elCmpNiter,
1301- [...elNiter.options].map((o) => Number(o.value)),
1302- cmpSelected.niter,
1303- String,
1304-);
1319+rebuildNiterChips();
13051320 rebuildLmaxChips();
13061321 buildChips(elCmpDt, DT_DIVISORS, cmpSelected.dt, (v) => (v === 1 ? 'dt' : `dt/${v}`));
13071322 refreshVariants();
@@ -1329,6 +1344,7 @@ function applyRefUi(): void {
13291344 ` (${rc.steps} × dt ${rc.params.dt})`;
13301345 elCmpFileInfo.replaceChildren(name, info);
13311346 }
1347+ rebuildNiterChips();
13321348 rebuildLmaxChips();
13331349 refreshVariants();
13341350 }
@@ -1346,8 +1362,30 @@ elCmpFile.addEventListener('change', () => {
13461362 } catch (e) {
13471363 refCase = null;
13481364 elErr.textContent = `reference file ${file.name}: ${e instanceof Error ? e.message : e}`;
1365+ applyRefUi();
1366+ return;
13491367 }
1368+ // One click, one study: the file's own settings become the single
1369+ // variant — its recorded niter, its band, its dt undivided — and the
1370+ // comparison opens on them, paused at the initial state so what runs is
1371+ // the user's choice. (Widening it is: stop comparing, pick more chips,
1372+ // press Compare — the file stays loaded.)
1373+ cmpSelected.niter.clear();
1374+ cmpSelected.niter.add(refCase.niter);
1375+ cmpSelected.lmax.clear();
1376+ cmpSelected.lmax.add(refCase.lmax);
1377+ cmpSelected.dt.clear();
1378+ cmpSelected.dt.add(1);
13501379 applyRefUi();
1380+ elCompareBar.hidden = false;
1381+ if (compareRun) {
1382+ // A study is already up (this one loaded over it): same teardown as
1383+ // rebuildCompare, then the new file's study takes its place.
1384+ compareRun.dispose();
1385+ compareRun = null;
1386+ setCompareUi(false);
1387+ }
1388+ await startCompare();
13511389 })();
13521390 });
13531391 elCmpFileClear.addEventListener('click', () => {
@@ -1368,9 +1406,10 @@ elCmpStart.addEventListener('click', () => {
13681406 function setCompareUi(on: boolean): void {
13691407 for (const el of [
13701408 elNiter, elLmax, elOversample, elBenchmark, elMovieToggle,
1371- // Swapping the reference file out from under a running study would leave
1372- // it checking against a file that is no longer the loaded one.
1373- elCmpLoad, elCmpFileClear,
1409+ // Clearing the file out from under a running study would leave it
1410+ // checking against a file that is no longer loaded. Loading stays
1411+ // enabled: a new file tears the study down and opens its own.
1412+ elCmpFileClear,
13741413 ]) {
13751414 el.disabled = on;
13761415 }
@@ -1384,8 +1423,8 @@ function setCompareUi(on: boolean): void {
13841423
13851424 async function startCompare(): Promise<void> {
13861425 if (compareRun || !device) return;
1387- // Snapshotted for the whole study: `refCase` itself only changes while no
1388- // study is up (the load and clear buttons are disabled during one).
1426+ // Snapshotted for the whole study: `refCase` only changes with no study up
1427+ // (clearing is disabled during one, and loading tears it down first).
13891428 const rc = refCase;
13901429 const cmpModel = rc?.model ?? model;
13911430 const variants = cmpVariants();