Stochastic Forcing API

Types

Abstract Types

abstract type Forcing end
abstract type StochasticForcingType <: Forcing end
abstract type DeterministicForcingType <: Forcing end

StochasticForcing

Tarang.StochasticForcingType
StochasticForcing{T, N, A<:AbstractArray{T,N}, CA<:AbstractArray{Complex{T},N}}

Stochastic forcing in Fourier space with white-noise temporal correlation. Supports both CPU and GPU architectures.

Mathematical Properties

The forcing F̂(k,t) satisfies:

  • ⟨F̂(k,t)⟩ = 0 (zero mean)
  • ⟨F̂(k,t) F̂*(k',t')⟩ = Q̂(k) δ(k-k') δ(t-t')/(dt) (white noise)

Implementation

At each timestep, the forcing is computed as: F̂(k) = √(Q̂(k)) · ξ(k) / √(dt)

where ξ(k) is complex white noise with |ξ| = 1 and random phase.

Fields

  • forcing_spectrum::A: √Q̂(k) - square root of power spectrum
  • energy_injection_rate::T: Target energy injection rate ε
  • injection_metric::Symbol: Quadratic invariant used to normalize ε
  • k_forcing::T: Central forcing wavenumber k_f
  • dk_forcing::T: Forcing bandwidth δ_f
  • dt::T: Current timestep (for proper scaling)
  • domain_size::NTuple{N,T}: Domain size (Lx, Ly, ...)
  • field_size::NTuple{N,Int}: Grid size (Nx, Ny, ...)
  • wavenumbers::NTuple{N,Vector{T}}: Wavenumber arrays (kx, ky, ...)
  • cached_forcing::AbstractArray{Complex{T},N}: Cached forcing (constant within timestep)
  • prevsol::Union{Nothing,AbstractArray{Complex{T},N}}: Previous solution (for Stratonovich work)
  • rng::AbstractRNG: Random number generator (default: fresh MersenneTwister per instance for thread/parallel safety)
  • random_phases::AbstractArray{T,N}: Pre-allocated random phase buffer (on target architecture)
  • diagnostic_weights: Cached backend weights for the active GPU diagnostic layout
  • last_update_time::T: Time of last forcing update
  • spectrum_type::Symbol: Type of forcing spectrum
  • enforce_hermitian::Bool: Enforce Hermitian symmetry for real-valued fields
  • architecture::AbstractArchitecture: CPU() or GPU() architecture
source

Stochastic forcing in Fourier space with white-noise temporal correlation.

Type signature:

mutable struct StochasticForcing{
    T<:AbstractFloat, N,
    A<:AbstractArray{T, N},
    CA<:AbstractArray{Complex{T}, N}
} <: StochasticForcingType

A and CA are the concrete real and complex array types on the chosen architecture, so the same struct holds Arrays on the CPU and CuArrays on the GPU.

Fields:

FieldTypeDescription
forcing_spectrumA√Q̂(k) - amplitude spectrum
energy_injection_rateTTarget ε
injection_metricSymbol:direct or :vorticity_kinetic
k_forcingTCentral forcing wavenumber
dk_forcingTForcing bandwidth
dtTCurrent timestep
domain_sizeNTuple{N, T}Domain extent (Lx, Ly, ...)
field_sizeNTuple{N, Int}Grid size (Nx, Ny, ...)
wavenumbersNTuple{N, Vector{T}}Wavenumber arrays (kx, ky, ...), always on the CPU
cached_forcingCACached F̂ (constant within timestep)
prevsolUnion{Nothing, CA}Previous solution for Stratonovich work
rngAbstractRNGRandom number generator
random_phasesAPre-allocated random phase buffer
diagnostic_weightsUnion{Nothing,A}Cached backend diagnostic weights
diagnostic_global_shapeNTuple{N,Int}Shape associated with cached weights
diagnostic_local_rangesNTuple{N,UnitRange{Int}}Local ranges associated with cached weights
diagnostic_metricSymbolMetric associated with cached weights
last_update_timeTTime of last forcing update
spectrum_typeSymbolType of forcing spectrum
enforce_hermitianBoolEnforce F̂(-k) = F̂(k)* for real-valued fields
architectureAbstractArchitectureCPU() or GPU() backend

