1/**
2 * Every model the app offers: that it compiles, what it compiles to, and that it
3 * runs stably and produces a pattern.
4 *
5 * Numerical correctness of the pipeline is analyticChecks.ts's job. This file is
6 * about the models themselves and about the compilation staying as intended — in
7 * particular the kernel count, which is a fusion guard: numbl's lowering emits
8 * one statement per *operator*, and its inline pass folds those back into
9 * per-line expression trees. If that stops happening the results stay correct
10 * but every operator becomes its own dispatch, which is invisible except here.
11 */
12import { ModelSession } from '../src/mgpu/session.ts';
13import { mModels, defaultParams } from '../src/mgpu/registry.ts';
14import type { Check, Log } from './analyticChecks.ts';
16/** Kernels the step of each model should compile to — one per element-wise line. */
17const EXPECTED_KERNELS: Record<string, number> = {
18 schnakenberg: 5,
19 brusselator: 5,
20 allencahn: 2,
21};
23const LMAX = 31;
24const STEPS = 40;
26export async function modelChecks(
27 device: GPUDevice,
28 check: Check,
29 log: Log,
30): Promise<void> {
31 check('models: registry populated', mModels.length === 3, `${mModels.length} models`);
33 for (const model of mModels) {
34 const session = await ModelSession.create({
35 device,
36 model,
37 params: defaultParams(model),
38 lmax: LMAX,
39 });
41 const plan = session.describe();
42 const kernels = plan.step.filter((l) => l.startsWith('kernel')).length;
43 const xforms = plan.step.filter(
44 (l) => l.startsWith('synth') || l.startsWith('analys'),
45 ).length;
46 log(
47 ` ${model.key}.m -> ${plan.step.length} ops/step ` +
48 `(${kernels} generated kernels, ${xforms} transforms)`,
49 );
50 check(
51 `${model.key}: element-wise lines fused into one kernel each`,
52 kernels === EXPECTED_KERNELS[model.key],
53 `${kernels} kernels (expected ${EXPECTED_KERNELS[model.key]})`,
54 );
56 session.seed(1);
57 session.step(STEPS);
59 // Every rendered field must be finite and have developed some contrast.
60 for (const field of model.species) {
61 const values = await session.read(field);
62 let lo = Infinity;
63 let hi = -Infinity;
64 let finite = true;
65 for (const v of values) {
66 if (!Number.isFinite(v)) finite = false;
67 if (v < lo) lo = v;
68 if (v > hi) hi = v;
69 }
70 check(
71 `${model.key}: '${field}' is finite and patterned after ${STEPS} steps`,
72 finite && hi - lo > 1e-6,
73 finite
74 ? `range [${lo.toFixed(5)}, ${hi.toFixed(5)}]`
75 : 'contains NaN or Infinity',
76 );
77 }
79 session.destroy();
80 }
81}