/ concept-collection / turing-surface
Sign in
concept-collection / turing-surface
132 lines · 5.2 KBCodeBlameHistory
2 * The reference-file reader, against a file this test writes itself.
3 *
4 * No GPU: this is about the format — that what h5wasm writes in the
5 * documented layout (docs/ellipsoid-reference-spec.md) comes back through
6 * `extractReferenceCase` with nothing renamed, rescaled or truncated, and
7 * that a file the replay could not act on is refused with a message rather
8 * than half-read. The h5wasm module is injected: the node harness passes
9 * `h5wasm/node` (real files), the browser harness `h5wasm` (in-memory wasm
10 * filesystem) — so the browser run also proves the wasm build actually ships.
11 */
12import { extractReferenceCase, type H5Node } from '../src/compare/referenceCase.ts';
13import { nlmCalc } from '../src/sht/layout.ts';
15type Check = (name: string, ok: boolean, detail: string) => void;
16type Log = (line: string) => void;
18/** The slice of h5wasm's writing API these checks touch — the node and
19 * browser builds both satisfy it structurally. */
20interface H5Out {
21 create_group(name: string): H5Out;
22 create_attribute(name: string, data: unknown): void;
23 create_dataset(args: { name: string; data: unknown; dtype?: string }): unknown;
25export interface H5Rt {
26 ready: Promise<unknown>;
27 File: new (path: string, mode?: string) => H5Out & H5Node & { close(): unknown };
30const LMAX = 3;
31const STEPS = 8;
33export async function referenceChecks(
34 h5: H5Rt,
35 /** Where a named scratch file may live: a temp dir on node, '/' in the
36 * browser's in-memory filesystem. */
37 pathFor: (name: string) => string,
38 check: Check,
39 log: Log,
40): Promise<void> {
41 log('\nreference files (HDF5 layout):');
42 const mod = (await h5.ready) as { FS?: { unlink(path: string): void } };
43 const nlm = nlmCalc(LMAX, LMAX);
44 const series = (offset: number): Float32Array =>
45 Float32Array.from({ length: 2 * nlm }, (_, i) => offset + i / 16);
46 const arrays = {
47 Gx: series(100), Gy: series(200), Gz: series(300),
48 initialU: series(1), finalU: series(2),
49 };
51 // ---- write the documented layout, read it back ---------------------------
52 const goodPath = pathFor('ref-roundtrip.h5');
53 {
54 const f = new h5.File(goodPath, 'w');
55 f.create_attribute('model', 'allencahn');
56 f.create_attribute('species', ['U']);
57 const spec = f.create_group('spec');
58 spec.create_attribute('geometry', 'ellipsoid');
59 spec.create_attribute('lmax', LMAX);
60 spec.create_attribute('steps', STEPS);
61 spec.create_attribute('niter', 2);
62 spec.create_attribute('seed', 1);
63 spec.create_attribute('warmup', 0);
64 const params = spec.create_group('params');
65 params.create_attribute('dt', 0.0625);
66 params.create_attribute('eps2', 0.5);
67 const geomParams = spec.create_group('geometry_params');
68 geomParams.create_attribute('ax', 2.5);
69 geomParams.create_attribute('ay', 1.25);
70 geomParams.create_attribute('az', 0.75);
71 const geom = f.create_group('geometry');
72 geom.create_dataset({ name: 'Gx', data: arrays.Gx, dtype: '<f4' });
73 geom.create_dataset({ name: 'Gy', data: arrays.Gy, dtype: '<f4' });
74 geom.create_dataset({ name: 'Gz', data: arrays.Gz, dtype: '<f4' });
75 f.create_group('initial').create_dataset({ name: 'U', data: arrays.initialU, dtype: '<f4' });
76 f.create_group('final').create_dataset({ name: 'U', data: arrays.finalU, dtype: '<f4' });
77 f.close();
78 }
79 {
80 const f = new h5.File(goodPath, 'r');
81 const rc = extractReferenceCase(f, 'ref-roundtrip.h5');
82 f.close();
83 mod.FS?.unlink(goodPath);
85 check(
86 'reference: the run identity survives the round trip',
87 rc.model.key === 'allencahn' && rc.geometry.key === 'ellipsoid' &&
88 rc.lmax === LMAX && rc.steps === STEPS && rc.niter === 2,
89 `${rc.model.key} on ${rc.geometry.key}, lmax ${rc.lmax}, ` +
90 `${rc.steps} steps, niter ${rc.niter}`,
91 );
92 check(
93 'reference: the file’s parameters override the defaults',
94 rc.params.dt === 0.0625 && rc.params.eps2 === 0.5 &&
95 rc.geometryParams.ax === 2.5 && rc.geometryParams.ay === 1.25 &&
96 rc.geometryParams.az === 0.75,
97 `dt ${rc.params.dt}, eps2 ${rc.params.eps2}, ` +
98 `ax/ay/az ${rc.geometryParams.ax}/${rc.geometryParams.ay}/${rc.geometryParams.az}`,
99 );
100 const same = (a: Float32Array, b: Float32Array): boolean =>
101 a.length === b.length && a.every((v, i) => v === b[i]);
102 check(
103 'reference: every coefficient array comes back bit-exact',
104 same(rc.geometryCoeffs.X, arrays.Gx) && same(rc.geometryCoeffs.Y, arrays.Gy) &&
105 same(rc.geometryCoeffs.Z, arrays.Gz) && same(rc.initial.U, arrays.initialU) &&
106 same(rc.final.U, arrays.finalU),
107 `5 arrays x ${2 * nlm} float32 values`,
108 );
109 }
111 // ---- a file the replay cannot act on is refused, not half-read -----------
112 {
113 const badPath = pathFor('ref-unknown-model.h5');
114 const f = new h5.File(badPath, 'w');
115 f.create_attribute('model', 'nosuchmodel');
116 f.close();
117 const r = new h5.File(badPath, 'r');
118 let message = '';
119 try {
120 extractReferenceCase(r, 'ref-unknown-model.h5');
121 } catch (e) {
122 message = e instanceof Error ? e.message : String(e);
123 }
124 r.close();
125 mod.FS?.unlink(badPath);
126 check(
127 'reference: an unknown model is refused with its name',
128 message.includes('nosuchmodel'),
129 message || 'no error thrown',
130 );
131 }
moveopenescclose