Property aliases (via getproperty, not stored fields):

PropertyReturns
forcing.forcing_rateforcing.energy_injection_rate
forcing.spectrumforcing.forcing_spectrum
forcing.is_stochastictrue
forcing.is_gpuwhether architecture is a GPU

Constructor:

StochasticForcing(;
    field_size::NTuple{N, Int},                              # Required
    domain_size::Union{Nothing, NTuple{N, Real}} = nothing,  # nothing => 2π in each direction
    energy_injection_rate::Real = 1.0,
    forcing_rate::Union{Nothing, Real} = nothing,            # alias for energy_injection_rate
    injection_metric::Symbol = :direct,
    k_forcing::Real = 4.0,
    dk_forcing::Real = 1.0,
    dt::Real = 0.01,
    spectrum_type::Symbol = :ring,
    rng::AbstractRNG = Random.MersenneTwister(),
    dtype::Type{T} = Float64,
    enforce_hermitian::Bool = true,
    architecture::AbstractArchitecture = CPU()
) where {T<:AbstractFloat, N}

forcing_rate and energy_injection_rate set the same target ε. forcing_rate wins when provided. If a non-default energy_injection_rate is also supplied and the values disagree, a warning is emitted.

injection_metric = :direct makes ε the injection rate of the forced variable's direct quadratic invariant. Use :vorticity_kinetic when forcing 2-D vorticity and ε should be kinetic-energy injection; it weights each nonzero Fourier mode by 1/|k|².

The default rng is a fresh MersenneTwister per instance (not Random.GLOBAL_RNG), which avoids accidental RNG sharing between forcing instances. Concurrent mutation of the same forcing object is not supported. Under MPI with more than one rank the constructor broadcasts a seed from rank 0 on MPI.COMM_WORLD, so every rank draws the same phases and the forcing stays coherent across the decomposition.

Set enforce_hermitian = false when the forced field is genuinely complex; with the default true, F̂(-k) = F̂(k)* is imposed so that the inverse transform of the forcing is real. Self-conjugate modes are projected as sqrt(2) * real(z); the factor preserves the variance that would otherwise be halved by discarding the imaginary part.

dk_forcing must be positive for :ring, :band and :kolmogorov (an ArgumentError is thrown otherwise); :lowk ignores it. :kolmogorov also requires k_forcing > 0.

Spectrum types:

SymbolDescriptionUnnormalized amplitude √Q̂(k)
:ringGaussian ring around k_fexp(-(|k| - kf)² / 4δf²)
:bandSharp band |k| ∈ (kf - δf, kf + δf)1 if ||k| - kf| < δf, else 0
:lowkLow wavenumber forcing1 if |k| < k_f, else 0
:kolmogorovLarge scales with a smooth cutoff at kf + δf√(|k|/kf) · exp(-(|k| - kf)² / 4δf²) if |k| < kf + δ_f, else 0

In every case the k = 0 mode is zero, and the full stored spectrum is rescaled so sum(Q̂ .* w) / (2M^2) == ε, where M = prod(field_size), w = 1 for :direct, and w = 1/|k|² for :vorticity_kinetic. A positive ε whose selected spectrum contains no representable nonzero mode raises ArgumentError. :isotropic aliases :ring, and :bandlimited aliases :band.

SeparableStochasticForcing

Tarang.SeparableStochasticForcingType
SeparableStochasticForcing

Stochastic forcing for a Fourier product domain with one trailing Chebyshev dimension. Randomness and spectral localization live only in the Fourier dimensions; chebyshev_profile supplies the fixed Chebyshev dependence.

source

Use SeparableStochasticForcing for a Fourier–Chebyshev product domain. The random spectrum is defined only over the Fourier axes and is multiplied by a fixed Chebyshev profile:

\[\hat F(k_1,\ldots,k_d,n,t) = \hat\xi(k_1,\ldots,k_d,t)\,p_n.\]

