concept-collection / turing-surface
26.9 KBPreviewCodeBlameHistoryRaw

Reducing spherical-harmonic transforms in ΔΓ\Delta_\Gamma#

Summary. Algorithm 4 costs 12 transforms per matvec (8 syntheses, 4 analyses). A flux-form reformulation, with weights chosen so that every analyzed field is smooth on S2S^2, evaluates the same operator in 6 transforms (4 syntheses, 2 analyses), or 7 with the divergence split against the round sphere that §5 turned out to require. Notation follows algos.pdf.


1. Where the current cost comes from#

Algorithm 4 line Work Transforms
1 θu, φu\partial_\theta u,\ \partial_\varphi u 2 S\mathcal{S}
5 analysis of 3 Cartesian components of Γu\nabla_\Gamma u 3 A\mathcal{A}
6 θ\partial_\theta and φ\partial_\varphi of each of those 3 components 6 S\mathcal{S}
8 final analysis 1 A\mathcal{A}
12

Two sources of waste:

  1. The gradient is carried in ambient R3\mathbb{R}^3 components — 3 fields for an intrinsically 2-dimensional object.
  2. Line 6 takes both derivatives of each component, where the divergence needs only one derivative of each of two fluxes.

There is also a possible free win independent of everything below: Algorithm 1 as specified returns all five derivatives. The Laplacian path needs only θu\partial_\theta u and φu\partial_\varphi u; the second-derivative and mixed-derivative machinery is used exclusively by Algorithm 3 (curvature). If surface_screened_laplacian calls Algorithm 1 wholesale, it is doing 5 syntheses where 2 suffice at lines 1 and 6.


2. The constraint that shapes the solution#

The Cartesian design in Algorithm 4 exists to avoid pole singularities, and it is correct to do so. The relevant property is smoothness as a scalar function on S2S^2, since that is what controls SH coefficient decay and hence whether A\mathcal{A} is meaningful.

Quantity Smooth on S2S^2?
φu\partial_\varphi u yes — exactly band-limited, eq. (2.2)
sinθθu\sin\theta\,\partial_\theta u yes — exactly band-limited, eq. (2.4)
θu\partial_\theta u no — bounded, but φ\varphi-dependent limit at the poles
gθθ, gθθ, Vθ, Vφg_{\theta\theta},\ g^{\theta\theta},\ V_\theta,\ V_\varphi no in general
(Γu)x, (Γu)y, (Γu)z(\nabla_\Gamma u)_x,\ (\nabla_\Gamma u)_y,\ (\nabla_\Gamma u)_z yes

Concretely, for the ellipsoid X=(asinθcosφ, bsinθsinφ, ccosθ)X = (a\sin\theta\cos\varphi,\ b\sin\theta\sin\varphi,\ c\cos\theta), Xθ2a2cos2φ+b2sin2φ|X_\theta|^2 \to a^2\cos^2\varphi + b^2\sin^2\varphi as θ0\theta\to 0: no limit exists.

Algorithm 4 never analyzes anything in the "no" rows — the non-smooth quantities appear only as pointwise grid factors. Any replacement must preserve this property. The naive flux form P=g(gθθuθ+gθφuφ)P = \sqrt{g}\,(g^{\theta\theta}u_\theta + g^{\theta\varphi}u_\varphi), Q=g(gφθuθ+gφφuφ)Q = \sqrt{g}\,(g^{\varphi\theta}u_\theta + g^{\varphi\varphi}u_\varphi) does not: on the round sphere with u=xu = x, Q=sinφQ = -\sin\varphi, which is not a function on S2S^2.

The correct weighting#

PP and QQ are g\sqrt{g} times the contravariant components of G:=ΓuG := \nabla_\Gamma u. Using det[Xθ,Xφ,n]=g\det[X_\theta, X_\varphi, n] = -\sqrt{g}:

P=G(Xφ×n),sinθQ=G(n×sinθXθ).P = -\,G\cdot(X_\varphi \times n), \qquad \sin\theta\,Q = -\,G\cdot\big(n \times \sin\theta\,X_\theta\big).

