concept-collection / turing-surface-cache
Add Brusselator and Allen-Cahn models
The three flux-form models from turing-surface, each with its own discrete parameter lists. The model joins the URL fragment and the reset; unlike every other choice it is compiled into the GPU session, so switching it rebuilds the session and the panels (Allen-Cahn evolves one species, so one panel). The Algorithm-4 reference variant is deliberately absent: it solves the same equations as Schnakenberg and would only duplicate cache entries under different hashes.
Jeremy Magland <jmagland@flatironinstitute.org> committed commit 481eeb96b902 parent 4f822e1 Browse files
8 changed files+338−68
README.mdmodified+13−7View file
@@ -19,21 +19,27 @@ rather than a minute.
1919
2020 ## The discrete parameter space
2121
22-One model ships (Schnakenberg, in turing-surface's 6-transform flux form) on
23-three geometries (sphere, ellipsoid, peanut). The choices, defined in
22+Three models ship, all in turing-surface's 6-transform flux form —
23+Schnakenberg (the default), Brusselator, and Allen–Cahn — on three geometries
24+(sphere, ellipsoid, peanut). The Algorithm-4 reference variant is deliberately
25+absent: it solves the same equations as Schnakenberg and would only duplicate
26+cache entries under different hashes. The choices, defined in
2427 [`src/cache/options.ts`](src/cache/options.ts):
2528
2629 | setting | choices |
2730 |---|---|
28-| a | 0.05, **0.1**, 0.15, 0.2 |
29-| b | 0.7, **0.9**, 1.1, 1.3 |
30-| D₁ | 1.6e-4, **4e-4**, 1e-3 |
31-| D₂ | 3.2e-3, **8e-3**, 2e-2 |
32-| dt | 0.02, **0.05**, 0.1 |
31+| Schnakenberg | a: 0.05/**0.1**/0.15/0.2 · b: 0.7/**0.9**/1.1/1.3 · D₁: 1.6e-4/**4e-4**/1e-3 · D₂: 3.2e-3/**8e-3**/2e-2 · dt: 0.02/**0.05**/0.1 |
32+| Brusselator | A: 2/**3**/4 · B: 7/**9**/11 · D₁: 1.7e-3/**3.33e-3**/6.7e-3 · D₂: 8.3e-3/**1.67e-2**/3.3e-2 · dt: 0.01/**0.02**/0.05 |
33+| Allen–Cahn | ε²: 5e-4/**1e-3**/2e-3 · dt: 0.01/**0.02**/0.05 |
3334 | geometry | sphere, **ellipsoid** (axes each 0.6/1/1.5), peanut (waist 0.4/0.6/0.8, stretch 0/0.6/1.2) |
3435 | seed | **1**–5 |
3536 | end time | **100**, 200, 400, 800, 1600 |
3637
38+The model, unlike every other choice, is compiled into the GPU session, so
39+switching it pays a recompile of a second or two; everything else swaps into
40+the running session. Allen–Cahn evolves one species, so it shows one panel
41+where the others show two.
42+
3743 (Defaults in bold.) The numerical-scheme settings are fixed — lmax 63, 8
3844 solve iterations, seed wavelength λ = 0.5 — but are recorded in every cache
3945 key, so offering them as choices later invalidates nothing.
index.htmlmodified+6−1View file
@@ -101,7 +101,12 @@
101101 <a href="https://github.com/concept-collection/turing-surface">turing-surface</a>'s
102102 spectral solver). Drag to rotate.
103103 </p>
104- <div class="controls" id="params"></div>
104+ <div class="controls">
105+ <label title="The reaction-diffusion system being solved">model
106+ <select id="model"></select>
107+ </label>
108+ <span id="params" class="controls" style="padding: 0"></span>
109+ </div>
105110 <div class="controls">
106111 <label title="The surface the pattern is solved on">geometry
107112 <select id="geometry"></select>
models/allencahn.madded+43−0View file
@@ -0,0 +1,43 @@
1+% Allen-Cahn on a closed surface: interfaces form, then coarsen.
2+%
3+% du/dt = eps2*lap_g(u) + u - u^3
4+%
5+% Same scheme as models/schnakenberg.m, sphere-split flux divergence included.
6+
7+% Seeded from a smooth random field -- see models/schnakenberg.m.
8+function [U, u] = init(lam3, gx, gy, gz)
9+ U = analys(0.01 * randnfun3(lam3, gx, gy, gz));
10+ u = synth(U);
11+end
12+
13+function [Un, u] = step(U, lam, filt, gx, gy, gz, p2, r, dp1, dq2, jinv, jhat, eps2, dt, niter)
14+ u = synth(U);
15+
16+ Bu = U + dt * analys(u - u.^3);
17+
18+ % Mean-J preconditioning -- see models/schnakenberg.m.
19+ lamJ = lam ./ jhat;
20+ Un = Bu ./ (1 + (dt * eps2) * lamJ);
21+
22+ for k = 1:niter
23+ % dlap = lap_g - lap_s, evaluated at the current iterate in flux form
24+ % (see models/schnakenberg.m, docs/richardson-iteration.md and
25+ % docs/reduced-transforms.md for the derivation; the grouped calls run
26+ % the gradient syntheses and the flux analyses as batched dispatches).
27+ Fu = Un .* filt;
28+ vtu = dthetac(Fu);
29+ vpu = dphic(Fu);
30+ [Ftu, Fpu, Su] = synth(vtu, vpu, lam .* Fu);
31+ Pu = dp1 .* Ftu + p2 .* Fpu;
32+ Qu = p2 .* Ftu + dq2 .* Fpu;
33+ PAu = analys(Pu);
34+ Pcu = PAu .* filt;
35+ scu = dthetac(Pcu);
36+ Lu = synth(scu);
37+ dQu = dphig(Qu);
38+ lapu = r .* (Lu + dQu) - jinv .* Su;
39+ dLu = (analys(lapu) + lamJ .* Un) .* filt;
40+
41+ Un = (Bu + (dt * eps2) * dLu) ./ (1 + (dt * eps2) * lamJ);
42+ end
43+end
models/brusselator.madded+65−0View file
@@ -0,0 +1,65 @@
1+% Brusselator reaction-diffusion on a closed surface.
2+%
3+% du/dt = D1*lap_g(u) + A - (B+1)*u + u^2*v
4+% dv/dt = D2*lap_g(v) + B*u - u^2*v
5+%
6+% Same scheme as models/schnakenberg.m, including the grouped transforms:
7+% [a, b] = synth(x, y) runs the group as batched Legendre dispatches, and the
8+% sphere-split flux divergence that keeps r ~ 1/sin^2(theta) off the round
9+% sphere's share of the operator.
10+
11+% Seeded from a smooth random field -- see models/schnakenberg.m.
12+function [U, V, u, v] = init(lam3, gx, gy, gz, A, B)
13+ f = randnfun3(lam3, gx, gy, gz);
14+ [U, V] = analys(A + 0.01*f, (B / A) * ones(numel(f), 1));
15+ [u, v] = synth(U, V);
16+end
17+
18+function [Un, Vn, u, v] = step(U, V, lam, filt, gx, gy, gz, p2, r, dp1, dq2, jinv, jhat, A, B, D1, D2, dt, niter)
19+ [u, v] = synth(U, V);
20+ uuv = u .* u .* v;
21+
22+ ru = A - (B + 1) * u + uuv;
23+ rv = B * u - uuv;
24+ [Ru, Rv] = analys(ru, rv);
25+ Bu = U + dt * Ru;
26+ Bv = V + dt * Rv;
27+
28+ % Mean-J preconditioning -- see models/schnakenberg.m.
29+ lamJ = lam ./ jhat;
30+ Un = Bu ./ (1 + (dt * D1) * lamJ);
31+ Vn = Bv ./ (1 + (dt * D2) * lamJ);
32+
33+ for k = 1:niter
34+ % dlap = lap_g - lap_s, evaluated at the current iterate in flux form
35+ % (see models/schnakenberg.m, docs/richardson-iteration.md and
36+ % docs/reduced-transforms.md for the derivation and the ordering).
37+ Fu = Un .* filt;
38+ Fv = Vn .* filt;
39+ vtu = dthetac(Fu);
40+ vpu = dphic(Fu);
41+ vtv = dthetac(Fv);
42+ vpv = dphic(Fv);
43+ [Ftu, Fpu, Ftv, Fpv, Su, Sv] = synth(vtu, vpu, vtv, vpv, lam .* Fu, lam .* Fv);
44+ Pu = dp1 .* Ftu + p2 .* Fpu;
45+ Qu = p2 .* Ftu + dq2 .* Fpu;
46+ Pv = dp1 .* Ftv + p2 .* Fpv;
47+ Qv = p2 .* Ftv + dq2 .* Fpv;
48+ [PAu, PAv] = analys(Pu, Pv);
49+ Pcu = PAu .* filt;
50+ Pcv = PAv .* filt;
51+ scu = dthetac(Pcu);
52+ scv = dthetac(Pcv);
53+ [Lu, Lv] = synth(scu, scv);
54+ dQu = dphig(Qu);
55+ dQv = dphig(Qv);
56+ lapu = r .* (Lu + dQu) - jinv .* Su;
57+ lapv = r .* (Lv + dQv) - jinv .* Sv;
58+ [LAu, LAv] = analys(lapu, lapv);
59+ dLu = (LAu + lamJ .* Un) .* filt;
60+ dLv = (LAv + lamJ .* Vn) .* filt;
61+
62+ Un = (Bu + (dt * D1) * dLu) ./ (1 + (dt * D1) * lamJ);
63+ Vn = (Bv + (dt * D2) * dLv) ./ (1 + (dt * D2) * lamJ);
64+ end
65+end
scripts/check-app.mjsmodified+37−0View file
@@ -243,6 +243,43 @@ print('h5py check ok; species', list(f.attrs['species']), '; adapter:', f.attrs.
243243 problems.push(`warm: odd file name: ${warmFile}`);
244244 }
245245 await page3.close();
246+
247+ // ---- pass 5: model in the URL, and a live model switch -------------------
248+ // Allen–Cahn arrives via the fragment (one species -> one panel); switching
249+ // to Brusselator recompiles the session and rebuilds the panels (two).
250+ // Nothing computes: both settle on the idle miss status.
251+ const page4 = await browser.newPage();
252+ watch(page4, 'model:');
253+ await interceptCache(page4, null, '');
254+ await openAndSelect(page4, null, '#model=allencahn');
255+ await page4.waitForFunction(
256+ () => /press Compute solution|failed/.test(
257+ document.getElementById('status')?.textContent ?? '') ||
258+ (document.getElementById('err')?.textContent?.length ?? 0) > 4,
259+ { timeout: 600_000 },
260+ );
261+ const modelRestored = await page4.$eval('#model', (el) => el.value);
262+ if (modelRestored !== 'allencahn') {
263+ problems.push(`model: URL restore failed, model = ${modelRestored}`);
264+ }
265+ const acPanels = await page4.$$eval('.sphere-box canvas', (els) => els.length);
266+ if (acPanels !== 1) problems.push(`model: allencahn should have 1 panel, got ${acPanels}`);
267+ await page4.select('#model', 'brusselator');
268+ // The old idle status is still on screen while the recompile runs, so the
269+ // wait must demand the new panel count as well.
270+ await page4.waitForFunction(
271+ () => (document.querySelectorAll('.sphere-box canvas').length === 2 &&
272+ /press Compute solution/.test(document.getElementById('status')?.textContent ?? '')) ||
273+ /failed/.test(document.getElementById('status')?.textContent ?? '') ||
274+ (document.getElementById('err')?.textContent?.length ?? 0) > 4,
275+ { timeout: 600_000 },
276+ );
277+ const brPanels = await page4.$$eval('.sphere-box canvas', (els) => els.length);
278+ if (brPanels !== 2) problems.push(`model: brusselator should have 2 panels, got ${brPanels}`);
279+ const e4 = await errOf(page4);
280+ if (e4) problems.push(`model: err: ${e4}`);
281+ console.log(`pass 5: allencahn ${acPanels} panel, brusselator ${brPanels} panels`);
282+ await page4.close();
246283 } catch (e) {
247284 problems.push(`fatal: ${e.message}`);
248285 } finally {
src/cache/options.tsmodified+24−8View file
@@ -22,14 +22,30 @@ export interface DiscreteChoice {
2222 value: number;
2323 }
2424
25-/** Model parameter choices (Schnakenberg). */
26-export const MODEL_CHOICES: DiscreteChoice[] = [
27- { key: 'a', label: 'a', values: [0.05, 0.1, 0.15, 0.2], value: 0.1 },
28- { key: 'b', label: 'b', values: [0.7, 0.9, 1.1, 1.3], value: 0.9 },
29- { key: 'D1', label: 'D₁', values: [1.6e-4, 4e-4, 1e-3], value: 4e-4 },
30- { key: 'D2', label: 'D₂', values: [3.2e-3, 8e-3, 2e-2], value: 8e-3 },
31- { key: 'dt', label: 'dt', values: [0.02, 0.05, 0.1], value: 0.05 },
32-];
25+/** The model offered first, and what Reset returns to. */
26+export const DEFAULT_MODEL_KEY = 'schnakenberg';
27+
28+/** Model parameter choices, by model key. Every dt divides every end time. */
29+export const MODEL_CHOICES: Record<string, DiscreteChoice[]> = {
30+ schnakenberg: [
31+ { key: 'a', label: 'a', values: [0.05, 0.1, 0.15, 0.2], value: 0.1 },
32+ { key: 'b', label: 'b', values: [0.7, 0.9, 1.1, 1.3], value: 0.9 },
33+ { key: 'D1', label: 'D₁', values: [1.6e-4, 4e-4, 1e-3], value: 4e-4 },
34+ { key: 'D2', label: 'D₂', values: [3.2e-3, 8e-3, 2e-2], value: 8e-3 },
35+ { key: 'dt', label: 'dt', values: [0.02, 0.05, 0.1], value: 0.05 },
36+ ],
37+ brusselator: [
38+ { key: 'A', label: 'A', values: [2, 3, 4], value: 3 },
39+ { key: 'B', label: 'B', values: [7, 9, 11], value: 9 },
40+ { key: 'D1', label: 'D₁', values: [1.7e-3, 3.33e-3, 6.7e-3], value: 3.33e-3 },
41+ { key: 'D2', label: 'D₂', values: [8.3e-3, 1.67e-2, 3.3e-2], value: 1.67e-2 },
42+ { key: 'dt', label: 'dt', values: [0.01, 0.02, 0.05], value: 0.02 },
43+ ],
44+ allencahn: [
45+ { key: 'eps2', label: 'ε²', values: [5e-4, 1e-3, 2e-3], value: 1e-3 },
46+ { key: 'dt', label: 'dt', values: [0.01, 0.02, 0.05], value: 0.02 },
47+ ],
48+};
3349
3450 /** Geometry parameter choices, by geometry key. The sphere has none. */
3551 export const GEOMETRY_CHOICES: Record<string, DiscreteChoice[]> = {
src/main.tsmodified+108−48View file
@@ -19,7 +19,7 @@
1919 */
2020 import { requestShtDevice, describeAdapter } from './sht/sht.ts';
2121 import { ModelSession } from './mgpu/session.ts';
22-import { mModels, type MModel, type Params } from './mgpu/registry.ts';
22+import { mModels, mModelByKey, type MModel, type Params } from './mgpu/registry.ts';
2323 import { formatFailure } from './mgpu/errors.ts';
2424 import {
2525 mGeometryByKey,
@@ -39,6 +39,7 @@ import { Colorbar, floorRange } from './render/colorbar.ts';
3939 import { colormaps } from './render/colormaps.ts';
4040 import {
4141 MODEL_CHOICES,
42+ DEFAULT_MODEL_KEY,
4243 GEOMETRY_CHOICES,
4344 SEED_CHOICE,
4445 T_END_CHOICE,
@@ -56,6 +57,7 @@ import { encodeCacheFile, decodeCacheFile, type DecodedCacheFile } from './cache
5657 const $ = <T extends HTMLElement>(id: string): T =>
5758 document.getElementById(id) as T;
5859
60+const elModel = $<HTMLSelectElement>('model');
5961 const elParams = $('params');
6062 const elGeometry = $<HTMLSelectElement>('geometry');
6163 const elGeomParams = $('geomparams');
@@ -109,7 +111,7 @@ const CHUNK_STEPS = 32;
109111 const RENDER_EVERY_MS = 250;
110112
111113 // ---------------------------------------------------------------- state
112-const model: MModel = mModels[0];
114+let model: MModel = mModelByKey(DEFAULT_MODEL_KEY)!;
113115 let device: GPUDevice | null = null;
114116 let session: ModelSession | null = null;
115117 let adapterName = '';
@@ -118,7 +120,7 @@ let adapterName = '';
118120 let stepsPerSubmit = 4;
119121
120122 /** The discrete selections, always exactly values from options.ts. */
121-let params: Params = Object.fromEntries(MODEL_CHOICES.map((c) => [c.key, c.value]));
123+let params: Params = defaultChoiceParams(MODEL_CHOICES[DEFAULT_MODEL_KEY]);
122124 let geometry: MGeometry = mGeometryByKey(DEFAULT_GEOMETRY_KEY)!;
123125 let geomParams: Params = Object.fromEntries(
124126 GEOMETRY_CHOICES[DEFAULT_GEOMETRY_KEY].map((c) => [c.key, c.value]),
@@ -131,8 +133,11 @@ let tEnd = T_END_CHOICE.value;
131133 // cached solution). Read once at startup; rewritten on every change.
132134 readUrlState();
133135
134-/** What the session currently has applied (params are cheap; geometry is a
135- * rebuild of the surface and the mesh, so it is compared before applying). */
136+/** What the session currently has applied. Params are cheap (uniforms); a
137+ * geometry change re-evaluates the surface and rebuilds the mesh; a model
138+ * change recompiles the whole session, since the model is compiled into the
139+ * GPU step. */
140+let sessionModelKey = '';
136141 let sessionGeomKey = '';
137142 let sessionGeomParams: Params = {};
138143
@@ -199,12 +204,17 @@ function readUrlState(): void {
199204 const v = Number(raw);
200205 return choice.values.includes(v) ? v : current;
201206 };
207+ const m = p.get('model');
208+ if (m && mModelByKey(m) && MODEL_CHOICES[m]) {
209+ model = mModelByKey(m)!;
210+ params = defaultChoiceParams(MODEL_CHOICES[m]);
211+ }
202212 const g = p.get('geometry');
203213 if (g && mGeometryByKey(g) && GEOMETRY_CHOICES[g]) {
204214 geometry = mGeometryByKey(g)!;
205215 geomParams = defaultChoiceParams(GEOMETRY_CHOICES[g]);
206216 }
207- for (const c of MODEL_CHOICES) params[c.key] = pick(c, params[c.key]);
217+ for (const c of MODEL_CHOICES[model.key]) params[c.key] = pick(c, params[c.key]);
208218 for (const c of GEOMETRY_CHOICES[geometry.key]) geomParams[c.key] = pick(c, geomParams[c.key]);
209219 seed = pick(SEED_CHOICE, seed);
210220 tEnd = pick(T_END_CHOICE, tEnd, 'tend');
@@ -212,7 +222,8 @@ function readUrlState(): void {
212222
213223 function writeUrlState(): void {
214224 const p = new URLSearchParams();
215- for (const c of MODEL_CHOICES) p.set(c.key, fmtChoice(params[c.key]));
225+ p.set('model', model.key);
226+ for (const c of MODEL_CHOICES[model.key]) p.set(c.key, fmtChoice(params[c.key]));
216227 p.set('geometry', geometry.key);
217228 for (const c of GEOMETRY_CHOICES[geometry.key]) p.set(c.key, fmtChoice(geomParams[c.key]));
218229 p.set('seed', String(seed));
@@ -257,8 +268,11 @@ function makeSelect(
257268
258269 /** Put every selection back to its default and refresh. */
259270 function resetDefaults(): void {
260- params = defaultChoiceParams(MODEL_CHOICES);
271+ model = mModelByKey(DEFAULT_MODEL_KEY)!;
272+ params = defaultChoiceParams(MODEL_CHOICES[DEFAULT_MODEL_KEY]);
261273 geometry = mGeometryByKey(DEFAULT_GEOMETRY_KEY)!;
274+ elModel.value = model.key;
275+ buildModelParamControls();
262276 geomParams = defaultChoiceParams(GEOMETRY_CHOICES[DEFAULT_GEOMETRY_KEY]);
263277 seed = SEED_CHOICE.value;
264278 tEnd = T_END_CHOICE.value;
@@ -270,12 +284,30 @@ function resetDefaults(): void {
270284 onSelectionChange();
271285 }
272286
273-function buildControls(): void {
274- for (const choice of MODEL_CHOICES) {
287+function buildModelParamControls(): void {
288+ elParams.replaceChildren();
289+ for (const choice of MODEL_CHOICES[model.key]) {
275290 elParams.append(
276291 makeSelect(choice, () => params[choice.key], (v) => (params[choice.key] = v)),
277292 );
278293 }
294+}
295+
296+function buildControls(): void {
297+ for (const m of mModels) {
298+ const opt = document.createElement('option');
299+ opt.value = m.key;
300+ opt.textContent = m.label;
301+ elModel.append(opt);
302+ }
303+ elModel.value = model.key;
304+ elModel.addEventListener('change', () => {
305+ model = mModelByKey(elModel.value)!;
306+ params = defaultChoiceParams(MODEL_CHOICES[model.key]);
307+ buildModelParamControls();
308+ onSelectionChange();
309+ });
310+ buildModelParamControls();
279311 for (const g of mGeometries) {
280312 const opt = document.createElement('option');
281313 opt.value = g.key;
@@ -539,11 +571,65 @@ function offerDownload(bytes: Uint8Array, name: string): void {
539571 }
540572
541573 // ---------------------------------------------------------------- solving
542-/** Apply the current selection to the (one, reused) session: params are a
543- * uniform upload; a geometry change re-evaluates the surface and rebuilds
544- * the mesh, keeping the camera. */
574+/** Rebuild the mesh and panels from the session's current surface, keeping
575+ * the camera. Fresh buffers render black until the first fill, so the bare
576+ * surface is shown; the caller's draw or clearDisplay follows right behind. */
577+async function rebuildViewFromSession(): Promise<void> {
578+ if (!session) return;
579+ const surface = await session.renderPositions();
580+ const cam = scenes[0]?.cameraState();
581+ disposeView();
582+ buildView(surface);
583+ if (cam) for (const s of scenes) s.setCameraState(cam);
584+ clearDisplay();
585+}
586+
587+/**
588+ * Compile a full session for the spec's model. The model is the one
589+ * selection that cannot be swapped into a running session — its step is
590+ * compiled into the GPU pipelines — so changing it pays a recompile
591+ * (a second or two on a real GPU). The panel count follows the model's
592+ * species (Allen–Cahn has one), so the view is rebuilt too.
593+ */
594+async function rebuildSession(spec: CacheSpec): Promise<void> {
595+ if (!device) throw new Error('no GPU device');
596+ const nextModel = mModelByKey(spec.model)!;
597+ const geomModel = mGeometryByKey(spec.geometry)!;
598+ session?.destroy();
599+ session = null;
600+ sessionModelKey = '';
601+ status(`compiling ${nextModel.label}…`);
602+ session = await ModelSession.create({
603+ device,
604+ model: nextModel,
605+ params: spec.params,
606+ lmax: spec.lmax,
607+ oversample: OVERSAMPLE,
608+ geometry: geomModel,
609+ geometryParams: spec.geometryParams,
610+ niter: spec.niter,
611+ lam3: spec.lam3,
612+ });
613+ model = nextModel;
614+ sessionModelKey = spec.model;
615+ sessionGeomKey = spec.geometry;
616+ sessionGeomParams = { ...spec.geometryParams };
617+ // Never put more dispatches in one submission than the budget allows,
618+ // however expensive this model's step is.
619+ const opsPerStep = Math.max(1, session.describe().step.length);
620+ stepsPerSubmit = Math.max(1, Math.floor(DISPATCH_BUDGET / opsPerStep));
621+ await rebuildViewFromSession();
622+ updateStats();
623+}
624+
625+/** Apply the current selection to the session: params are a uniform upload;
626+ * a geometry change re-evaluates the surface and rebuilds the mesh; a model
627+ * change recompiles the session entirely. */
545628 async function applySelection(spec: CacheSpec): Promise<void> {
546- if (!session) throw new Error('no session');
629+ if (!session || spec.model !== sessionModelKey) {
630+ await rebuildSession(spec);
631+ return;
632+ }
547633 session.setParams(spec.params);
548634 const geomChanged =
549635 spec.geometry !== sessionGeomKey ||
@@ -553,14 +639,7 @@ async function applySelection(spec: CacheSpec): Promise<void> {
553639 await session.setGeometry(geomModel, spec.geometryParams);
554640 sessionGeomKey = spec.geometry;
555641 sessionGeomParams = { ...spec.geometryParams };
556- const surface = await session.renderPositions();
557- const cam = scenes[0]?.cameraState();
558- disposeView();
559- buildView(surface);
560- if (cam) for (const s of scenes) s.setCameraState(cam);
561- // Fresh buffers render black until the first fill; show the bare surface
562- // instead. The caller's draw or clearDisplay follows right behind.
563- clearDisplay();
642+ await rebuildViewFromSession();
564643 }
565644
566645 /** Decode a fetched cache file and put it on screen. */
@@ -596,7 +675,11 @@ async function displayCached(
596675 * running (the run is not disturbed — only the cache note follows).
597676 */
598677 async function refresh(): Promise<void> {
599- if (!session || busy) {
678+ // Before the GPU is up there is nothing to refresh; while a computation
679+ // runs the note follows the dropdowns and the refresh waits its turn. A
680+ // missing session is NOT a reason to bail: applySelection rebuilds it,
681+ // which is also what recovers from a failed compile.
682+ if (!device || busy) {
600683 void updateCacheNote();
601684 return;
602685 }
@@ -641,7 +724,7 @@ async function refresh(): Promise<void> {
641724
642725 /** The Compute solution button: cache lookup, then either load or compute. */
643726 async function solve(): Promise<void> {
644- if (!session || busy) return;
727+ if (!device || busy) return;
645728 generation++;
646729 const gen = generation;
647730 setBusy(true);
@@ -958,36 +1041,13 @@ async function boot(): Promise<void> {
9581041 }
9591042 });
9601043
961- status('compiling the solver…');
9621044 try {
963- session = await ModelSession.create({
964- device,
965- model,
966- params,
967- lmax: LMAX,
968- oversample: OVERSAMPLE,
969- geometry,
970- geometryParams: geomParams,
971- niter: NITER,
972- lam3: LAM3,
973- });
1045+ await rebuildSession(currentSpec());
9741046 } catch (e) {
9751047 elErr.textContent = formatFailure(e, model.source);
9761048 status('failed to compile.');
9771049 return;
9781050 }
979- sessionGeomKey = geometry.key;
980- sessionGeomParams = { ...geomParams };
981-
982- // Never put more dispatches in one submission than the budget allows,
983- // however expensive niter has made one step.
984- const opsPerStep = Math.max(1, session.describe().step.length);
985- stepsPerSubmit = Math.max(1, Math.floor(DISPATCH_BUDGET / opsPerStep));
986-
987- const surface = await session.renderPositions();
988- buildView(surface);
989- clearDisplay();
990- updateStats();
9911051 // Bring up the default selection if it is cached; otherwise show empty
9921052 // surfaces. Nothing is ever computed without pressing the button.
9931053 flowChain = flowChain.then(() => refresh()).catch(() => undefined);
src/mgpu/registry.tsmodified+42−4View file
@@ -7,15 +7,20 @@
77 * declares nothing about these — it just names the parameters it wants, and
88 * `CompiledModel` matches each against this table.
99 *
10- * Trimmed from turing-surface: this app ships one model (Schnakenberg, flux
11- * form). The discrete parameter choices the app actually offers live in
12- * src/cache/options.ts; the min/max/step here are only the numeric bounds.
10+ * Trimmed from turing-surface: the three flux-form models ship (Schnakenberg,
11+ * Brusselator, Allen-Cahn); the 12-transform Algorithm-4 reference does not,
12+ * since it solves the same equations as Schnakenberg and would only duplicate
13+ * cache entries under different hashes. The discrete parameter choices the
14+ * app actually offers live in src/cache/options.ts; the min/max/step here are
15+ * only the numeric bounds.
1316 *
1417 * Naming convention, documented in each .m:
1518 * `u`, `v`, ... grid fields the model computes and the app renders
1619 * `U`, `V`, ... the corresponding spectral state (uppercase)
1720 */
1821 import schnakenbergSource from '../../models/schnakenberg.m?raw';
22+import brusselatorSource from '../../models/brusselator.m?raw';
23+import allencahnSource from '../../models/allencahn.m?raw';
1924
2025 export type Params = Record<string, number>;
2126
@@ -74,7 +79,40 @@ const schnakenberg: MModel = {
7479 source: schnakenbergSource,
7580 };
7681
77-export const mModels: MModel[] = [schnakenberg];
82+const brusselator: MModel = {
83+ key: 'brusselator',
84+ label: 'Brusselator',
85+ blurb: 'Turing stripes and spots.',
86+ species: ['u', 'v'],
87+ state: stateFor(['u', 'v']),
88+ params: [
89+ { key: 'A', label: 'A', value: 3, min: 0.5, max: 6, step: 0.1 },
90+ { key: 'B', label: 'B', value: 9, min: 1, max: 15, step: 0.25 },
91+ { key: 'D1', label: 'D₁', value: 3.33e-3, min: 1e-4, max: 2e-2, step: 1e-4 },
92+ { key: 'D2', label: 'D₂', value: 1.67e-2, min: 1e-3, max: 1e-1, step: 1e-3 },
93+ { key: 'dt', label: 'dt', value: 0.02, min: 0.002, max: 0.1, step: 0.002 },
94+ ],
95+ pdeg: 3,
96+ seedAmp: 1e-2,
97+ source: brusselatorSource,
98+};
99+
100+const allencahn: MModel = {
101+ key: 'allencahn',
102+ label: 'Allen–Cahn',
103+ blurb: 'One species: interfaces form, then coarsen.',
104+ species: ['u'],
105+ state: stateFor(['u']),
106+ params: [
107+ { key: 'eps2', label: 'ε²', value: 1e-3, min: 1e-4, max: 1e-2, step: 1e-4 },
108+ { key: 'dt', label: 'dt', value: 0.02, min: 0.002, max: 0.2, step: 0.002 },
109+ ],
110+ pdeg: 3,
111+ seedAmp: 1e-2,
112+ source: allencahnSource,
113+};
114+
115+export const mModels: MModel[] = [schnakenberg, brusselator, allencahn];
78116
79117 export const mModelByKey = (key: string): MModel | undefined =>
80118 mModels.find((m) => m.key === key);