forcing = SeparableStochasticForcing(
    fourier_size=(Nx,),
    chebyshev_basis=zbasis,
    chebyshev_profile=z -> z * (1 - z),
    domain_size=(Lx,),
    energy_injection_rate=0.1,
    k_forcing=4.0,
    dk_forcing=1.0,
    dt=dt,
    architecture=GPU(),
)
add_stochastic_forcing!(problem, :b, forcing)

chebyshev_profile can be either a function of the physical Chebyshev coordinate or a vector of Chebyshev coefficients whose length equals chebyshev_basis.meta.size. A function is sampled on the basis grid and transformed once during construction. In either form the profile is normalized to unit physical mean square with the basis quadrature weights. Non-finite, zero-norm, and mis-sized profiles are rejected.

The mixed normalization currently supports only injection_metric=:direct. Passing :vorticity_kinetic raises ArgumentError, because a kinetic-energy metric in a bounded direction requires model-specific inverse operators rather than the Fourier 1/|k|² weight.

All forcing arrays and outer-product views are persistent. With a supplied RNG, the Fourier phase draw is reproducible between CPU and GPU: one scalar seed is drawn from the RNG and a counter-based kernel generates the phase array on the field architecture, without host staging. Only Fourier axes may be shortened to real-FFT half spectra when the forcing is added to a field. The Chebyshev coefficient count must match exactly.

For Fourier–Fourier domains, continue to use StochasticForcing; for Fourier–Chebyshev domains, use SeparableStochasticForcing.

DeterministicForcing

Tarang.DeterministicForcingType
DeterministicForcing{T, N, A<:AbstractArray{T,N}}

Deterministic (non-random) forcing.

The forcing function is called as forcing_function(grid..., t, parameters) and must return an array whose size is exactly field_size, so write it with broadcasting over the grid arrays.

Example

# Sinusoidal forcing on a 16×16 grid
forcing = DeterministicForcing(
    (x, y, t, p) -> p[:A] .* sin.(p[:k] .* x) .* cos.(y) .* cos(p[:omega] * t),
    (16, 16);
    parameters = Dict(:A => 1.0, :k => 4.0, :omega => 1.0)
)

x = reshape(range(0, 2π, length=17)[1:16], 16, 1)   # column vector
y = reshape(range(0, 2π, length=17)[1:16], 1, 16)   # row vector
F = generate_forcing!(forcing, (x, y), 0.0)         # 16×16 Matrix{Float64}
source

Deterministic (non-random) forcing.

Type signature:

mutable struct DeterministicForcing{T<:AbstractFloat, N, A<:AbstractArray{T, N}} <: DeterministicForcingType

Fields:

FieldTypeDescription
forcing_functionFunctionf(x, y, ..., t, params) → forcing array
field_sizeNTuple{N, Int}Grid size
cached_forcingACached forcing values on the chosen architecture
parametersDict{Symbol, Any}Parameters for forcing function
architectureAbstractArchitectureCPU() or GPU() backend

forcing.is_gpu is available as a property alias.

Constructor:

DeterministicForcing(
    forcing_function::Function,
    field_size::NTuple{N, Int};
    parameters::AbstractDict{Symbol} = Dict{Symbol, Any}(),
    dtype::Type{T} = Float64,
    architecture::AbstractArchitecture = CPU()
) where {T<:AbstractFloat, N}

parameters may be any Dict keyed by Symbol (e.g. Dict(:A => 1.0, :k => 4.0)); it is copied into a Dict{Symbol, Any}.

The forcing function is called as forcing_function(grid..., t, parameters) and must return an array whose size is exactly field_size — a scalar-valued function raises ArgumentError: Deterministic forcing function must return an array, and a mis-shaped one raises ArgumentError: Deterministic forcing output size ... does not match field size .... Write it with broadcasting over the grid arrays:

forcing = DeterministicForcing(
    (x, y, t, p) -> p[:A] .* sin.(p[:k] .* x) .* cos.(y) .* cos(p[:omega] * t),
    (16, 16);
    parameters = Dict(:A => 1.0, :k => 4.0, :omega => 1.0),
)

