CoarseGrainingEnergyFluxes.jl

Spatial coarse-graining analysis of energy fluxes in geophysical fluid dynamics.

Overview

This package implements the coarse-graining (spatial filtering) framework. The core quantity is the cross-scale energy flux Π(x, ℓ), the local rate of kinetic-energy transfer across scale ℓ. Around it:

Function
Cumulative coarse energy and the filtering spectral density Ẽ(k_ℓ)Diagnostics.cumulative_energy, Diagnostics.filtering_spectrum
Strain/convergence split of ΠDiagnostics.compute_Π_strain_convergence
Rotational/divergent (Helmholtz) split, with the interaction channelDiagnostics.compute_Π_decomposed
Leonard/Cross/Reynolds stress decompositionDiagnostics.tau_decomposition
Tracer / buoyancy-variance fluxDiagnostics.tracer_variance_flux
Enstrophy flux, in the same gauge as ΠDiagnostics.enstrophy_flux
Energy per scale bandDiagnostics.band_energies
Variable-density (Favre) budget: Π, baropycnal work Λ, pressure dilatationDiagnostics.compressible_flux

Six filter kernels are available — top-hat, Gaussian, sharp-spectral, smooth-hat, hyper-Gaussian and the high-order M^I/M^II pair — and they are not interchangeable: only some have a spectral transfer function, only some can carry a filtering spectrum, and only some are valid for Π. check_setup reports which, for your grid and scale, without running anything.

The approach follows Aluie (2011, 2019) and Aluie, Hecht, & Vallis (2018), using real-space convolution kernels to separate large-scale (ū) and sub-scale (u') motions at each point in space.

RealSpace() names that operator: a local space average against the compact kernel. Which engine evaluates it is chosen from the grid and kernel — exact prefix sums for a top-hat in 2-D and 3-D, separable passes for the factorizing kernels, a vectorized banded footprint otherwise — and check_setup reports the choice and its cost before anything runs. Two further engines evaluate the same convolution by transform under method = AutoMethod(): a padded FFT of the sampled kernel on a uniform Cartesian lattice, and a transform along the longitude ring on a global sphere, where a great-circle kernel depends on the longitude difference alone. See Architecture for the full table.

Sweeping scales, the parts of a plan that the scale does not change — transform objects, measure prefix scans, point sorts — are built once by Filtering.plan_filter_sweep rather than once per scale, and every diagnostic has an in-place form whose workspace holds only the buffers its configuration can reach.

Every diagnostic works across the full grid×dimensionality matrix — StructuredGrid (1D, 2D, and true 3D Cartesian or spherical-volumetric), CurvilinearGrid (model-native orthogonal curvilinear meshes), and UnstructuredGrid (scattered points, via k-d tree neighbors, Voronoi cell areas, and non-uniform spectral transforms) — see Architecture for the full capability matrix.

Installation

using Pkg
Pkg.add(url="https://github.com/jbphyswx/CoarseGrainingEnergyFluxes.jl")

Key Concepts

Coarse-Graining vs Fourier Spectra

Traditional Fourier spectral analysis provides wavenumber spectra E(k) but:

  • Requires periodicity or windowing
  • Cannot localize energy transfer in physical space
  • Poorly suited to irregular domains and boundaries

Coarse-graining provides:

  • Local energy flux Π(x, ℓ) at every grid point
  • Works on arbitrary domains with masked (excluded) regions
  • No periodicity assumption
  • Direct physical-space interpretation

The Energy Flux Π

The cross-scale energy flux at position x and scale ℓ is:

Π(x, ℓ) = −τ_ℓ : S̄_ℓ

where:

  • S̄ℓ = ½(∇ūℓ + (∇ū_ℓ)ᵀ) is the filtered strain rate
  • τℓ = (u⊗u)̄ℓ − ūℓ⊗ūℓ is the sub-scale stress

When Π > 0, energy flows from large to small scales (forward cascade). When Π < 0, energy flows from small to large scales (inverse cascade).

The Filtering Spectrum

The cumulative coarse-grained kinetic energy (cumulative_energy; Sadek & Aluie 2018, Eq. 15) is the domain average of the filtered KE:

E(ℓ) = ½ ⟨|ū_ℓ|²⟩

This is a cumulative quantity, not a spectral density. The filtering spectral density (filtering_spectrum; their Eq. 14 — comparable to a Fourier energy spectrum) is its derivative with respect to the filtering wavenumber k_ℓ = L/ℓ:

Ẽ(k_ℓ) = d/dk_ℓ [ ½ ⟨|ū_ℓ|²⟩ ]

coarse_grain returns both (result.cumulative_energy, result.filtering_spectrum, result.wavenumber).

Ẽ(k_ℓ) ≥ 0 is guaranteed only for a kernel whose |Ĝ(k)|² is monotone decreasing, which the default TopHatKernel is not. By default coarse_grain therefore throws rather than return a density it cannot vouch for — but the condition is sufficient, not necessary, so all three readings are reachable: kernel = GaussianKernel() for a guaranteed-non-negative spectrum, spectrum = CGEF.Diagnostics.ForceSpectrum() to compute the top-hat's anyway and check its sign yourself, or spectrum = CGEF.Diagnostics.NoSpectrum() for Π and the cumulative energy alone. Note also that a p = 1 kernel — top-hat and Gaussian both — saturates the recovered slope at k⁻³; see Diagnostics.AbstractSpectrumPolicy and Diagnostics.filtering_spectrum.