/ concept-collection / turing-surface
Sign in
concept-collection / turing-surface
Updated command now tracks L_infty error too.
Owen Melia <owenjmelia@gmail.com> committed commit 66ae13e5b13b parent b460a9f Browse files
4 changed files+76−32
README.mdmodified+4−3View file
@@ -480,9 +480,10 @@ restates it in this repo's own terms.
480480
481481 `npm run ref -- --in <file>` (`scripts/ref.ts`) loads a reference file, runs
482482 the solver from its exact initial spectral state to the same physical end
483-time, and reports the relative-L2 error against its final state (plus a
484-geometry sanity check). `--niter` overrides the surface-correction iteration
485-count independent of the file, and `--tolerance` turns the check into a
483+time, and reports the relative-L2 and relative-L-infinity (max-norm) error
484+against its final state (plus a geometry sanity check). `--niter` overrides
485+the surface-correction iteration count independent of the file, and
486+`--tolerance`/`--tolerance-linf` each independently turn their metric into a
486487 pass/fail for CI.
487488
488489 ## Development
docs/ellipsoid-reference-spec.mdmodified+9−8View file
@@ -97,14 +97,15 @@ what it describes rather than as a single flat namespace:
9797
9898 `npm run ref -- --in <file>` loads a reference file, runs this
9999 repo's own solver from its exact initial condition to the same physical end
100-time, and reports the relative-L2 error of the resulting state against the
101-file's final state (and, as a sanity check, of the regenerated geometry
102-against the file's own geometry coefficients — this should be ~0 unless
103-geometry construction itself has changed). `--niter <n>` overrides the
104-solve's own iteration count for the surface correction, independent of what
105-the reference file was generated with — useful for seeing how much that
106-correction term actually matters for a given run. `--tolerance <n>` turns
107-the check into a pass/fail (nonzero exit code on failure), for use in CI.
100+time, and reports the relative-L2 and relative-L-infinity (max-norm) error of
101+the resulting state against the file's final state (and, as a sanity check,
102+of the regenerated geometry against the file's own geometry coefficients —
103+this should be ~0 unless geometry construction itself has changed). `--niter
104+<n>` overrides the solve's own iteration count for the surface correction,
105+independent of what the reference file was generated with — useful for seeing
106+how much that correction term actually matters for a given run. `--tolerance
107+<n>` and `--tolerance-linf <n>` each independently turn their metric into a
108+pass/fail (nonzero exit code on failure), for use in CI.
108109
109110 ## Caveat
110111
scripts/ref.tsmodified+50−21View file
@@ -17,22 +17,25 @@ import { requestShtDevice, describeAdapter } from '../src/sht/sht.ts';
1717 import { ModelSession } from '../src/mgpu/session.ts';
1818 import { mModelByKey, defaultParams, type Params } from '../src/mgpu/registry.ts';
1919 import { mGeometryByKey, defaultGeometryParams } from '../src/geom/registry.ts';
20-import { relL2 } from '../src/mgpu/digest.ts';
20+import { relL2, relLinf } from '../src/mgpu/digest.ts';
2121 import { installWebGpu, errMsg, NO_ADAPTER_HINT } from './nodeWebGpu.ts';
2222 import * as h5wasm from 'h5wasm/node';
2323
2424 const USAGE = `usage: npm run ref -- --in <file> [options]
2525
26- --in <file> the reference HDF5 file to check against (required)
27- --niter <n> override the solve iteration count (default: the file's own)
28- --tolerance <n> if given, exit 1 when any reported relL2 meets or exceeds it
29- --json machine-readable output
26+ --in <file> the reference HDF5 file to check against (required)
27+ --niter <n> override the solve iteration count (default: the file's own)
28+ --tolerance <n> if given, exit 1 when any reported relL2 meets or exceeds it
29+ --tolerance-linf <n> if given, exit 1 when any reported relLinf meets or exceeds it
30+ --json machine-readable output
3031 --help
3132
3233 Runs this repo's solver from the file's exact initial spectral state, to the
33-same physical end time, and reports the relative-L2 error of the resulting
34-state against the file's final state (and, as a sanity check, of the
35-regenerated geometry against the file's own geometry coefficients).`;
34+same physical end time, and reports the relative-L2 and relative-L-infinity
35+(max-norm) error of the resulting state against the file's final state (and,
36+as a sanity check, of the regenerated geometry against the file's own
37+geometry coefficients). --tolerance and --tolerance-linf gate independently:
38+either can fail the run on its own.`;
3639
3740 function fail(msg: string, code = 1): never {
3841 console.error(`ref: ${msg}`);
@@ -47,6 +50,7 @@ if (argv.includes('--help') || argv.includes('-h')) {
4750 let inFile: string | null = null;
4851 let niterOverride: number | null = null;
4952 let tolerance: number | null = null;
53+let toleranceLinf: number | null = null;
5054 const wantJson = argv.includes('--json');
5155 for (let i = 0; i < argv.length; i++) {
5256 const a = argv[i];
@@ -69,6 +73,12 @@ for (let i = 0; i < argv.length; i++) {
6973 }
7074 continue;
7175 }
76+ const tolLinfv = valued('tolerance-linf');
77+ if (tolLinfv !== null) {
78+ toleranceLinf = Number(tolLinfv);
79+ if (!Number.isFinite(toleranceLinf)) fail(`--tolerance-linf must be a number (got '${tolLinfv}')`, 2);
80+ continue;
81+ }
7282 const tolv = valued('tolerance');
7383 if (tolv !== null) {
7484 tolerance = Number(tolv);
@@ -156,25 +166,31 @@ try {
156166 niter,
157167 });
158168
169+ const errorOf = (a: Float32Array, b: Float32Array) => ({ relL2: relL2(a, b), relLinf: relLinf(a, b) });
170+
159171 const geometryError = {
160- Gx: relL2(session.geometry.X, fileGeom.X),
161- Gy: relL2(session.geometry.Y, fileGeom.Y),
162- Gz: relL2(session.geometry.Z, fileGeom.Z),
172+ Gx: errorOf(session.geometry.X, fileGeom.X),
173+ Gy: errorOf(session.geometry.Y, fileGeom.Y),
174+ Gz: errorOf(session.geometry.Z, fileGeom.Z),
163175 };
164176
165177 session.loadState(fileInitial);
166178 session.step(steps);
167179
168- const stateError: Record<string, number> = {};
180+ const stateError: Record<string, { relL2: number; relLinf: number }> = {};
169181 for (const name of model.state) {
170182 // Sequential: GpuModel.read() shares one staging buffer across calls.
171183 const ours = await session.read(name);
172- stateError[name] = relL2(ours, fileFinal[name]);
184+ stateError[name] = errorOf(ours, fileFinal[name]);
173185 }
174186
175187 const allErrors = [...Object.values(geometryError), ...Object.values(stateError)];
176- const worst = Math.max(...allErrors);
177- const pass = tolerance === null ? null : worst < tolerance;
188+ const worstL2 = Math.max(...allErrors.map((e) => e.relL2));
189+ const worstLinf = Math.max(...allErrors.map((e) => e.relLinf));
190+ const passL2 = tolerance === null ? null : worstL2 < tolerance;
191+ const passLinf = toleranceLinf === null ? null : worstLinf < toleranceLinf;
192+ const checks = [passL2, passLinf].filter((p): p is boolean => p !== null);
193+ const pass = checks.length === 0 ? null : checks.every(Boolean);
178194
179195 if (wantJson) {
180196 console.log(
@@ -191,7 +207,12 @@ try {
191207 backend: { adapter, runtime, precision: 'fp32' },
192208 geometryError,
193209 stateError,
210+ worstL2,
211+ worstLinf,
194212 tolerance,
213+ toleranceLinf,
214+ passL2,
215+ passLinf,
195216 pass,
196217 },
197218 null,
@@ -208,14 +229,22 @@ try {
208229 ` niter ${niter}${niterOverride !== null ? ` (file: ${specAttrs.niter})` : ''}\n` +
209230 ` run ${steps} steps, dt=${params.dt} (T=${(steps * (params.dt ?? 0)).toFixed(2)})\n`,
210231 );
211- console.log(` geometry check (regenerated vs file, relL2):`);
212- for (const [k, v] of Object.entries(geometryError)) console.log(` ${k} ${v.toExponential(3)}`);
213- console.log(`\n final state (this run vs file, relL2):`);
214- for (const [k, v] of Object.entries(stateError)) console.log(` ${k} ${v.toExponential(3)}`);
232+ const fmtErr = (v: { relL2: number; relLinf: number }) =>
233+ `relL2 ${v.relL2.toExponential(3)} relLinf ${v.relLinf.toExponential(3)}`;
234+ console.log(` geometry check (regenerated vs file):`);
235+ for (const [k, v] of Object.entries(geometryError)) console.log(` ${k} ${fmtErr(v)}`);
236+ console.log(`\n final state (this run vs file):`);
237+ for (const [k, v] of Object.entries(stateError)) console.log(` ${k} ${fmtErr(v)}`);
215238 if (tolerance !== null) {
216239 console.log(
217- `\n worst relL2 ${worst.toExponential(3)} vs tolerance ${tolerance.toExponential(3)}: ` +
218- (pass ? 'PASS' : 'FAIL'),
240+ `\n worst relL2 ${worstL2.toExponential(3)} vs tolerance ${tolerance.toExponential(3)}: ` +
241+ (passL2 ? 'PASS' : 'FAIL'),
242+ );
243+ }
244+ if (toleranceLinf !== null) {
245+ console.log(
246+ ` worst relLinf ${worstLinf.toExponential(3)} vs tolerance-linf ${toleranceLinf.toExponential(3)}: ` +
247+ (passLinf ? 'PASS' : 'FAIL'),
219248 );
220249 }
221250 }
src/mgpu/digest.tsmodified+13−0View file
@@ -69,6 +69,19 @@ export function relL2(a: ArrayLike<number>, b: ArrayLike<number>): number {
6969 return Math.sqrt(num / Math.max(den, 1e-300));
7070 }
7171
72+/** Relative L-infinity (max-norm) difference of two states of equal length. */
73+export function relLinf(a: ArrayLike<number>, b: ArrayLike<number>): number {
74+ let num = 0;
75+ let den = 0;
76+ for (let i = 0; i < a.length; i++) {
77+ const d = Math.abs(a[i] - b[i]);
78+ if (d > num) num = d;
79+ const bd = Math.abs(b[i]);
80+ if (bd > den) den = bd;
81+ }
82+ return num / Math.max(den, 1e-300);
83+}
84+
7285 export function formatDigest(d: StateDigest): string {
7386 const g = (v: number): string => v.toPrecision(9);
7487 return (
moveopenescclose