x = reshape(range(0, 2π, length=17)[1:16], 16, 1)   # column vector
y = reshape(range(0, 2π, length=17)[1:16], 1, 16)   # row vector
F = generate_forcing!(forcing, (x, y), 0.0)         # 16×16 Matrix{Float64}

Forcing Generation

generate_forcing!

Tarang.generate_forcing!Function
generate_forcing!(forcing::StochasticForcing, t::Real, substep::Int=1)

Generate stochastic forcing at time t. Works on both CPU and GPU.

Key Points

  1. Forcing is regenerated only on substep 1 - ensures forcing stays constant within a timestep for IMEX and multi-stage methods.

  2. Scaling: F̂(k) = √Q̂(k) · exp(i·φ) / √dt, where φ ∼ Uniform[0, 2π) (generated by _fill_random_phases!)

  3. Zero mean: The k=0 mode is always set to zero.

  4. Hermitian symmetry (optional): F̂(-k) = F̂(k)* so that ifft produces real fields.

  5. GPU support: Random phases are generated on the target architecture when possible, then combined with the spectrum using broadcasted operations.

Arguments

  • forcing: StochasticForcing configuration
  • t: Current simulation time
  • substep: Current substep (1 for first substep)

Returns

The cached forcing array (modified in-place).

source
generate_forcing!(forcing::StochasticForcing, t::Real)

Generate forcing without substep tracking. Equivalent to substep=1.

source
generate_forcing!(forcing::DeterministicForcing, grid, t::Real)

Evaluate deterministic forcing at time t on the given grid.

source

Generate forcing realization. Returns cached value for substeps > 1.

Signatures:

# Stochastic forcing
generate_forcing!(forcing::StochasticForcing, t::Real, substep::Int=1)
generate_forcing!(forcing::StochasticForcing, t::Real)
generate_forcing!(forcing::SeparableStochasticForcing, t::Real, substep::Int=1)

# Deterministic forcing
generate_forcing!(forcing::DeterministicForcing, grid, t::Real)

Key behavior:

  • substep == 1: Generates new random forcing
  • substep > 1: Returns cached forcing (same as substep 1)
  • Time check: Won't regenerate if t is exactly forcing.last_update_time
  • The k = 0 mode is always zeroed, and Hermitian symmetry is imposed when enforce_hermitian = true
  • Requires forcing.dt > 0; otherwise an error is raised

Returns: The cached forcing array forcing.cached_forcing

Automatic registration and timestepper compatibility

add_stochastic_forcing!(problem, variable, forcing) registers automatic RHS forcing. variable names a scalar field or a flattened vector/tensor component (for example :u_x). A container name such as :u is ambiguous and raises ArgumentError.

One forcing realization is reused across the stages of supported one-step RK/ETD methods; CNAB1 and SBDF1 are supported first-order methods. Tarang rejects CNAB2, MCNAB2, CNLF2, SBDF2, SBDF3, SBDF4, DiagonalIMEX_SBDF2, ETD_CNAB2, and ETD_SBDF2 before drawing forcing. Those schemes reuse or combine values across time levels, which colors white noise or gives it the wrong variance.

apply_forcing!

Tarang.apply_forcing!Function
apply_forcing!(rhs::AbstractArray, forcing::StochasticForcing, t::Real, substep::Int=1)

Add stochastic forcing to the RHS in spectral space. Works on both CPU and GPU.

Arguments

  • rhs: Right-hand side array (modified in-place)
  • forcing: StochasticForcing configuration
  • t: Current simulation time
  • substep: Current substep number
source

Add forcing to a field in spectral space.

Signature:

apply_forcing!(
    rhs::AbstractArray{Complex{T}, N},
    forcing::StochasticForcing{T, N, A, CA},
    t::Real,
    substep::Int=1
) where {T, N, A, CA}

Equivalent to rhs .+= generate_forcing!(forcing, t, substep) when the shapes match exactly. When they do not, the forcing is first sliced to the region rhs covers — the leading half-spectrum for a real-FFT rhs, and this rank's local wavenumber block for a distributed PencilArray rhs. If no such slice exists, an ArgumentError is thrown.


Configuration

set_dt!

Tarang.set_dt!Function
set_dt!(forcing::StochasticForcing, dt::Real)

