Cross-spectra & flux by scale

The co-spectrum Co_{fg}(k) = Re S_{fg}(k) distributes a covariance such as the momentum flux ⟨u'w'⟩ across scales — a staple of boundary-layer and turbulence analysis. Here two correlated fields share a common large-scale mode plus independent small-scale structure.

using FlowFieldSpectra: FlowFieldSpectra as FFS
using FFTW: FFTW                          # activates the FFTBackend extension

L = 2π
N = 64
dx = L / N
xs = range(0.0, stop = L - dx, length = N)
xv = vec([x for x in xs, y in xs])
yv = vec([y for x in xs, y in xs])

u = @. cos(2xv) + 0.5 * sin(6yv)             # shared mode at k≈2, own structure at k≈6
w = @. cos(2xv) - 0.4 * cos(9xv)             # shared mode at k≈2, own structure at k≈9

grid = FFS.UniformCartesianGrid((xv, yv); domain_size = (L, L))
cu, ks = FFS.calculate_spectrum(grid, (u,), (N, N); transform = FFS.FFTBackend())
cw, _ = FFS.calculate_spectrum(grid, (w,), (N, N); transform = FFS.FFTBackend())

k, Co = FFS.cospectrum(ks, cu, cw; num_bins = 24)

The co-spectrum peaks at the shared scale (k ≈ 2) and is near zero where the two fields have independent structure.

Welch averaging, coherence & phase

A single realization gives a noisy estimate and no meaningful coherence. Averaging over an ensemble of realizations (the trailing axis of the coefficient array) reduces variance and lets us estimate the magnitude-squared coherence γ²(k) ∈ [0, 1] and the phase between the two fields.

To recover a meaningful phase we use complex (rotary) signals — e.g. a horizontal velocity u + iv. A complex field has spectral content at +k only, so the cross-spectrum phase survives the radial binning; for a pair of real fields the ±k modes are complex conjugates and the binned phase cancels to zero. Each realization here shares a rotating mode at k ≈ 2 with a fixed phase lead ϕ, plus independent structure elsewhere.

using Random: Random
Random.seed!(1)

nreal = 32
ϕ = 0.7                                        # fixed phase lead of g over f at the shared mode
Cf = zeros(ComplexF64, N, N, nreal)
Cg = zeros(ComplexF64, N, N, nreal)
for r in 1:nreal
    a = 1.0 + 0.1 * randn()                    # shared-mode amplitude jitter
    fr = @. a * exp(im * 2 * xv) + 0.5 * exp(im * (5 * xv) + im * 2π * rand())
    gr = @. a * exp(im * (2 * xv - ϕ)) + 0.5 * exp(im * (7 * yv) + im * 2π * rand())
    cfr, _ = FFS.calculate_spectrum(grid, (fr,), (N, N); transform = FFS.FFTBackend())
    cgr, _ = FFS.calculate_spectrum(grid, (gr,), (N, N); transform = FFS.FFTBackend())
    Cf[:, :, r] .= cfr[:, :, 1]
    Cg[:, :, r] .= cgr[:, :, 1]
end

kw, Ef = FFS.welch_power_spectrum(ks, Cf; num_bins = 24)
kc, γ², phase = FFS.coherence_spectrum(ks, Cf, Cg; num_bins = 24)
# Phase is only meaningful where coherence is appreciable; mask the rest.
phase_plot = [γ²[i] > 0.3 ? phase[i] / π : NaN for i in eachindex(phase)]

Welch power spectrum alongside coherence² and phase between the two fields

Coherence is high only at the shared scale k ≈ 2, where the recovered phase matches the imposed lead ϕ (dashed line); elsewhere the independent structure drives coherence toward zero (and the phase, masked here, is meaningless).