Pipeline
CoarseGrainingEnergyFluxes.Pipeline.CoarseGrainBatchResult — Type
CoarseGrainBatchResult(grid, Nscales, batch_size)Batched result storage plus the per-slice CoarseGrainResult views a batch sweep writes into.
The batch axes are trailing, which is what makes each slice's Π a contiguous (spatial..., Nscales) view: fixing every trailing index of a column-major array selects a contiguous slab. So slices[i] is a genuine CoarseGrainResult aliasing this storage, and coarse_grain! fills it with no copy and no shape special-casing.
batch_size may hold any number of axes — (Nt,) for a time batch, (Nlevels,) for a vertical one, (Nlevels, Nt) for both.
CoarseGrainingEnergyFluxes.Pipeline.CoarseGrainResult — Type
CoarseGrainResult(scales, Π, cumulative_energy, wavenumber, filtering_spectrum)Container for results of a complete coarse-graining multiscale analysis. Every field's container type is a type parameter, inferred by the constructor above, so nothing here is stored behind an abstract annotation.
Fields
scales::AbstractVector{T}: filter scales ℓ in metersΠ::A: energy-flux maps stacked into ONE contiguous(spatial dims..., Nscales)array (W/m³) — not aVectorof separately-allocated per-scale matrices, so the whole sweep is a single allocation and each scale's map is a zero-copy view (compute_Π!writes directly into its slice).cumulative_energy::AbstractArray{T}: cumulative coarse specific KE ½⟨|ū_ℓ|²⟩ per scale (Sadek–Aluie Eq.- — a
Vector(per scale) forcoarse_grain, or a(Nlevels, Nscales)Matrixfor
coarse_grain_profile(per vertical level AND scale — deliberately not summed across levels, since that would need volume/thickness weighting this function doesn't have).- — a
wavenumber::AbstractVector{T}: filtering wavenumberk_ℓ = L/ℓper scale (level-independent)filtering_spectrum::AbstractArray{T}: filtering spectral densityẼ(k_ℓ)per scale (Eq. 14), same shape convention ascumulative_energy
Examples
res = coarse_grain(u, v, grid; scales=[10e3, 20e3, 30e3], kernel=TopHatKernel())
# Access results:
res.scales[1] # First scale (10 km)
res.Π[:, :, 1] # Energy-flux map at 10 km (a view; use `@view` to avoid copying)
res.cumulative_energy[1] # cumulative coarse KE at 10 km
res.filtering_spectrum[1] # filtering spectral density at k_ℓ = res.wavenumber[1]CoarseGrainingEnergyFluxes.Pipeline.SetupReport — Type
SetupReportWhat check_setup found. Printed as a readable report, and every field is also readable programmatically so a script can gate on it.
Fields
grid_kind::String,grid_size::Tuple,spacing::Tuple: the grid, and the SMALLEST spacing per axis (the one that sets stencil widths)scale::Float64,cells_per_scale::Tuple:ℓ, andℓ / Δxper axiskernel::String,mask_strategy::String,method::Stringbackend_requested::String,backend_resolved::String: what was asked for, and what will runengine::String: which real-space engine class the filter will takeresolvable::Bool:ℓis wide enough to mean anything on this grid (> 2Δxon every axis)supports_flux::Bool: the kernel is non-negative, soτis realizable andΠis a pointwise transfersupports_spectrum::Bool:Kernels.transfer_monotone— whether this kernel's spectral density is guaranteed non-negative, and so whether the defaultDiagnostics.StrictSpectrumpolicy will accept it.falsedoes not mean unreachable:Diagnostics.ForceSpectrum()computes it regardless.supports_spectral_method::Bool: the kernel has an isotropic transfer function, somethod = Spectral()is availableboundary_buffer_cells::Tuple: how many cells in from a coast or domain edge are contaminated — the kernel radius in cells. Results inside this band are not trustworthy under any mask strategy.notes::Vector{String}: everything above that needs acting on, in words
CoarseGrainingEnergyFluxes.Pipeline.batch_alloc_shared — Method
batch_alloc_shared(T, dims...) -> AbstractArray{T}Zero-filled shared-memory storage for a batch result, so worker processes write where the caller can see them. Needs using Distributed; pass as CoarseGrainBatchResult(...; alloc = batch_alloc_shared).
CoarseGrainingEnergyFluxes.Pipeline.batch_concurrency — Method
batch_concurrency(ctx, nslices) -> IntHow many slices a batch may run at once. A worker indexes its scratch by chunk, so supplying shorter pools than nthreads() deliberately caps concurrency rather than aliasing scratch between workers.
CoarseGrainingEnergyFluxes.Pipeline.check_setup — Method
check_setup(grid, kernel, scale; backend, mask_strategy, method) -> SetupReportReport what a filter/flux call on this (grid, kernel, scale) will actually do, and what it can support, without running or even planning it. Cheap: it consults the same dispatch predicates the builders use rather than constructing a plan, so it is safe on a configuration whose plan would be enormous.
It answers the questions that otherwise need reading src/:
- which real-space engine will run, and which backend the request resolves to;
- whether
ℓis meaningful on this grid at all (ℓ > 2Δx), and how many cells it spans; - whether this kernel can carry
Π(is it non-negative?) and a filtering spectrum (is|Ĝ|²monotone?), and whethermethod = Spectral()exists for it; - how wide the contaminated band along a coast or domain edge is.
Examples
julia> CGEF.check_setup(grid, CGEF.TopHatKernel(), 5000.0)CoarseGrainingEnergyFluxes.Pipeline.coarse_grain! — Method
coarse_grain!(result, u, v, w, grid; scales, kernel, workspace, filter_plans, deriv_plan, backend, mask_strategy, method, L, spectrum)
coarse_grain!(result, u, v, grid; scales, ...) # 2D convenience wrapperIn-place coarse_grain: refills an existing CoarseGrainResult's buffers — scales, the stacked Π array, cumulative energy, wavenumber, spectrum — instead of allocating fresh ones. Supplying workspace and filter_plans reuses the scratch arrays and the per-scale plans too, which is the zero-reallocation entry point for a sweep repeated across timesteps.
result must already be sized for length(scales) scales over grid's shape; a mismatch throws DimensionMismatch.
One driver for every grid architecture: the scale loop, the plan reuse and the spectrum are the same for all of them. Only the derivative plan and the trailing dimension of result.Π vary.
CoarseGrainingEnergyFluxes.Pipeline.coarse_grain — Method
coarse_grain(u, v, w, grid; scales, kernel=TopHatKernel(), backend=AutoBackend(), mask_strategy=ZeroFill(), method=nothing, L=1, spectrum=StrictSpectrum())
coarse_grain(u, v, grid; scales, ...) # 2D convenience wrapperPerform complete coarse-graining analysis across multiple filter scales, allocating a fresh CoarseGrainResult and workspace. This is a thin wrapper around coarse_grain!; for repeated sweeps over the same grid/scales (e.g. successive timesteps), allocate the result once and call coarse_grain! directly to reuse its buffers.
Arguments
u::AbstractMatrix: Eastward/zonal velocity component (m/s)v::AbstractMatrix: Northward/meridional velocity component (m/s)w::Union{Nothing,AbstractMatrix}: Vertical velocity (nothing for 2D)grid::StructuredGrid: Grid geometry and active-cell mask
Keyword Arguments
scales::AbstractVector: Vector of filter scales ℓ in meters (e.g.,10e3:10e3:100e3)kernel::AbstractFilterKernel=TopHatKernel(): Filter kernelbackend::AbstractExecutionBackend=AutoBackend(): Execution backendmask_strategy::AbstractMaskStrategy=ZeroFill(): Masking strategy (ZeroFill()orDeformable()).ZeroFillis the default because it keeps the kernel position-independent, so filtering commutes with spatial derivatives — the property the flux budget is derived by. SeeFiltering.filter_field!for the boundary artifacts of both choices.method::Union{Nothing,AbstractFilterMethod}=nothing: filtering engine;nothingtakesplan_filter's per-grid default (real space where a grid has that engine)L::Real=1: reference length setting the wavenumber normalizationk_ℓ = L/ℓ. The choice rescales the spectral density by1/L— seeDiagnostics.filtering_spectrum.spectrum::AbstractSpectrumPolicy=Diagnostics.StrictSpectrum(): how to fillfiltering_spectrum. The spectral density is guaranteed non-negative only for a kernel whose|Ĝ|²is monotone decreasing (Kernels.transfer_monotone), which the defaultTopHatKernelis not — socoarse_grain(u, v, grid; scales)throws under the default policy. The alternatives areDiagnostics.ForceSpectrum()(compute it anyway, warning once — the condition is sufficient, not necessary),Diagnostics.NoSpectrum()(fillNaN), orkernel = GaussianKernel().Πandcumulative_energyare unaffected under every policy, since neither depends on that condition.
Returns
CoarseGrainResult: Container with scales, Π maps, and spectrum
Examples
geom = SphericalGeometry(6371000.0)
grid = StructuredGrid(geom, lon_rad, lat_rad, mask)
scales = collect(10e3:10e3:100e3)
res = coarse_grain(u, v, grid; scales=scales, kernel=TopHatKernel())
plot(res.wavenumber, res.filtering_spectrum, xscale=:log10, yscale=:log10)
heatmap(res.Π[:, :, 3]) # 3rd scale is 30 kmCoarseGrainingEnergyFluxes.Pipeline.coarse_grain_batch! — Method
coarse_grain_batch!(batch, u, v, w, grid; scales, kernel, workspaces, filter_plans, deriv_plans, backend, mask_strategy, method, L, spectrum) -> batch
coarse_grain_batch!(batch, u, v, grid; scales, ...) # no vertical componentRun a full coarse_grain! sweep per slice over a batch that shares one grid.
The grid fixes the spatial rank; every trailing dimension of u, v and w beyond it is a batch axis. So against a 2D grid, (x, y, t) is a time batch and (x, y, z, t) is a level and time batch — Nlevels·Nt independent slices on one parallel axis, with no reshape. Against a true-3D grid the same (x, y, z) array batches nothing, because the grid claims all three axes. Slices are independent, so this is the outermost race-free axis and the one that turns thread count into throughput: threading inside one sweep is bounded by the fraction of it that is filtering, while this axis scales with the batch.
Each slice runs serially inside, as in Filtering.filter_slices!: nesting a threaded sweep under a threaded batch loop would have both levels claim the whole pool.
workspaces, filter_plans and deriv_plans are scratch pooled per worker, not per slice — pass vectors of length ≥ the concurrency actually used (Threads.nthreads() is always enough) and a repeated batch is allocation-free. Leaving them nothing builds one set per worker.
For a batch whose slices do NOT share a grid, use coarse_grain_slices! instead.
The spectrum policy behaves exactly as in coarse_grain: the default TopHatKernel cannot produce a guaranteed non-negative spectral density, so pass kernel = GaussianKernel(), or spectrum = Diagnostics.ForceSpectrum() to compute it anyway, or spectrum = Diagnostics.NoSpectrum().
CoarseGrainingEnergyFluxes.Pipeline.coarse_grain_batch_slice! — Method
coarse_grain_batch_slice!(batch, u, v, w, grid, Val(R), ctx, t, p) -> batch.slices[t]Slice t's sweep, drawing scratch from pool entry p and forcing the inner backend serial. Shared by the serial loop and every parallel driver so all of them run identical code.
CoarseGrainingEnergyFluxes.Pipeline.coarse_grain_profile! — Method
coarse_grain_profile!(batch, u, v, w, grid; scales, kernel, workspaces, filter_plans, deriv_plans, backend, mask_strategy, method, L, spectrum) -> batch
coarse_grain_profile!(batch, u, v, grid; scales, ...) # no vertical componentVertical-profile sweep, in place: the literature-standard independent-per-level 2D/2.5D method (see Diagnostics.compute_Π!'s thin-layer/QG regime note), filtering horizontally and treating each vertical level as its own 2D problem on the shared horizontal grid.
That is exactly a shared-grid batch whose axis happens to be the vertical one, so this delegates to coarse_grain_batch! rather than reimplementing the level loop. Two consequences worth knowing: the level axis is parallel on every backend the batch entry point supports, and E(ℓ) is read out of the workspace in the same pass as the flux, so u and v are not filtered a second time per level.
batch is a CoarseGrainBatchResult with batch size (Nlevels,), so Π is (Nx, Ny, Nscales, Nlevels) and cumulative_energy/filtering_spectrum are (Nscales, Nlevels) — the level axis trailing, which is what makes each level's result a contiguous view. Per-level energies are deliberately not summed across levels; that needs thickness weighting this function is not given.
The spectrum policy behaves exactly as in coarse_grain: the default TopHatKernel cannot produce a guaranteed non-negative spectral density, so pass kernel = GaussianKernel(), or spectrum = Diagnostics.ForceSpectrum() to compute it anyway, or spectrum = Diagnostics.NoSpectrum().
CoarseGrainingEnergyFluxes.Pipeline.coarse_grain_profile — Method
coarse_grain_profile(u, v, w, grid; scales, kernel=TopHatKernel(), backend=AutoBackend(), mask_strategy=ZeroFill(), method=nothing, L=1, spectrum=StrictSpectrum())Allocating coarse_grain_profile!: sizes a CoarseGrainBatchResult for size(u, 3) levels and fills it. Pass a prebuilt batch to coarse_grain_profile! to sweep timesteps without reallocating the result, which for a (Nx, Ny, Nlevels, Nscales) flux array is the dominant allocation.
CoarseGrainingEnergyFluxes.Pipeline.coarse_grain_slice_serial! — Method
coarse_grain_slice_serial!(results, us, vs, ws, grids, ctx, t) -> results[t]Slice t's sweep with the inner backend forced serial. Shared by the serial loop and every parallel driver so all of them run identical code.
CoarseGrainingEnergyFluxes.Pipeline.coarse_grain_slices! — Method
coarse_grain_slices!(results, us, vs, ws, grids; scales, kernel, workspaces, filter_plans, deriv_plans, backend, mask_strategy, method, L, spectrum) -> results
coarse_grain_slices!(results, us, vs, grids; scales, ...) # no vertical componentRun a full coarse_grain! sweep per slice over a batch whose slices each carry their own grid — different regions, different point counts, a separate node set per slice.
Use coarse_grain_batch! when every slice shares one grid: it stores the batch on trailing array axes and slices it with contiguous views, which this form cannot do because the spatial shapes differ. results is correspondingly a vector of independent CoarseGrainResults rather than views into shared storage.
Slices are dispatched longest-first under a dynamic scheduler (see slice_pipeline_costs). Unlike coarse_grain_batch!, where one shared grid makes every slice cost the same, ragged counts mean equal chunking would leave one worker holding the largest slice after the others have drained.
Scratch is per slice here, not per worker: a workspace and a filter plan are grid-shaped, so they cannot be reused across slices of different shape. Where many slices do share a shape, group them and call this once per group to keep pool memory bounded by thread count instead of batch length.
Each slice runs serially inside, the same non-nesting rule as coarse_grain_batch!.
The spectrum policy behaves exactly as in coarse_grain: the default TopHatKernel cannot produce a guaranteed non-negative spectral density, so pass kernel = GaussianKernel(), or spectrum = Diagnostics.ForceSpectrum() to compute it anyway, or spectrum = Diagnostics.NoSpectrum().
CoarseGrainingEnergyFluxes.Pipeline.slice_pipeline_costs — Method
slice_pipeline_costs(grids, Nscales) -> Vector{Int}Relative per-slice sweep cost, for longest-first scheduling of a ragged batch. Sweep cost grows at least linearly in a slice's point count and every scale repeats the sweep, so points × scales orders the slices correctly even though it is not an absolute time.
Visualization
CoarseGrainingEnergyFluxes.Visualization.plot_spectrum — Function
plot_spectrum(res; which=:density) -> FigurePlot the filtering spectrum from a CoarseGrainResult. which = :density plots the filtering spectral density Ẽ(k_ℓ) against filtering wavenumber k_ℓ (log x); which = :cumulative plots the cumulative coarse KE against scale ℓ (log–log). Provided by the CairoMakie package extension — run using CairoMakie to enable it.
CoarseGrainingEnergyFluxes.Visualization.plot_Π_map — Function
plot_Π_map(res, scale_idx, grid; colormap=:balance, title=nothing) -> FigureHeatmap of the cross-scale energy-flux map Π from a CoarseGrainResult at scale index scale_idx. Provided by the CairoMakie package extension — run using CairoMakie to enable it.