Update the timestep for Stratonovich scaling.

Call this when dt changes (e.g., adaptive timestepping).

source

Update the timestep used for Stratonovich scaling.

set_dt!(forcing::StochasticForcing{T, N, A, CA}, dt::Real) where {T, N, A, CA}

Call this when dt changes (e.g., adaptive timestepping). If the value actually changes, the cache is zeroed and the next generate_forcing! call regenerates with the new √dt scaling.

reset_forcing!

Tarang.reset_forcing!Function
reset_forcing!(forcing::StochasticForcing)

Reset the forcing cache, causing regeneration on next call. Works on both CPU and GPU.

source

Reset forcing cache. Forces regeneration on next generate_forcing! call.

reset_forcing!(forcing::StochasticForcing{T, N, A, CA}) where {T, N, A, CA}

Work Calculation

store_prevsol!

Tarang.store_prevsol!Function
store_prevsol!(forcing::StochasticForcing, sol::AbstractArray)

Store the current solution for Stratonovich work calculation. Works on both CPU and GPU arrays.

Call this at the beginning of each timestep, before advancing.

source

Store the current solution for Stratonovich work calculation.

store_prevsol!(forcing::StochasticForcing{T, N, A, CA}, sol::AbstractArray{Complex{T}, N})

Call at the beginning of each timestep, before advancing.

work_stratonovich

Tarang.work_stratonovichFunction
work_stratonovich(forcing::StochasticForcing, sol::AbstractArray)

Compute work done by forcing using Stratonovich interpretation. Works on both CPU and GPU arrays.

Formula

W = Re⟨(ψⁿ + ψⁿ⁺¹)/2 · ΔF̂*⟩

where ΔF̂ = √Q̂ · ξ · √dt is the forcing increment over dt.

This correctly accounts for the correlation between forcing and response.

Arguments

  • forcing: StochasticForcing with prevsol stored
  • sol: Current solution ψⁿ⁺¹

Returns

Work done during this timestep (scalar, units of energy).

source

Compute work done using Stratonovich interpretation.

work_stratonovich(forcing::StochasticForcing{T, N, A, CA},
                  sol::AbstractArray{Complex{T}, N}) -> T

Formula (with M = prod(field_size)):

\[W = \frac{dt}{M^2}\,\text{Re}\sum_k m_k w_k \frac{q^n_k + q^{n+1}_k}{2}\, \hat{F}_k^*\]

Uses midpoint evaluation and returns zero(T) unless store_prevsol! was called first. Here w_k is the selected injection-metric weight and m_k is the real-FFT multiplicity: 2 for an omitted conjugate partner, 1 for DC and an even-length Nyquist endpoint. For a full complex spectrum, every m_k = 1.

work_ito

Tarang.work_itoFunction
work_ito(forcing::StochasticForcing, sol::AbstractArray)

Compute work done by forcing using Itô interpretation. Works on both CPU and GPU arrays.

Formula

W_Itô = Re⟨ψⁿ · ΔF̂*⟩ + ε · dt

where ΔF̂ = √Q̂ · ξ · √dt is the forcing increment.

The drift correction ε · dt accounts for the Itô-Stratonovich conversion. In Itô calculus, ψⁿ is independent of Fⁿ⁺¹, so ⟨ψⁿ · ΔF̂⟩ = 0. The drift ensures ⟨WItô⟩ = ⟨WStratonovich⟩ = ε · dt.

source

Compute work done using Itô interpretation.

work_ito(forcing::StochasticForcing{T, N, A, CA},
         sol_prev::AbstractArray{Complex{T}, N}) -> T

Formula:

\[W_{\text{Itô}} = \frac{dt}{M^2}\,\text{Re}\sum_k m_k w_k q^n_k\, \hat{F}_k^* \;+\; \varepsilon\, dt\]

Uses the solution before the step (independent of the current forcing) plus the drift correction ε·dt, which makes ⟨WItô⟩ = ⟨WStratonovich⟩ = ε·dt.

Under MPI both work functions reduce over this rank's local slab and then Allreduce the partial sums, so every rank returns the same global value.


Diagnostics