Every factor on the right is smooth on S2S^2: GG smooth, nn smooth, XφX_\varphi smooth, and sinθXθ\sin\theta\,X_\theta smooth because it is exactly band-limited by the recurrence already implemented. Hence PP and Q~:=sinθQ\tilde{Q} := \sin\theta\,Q are analyzable, on the same footing and for the same structural reason as the Cartesian gradient components.

The cross products are the smoothness certificate only — they are not needed in the code.


3. Precompute (once per surface, grid space)#

Replaces _precompute_metric_quantities(). From the embedding coefficients X^m\hat{X}^m_\ell, obtain XφX_\varphi and sinθXθ\sin\theta\,X_\theta componentwise via Algorithm 1, where the latter is the undivided output of Algorithm 1 line 4, i.e. S(vm)\mathcal{S}(v^m_\ell) with no /sinθ/\sin\theta. Then, pointwise:

g~θθ:=sinθXθ2,g~θφ:=(sinθXθ)Xφ,gφφ:=Xφ2\tilde{g}_{\theta\theta} := |\sin\theta\,X_\theta|^2, \qquad \tilde{g}_{\theta\varphi} := (\sin\theta\,X_\theta)\cdot X_\varphi, \qquad g_{\varphi\varphi} := |X_\varphi|^2
J:=g~θθgφφg~θφ2sin2θso that detg=JsinθJ := \frac{\sqrt{\tilde{g}_{\theta\theta}\,g_{\varphi\varphi} - \tilde{g}_{\theta\varphi}^{\,2}}}{\sin^2\theta} \qquad\text{so that } \sqrt{\det g} = J\sin\theta

(The radicand is sin2θdetg=J2sin4θ\sin^2\theta\det g = J^2\sin^4\theta, so the square root is Jsin2θJ\sin^2\theta — hence sin2θ\sin^2\theta, not sinθ\sin\theta, in the denominator.)

Store four scalar grid arrays:

p1=gφφJsin2θ,p2=g~θφJsin2θ,q2=g~θθJsin2θ,r=1Jsin2θp_1 = \frac{g_{\varphi\varphi}}{J\sin^2\theta}, \qquad p_2 = -\frac{\tilde{g}_{\theta\varphi}}{J\sin^2\theta}, \qquad q_2 = \frac{\tilde{g}_{\theta\theta}}{J\sin^2\theta}, \qquad r = \frac{1}{J\sin^2\theta}

All four are bounded: the sin2θ\sin^2\theta denominators cancel against vanishing numerators (g~θφ=O(sin2θ)\tilde{g}_{\theta\varphi} = O(\sin^2\theta), gφφ=O(sin2θ)g_{\varphi\varphi} = O(\sin^2\theta)), the same finite limits the current Vθ,VφV_\theta, V_\varphi have. Note p1,p2,q2p_1, p_2, q_2 are bounded where Vφ=O(1/sinθ)V_\varphi = O(1/\sin\theta) is not.

Three scalar arrays replace the six components of Vθ,VφV_\theta, V_\varphi. The surface representation is unchanged: Xθ,XφX_\theta, X_\varphi still come componentwise from X^m\hat{X}^m_\ell via Algorithm 1.


4. The per-matvec algorithm#

Input {um}\{u^m_\ell\}; output {(ΔΓu)m}\{(\Delta_\Gamma u)^m_\ell\}.

# Step Transforms
1 vmα+(1,m)u1m+α(+1,m)u+1mv^m_\ell \leftarrow \alpha^+(\ell-1,m)u^m_{\ell-1} + \alpha^-(\ell+1,m)u^m_{\ell+1}; AS(vm)A \leftarrow \mathcal{S}(v^m_\ell) S\mathcal{S}
2 BS(imum)B \leftarrow \mathcal{S}(im\,u^m_\ell) S\mathcal{S}
3 Pp1A+p2BP \leftarrow p_1 A + p_2 B, Q~p2A+q2B\tilde{Q} \leftarrow p_2 A + q_2 B — pointwise
4 P^A(P)\hat{P} \leftarrow \mathcal{A}(P), Q~^A(Q~)\hat{\tilde{Q}} \leftarrow \mathcal{A}(\tilde{Q}) 2 A\mathcal{A}
5 smα+(1,m)P^1m+α(+1,m)P^+1m+imQ~^ms^m_\ell \leftarrow \alpha^+(\ell-1,m)\hat{P}^m_{\ell-1} + \alpha^-(\ell+1,m)\hat{P}^m_{\ell+1} + im\,\hat{\tilde{Q}}^m_\ell
6 ΔΓurS(sm)\Delta_\Gamma u \leftarrow r \cdot \mathcal{S}(s^m_\ell) S\mathcal{S}
7 {(ΔΓu)m}A(ΔΓu)\{(\Delta_\Gamma u)^m_\ell\} \leftarrow \mathcal{A}(\Delta_\Gamma u); zero L2\ell \ge L-2 A\mathcal{A}

