1/**
2 * Model metadata and the reference reaction terms.
3 *
4 * The parameter metadata here (names, labels, defaults, slider ranges) is what
5 * the app uses; `src/mgpu/registry.ts` attaches each model's .m source to it, so
6 * the .m running on the GPU and the reference solver used by the tests cannot be
7 * configured differently. The `reaction` / `init` closures below are the
8 * reference implementation only — the app runs the .m instead.
9 *
10 * Reaction-diffusion model presets, ported from websph's
11 * SphericalReactionDiffusionDriver.m. Each species k solves
12 *
13 * d(u_k)/dt = D_k * lap_s(u_k) + f_k(t, x, y, z, u_1, ..., u_N)
14 *
15 * on the unit sphere. Reactions are vectorized over the grid.
16 */
18export type Params = Record<string, number>;
20export interface ParamSpec {
21 key: string;
22 label: string;
23 value: number;
24 min: number;
25 max: number;
26 step: number;
27}
29export interface ModelSpec {
30 key: string;
31 label: string;
32 blurb: string;
33 species: string[];
34 params: ParamSpec[];
35 /** Polynomial degree of the reaction in the fields (for dealiasing). */
36 pdeg: number;
37 /** Amplitude of the random perturbation seeded into the initial state. */
38 seedAmp: number;
39 diffusivities(p: Params): number[];
40 /** Fill out[k][i] with f_k evaluated at every grid point. */
41 reaction(
42 p: Params,
43 t: number,
44 x: Float64Array,
45 y: Float64Array,
46 z: Float64Array,
47 V: ArrayLike<number>[],
48 out: Float64Array[],
49 ): void;
50 /** Fill out[k][i] with the initial condition (noise added via randn). */
51 init(
52 p: Params,
53 x: Float64Array,
54 y: Float64Array,
55 z: Float64Array,
56 randn: () => number,
57 out: Float64Array[],
58 ): void;
59}
61const schnakenberg: ModelSpec = {
62 key: 'schnakenberg',
63 label: 'Schnakenberg',
64 blurb:
65 'Turing spots. The homogeneous state is stable to uniform perturbations ' +
66 'but unstable to degrees 14 ≤ l ≤ 40, most strongly at l = 24.',
67 species: ['u', 'v'],
68 params: [
69 { key: 'a', label: 'a', value: 0.1, min: 0.01, max: 0.5, step: 0.01 },
70 { key: 'b', label: 'b', value: 0.9, min: 0.1, max: 2, step: 0.05 },
71 { key: 'D1', label: 'D₁', value: 4e-4, min: 1e-5, max: 5e-3, step: 1e-5 },
72 { key: 'D2', label: 'D₂', value: 8e-3, min: 1e-4, max: 5e-2, step: 1e-4 },
73 { key: 'dt', label: 'dt', value: 0.05, min: 0.005, max: 0.5, step: 0.005 },
74 ],
75 pdeg: 3,
76 seedAmp: 1e-2,
77 diffusivities: (p) => [p.D1, p.D2],
78 reaction(p, _t, _x, _y, _z, V, out) {
79 const [u, v] = V;
80 const [fu, fv] = out;
81 const a = p.a, b = p.b;
82 const n = fu.length;
83 for (let i = 0; i < n; i++) {
84 const ui = u[i], vi = v[i];
85 const uuv = ui * ui * vi;
86 fu[i] = a - ui + uuv;
87 fv[i] = b - uuv;
88 }
89 },
90 init(p, x, _y, _z, randn, out) {
91 const [u, v] = out;
92 const a = p.a, b = p.b;
93 const us = a + b;
94 const vs = b / (us * us);
95 const n = x.length;
96 for (let i = 0; i < n; i++) {
97 u[i] = us + this.seedAmp * randn();
98 v[i] = vs;
99 }
100 },
101};
103const brusselator: ModelSpec = {
104 key: 'brusselator',
105 label: 'Brusselator',
106 blurb:
107 'Turing stripes and spots, from a smaller diffusivity contrast than ' +
108 'Schnakenberg but with a stiffer reaction.',
109 species: ['u', 'v'],
110 params: [
111 { key: 'A', label: 'A', value: 3, min: 0.5, max: 6, step: 0.1 },
112 { key: 'B', label: 'B', value: 9, min: 1, max: 15, step: 0.25 },
113 { key: 'D1', label: 'D₁', value: 3.33e-3, min: 1e-4, max: 2e-2, step: 1e-4 },
114 { key: 'D2', label: 'D₂', value: 1.67e-2, min: 1e-3, max: 1e-1, step: 1e-3 },
115 { key: 'dt', label: 'dt', value: 0.02, min: 0.002, max: 0.1, step: 0.002 },
116 ],
117 pdeg: 3,
118 seedAmp: 1e-2,
119 diffusivities: (p) => [p.D1, p.D2],
120 reaction(p, _t, _x, _y, _z, V, out) {
121 const [u, v] = V;
122 const [fu, fv] = out;
123 const A = p.A, B = p.B;
124 const n = fu.length;
125 for (let i = 0; i < n; i++) {
126 const ui = u[i], vi = v[i];
127 const uuv = ui * ui * vi;
128 fu[i] = A - (B + 1) * ui + uuv;
129 fv[i] = B * ui - uuv;
130 }
131 },
132 init(p, x, _y, _z, randn, out) {
133 const [u, v] = out;
134 const n = x.length;
135 for (let i = 0; i < n; i++) {
136 u[i] = p.A + this.seedAmp * randn();
137 v[i] = p.B / p.A;
138 }
139 },
140};
142const allenCahn: ModelSpec = {
143 key: 'allencahn',
144 label: 'Allen–Cahn',
145 blurb:
146 'A single species: interfaces form and then coarsen until one domain ' +
147 'swallows the sphere.',
148 species: ['u'],
149 params: [
150 { key: 'eps2', label: 'ε²', value: 1e-3, min: 1e-4, max: 1e-2, step: 1e-4 },
151 { key: 'dt', label: 'dt', value: 0.02, min: 0.002, max: 0.2, step: 0.002 },
152 ],
153 pdeg: 3,
154 seedAmp: 1e-2,
155 diffusivities: (p) => [p.eps2],
156 reaction(_p, _t, _x, _y, _z, V, out) {
157 const [u] = V;
158 const [fu] = out;
159 const n = fu.length;
160 for (let i = 0; i < n; i++) {
161 const ui = u[i];
162 fu[i] = ui - ui * ui * ui;
163 }
164 },
165 init(_p, x, _y, _z, randn, out) {
166 const [u] = out;
167 const n = x.length;
168 for (let i = 0; i < n; i++) {
169 u[i] = this.seedAmp * randn();
170 }
171 },
172};
174export const models: ModelSpec[] = [schnakenberg, brusselator, allenCahn];
176export const defaultParams = (m: ModelSpec): Params =>
177 Object.fromEntries(m.params.map((p) => [p.key, p.value]));
179/** Named parameter presets shown in the UI dropdown. The pattern length
180 * scale goes as 1/sqrt(D), so scaling both diffusivities moves the spot
181 * size without changing the dynamics. */
182export interface Preset {
183 key: string;
184 label: string;
185 modelKey: string;
186 /** Overrides applied on top of the model's default parameters. */
187 params?: Params;
188}
190export const presets: Preset[] = [
191 { key: 'schnak-spots', label: 'Schnakenberg — spots', modelKey: 'schnakenberg' },
192 {
193 key: 'schnak-coarse',
194 label: 'Schnakenberg — coarse spots',
195 modelKey: 'schnakenberg',
196 params: { D1: 1e-3, D2: 2e-2 },
197 },
198 {
199 key: 'schnak-fine',
200 label: 'Schnakenberg — fine spots',
201 modelKey: 'schnakenberg',
202 params: { D1: 1.6e-4, D2: 3.2e-3 },
203 },
204 { key: 'brussel', label: 'Brusselator — stripes & spots', modelKey: 'brusselator' },
205 { key: 'allencahn', label: 'Allen–Cahn — coarsening', modelKey: 'allencahn' },
206];