meanenergyinjection_rate

Tarang.mean_energy_injection_rateFunction
mean_energy_injection_rate(forcing::StochasticForcing)

Return the target (mean) energy injection rate ε.

This is the ensemble average of work done per unit time.

source

Return the target (mean) energy injection rate ε.

mean_energy_injection_rate(forcing::StochasticForcing) -> T
energy_injection_rate(forcing::StochasticForcing) -> T      # same value

instantaneous_power

Tarang.instantaneous_powerFunction
instantaneous_power(forcing::StochasticForcing, sol::AbstractArray)

Compute instantaneous power input from the Parseval-normalized spectral pairing. Works on both CPU and GPU arrays.

This is the correlation between the solution and forcing at a given instant. For white noise forcing, the expected value depends on which solution is passed:

  • ⟨P⟩ = 0 if sol is the solution BEFORE the forcing was applied (independent)
  • ⟨P⟩ = ε if sol is the MIDPOINT (ψⁿ + ψⁿ⁺¹)/2
  • ⟨P⟩ = 2ε if sol is the solution AFTER forcing (includes full response)

Note: For the Stratonovich-consistent time-averaged power over the timestep, use work_stratonovich(forcing, sol) / forcing.dt instead.

Returns

Instantaneous power (energy per unit time).

source

Compute the instantaneous power input.

instantaneous_power(forcing::StochasticForcing{T, N, A, CA},
                    sol::AbstractArray{Complex{T}, N}) -> T

Formula:

\[P = \frac{1}{M^2}\,\text{Re}\sum_k m_k w_k q_k\, \hat{F}_k^*\]

This is the instantaneous correlation between solution and forcing, and it fluctuates. Its mean depends on which solution you pass: 0 for the solution before the forcing was applied, ε for the midpoint (ψⁿ + ψⁿ⁺¹)/2, and 2ε for the solution after. For the Stratonovich-consistent time-averaged power over a step, use work_stratonovich(forcing, sol) / forcing.dt instead.

forcingenstrophyinjection_rate

Tarang.forcing_enstrophy_injection_rateFunction
forcing_enstrophy_injection_rate(forcing::StochasticForcing)

Compute mean enstrophy injection rate (for 2D turbulence).

This assumes the stochastic forcing is applied directly to vorticity. With M = prod(field_size) and Tarang's unnormalized FFT convention,

η = ∑_k Q̂(k) / (2M²)

For injection_metric=:direct, this equals the configured energy injection rate. For :vorticity_kinetic, it is generally of order k_forcing² times the configured kinetic-energy injection rate.

source

Compute mean enstrophy injection rate (for 2D turbulence).

forcing_enstrophy_injection_rate(forcing::StochasticForcing{T, N, A, CA}) -> T

Formula:

\[\eta = \frac{1}{2M^2} \sum_k \hat{Q}(k)\]

This assumes the forcing is applied directly to 2-D vorticity. With injection_metric=:direct, η equals the configured ε. With :vorticity_kinetic, ε is kinetic-energy injection and η is generally of order k_forcing^2 * ε. For N != 2 the function warns and returns zero(T).

getforcingspectrum

Return the forcing amplitude spectrum √Q̂(k), as the array type A of the chosen architecture.

get_forcing_spectrum(forcing::StochasticForcing) -> A

getcachedforcing

Tarang.get_cached_forcingFunction
get_cached_forcing(forcing::StochasticForcing)

Return the current cached forcing F̂(k).

source
get_cached_forcing(state::TimestepperState)

Get the cached forcing array. Returns nothing if no forcing is configured.

source

Return the current cached forcing F̂(k), as the complex array type CA of the chosen architecture.

get_cached_forcing(forcing::StochasticForcing) -> CA
get_forcing_real(forcing::StochasticForcing)   -> real part of F̂(k)

Spectrum Building Utilities

These are exported so that you can inspect or build custom forcing spectra.

build_wavenumbers

Build wavenumber arrays for each dimension, in standard FFT ordering (0, dk, ..., k_nyq, -(k_nyq - dk), ..., -dk with dk = 2π/L).