4 syntheses + 2 analyses = 6, versus 12.

Implemented variation (2026-08-05): the φ\varphi-flux never needs the Legendre basis#

Step 4's analysis of Q~\tilde{Q} exists only so step 5 can apply φ\partial_\varphi — but φ\partial_\varphi is diagonal in the Fourier index, so the implementation differentiates Q~\tilde{Q} on the grid instead: FFT each latitude row, multiply mode mm by imim (zeroing mL2m \ge L-2 to mirror the top-degree filter; the Fourier analysis stage truncates m>mmaxm > m_{\max} for free), inverse FFT. 5 Legendre transforms + one Legendre-free FFT derivative, versus 6. The caveat is that the grid route skips Q~\tilde Q's band projection in \ell; measured, this does not bite — the band-edge spectra are identical to the 6-transform route's (the mm mask and the final analysis's projection contain it), the Algorithm-4 A/B agreement is unchanged (3.6×1043.6\times10^{-4} after 20 steps at L=63L=63), and the step gets ~8% faster at L=255L=255 (~2% at L=127L=127, where transform batching had already amortized most of what this removes).


5. Numerical trade-off#

Both schemes contain two powers of sinθ\sin\theta division in total. What differs is placement.

The mechanism: with NL+1N \approx L+1 Gauss–Legendre nodes, 1x1=O(N2)1 - x_1 = O(N^{-2}) so sinθ1=O(N1)\sin\theta_1 = O(N^{-1}). Since s=Jsin2θΔΓus = J\sin^2\theta\,\Delta_\Gamma u is O(L2)O(L^{-2}) at the polar ring but O(1)O(1) over the bulk, and synthesis commits roundoff scaled by the field's global size at every node alike, multiplying by rL2r \sim L^2 recovers the signal and inflates the noise.

divisions placement grid-space relative error
Algorithm 4 sinθ\sin\theta, sinθ\sin\theta separated by A\mathcal{A} εL\varepsilon L
Six-transform sin2θ\sin^2\theta all at the end εL2\varepsilon L^2

It does reach the returned coefficients, and it matters. The suppression argument above is right as far as it goes — step 7's analysis knocks the spike down to εL1/2\varepsilon L^{1/2}, and this document originally concluded from that the extra power would be invisible inside the solve. It is not, and the reason is not about accuracy. Measured on the default ellipsoid at L=63L=63: starting from the exact uniform steady state, three Richardson iterations per step leave a standing coefficient-space perturbation 50×50\times the no-correction floor (1.4×1051.4\times10^{-5} vs 2.8×1072.8\times10^{-7}), against  ⁣1×\sim\!1\times for Algorithm 4. That perturbation is static, polar, and re-injected every step. In a Turing problem the pattern is seeded by whatever is largest in the unstable band, so a forcing four orders below the field selects the nucleation site: the run grows a spot at the pole, on every seed, regardless of the initial condition.

The fix is to keep rr off the round sphere. Write p1=1+δp1p_1 = 1 + \delta p_1, q2=1+δq2q_2 = 1 + \delta q_2 (p2p_2 is already zero on the sphere). The sphere's share of the divergence is the cancelling part, and it is known in closed form: sinθθA+φB=sin2θΔS2u\sin\theta\,\partial_\theta A + \partial_\varphi B = -\sin^2\theta\,\Delta_{S^2}u, and ΔS2\Delta_{S^2} is diagonal. So

