/ concept-collection / turing-sphere
Sign in
concept-collection / turing-sphere
165 lines · 7.8 KBCodeBlameHistory
3Reaction–diffusion systems (Turing patterns) solved **live in the browser on the
4surface of a sphere**, using a spectral spherical-harmonic method with the
5transforms running on the GPU via WebGPU.
7**Live demo:** <https://concept-collection.github.io/turing-sphere/>
9## What it does
11It solves the N-species system
13```
14d(u_k)/dt = D_k*lap_s(u_k) + f_k(t, x, y, z, u_1, ..., u_N), k = 1, ..., N
15```
17on the unit sphere, where `lap_s` is the Laplace–Beltrami operator. Diffusion is
18treated implicitly in spherical-harmonic coefficient space, where `lap_s` is
19diagonal with eigenvalues `-l(l+1)`; reaction is treated explicitly on the grid.
20The two are combined with a first-order IMEX Euler step — the entire time loop is
22```
23V_k = synth(U_k) # spectral -> grid
24R_k = analys(f_k(t, x, y, z, V_1..V_N)) # reaction on grid -> spectral
25U_k = (U_k + dt*R_k) / (1 + dt*D_k*l(l+1))
26```
28You watch the patterns emerge in real time on orbitable 3D spheres (one per
29species, cameras synced), with pause/resume, re-seeding, live parameter editing,
30and colormap selection.
32Three presets are included:
34- **Schnakenberg** — Turing spots (unstable band 14 ≤ l ≤ 40, peak l = 24)
35- **Brusselator** — stripes and spots from a stiffer reaction
36- **Allen–Cahn** — a single species whose interfaces form and coarsen
38## Provenance
40This is the browser port of a MATLAB reference implementation
41(`SphericalReactionDiffusion.m`, "websph"), which defines the solver through a
42four-member porting boundary: `coeffs2vals`, `vals2coeffs`, `grid.lat`,
43`grid.lon`. Profiling of the MATLAB version shows the transforms are ~96% of
44compute, so this port swaps in:
46- **Transforms:** [shtns-webgpu](https://github.com/concept-collection/shtns-webgpu) —
47 fp32 spherical harmonic transforms in WGSL compute shaders, modeled on
48 [SHTNS](https://nschaeff.bitbucket.io/shtns/). Its source is vendored under
49 [`src/sht/`](src/sht/) (CECILL-2.1), including the f64 CPU reference
50 transform used for testing and as a no-WebGPU fallback.
51- **Rendering:** three.js spheres with per-vertex colormaps, adapted from the
52 `SphereEmbedding` view in
53 [figpack](https://github.com/flatironinstitute/figpack)'s experimental
54 extension package ([`src/render/`](src/render/)).
55- **Solver:** [`src/solver/simulation.ts`](src/solver/simulation.ts), a direct
56 TypeScript port of the MATLAB IMEX loop, in f64 on the coefficients with the
57 transforms in fp32 on the GPU.
59## Numerics
61- Grid: Gauss–Legendre × equispaced-phi, dealiased for the cubic reactions with
62 the `(pdeg+1)` rule from the reference implementation:
63 `nlat ≥ ((pdeg+1)·lmax+1)/2`, `nphi ≥ (pdeg+1)·lmax+1` (rounded up to a power
64 of two for the GPU FFT path). At the default lmax 63 that is a 128×256 grid.
65- Spectral layout: SHTNS conventions — orthonormal + Condon–Shortley, complex
66 coefficients for m ≥ 0, m-major ordering.
67- fp32 transforms introduce ~1e-6 relative error per step (verified against the
68 f64 CPU path); for pattern formation from 1e-2 seeded noise this is
69 inconsequential.
73How much does running this in a browser cost? [`scripts/bench.ts`](scripts/bench.ts)
74answers that by running the *same* code — same `Simulation`, same WGSL
75transforms, same parameters — from Node on desktop WebGPU (Google Dawn), and
76the app prints the command line that reproduces whatever it is currently
77simulating:
79```
84005eeMake the benchmark command run on older NodeJeremy Magland 80node scripts/bench.mjs --preset schnak-spots --lmax 63 --backend webgpu --steps 2000 \
15a77e2Add a desktop WebGPU benchmark and show its command in the appJeremy Magland 81 --seed 1 --a 0.1 --b 0.9 --D1 0.0004 --D2 0.008 --dt 0.05
82```
84Copy it from under the stats line, run it, and compare the `ms/step` it reports
85with the app's. Both sides go through the one shared
86[`src/bench/runSpec.ts`](src/bench/runSpec.ts) — the app formats a run into that
87command, the benchmark parses it back — so there is no second copy of the
88defaults for the two runs to drift apart on. Node runs the TypeScript sources
89directly, so `src/` is literally the same code in both places, down to the
90device request in `requestShtDevice()` (Dawn is installed under `navigator.gpu`
91and the WebGPU globals, and the rest runs unchanged).
e5b7827Fix CI: do not omit optional dependenciesJeremy Magland 93Desktop WebGPU comes from the `webgpu` package (prebuilt Dawn, ~70 MB), listed
94as an optional dependency so that a platform it has no binaries for fails the
95install of that package alone rather than the whole tree. `npm install` picks it
182fa98Name the glibc case when Dawn's binary will not loadJeremy Magland 96up; without it, only `--backend cpu` runs and the benchmark says so. Those
97binaries need glibc 2.29+, which rules out older cluster images (RHEL/Rocky 8 is
982.28) unless you run inside a container with a newer base. Other
e5b7827Fix CI: do not omit optional dependenciesJeremy Magland 99flags: `--steps`, `--warmup`, `--json`, `--help`; `DAWN_FLAGS='backend=vulkan'`
100(`;`-separated) passes Dawn options through, e.g. to pick a backend or to
101compare against Dawn's own software adapter.
103What the comparison does and does not control for:
105- the benchmark is **solver only**; the app's `ms/step` excludes `draw()` but is
106 still measured on a page that renders two spheres between steps. For a browser
107 number with no rendering at all, open `test.html?soak=2000&lmax=63`.
108- each step is four transforms, each ending in a buffer readback, so both sides
109 are dominated by submit-and-map latency rather than arithmetic — this measures
110 a driver round-trip more than it measures a GPU.
111- the browser adds its own GPU-process boundary and, for a page that is not
112 cross-origin isolated, coarser timers.
15a77e2Add a desktop WebGPU benchmark and show its command in the appJeremy Magland 116- `npm run bench -- --help` — the desktop benchmark above (see
117 [Desktop vs browser](#desktop-vs-browser)).
2dedc35turing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPUJeremy Magland 118- `npm run test:node` — f64 solver correctness in Node: exact single-mode
119 linear recurrence, exact uniform-state reaction ODE, and the linearized
120 Turing-mode 2×2 IMEX recurrence (all at ~1e-12).
121- `npm run test:gpu` — builds and drives headless Chrome: GPU-vs-CPU transform
122 and solver cross-checks, plus a 100-step stability run.
123- `node scripts/longrun-node.ts` — CPU run to t = 100 confirming pattern
124 saturation.
e7bcd70Add soak, live-check and solver-only soak toolingJeremy Magland 125- `node scripts/soak.mjs [steps] [lmax] [backend]` — drive the demo for many
126 steps, sampling JS heap and catching crashes. A 900-step run at lmax 63 on
127 software WebGPU (SwiftShader) completes with a flat ~4 MB heap.
2dedc35turing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPUJeremy Magland 128- `node scripts/screenshot.mjs out.png [light|dark] [minSteps]` — screenshot
129 the demo after a number of steps.
e7bcd70Add soak, live-check and solver-only soak toolingJeremy Magland 130- `node scripts/check-live.mjs [url]` — smoke-check a deployed URL in a real
131 browser: load, press Run, confirm the solver advances.
132- `test.html?soak=<steps>&lmax=<n>` — solver-only soak with no rendering.
134### A note on canvas resizing
136Early long runs killed the browser after ~700–800 steps. The cause was the
137colorbar's min/max labels changing width as their digit count changed, which
138reflowed the panel, fired the `ResizeObserver`, and called
139`renderer.setSize()` — reallocating the WebGL drawing buffer. Assigning
140`canvas.width` also blanks the canvas even when the value is unchanged, so the
141same bug caused visible flicker. Fixed by giving the colorbar column a fixed
142width and making `SphereScene.resize()` return early on no-op resizes.
144## Development
146```
147npm install
148npm run dev # local dev server
149npm run build # type-check + production build to dist/
150```
84005eeMake the benchmark command run on older NodeJeremy Magland 152The `.ts` entry points under `scripts/` are run by Node directly, which strips
153types without being asked only from Node 22.18 / 23.6 / 24 on. Everything here
154works back to 22.6, where stripping exists but is flagged: the npm scripts pass
155`--experimental-strip-types` themselves, and the benchmark — the one command
156that gets copied to other machines — goes through
157[`scripts/bench.mjs`](scripts/bench.mjs), which re-runs itself with the flag
158when it has to. Invoking a `scripts/*.ts` file by hand on 22.6–22.17 needs the
159flag spelled out.
2dedc35turing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPUJeremy Magland 161Deployed to GitHub Pages by `.github/workflows/deploy.yml` on push to `main`.
163## License
165CECILL-2.1 (inherited from SHTNS via shtns-webgpu, whose sources are vendored).
moveopenescclose