1/**
2 * The MATLAB export (src/export/matlabScript.ts) is string assembly, so these
3 * checks are cheap and need no GPU: every preset x geometry combination must
4 * generate, the assembled file must keep its local-function namespace free of
5 * collisions, and the driver's calls must match the signatures the .m files
6 * declare. Whether the generated MATLAB actually reproduces a run is checked
7 * against MATLAB itself, not here: a run exported at defaults and executed in
8 * MATLAB R2026b lands within fp32 accumulation error of the app's own replay
9 * (relL2 ~1e-7 over 60 steps via `npm run ref`), and the flux and Algorithm-4
10 * exports track each other to ~3e-10 in f64.
11 */
12import { generateMatlabScript, MATLAB_SCRIPT_NAME } from '../src/export/matlabScript.ts';
13import { presets, mModelByKey } from '../src/mgpu/registry.ts';
14import { mGeometries } from '../src/geom/registry.ts';
15import { formatCommand, resolvePreset, DEFAULT_WARMUP } from '../src/bench/runSpec.ts';
17type Check = (name: string, ok: boolean, detail: string) => void;
18type Log = (s: string) => void;
20export function matlabExportChecks(check: Check, log: Log): void {
21 log('--- MATLAB export ---');
22 for (const preset of presets) {
23 const { model, params } = resolvePreset(preset.key);
24 for (const geometry of mGeometries) {
25 const geometryParams = Object.fromEntries(
26 geometry.params.map((p) => [p.key, p.value]),
27 );
28 const spec = {
29 preset: preset.key,
30 lmax: 63,
31 seed: 1,
32 steps: 2000,
33 warmup: DEFAULT_WARMUP,
34 params,
35 geometry: geometry.key,
36 geometryParams,
37 niter: 8,
38 };
39 const name = `matlab-export ${preset.key} on ${geometry.key}`;
40 let text: string;
41 try {
42 text = generateMatlabScript({
43 model,
44 modelSource: model.source,
45 params,
46 geometry,
47 geometrySource: geometry.source,
48 geometryParams,
49 lmax: 63,
50 niter: 8,
51 lam3: 0.5,
52 seed: 1,
53 preset: preset.key,
54 command: formatCommand(spec),
55 });
56 } catch (e) {
57 check(name, false, e instanceof Error ? e.message : String(e));
58 continue;
59 }
61 // One file, one namespace: every local function name must be unique,
62 // or MATLAB silently shadows one definition with another.
63 const fnNames = [...text.matchAll(/^[ \t]*function\s+(?:\[[^\]]*\]|\w+)\s*=\s*(\w+)\s*\(/gm)]
64 .map((m) => m[1]);
65 const dupes = fnNames.filter((n, i) => fnNames.indexOf(n) !== i);
67 // The driver must define what it calls: the state it steps, the model
68 // call mapped through the mp struct, and the transform setup.
69 const wants = [
70 `function ${MATLAB_SCRIPT_NAME}()`,
71 'sht_tables(sht_setup(lmax, mmax, nlat, nphi));',
72 `= init(`,
73 `= step(${model.state.join(', ')}, `,
74 'surface_tables(gxr, gyr, gzr)',
75 `'/final/${model.state[0]}'`,
76 ];
77 const missing = wants.filter((w) => !text.includes(w));
79 // randnfunsphere rides along exactly when the geometry draws on it.
80 const wantsSphereTool = /\brandnfunsphere\b/.test(geometry.source);
81 const carriesSphereTool = /function f = randnfunsphere\(/.test(text);
83 const problems = [
84 ...(dupes.length ? [`duplicate local functions: ${[...new Set(dupes)].join(', ')}`] : []),
85 ...(missing.length ? [`missing: ${missing.join(' | ')}`] : []),
86 ...(wantsSphereTool !== carriesSphereTool
87 ? [`randnfunsphere ${wantsSphereTool ? 'missing' : 'included needlessly'}`]
88 : []),
89 ];
90 check(name, problems.length === 0, problems.join('; ') || `${fnNames.length} local functions`);
91 }
92 }
94 // An edited working copy that dropped a required function is refused with a
95 // message naming the file, not exported broken.
96 const { model, params } = resolvePreset(presets[0].key);
97 const geometry = mGeometries[0];
98 try {
99 generateMatlabScript({
100 model,
101 modelSource: '% nothing here',
102 params,
103 geometry,
104 geometrySource: geometry.source,
105 geometryParams: {},
106 lmax: 63,
107 niter: 8,
108 lam3: 0.5,
109 seed: 1,
110 preset: presets[0].key,
111 command: '',
112 });
113 check('matlab-export refuses a source without init', false, 'no error thrown');
114 } catch (e) {
115 const msg = e instanceof Error ? e.message : String(e);
116 check(
117 'matlab-export refuses a source without init',
118 msg.includes("'init'") && msg.includes(model.key),
119 msg,
120 );
121 }
122 // Guard the assumption the model registry makes for stateFor: state names
123 // are used as `<name>0` initial-capture variables, which must not collide
124 // with the species names.
125 for (const p of presets) {
126 const m = mModelByKey(p.modelKey)!;
127 const all = new Set([...m.state, ...m.species]);
128 check(
129 `matlab-export names disjoint for ${m.key}`,
130 all.size === m.state.length + m.species.length,
131 [...all].join(', '),
132 );
133 }
134}