ΔΓu=1JΔS2u  +  r(sinθθP+φQ~),P=δp1A+p2B,Q~=p2A+δq2B\Delta_\Gamma u = -\frac{1}{J}\,\Delta_{S^2}u \;+\; r\,(\sin\theta\,\partial_\theta P' + \partial_\varphi \tilde{Q}'), \qquad P' = \delta p_1 A + p_2 B, \quad \tilde{Q}' = p_2 A + \delta q_2 B

with 1/J=rsin2θ1/J = r\sin^2\theta bounded. Only the geometry deviation now meets the concentrated division. Cost: one extra synthesis per species per iteration for λu-\lambda u — 7 transforms, not 6 — which batches into the gradient's existing grouped call and measures at ~10% of a step, against 3×3\times for reverting to Algorithm 4. δp1\delta p_1 and δq2\delta q_2 must be formed in float64 at precompute time (src/geom/geometry.ts): on a near-sphere they are the small quantity, and subtracting 1 in float32 on device would lose them.

Measured against Algorithm 4 through a real run (relative L2L^2 of uu at t=8t=8, niter=6\texttt{niter}=6):

plain flux sphere-split
ellipsoid, L=63L=63 3.5×1043.5\times10^{-4} 8.9×1068.9\times10^{-6}
blob, L=63L=63 4.4×1044.4\times10^{-4} 8.5×1068.5\times10^{-6}
ellipsoid, L=127L=127 7.2×1037.2\times10^{-3} 6.0×1066.0\times10^{-6}

and the polar noise gain is asserted in test/fluxChecks.ts, which fails at 50×50\times on the unsplit form.

The residual εL2\varepsilon L^2 still applies to grid values of ΔΓu\Delta_\Gamma u consumed directly — a nonlinear reaction term, max-norm diagnostics, an adaptive error estimator — for the deviation part alone.

Algorithm 1 line 7 already divides by sin2θ\sin^2\theta, so the code is exposed to εL2\varepsilon L^2 today — just on the second-derivative path, which the Laplacian never touches.


5a. float32 / WebGPU#

Target is WebGPU, which is float32-only: ε=2246×108\varepsilon = 2^{-24} \approx 6\times10^{-8} (spacing 2231.2×1072^{-23} \approx 1.2\times10^{-7}). No float64 fallback exists on device. All estimates in §5 are linear in ε\varepsilon, so they scale directly:

εL\varepsilon\sqrt{L} (coeffs) εL\varepsilon L (Alg. 4 grid) εL2\varepsilon L^2 (new, grid)
L=64L=64 5×1075\times10^{-7} 4×1064\times10^{-6} 2×1042\times10^{-4}
L=128L=128 7×1077\times10^{-7} 8×1068\times10^{-6} 1×1031\times10^{-3}
L=256L=256 1×1061\times10^{-6} 1.5×1051.5\times10^{-5} 4×1034\times10^{-3}

Coefficient space is fine (~10610^{-6}), which is the floor a float32 iterative solve sits at anyway. Grid space is not: 0.1–0.4% relative on the polar rings at L128L\ge128. For a reaction–diffusion solver this matters only if grid-space ΔΓu\Delta_\Gamma u is consumed outside the matvec. If the IMEX splitting evaluates f(u)f(u) from uu on the grid (typical), it never is.

Two float32-specific arguments in favour of the new scheme:

Mitigations available without float64:


5b. Summation order in the Legendre transform#

This is orthogonal to the 12→6 change, applies equally to the current code, and in float32 is probably worth more than the transform-count reduction. Do it first and independently, so its effect can be measured on its own.

Why. Every εL\varepsilon L and εL2\varepsilon L^2 in §5 rides on the per-transform roundoff floor, and in float32 that floor is set by how the sums are accumulated, not by the mathematics. For each (m,θi)(m, \theta_i) the synthesis evaluates

um(θi)==mLumPˉm(cosθi),u^m(\theta_i) = \sum_{\ell=|m|}^{L} u^m_\ell\,\bar P^m_\ell(\cos\theta_i),

an O(L)O(L)-term sum. Error growth by accumulation strategy, for an NN-term sum:

Strategy Worst case Typical (random signs)
Sequential εN\varepsilon N εN\varepsilon\sqrt{N}
Pairwise / tree εlog2N\varepsilon\log_2 N εlog2N\varepsilon\sqrt{\log_2 N}
Kahan compensated ε\varepsilon (+ O(ε2N)O(\varepsilon^2 N)) ε\varepsilon

