Architecture

This document explains how FlowInvariantTransfer.jl is organized internally and how computations are dispatched.

Table of Contents

Module Organization

Core Module: FlowInvariantTransfer

The main module (src/FlowInvariantTransfer.jl) includes a set of submodules, each responsible for a distinct concern:

FlowInvariantTransfer (top-level)
├── Types              # All type definitions (methods, invariants, backends, results)
├── Utils              # Wavenumber grids, magnitude, dealiasing masks
├── Invariants         # Per-mode transfer density for each invariant
├── Decomposition      # Field decomposition stubs (Helmholtz etc.)
├── ShellBinning       # Shell edge/center/assignment logic
├── Filters            # Spectral filter responses and application
├── Workspaces         # Preallocated buffer structs for zero-alloc paths
├── NonlinearTerm      # Pseudospectral (u·∇)u computation
├── SpectralFlux       # Π(K) and T(k) accumulation
├── CoarseGrainingFlux # Wrapper for CoarseGrainingEnergyFluxes.jl
├── ShellToShellTransfer    # T(n,m) matrix computation
├── ModeToModeTransfer    # S(k|p|q) triad computation
└── TriadicOrthogonalDecomposition  # TOD (frequency-domain SVD)

Architecture Pattern: Method × Backend

FlowInvariantTransfer.jl uses a method–backend composition pattern:

Spectral data (û, ks)
    ↓
[Method Type] ← Specifies WHICH diagnostic to compute
    ↓
[Invariant Trait] ← Specifies WHICH quadratic invariant (KE/H/Ω)
    ↓
[Execution Backend] ← Specifies HOW to compute (Serial/FFT/GPU/...)
    ↓
Result Container ← Stores transfer spectra, matrices, modes

Example:

# Method: "Compute shell-to-shell transfer"
method = ShellToShellTransferMethod(LinearBinning(1.0))

# Invariant: "Accumulate enstrophy"
# Backend: "Use 4 threads"
result = calculate_energy_transfer(method, û, ks;
    invariant = Enstrophy(),
    backend = ThreadedBackend())

The method type determines what to calculate. The invariant trait determines which physical quantity to track. The backend type determines how to execute.


Type Hierarchy

Method Types

AbstractEnergyTransferMethod (abstract)
├── SpectralFluxMethod{B}
├── ShellToShellTransferMethod{B}
├── ModeToModeTransferMethod{B, I}
├── CoarseGrainingFluxMethod{F}
└── TriadicOrthogonalDecompositionMethod{N, O, M}

Invariant Traits

AbstractInvariant (abstract)
├── KineticEnergy       # E = ½∫|u|²  (default)
├── Helicity            # H = ∫u·ω    (3D only)
└── Enstrophy           # Ω = ½∫ω²   (2D only)

Field Decompositions

AbstractFieldDecomposition (abstract)
├── NoDecomposition              # Full velocity (default)
├── HelmholtzDecomposition       # Both rot + div
├── RotationalDecomposition      # Rot component only
└── DivergentDecomposition       # Div component only

Execution Backends

AbstractExecutionBackend (abstract)
├── SerialBackend           # Reference O(N²) implementation
├── FFTSpectralBackend              # O(N log N) via FFTW
├── ThreadedBackend         # OhMyThreads parallelism
├── DistributedBackend      # Distributed.jl + SharedArrays
├── GPUBackend{B}           # KernelAbstractions (parametric on device)
├── AutoBackend             # Auto-detect best available
├── NUFFTSpectralBackend            # Non-uniform FFT (FINUFFT)
├── FSHTSpectralBackend              # Regular spherical harmonics (FSH)
└── NUFSHTSpectralBackend           # Scattered spherical harmonics (NUFSHT)

Shell Binning

AbstractShellBinning (abstract)
├── LinearBinning(Δk)           # Uniform width Δk
├── LogarithmicBinning(k₀, λ)   # Geometric ratio λ
├── DyadicBinning(k₀)           # Log with λ=2
└── CustomBinning(edges)         # User-specified edges

Filters

AbstractFilter (abstract)
├── SharpSpectralFilter     # Brick-wall |k| < π/ℓ
├── GaussianFilter          # exp(-k²ℓ²/24)
└── TopHatFilter            # sinc(kℓ/2π)

Result Containers

SpectralFluxResult{V}                           # k_shells, T(k), Π(K)
ShellToShellResult{V, M, E}                     # T(n,m) matrix, net transfer
ModeToModeTriadResult{I, KS, A, S}              # net_transfer T(k), resolved S(k|p)
CoarseGrainingFluxResult{S, A}                  # Π_ℓ(x) field
CoarseGrainingFluxResultWithDiagnostics{S, A}   # + stress/strain tensors
TriadicOrthogonalDecompositionResult{V, A3, PM} # mode bispectrum, modes, energy budget

All result types are fully parametric on their array/scalar types — no hardcoded Vector{Float64}. This means they work natively with Float32, GPU arrays, AD dual numbers, and Unitful quantities.

Workspace Types

NonlinearTermWorkspace    # FFT plans, physical-space buffers, N̂
SpectralFluxWorkspace     # T_spec, flux accumulators
ShellToShellWorkspace     # Shell-filtered velocity, shell_idx

Workspaces own all temporaries for their respective computations. Preallocate once, reuse across timesteps — zero heap allocation in the hot path.


Dispatch Pattern

Unified Entry Point

calculate_energy_transfer(method, data, coords; kwargs...) dispatches on the method type:

calculate_energy_transfer(::SpectralFluxMethod, û, ks; ...) →
    calculate_spectral_flux(û, ks; binning=method.binning, ...)

