/ concept-collection / turing-sphere
Sign in
concept-collection / turing-sphere
Go to fileHistoryFork
.githubturing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPU
scriptsAdd soak, live-check and solver-only soak tooling
srcturing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPU
testAdd soak, live-check and solver-only soak tooling
.gitignoreturing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPU
index.htmlturing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPU
package-lock.jsonturing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPU
package.jsonturing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPU
README.mdAdd soak, live-check and solver-only soak tooling
test.htmlturing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPU
tsconfig.jsonturing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPU
vite.config.tsturing-sphere: reaction-diffusion on the sphere, spectral solver on WebGPU

turing-sphere#

Reaction–diffusion systems (Turing patterns) solved live in the browser on the surface of a sphere, using a spectral spherical-harmonic method with the transforms running on the GPU via WebGPU.

Live demo: https://concept-collection.github.io/turing-sphere/

What it does#

It solves the N-species system

d(u_k)/dt = D_k*lap_s(u_k) + f_k(t, x, y, z, u_1, ..., u_N),    k = 1, ..., N

on the unit sphere, where lap_s is the Laplace–Beltrami operator. Diffusion is treated implicitly in spherical-harmonic coefficient space, where lap_s is diagonal with eigenvalues -l(l+1); reaction is treated explicitly on the grid. The two are combined with a first-order IMEX Euler step — the entire time loop is

V_k  = synth(U_k)                          # spectral -> grid
R_k  = analys(f_k(t, x, y, z, V_1..V_N))   # reaction on grid -> spectral
U_k  = (U_k + dt*R_k) / (1 + dt*D_k*l(l+1))

You watch the patterns emerge in real time on orbitable 3D spheres (one per species, cameras synced), with pause/resume, re-seeding, live parameter editing, and colormap selection.

Three presets are included:

  • Schnakenberg — Turing spots (unstable band 14 ≤ l ≤ 40, peak l = 24)
  • Brusselator — stripes and spots from a stiffer reaction
  • Allen–Cahn — a single species whose interfaces form and coarsen

Provenance#

This is the browser port of a MATLAB reference implementation (SphericalReactionDiffusion.m, "websph"), which defines the solver through a four-member porting boundary: coeffs2vals, vals2coeffs, grid.lat, grid.lon. Profiling of the MATLAB version shows the transforms are ~96% of compute, so this port swaps in:

  • Transforms: shtns-webgpu — fp32 spherical harmonic transforms in WGSL compute shaders, modeled on SHTNS. Its source is vendored under src/sht/ (CECILL-2.1), including the f64 CPU reference transform used for testing and as a no-WebGPU fallback.
  • Rendering: three.js spheres with per-vertex colormaps, adapted from the SphereEmbedding view in figpack's experimental extension package (src/render/).
  • Solver: src/solver/simulation.ts, a direct TypeScript port of the MATLAB IMEX loop, in f64 on the coefficients with the transforms in fp32 on the GPU.

Numerics#

  • Grid: Gauss–Legendre × equispaced-phi, dealiased for the cubic reactions with the (pdeg+1) rule from the reference implementation: nlat ≥ ((pdeg+1)·lmax+1)/2, nphi ≥ (pdeg+1)·lmax+1 (rounded up to a power of two for the GPU FFT path). At the default lmax 63 that is a 128×256 grid.
  • Spectral layout: SHTNS conventions — orthonormal + Condon–Shortley, complex coefficients for m ≥ 0, m-major ordering.
  • fp32 transforms introduce ~1e-6 relative error per step (verified against the f64 CPU path); for pattern formation from 1e-2 seeded noise this is inconsequential.

Tests#

  • npm run test:node — f64 solver correctness in Node: exact single-mode linear recurrence, exact uniform-state reaction ODE, and the linearized Turing-mode 2×2 IMEX recurrence (all at ~1e-12).
  • npm run test:gpu — builds and drives headless Chrome: GPU-vs-CPU transform and solver cross-checks, plus a 100-step stability run.
  • node scripts/longrun-node.ts — CPU run to t = 100 confirming pattern saturation.
  • node scripts/soak.mjs [steps] [lmax] [backend] — drive the demo for many steps, sampling JS heap and catching crashes. A 900-step run at lmax 63 on software WebGPU (SwiftShader) completes with a flat ~4 MB heap.
  • node scripts/screenshot.mjs out.png [light|dark] [minSteps] — screenshot the demo after a number of steps.
  • node scripts/check-live.mjs [url] — smoke-check a deployed URL in a real browser: load, press Run, confirm the solver advances.
  • test.html?soak=<steps>&lmax=<n> — solver-only soak with no rendering.

A note on canvas resizing#

Early long runs killed the browser after ~700–800 steps. The cause was the colorbar's min/max labels changing width as their digit count changed, which reflowed the panel, fired the ResizeObserver, and called renderer.setSize() — reallocating the WebGL drawing buffer. Assigning canvas.width also blanks the canvas even when the value is unchanged, so the same bug caused visible flicker. Fixed by giving the colorbar column a fixed width and making SphereScene.resize() return early on no-op resizes.

Development#

npm install
npm run dev       # local dev server
npm run build     # type-check + production build to dist/

Deployed to GitHub Pages by .github/workflows/deploy.yml on push to main.

License#

CECILL-2.1 (inherited from SHTNS via shtns-webgpu, whose sources are vendored).

moveopenescclose