At L=256L=256 in float32 that is the difference between 1.5×105\sim1.5\times10^{-5} and 5×107\sim5\times10^{-7} per transform — more than an order of magnitude, for no change in operation count.

On GPU this may already be partly free. A workgroup tree reduction over \ell is pairwise summation. The failure mode is a serial for loop over \ell inside a single thread, which is the natural way to write the shader if each thread owns one (m,θi)(m,\theta_i) pair and is exactly the εN\varepsilon N row above. Check which shape the kernel has before assuming anything.

Where it applies.

Practical notes.

Measurement. Transform a band-limited field forward then back and compare to the input, in float32, sweeping L{64,128,256}L\in\{64,128,256\}. Sequential accumulation shows error growing roughly linearly in LL; pairwise shows near-flat growth. This isolates the transform floor from everything else in §7 and should be run before the validation gate there, since it sets the baseline that gate is measured against.

Measured (2026-08-04, Dawn/Metal, scripts/sht-accuracy.ts)#

The sweep was run and the summation-order changes tried. Outcome: withdrawn — the floor here is not summation-limited.

LL grid rel-L2L_2 roundtrip worst degree
63 64×128 3.4×1063.4\times10^{-6} =62\ell=62: 4.5×1064.5\times10^{-6}
127 128×256 4.7×1064.7\times10^{-6} =110\ell=110: 6.4×1066.4\times10^{-6}
255 256×512 1.1×1051.1\times10^{-5} =246\ell=246: 1.4×1051.4\times10^{-5}

6. Code changes#

Location Change
src/surface_gradient/partial_derivatives Expose S(vm)\mathcal{S}(v^m_\ell) pre-division (flag or separate entry point). Needed by both the precompute and step 1.
SurfaceDiffOperator._precompute_metric_quantities() Return p1, p2, q2, r instead of V_theta, V_phi, per §3.
src/surface_screened_laplacian::surface_screened_laplacian() Replace body with §4. Both for i in {x,y,z} loops disappear.
SurfaceDiffOperator._precompute_curvature(), Algorithm 3 Unchanged. Still needs Xθθ,Xθφ,XφφX_{\theta\theta}, X_{\theta\varphi}, X_{\varphi\varphi} and the full Algorithm 1.
src/timestepping::make_implicit_op(), Algorithm 5 Unchanged. Only what line 8 calls changes.
src/real_embedding.py Unchanged.

The deprecated SurfaceDiffOperator methods for ΔΓ\Delta_\Gamma and (I+cΔΓ)(I + c\Delta_\Gamma) are the natural place to keep the old path as a reference implementation for the validation below.


Correction: precompute conditioning#

An earlier draft claimed the polar relative error in g~θθ\tilde g_{\theta\theta} is εL2\varepsilon L^2, making float64 precompute essential. That was wrong by a factor of LL, in the safe direction. g~θθ\tilde g_{\theta\theta} is not synthesized directly; it is the square of sinθXθ\sin\theta\,X_\theta, which is synthesized, is O(sinθ)O(\sin\theta) at the poles, and carries absolute error ε\varepsilon — so relative error εL\varepsilon L, preserved (up to a factor 2) by squaring. Same for gφφg_{\varphi\varphi} and g~θφ\tilde g_{\theta\varphi}. The determinant combination is O(sin4θ)O(\sin^4\theta) and so are both of its terms, so there is no extra cancellation generically; JJ inherits εL\sim\varepsilon L, i.e. 105\sim10^{-5} at L=128L=128 in float32. Acceptable.

Caveat: this assumes the difference is not small compared to its terms, which fails if XθX_\theta becomes nearly parallel to XφX_\varphi (near-degenerate parametrization). Worth a runtime check on detg\det g if the surface can deform that far.

Precompute error is also a fixed perturbation, identical every matvec, so it perturbs which operator is being solved but injects no noise into the Krylov space — GMRES converges normally.


7. Validation, in order#

