/ concept-collection / turing-surface
concept-collection / turing-surface
81 lines · 3.0 KBBlameHistoryRaw
1/**
2 * One point of a convergence study: a choice of the three knobs that decide
3 * *how well* the same problem is being solved, rather than what the problem is.
4 *
5 * niter iterations of the implicit solve (structural — it unrolls into the
6 * compiled step, so each value is its own compiled session)
7 * lmax the spectral band, and with it the grid (also structural)
8 * dtDiv the timestep, as an integer divisor of the model's own dt
9 *
10 * dt is a *divisor* rather than a free value on purpose, and it is the whole
11 * reason the comparison can be trusted: variants have to be compared at the
12 * same model time, and with dt = dtBase/K every variant lands exactly on the
13 * same t after K times as many steps — no rounding, no drift, no interpolation
14 * in time. A free dt would put each variant on its own timeline and every
15 * difference reported would be part real and part "these are 0.003 apart".
16 */
18export interface Variant {
19 /** Iterations of the implicit solve. */
20 niter: number;
21 /** Spectral band limit. */
22 lmax: number;
23 /** Timestep divisor: this variant runs at dtBase / dtDiv. */
24 dtDiv: number;
27/** Stable identity of a variant, for keying maps and the reference <select>. */
28export const variantKey = (v: Variant): string => `${v.niter}/${v.lmax}/${v.dtDiv}`;
30/** Human label. The dt term is dropped when nothing varies it, so the common
31 * case (niter x lmax) reads as just those two. */
32export const variantLabel = (v: Variant, showDt: boolean): string =>
33 `niter ${v.niter} · lmax ${v.lmax}` + (showDt ? ` · dt/${v.dtDiv}` : '');
35/**
36 * Every combination of the selected values, in a stable order: coarsest first,
37 * so the grid reads top-to-bottom from least to most resolved and the
38 * reference (the last row) is the one everything is measured against.
39 */
40export function crossProduct(
41 niters: number[],
42 lmaxes: number[],
43 dtDivs: number[],
44): Variant[] {
45 const out: Variant[] = [];
46 for (const lmax of [...lmaxes].sort((a, b) => a - b)) {
47 for (const dtDiv of [...dtDivs].sort((a, b) => a - b)) {
48 for (const niter of [...niters].sort((a, b) => a - b)) {
49 out.push({ niter, lmax, dtDiv });
50 }
51 }
52 }
53 return out;
56/**
57 * Index of the most-resolved variant: the natural reference, since it is the
58 * one every other choice is an approximation of. Finer band first (it bounds
59 * what can be represented at all), then more solve iterations, then smaller
60 * timestep.
61 */
62export function mostResolved(variants: Variant[]): number {
63 let best = 0;
64 for (let i = 1; i < variants.length; i++) {
65 const a = variants[i];
66 const b = variants[best];
67 if (
68 a.lmax > b.lmax ||
69 (a.lmax === b.lmax && a.niter > b.niter) ||
70 (a.lmax === b.lmax && a.niter === b.niter && a.dtDiv > b.dtDiv)
71 ) {
72 best = i;
73 }
74 }
75 return best;
78/** Distinguishable line/label colors, one per variant row. */
79export const VARIANT_COLORS = [
80 '#0969da', '#bf8700', '#1a7f37', '#cf222e', '#8250df', '#0f7c8a',
81];