build_wavenumbers(
    field_size::NTuple{N, Int},
    domain_size::NTuple{N, Real},
    dtype::Type{T}
) -> NTuple{N, Vector{T}}

For example, build_wavenumbers((8,), (2π,), Float64)[1] is [0.0, 1.0, 2.0, 3.0, 4.0, -3.0, -2.0, -1.0].

computeforcingspectrum

Tarang.compute_forcing_spectrumFunction
compute_forcing_spectrum(wavenumbers, k_f, dk_f, ε, domain_size, spectrum_type, dtype;
                         injection_metric=:direct)

Compute the forcing amplitude spectrum √Q̂(k). With M physical grid points, the full spectrum is normalized so sum(Q̂(k) * w(k)) / (2M²) == ε, where w=1 for :direct and w=1/|k|² for :vorticity_kinetic. A positive ε whose selected band contains no representable nonzero mode raises ArgumentError.

source

Compute the forcing amplitude spectrum √Q̂(k).

compute_forcing_spectrum(
    wavenumbers::NTuple{N, Vector{T}},
    k_f::Real,
    dk_f::Real,
    ε::Real,
    domain_size::NTuple{N, Real},
    spectrum_type::Symbol,
    dtype::Type{T};
    injection_metric::Symbol = :direct
) -> Array{T, N}

The full spectrum is normalized so that sum(abs2(spectrum[k]) * w[k]) / (2 * prod(field_size)^2) == ε, with the metric weights defined above. compute_forcing_spectrum uses the supplied wavenumbers; the constructor obtains those wavenumbers from domain_size before calling it.


Worked Example

A complete stochastically forced integration in Fourier space, with the work diagnostics.

using Tarang, Random

forcing = StochasticForcing(
    field_size = (32, 32),
    domain_size = (2π, 2π),
    energy_injection_rate = 0.1,
    k_forcing = 6.0,
    dk_forcing = 1.5,
    dt = 1e-3,
    spectrum_type = :ring,
    rng = MersenneTwister(7),
)

# Euler-Maruyama in spectral space: ψⁿ⁺¹ = ψⁿ + F̂ dt
function integrate!(forcing, nsteps)
    ψ = zeros(ComplexF64, forcing.field_size...)
    W = 0.0
    for n in 1:nsteps
        t = n * forcing.dt
        store_prevsol!(forcing, ψ)              # ψⁿ, BEFORE advancing
        F = generate_forcing!(forcing, t, 1)
        ψ .+= F .* forcing.dt
        W += work_stratonovich(forcing, ψ)      # work injected over this step
    end
    return ψ, W
end

ψ, W = integrate!(forcing, 1000)

@show mean_energy_injection_rate(forcing)       # 0.1, the target ε
@show W / (1000 * forcing.dt)                   # the realized injection rate
@show forcing_enstrophy_injection_rate(forcing)

With this seed the run prints

mean_energy_injection_rate(forcing) = 0.1
W / (1000 * forcing.dt) = 0.11453581797243557
forcing_enstrophy_injection_rate(forcing) = 3.937500006838822

The realized rate scatters around the target ε: a single 1000-step realization is a random variable, not the ensemble mean, so expect it to land within a few tens of percent of ε and to tighten as the run gets longer.

Inside a multi-stage timestepper, pass the substep index so the realization is held fixed across the stages, and call set_dt! whenever the timestep changes:

rhs = zeros(ComplexF64, 32, 32)
for substep in 1:3
    apply_forcing!(rhs, forcing, 2.0, substep)   # substeps 2 and 3 reuse the cached F̂
end

set_dt!(forcing, 5e-4)                           # zeroes the cache; next draw rescales by 1/√dt

Exports

export Forcing, StochasticForcingType, DeterministicForcingType
export StochasticForcing, DeterministicForcing
export generate_forcing!, apply_forcing!
export reset_forcing!, set_dt!
export store_prevsol!, work_stratonovich, work_ito
export mean_energy_injection_rate, energy_injection_rate, instantaneous_power
export forcing_enstrophy_injection_rate
export get_forcing_spectrum, get_cached_forcing, get_forcing_real
export build_wavenumbers, compute_forcing_spectrum

Index