At ε=6×108\varepsilon=6\times10^{-8} there is no margin for the εL\varepsilon\sqrt{L} suppression estimate in §5 to be off by an order of magnitude. Step 2 is a gate, not a confirmation.

  1. Smoothness check (do this first). On a deformed, non-axisymmetric surface, form PP and Q~\tilde{Q} on the grid and compare their SH coefficient decay against (Γu)x(\nabla_\Gamma u)_x from the current code. Matching tails confirm both are genuinely smooth on S2S^2. If this fails, nothing else is worth doing. Run this in float64 on CPU — it is a mathematical check, not a precision one.
  2. Coefficient-space diff in float32 at production LL, against a float64 CPU reference implementation of Algorithm 4. Landing near 10610^{-6} means the suppression argument holds. Landing near 10410^{-4} means the polar spike is surviving the analysis and the polar rings need separate handling.
  3. Sweep L{64,128,256}L \in \{64,128,256\} and fit the growth exponent of (2). Flat-ish confirms suppression; growth like L2L^2 means it is not working.
  4. Grid-space max-norm diff near the poles. Expect the extra power of LL here. If only this grows and (2) stays flat, the scheme is fine for use inside the implicit solve.
  5. GMRES iteration count and final achieved residual, float32, versus the current code. The operator is the same, so iterations should be unchanged; a stall above tolerance that does not occur in float64 indicates the matvec noise floor is binding.

8. Suggestions considered and withdrawn#

Steps 1–2 and 4–5 together are a vector/spin-weighted spherical harmonic transform. If SHTns (spat_to_SHsphtor, SHsphtor_to_spat) or SPHEREPACK (gradgs, divgs) can be linked, the gradient and divergence each become a single library call, the pole divisions are handled internally, and the hand-rolled α±\alpha^\pm recurrences on this path are no longer needed.

10. Beyond transform count#

The other lever is iteration count rather than cost per iteration. Since M1AM^{-1}A approaches multiplication by 1/J1/J at high \ell, folding a mean or smoothed JJ into the preconditioner could reduce GMRES iterations by more than any of the above reduces transforms.

Measured (2026-08-05, Richardson iteration, fp32)#

Implemented, with two corrections the measurements forced.

The right constant is the minimax over the symbol, not over JJ. The high-\ell per-mode factor is governed by the full principal symbol: in the orthonormal frame the symbol matrix is S=(1/J)(p1p2p2q2)S = (1/J)\begin{pmatrix} p_1 & p_2 \\ p_2 & q_2\end{pmatrix}, whose eigenvalues μ(x)\mu(x) are the inverse squared principal stretches — direction matters. Preconditioning with λ/J^\lambda/\hat J contracts every mode and direction iff J^μ(0,2)\hat J\mu \in (0,2), so

J^=2/(μmin+μmax),rate=(μmaxμmin)/(μmax+μmin)<1.\hat J = 2/(\mu_{\min} + \mu_{\max}), \qquad \text{rate} = (\mu_{\max}-\mu_{\min})/(\mu_{\max}+\mu_{\min}) < 1.

The det-based mean of JJ (this section's original suggestion; μ\mu's geometric mean, exact only for conformal surfaces) is insufficient: on the shipped ellipsoid it leaves directional high-degree bands with amplification >1> 1 — patterns went qualitatively high-frequency at moderate settings and diverged as niter or LL grew. With the symbol-based constant every niter/geometry combination in the test sweep converges (peanut: μ[0.44,6.2]\mu \in [0.44, 6.2], plain rate 5.2, preconditioned rate 0.87).

The correction must be band-projected. Algorithm 5's "zero L2\ell \ge L-2" is load-bearing: without applying the same mask to the correction dΔud\Delta u, the top two degrees iterate toward the undiffused right-hand side — each Richardson iteration strips more of their implicit diffusion, at species-dependent rates, manufacturing a spurious Turing band at the band edge (observed on the round sphere: top-degree energy growing 3%\sim 3\%/step at L=127L=127, 8 iterations).

Payoff shape: on mildly deformed surfaces one iteration already reaches the 104\sim10^{-4} fp32 accumulation floor, so iteration counts do not drop — the speculation above does not hold at fp32. The gain is reach and correctness: stiff geometries and high niter/LL combinations that previously diverged (or silently shifted the pattern's wavelength) now converge with a resolution-independent spectrum.