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.

source
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 a Vector of 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.
    1. — a Vector (per scale) for coarse_grain, or a (Nlevels, Nscales) Matrix for
    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).
  • wavenumber::AbstractVector{T}: filtering wavenumber k_ℓ = L/ℓ per scale (level-independent)
  • filtering_spectrum::AbstractArray{T}: filtering spectral density Ẽ(k_ℓ) per scale (Eq. 14), same shape convention as cumulative_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]
source
CoarseGrainingEnergyFluxes.Pipeline.SetupReport — Type
SetupReport

What 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 ℓ / Δx per axis
  • kernel::String, mask_strategy::String, method::String
  • backend_requested::String, backend_resolved::String: what was asked for, and what will run
  • engine::String: which real-space engine class the filter will take
  • resolvable::Bool: ℓ is wide enough to mean anything on this grid (> 2Δx on every axis)
  • supports_flux::Bool: the kernel is non-negative, so τ is realizable and Π is a pointwise transfer
  • supports_spectrum::Bool: Kernels.transfer_monotone — whether this kernel's spectral density is guaranteed non-negative, and so whether the default Diagnostics.StrictSpectrum policy will accept it. false does not mean unreachable: Diagnostics.ForceSpectrum() computes it regardless.
  • supports_spectral_method::Bool: the kernel has an isotropic transfer function, so method = Spectral() is available
  • boundary_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
source
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).

source
CoarseGrainingEnergyFluxes.Pipeline.batch_concurrency — Method
batch_concurrency(ctx, nslices) -> Int

How 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.

source
CoarseGrainingEnergyFluxes.Pipeline.check_setup — Method
check_setup(grid, kernel, scale; backend, mask_strategy, method) -> SetupReport

Report 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 whether method = 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)
source
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 wrapper

In-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.

source
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 wrapper

Perform 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 kernel
  • backend::AbstractExecutionBackend=AutoBackend(): Execution backend
  • mask_strategy::AbstractMaskStrategy=ZeroFill(): Masking strategy (ZeroFill() or Deformable()). ZeroFill is the default because it keeps the kernel position-independent, so filtering commutes with spatial derivatives — the property the flux budget is derived by. See Filtering.filter_field! for the boundary artifacts of both choices.
  • method::Union{Nothing,AbstractFilterMethod}=nothing: filtering engine; nothing takes plan_filter's per-grid default (real space where a grid has that engine)
  • L::Real=1: reference length setting the wavenumber normalization k_ℓ = L/ℓ. The choice rescales the spectral density by 1/L — see Diagnostics.filtering_spectrum.
  • spectrum::AbstractSpectrumPolicy=Diagnostics.StrictSpectrum(): how to fill filtering_spectrum. The spectral density is guaranteed non-negative only for a kernel whose |Ĝ|² is monotone decreasing (Kernels.transfer_monotone), which the default TopHatKernel is not — so coarse_grain(u, v, grid; scales) throws under the default policy. The alternatives are Diagnostics.ForceSpectrum() (compute it anyway, warning once — the condition is sufficient, not necessary), Diagnostics.NoSpectrum() (fill NaN), or kernel = GaussianKernel(). Π and cumulative_energy are 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 km
source
CoarseGrainingEnergyFluxes.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 component

Run 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().

source
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.

source
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 component

Vertical-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().

source
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.

source
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 component

Run 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().

source
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.

source

Visualization

CoarseGrainingEnergyFluxes.Visualization.plot_spectrum — Function
plot_spectrum(res; which=:density) -> Figure

Plot 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.

source
CoarseGrainingEnergyFluxes.Visualization.plot_Π_map — Function
plot_Π_map(res, scale_idx, grid; colormap=:balance, title=nothing) -> Figure

Heatmap 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.

source