calculate_energy_transfer(::ShellToShellTransferMethod, û, ks; ...) →
    calculate_shell_to_shell_transfer(û, ks; binning=method.binning, ...)

calculate_energy_transfer(::ModeToModeTransferMethod, û, ks; ...) →
    calculate_mode_to_mode_transfer(û, ks; binning=method.binning, ...)

Backend Dispatch (Two Levels)

Each diagnostic function dispatches on the backend keyword via an internal _calculate_*! method. The core module defines:

  1. SerialBackend — the reference implementation (always available)
  2. Stubs for other backends that throw informative errors

Extensions then override the stubs when their trigger packages are loaded:

# In core (src/ShellToShell/ShellToShellTransfer.jl):
function _calculate_shell_to_shell!(result, ws, û, ks, ::ThreadedBackend; ...)
    _shell_to_shell_threaded!(...)  # stub → throws helpful error
end

# In extension (ext/FlowInvariantTransferOhMyThreadsExt.jl):
function FIT.ShellToShellTransfer._shell_to_shell_threaded!(...)
    # Real OhMyThreads implementation
end

This ensures:

  • ✅ No runtime overhead choosing between backends (static dispatch)
  • ✅ Clear error messages when a backend's dependency isn't loaded
  • ✅ Each backend can use its optimal algorithm
  • ✅ Type-stable throughout

Extension System

Lazy Loading via Extensions

Optional dependencies are loaded only when needed via Julia's [weakdeps] + [extensions] mechanism in Project.toml:

[weakdeps]
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
OhMyThreads = "67456a42-1dca-4109-a031-0a68de7e3ad5"
KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
# ... 8 more

[extensions]
FlowInvariantTransferFFTWExt = "FFTW"
FlowInvariantTransferOhMyThreadsExt = "OhMyThreads"
FlowInvariantTransferDistributedExt = ["Distributed", "SharedArrays"]
# ... 8 more

Benefits:

  • Users who don't use GPU pay zero cost (no KernelAbstractions load time)
  • Fresh Julia sessions start fast (only LinearAlgebra + PrecompileTools in [deps])
  • Each extension is a self-contained module overriding specific stubs

Code Layout

Source Files

FilePurpose
src/FlowInvariantTransfer.jlTop-level module: includes, re-exports, unified entry point, precompilation
src/types.jlAll type definitions (methods, invariants, backends, results)
src/utils.jlWavenumber grids, magnitude, dealiasing masks, input validation
src/Invariants.jlPer-mode transfer density for KE, helicity, enstrophy
src/Decomposition.jlField decomposition dispatch (stubs for Helmholtz ext)
src/ShellToShell/ShellBinning.jlShell edge/center computation, assign_shells
src/Filters.jlFilter response functions and spectral application
src/Workspaces.jlPreallocated workspace structs
src/NonlinearTerm.jlPseudospectral (u·∇)u — Serial and FFT paths
src/SpectralFlux.jlSpectral flux Π(K) and transfer spectrum T(k)
src/CoarseGrainingFlux.jlWrapper stub for CGEF extension
src/ShellToShell/ShellToShellTransfer.jlShell-to-shell T(n,m) — Serial core
src/ScaleToScale/ModeToModeTransfer.jlMode-to-mode S(k|p|q) — Serial core
src/ScaleToScale/TriadicOrthogonalDecomposition/TOD implementation

Extension Files

FileTriggerOverrides
ext/FlowInvariantTransferFFTWExt.jlFFTWFFT-based nonlinear term, spectral flux, shell-to-shell
ext/FlowInvariantTransferOhMyThreadsExt.jlOhMyThreadsThreaded shell-to-shell and scale-to-scale
ext/FlowInvariantTransferDistributedExt.jlDistributed+SharedArraysDistributed shell-to-shell and scale-to-scale
ext/FlowInvariantTransferKernelAbstractionsExt.jlKernelAbstractionsGPU kernels for all transfer densities and triads
ext/FlowInvariantTransferCGEFExt.jlCoarseGrainingEnergyFluxesCoarse-graining flux computation
ext/FlowInvariantTransferHelmholtzDecompositionExt.jlHelmholtzDecompositionPhysical and spectral Helmholtz decomposition
ext/FlowInvariantTransferFINUFFTExt.jlFINUFFTNon-uniform FFT path
ext/FlowInvariantTransferNUFSHTExt.jlNUFSHTScattered spherical front-end
ext/FlowInvariantTransferFSHExt.jlFastSphericalHarmonicsRegular spherical front-end
ext/FlowInvariantTransferFlowFieldSpectraExt.jlFlowFieldSpectraSpectral analysis integration
ext/FlowInvariantTransferCairoMakieExt.jlCairoMakiePlotting recipes

Design Principles

  1. Type Dispatch — use Julia's type system for all method/backend/invariant selection. No string dispatch, no runtime branching. The compiler resolves everything statically.

  2. Zero-Cost Abstraction — backend dispatch adds no runtime overhead. Each backend is a singleton type resolved at compile time.

  3. !-First Design — every hot function has a mutating variant that writes into preallocated workspaces. The allocating convenience API calls the ! variant internally.

  4. Workspace-Based Allocation — all temporaries (FFT plans, physical-space buffers, shell indices) are owned by workspace structs. Preallocate once, reuse across timesteps.

  5. Parametric Result Types — result containers are parametric on array/scalar types. Float32, GPU arrays, AD dual numbers all work without specialization.

  6. Lean Core — only LinearAlgebra and PrecompileTools in hard dependencies. Everything else is a weak dependency loaded on demand via extensions.

  7. Invariant Traits — switching between kinetic energy, helicity, and enstrophy requires changing only one keyword argument. The algorithm structure is identical; only the per-mode inner product changes.