Internals
Not part of the interface and not subject to its stability. Documented because a reader following the operator construction or the solver dispatch will meet them, and because a manual that omits half of what a module defines is harder to trust than one that marks the boundary explicitly.
Spectral result types
The physical fields come back in a HelmholtzResult; these carry the spectral path's own intermediate form.
HelmholtzDecomposition.AbstractSpectralHelmholtzResult — Type
AbstractSpectralHelmholtzResult{T}Abstract supertype for spectral decomposition results.
HelmholtzDecomposition.SpectralCartesianResult — Type
SpectralCartesianResult{T,A}Cartesian spectral decomposition result holding the component-last complex Fourier coefficients of the rotational, divergent and harmonic velocity ((kdims..., N)).
u_harm carries the k = 0 mode. On a periodic domain the constant fields are exactly the harmonic subspace — H¹(Tᴺ) has dimension N, and a constant field is both curl-free and divergence-free — so the mean flow belongs to neither of the other two parts.
HelmholtzDecomposition.SpectralSphericalResult — Type
SpectralSphericalResult{T,V}Spherical spectral decomposition result holding the spherical-harmonic coefficients of the streamfunction ψ and velocity potential χ, up to degree lmax.
HelmholtzDecomposition.build_cartesian_result — Function
build_cartesian_result(grid, U, velocity_hat, ks, inverse) -> HelmholtzResultAssemble a physical HelmholtzResult from a component-last spectral velocity array. inverse(spectral_scalar) maps one spectral scalar field ((kdims...)) back to a physical one ((dims...)). Used by the regular-grid spectral extensions (FFTW).
N + P + 2 inverse transforms are taken, not 2N + 2P + 2:
u_rotisU − u_div − u_harmin physical space, so it costs nothing;u_harmis thek = 0mode, i.e. a constant field, and is the component mean — no transform;- the rotation tensor
W = ΔRis not materialised at all. It wasPinverse transforms for a quantity recomputable fromR, and no longer has a field on the result.
On this path rotation_potential holds cell-centred arrays, where the finite-difference path holds corner-staggered ones. The field is type-parameterised for exactly this reason, but the staggering does depend on which path produced the result.
Solver plumbing
HelmholtzDecomposition.CGWorkspace — Type
CGWorkspace{A}The four vectors a conjugate-gradient iteration needs, held so the solve allocates nothing.
HelmholtzDecomposition.CGState — Type
CGState{W,P}Everything CGSolver reuses across solves on one (grid, boundary): its vectors and its preconditioner.
The vectors belong here and not in a default argument. helmholtz_decompose! solves P + 1 right-hand sides and a batch multiplies that by the field count, so defaulting them allocates four grid arrays per solve for the lifetime of the program.
HelmholtzDecomposition._precondition! — Function
_precondition!(z, r, grid, c, preconditioner)Apply M⁻¹ to the residual: the Jacobi diagonal when there is no preconditioner, a V-cycle when there is (Multigrid.jl adds that method, where the type lives).
HelmholtzDecomposition._detect_singular — Function
_detect_singular(grid, c) -> BoolWhether the constants lie in L's null space, decided by applying L to the constant field and seeing whether anything comes back.
Asking the boundary condition instead is not enough, and the dual grid is why: its boundary ring of corners has no closed loop of cells around it, so those corners are masked out and every face of the active region is closed — the operator there is singular even under Dirichlet, which a condition-based test reports as nonsingular. Conjugate gradients on a singular system with no null-space projection then drifts along the null space instead of converging, which showed up as a harmonic fraction of 1.7e+32.
L·1 = 0 is exactly the property that matters and costs one operator application at plan time.
HelmholtzDecomposition._require_domain — Function
_require_domain(solver, grid)Throw unless grid is a domain solver can solve on. Reads the mask, so it is O(N) and is called once where a solver is chosen — never from solve_poisson!, which a decomposition enters P + 1 times per field and once more per field of a batch.
HelmholtzDecomposition._require_sampling — Function
_require_sampling(solver, grid)Extra, solver-specific demand on the node layout — a transform defined on one node set and no other says so here. A hook rather than an override of _require_domain: overriding that would silently drop the mask, uniformity and topology checks it also performs.
HelmholtzDecomposition._spectral_algorithms — Function
_spectral_algorithms(geometry_type) -> TupleThe algorithms worth trying on a geometry, in preference order: the uniform transform first, then the non-uniform one, which subsumes it at greater cost. A candidate that does not apply refuses itself through its own capability check, so this order only decides between several that do.
HelmholtzDecomposition._spectral_dispatch — Function
_spectral_dispatch(u, grid; solver=AutoSolver(), kwargs...)Resolve a spectral solver (extensions register them; AutoSolver picks the best available) and dispatch to the extension's _decompose_spectral(solver, geometry, u, grid; …). Dispatching on the solver type lets several spectral backends (FFTW + FINUFFT, FSH + NUFSHT) coexist for the same geometry without method clashes. The CUDA extension overrides this for CuArray inputs to take the CUFFT path directly.
Grid walking
HelmholtzDecomposition.cell_above — Function
cell_above(grid, F, d) -> CartesianIndex or nothingThe cell on the high side of face F along d; nothing at the outer edge of a bounded direction. On a periodic direction every face has both.
HelmholtzDecomposition.cell_below — Function
cell_below(grid, F, d) -> CartesianIndex or nothingThe cell on the low side of face F along d; nothing at the outer edge of a bounded direction.
HelmholtzDecomposition.face_below — Function
face_below(I, d) / face_above(grid, I, d) -> CartesianIndexThe two faces of cell I normal to d, as indices into face array d. On a periodic direction the face above the last cell wraps to face 1.
HelmholtzDecomposition.geometric_face_area — Function
geometric_face_area(grid, F, d, T) -> TArea of face F normal to d: the product of the physical cell widths in every other direction, evaluated at the face. Purely geometric — whether flux crosses it is face_area's question.
Batch and backend plumbing
HelmholtzDecomposition.copy_slice! — Function
copy_slice!(dest, i, src, j) -> destCopy slice j of one batch into slot i of another, fields and diagnostics alike.
HelmholtzDecomposition._resolve_batch_backend — Function
_resolve_batch_backend(backend, fields) -> AbstractExecutionBackendResolve Auto over the batch axis against real capability: more than one field, more than one thread, and the threading extension actually loaded. A concrete backend passes through and is then honoured or refused — never quietly replaced.
HelmholtzDecomposition._extension_loaded — Function
_extension_loaded(name) -> BoolWhether one of this package's extensions is loaded. hasmethod cannot answer this — the dispatch stubs exist unconditionally, so it is always true.
HelmholtzDecomposition._unsupported_backend_message — Function
_unsupported_backend_message(backend, entry) -> StringWhy entry cannot honour backend, and what to load so it can. Reaching this is always an error: a backend named explicitly is executed as named or refused, never downgraded.
Spectral plumbing
HelmholtzDecomposition._grid_wavenumbers — Function
_grid_wavenumbers(velocity_hat, grid) -> NTuple{N}Reconstruct the per-axis angular wavenumber vectors for a component-last spectral array on a Cartesian grid. Axis 1 is treated as an rfft axis when its spectral length equals N₁÷2 + 1, otherwise as a full fft axis.
HelmholtzDecomposition._stack_components — Function
_stack_components(grid, comps...) -> ArrayStack scalar component arrays into the component-last layout (dims..., M) the transforms take.