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}

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 ε
  • 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)
  • 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} <: StochasticForcingType

Fields:

FieldTypeDescription
forcing_spectrumArray{T, N}√Q̂(k) - amplitude spectrum
energy_injection_rateTTarget ε
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, ...)
cached_forcingArray{Complex{T}, N}Cached F̂ (constant within timestep)
prevsolUnion{Nothing, Array{Complex{T}, N}}Previous solution for Stratonovich work
rngAbstractRNGRandom number generator
last_update_timeTTime of last forcing update
spectrum_typeSymbolType of forcing spectrum

Constructor:

StochasticForcing(;
    field_size::NTuple{N, Int},                    # Required
    domain_size::NTuple{N, Real} = ntuple(i -> 2π, N),
    energy_injection_rate::Real = 1.0,
    k_forcing::Real = 4.0,
    dk_forcing::Real = 1.0,
    dt::Real = 0.01,
    spectrum_type::Symbol = :ring,
    rng::AbstractRNG = Random.GLOBAL_RNG,
    dtype::Type{T} = Float64
) where {T<:AbstractFloat, N}

Spectrum types:

SymbolDescriptionFormula
:ringGaussian ring around k_fexp(-(
:bandSharp band [kf - δf, kf + δf]1 if
:lowkLow wavenumber forcing1 if
:kolmogorovLarge-scale KolmogorovSmooth large-scale forcing

DeterministicForcing

Tarang.DeterministicForcingType
DeterministicForcing{T, N}

Deterministic (non-random) forcing.

Example

# Sinusoidal forcing
forcing = DeterministicForcing(
    (x, y, t, p) -> p[:A] * sin(p[:k] * x) * cos(p[:ω] * t),
    (64, 64);
    parameters = Dict(:A => 1.0, :k => 4.0, :ω => 1.0)
)
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
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

Constructor:

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

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)

# 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 already updated at this time

Returns: The cached forcing array forcing.cached_forcing

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},
    t::Real,
    substep::Int=1
) where {T, N}

Equivalent to: rhs .+= generate_forcing!(forcing, t, substep)


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}, dt::Real) where {T, N}

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

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}) where {T, N}

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}, 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}, sol::AbstractArray{Complex{T}, N}) -> T

Formula:

\[W = -\text{Re}\left\langle \frac{\psi^n + \psi^{n+1}}{2} \cdot \hat{F}^* \right\rangle\]

Uses midpoint evaluation (requires store_prevsol! called first).

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}, sol_prev::AbstractArray{Complex{T}, N}) -> T

Formula:

\[W_{\text{Itô}} = -\text{Re}\langle \psi^n \cdot \hat{F}^* \rangle \cdot dt + \varepsilon \cdot dt\]

Uses initial value plus drift correction.


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

instantaneous_power

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

Compute instantaneous power input P = Re⟨ψ · F̂*⟩ / A. 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 instantaneous power input P(t) = dW/dt.

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

This fluctuates around the mean ε due to randomness.

forcingenstrophyinjection_rate

Tarang.forcing_enstrophy_injection_rateFunction
forcing_enstrophy_injection_rate(forcing::StochasticForcing)

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

η = ∑_k |k|² Q̂(k) / (2 · domain_area)

Note: This assumes the forcing spectrum Q̂(k) corresponds to direct field forcing. For vorticity forcing, enstrophy injection is simply ε. For streamfunction forcing, this formula gives the enstrophy injection rate.

source

Compute mean enstrophy injection rate (for 2D vorticity forcing).

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

Formula:

\[\eta = \sum_k |k|^2 \hat{Q}(k) / 2\]

getforcingspectrum

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

get_forcing_spectrum(forcing::StochasticForcing) -> Array{T, N}

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

get_cached_forcing(forcing::StochasticForcing) -> Array{Complex{T}, N}

Internal Functions

build_wavenumbers

Build wavenumber arrays for each dimension.

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

computeforcingspectrum

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}
) -> Array{T, N}

The spectrum is normalized such that energy injection rate equals ε.


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, instantaneous_power
export forcing_enstrophy_injection_rate
export get_forcing_spectrum, get_cached_forcing

Index