1/**
2 * The available models: their MATLAB source, and the metadata the host owns.
3 *
4 * A model's *algorithm* lives in its .m file. Everything around it lives here:
5 * the parameter names the .m may take as arguments, their defaults and slider
6 * ranges, which grid fields to render, and the dealiasing degree. The .m
7 * declares nothing about these — it just names the parameters it wants, and
8 * `CompiledModel` matches each against this table.
9 *
10 * Naming convention, documented in each .m:
11 * `u`, `v`, ... grid fields the model computes and the app renders
12 * `U`, `V`, ... the corresponding spectral state (uppercase)
13 */
14import schnakenbergSource from '../../models/schnakenberg.m?raw';
15import schnakenbergAlg4Source from '../../models/schnakenberg_alg4.m?raw';
16import brusselatorSource from '../../models/brusselator.m?raw';
17import allencahnSource from '../../models/allencahn.m?raw';
19export type Params = Record<string, number>;
21/** A tunable scalar the .m may take as an argument. */
22export interface ParamSpec {
23 key: string;
24 label: string;
25 value: number;
26 min: number;
27 max: number;
28 step: number;
29 /**
30 * This parameter is a random seed: its value picks a draw and means nothing
31 * on its own, so the UI offers a button that jumps to another one rather
32 * than a box to type a number into. `min`/`max` still bound what the button
33 * picks.
34 */
35 reseed?: boolean;
36}
38export interface MModel {
39 key: string;
40 label: string;
41 blurb: string;
42 /** Grid fields to render, one panel each. */
43 species: string[];
44 /** Spectral state names the .m advances. */
45 state: string[];
46 params: ParamSpec[];
47 /** Polynomial degree of the reaction in the fields, for grid dealiasing. */
48 pdeg: number;
49 /** Amplitude of the seeded perturbation handed to `init`. */
50 seedAmp: number;
51 /** MATLAB source — the algorithm itself. */
52 source: string;
53}
55/** Spectral state names follow the grid-field names, uppercased. */
56const stateFor = (species: string[]): string[] => species.map((s) => s.toUpperCase());
58const schnakenberg: MModel = {
59 key: 'schnakenberg',
60 label: 'Schnakenberg',
61 blurb: 'Turing spots.',
62 species: ['u', 'v'],
63 state: stateFor(['u', 'v']),
64 params: [
65 { key: 'a', label: 'a', value: 0.1, min: 0.01, max: 0.5, step: 0.01 },
66 { key: 'b', label: 'b', value: 0.9, min: 0.1, max: 2, step: 0.05 },
67 { key: 'D1', label: 'D₁', value: 4e-4, min: 1e-5, max: 5e-3, step: 1e-5 },
68 { key: 'D2', label: 'D₂', value: 8e-3, min: 1e-4, max: 5e-2, step: 1e-4 },
69 { key: 'dt', label: 'dt', value: 0.05, min: 0.005, max: 0.5, step: 0.005 },
70 ],
71 pdeg: 3,
72 seedAmp: 1e-2,
73 source: schnakenbergSource,
74};
76/**
77 * The same PDE and parameters as `schnakenberg`, with the implicit solve's
78 * geometric correction in its original Cartesian-gradient form (Algorithm 4,
79 * 12 transforms per species per iteration) instead of the flux form's 6
80 * (docs/reduced-transforms.md). Shipped as a live reference:
81 * the two must agree to fp32 accuracy on any surface, and the tests hold
82 * them to that.
83 */
84const schnakenbergAlg4: MModel = {
85 ...schnakenberg,
86 key: 'schnakenberg-alg4',
87 label: 'Schnakenberg (12-transform reference)',
88 blurb: 'Same spots, Algorithm-4 Laplace-Beltrami — for A/B against the flux form.',
89 source: schnakenbergAlg4Source,
90};
92const brusselator: MModel = {
93 key: 'brusselator',
94 label: 'Brusselator',
95 blurb: 'Turing stripes and spots.',
96 species: ['u', 'v'],
97 state: stateFor(['u', 'v']),
98 params: [
99 { key: 'A', label: 'A', value: 3, min: 0.5, max: 6, step: 0.1 },
100 { key: 'B', label: 'B', value: 9, min: 1, max: 15, step: 0.25 },
101 { key: 'D1', label: 'D₁', value: 3.33e-3, min: 1e-4, max: 2e-2, step: 1e-4 },
102 { key: 'D2', label: 'D₂', value: 1.67e-2, min: 1e-3, max: 1e-1, step: 1e-3 },
103 { key: 'dt', label: 'dt', value: 0.02, min: 0.002, max: 0.1, step: 0.002 },
104 ],
105 pdeg: 3,
106 seedAmp: 1e-2,
107 source: brusselatorSource,
108};
110const allencahn: MModel = {
111 key: 'allencahn',
112 label: 'Allen–Cahn',
113 blurb: 'One species: interfaces form, then coarsen.',
114 species: ['u'],
115 state: stateFor(['u']),
116 params: [
117 { key: 'eps2', label: 'ε²', value: 1e-3, min: 1e-4, max: 1e-2, step: 1e-4 },
118 { key: 'dt', label: 'dt', value: 0.02, min: 0.002, max: 0.2, step: 0.002 },
119 ],
120 pdeg: 3,
121 seedAmp: 1e-2,
122 source: allencahnSource,
123};
125export const mModels: MModel[] = [schnakenberg, brusselator, allencahn, schnakenbergAlg4];
127export const mModelByKey = (key: string): MModel | undefined =>
128 mModels.find((m) => m.key === key);
130export const defaultParams = (m: MModel): Params =>
131 Object.fromEntries(m.params.map((p) => [p.key, p.value]));
133/** Named parameter presets shown in the UI dropdown. The pattern length scale
134 * goes as 1/sqrt(D), so scaling both diffusivities moves the spot size without
135 * changing the dynamics. */
136export interface Preset {
137 key: string;
138 label: string;
139 modelKey: string;
140 /** Overrides applied on top of the model's default parameters. */
141 params?: Params;
142}
144export const presets: Preset[] = [
145 { key: 'schnak-spots', label: 'Schnakenberg — spots', modelKey: 'schnakenberg' },
146 {
147 key: 'schnak-coarse',
148 label: 'Schnakenberg — coarse spots',
149 modelKey: 'schnakenberg',
150 params: { D1: 1e-3, D2: 2e-2 },
151 },
152 {
153 key: 'schnak-fine',
154 label: 'Schnakenberg — fine spots',
155 modelKey: 'schnakenberg',
156 params: { D1: 1.6e-4, D2: 3.2e-3 },
157 },
158 { key: 'brussel', label: 'Brusselator — stripes & spots', modelKey: 'brusselator' },
159 { key: 'allencahn', label: 'Allen–Cahn — coarsening', modelKey: 'allencahn' },
160 {
161 key: 'schnak-alg4',
162 label: 'Schnakenberg — spots (12-transform reference)',
163 modelKey: 'schnakenberg-alg4',
164 },
165];