Time Stepping

This page documents the time integration functions.

Available Time Stepping Schemes

QGYBJ+.jl provides two time stepping methods:

MethodDescriptionWhen to Use
LeapfrogExplicit, 2nd orderDefault, dt ≤ 2f/N² (~2s)
IMEX-CNImplicit dispersion, explicit advectionLarge dt (~20s), 10x speedup

Choosing a Method

Use the timestepper keyword in run_simulation!:

# Leapfrog (default, explicit)
run_simulation!(S, G, par, plans; timestepper=:leapfrog, ...)

# IMEX Crank-Nicolson (implicit dispersion)
run_simulation!(S, G, par, plans; timestepper=:imex_cn, ...)

Leapfrog Scheme (Default)

The Leapfrog scheme with Robert-Asselin filter provides second-order accuracy with computational mode damping.

Overview

The time stepping consists of two functions:

  1. first_projection_step! - Forward Euler initialization
  2. leapfrog_step! - Main leapfrog integration with Robert-Asselin filter

Forward Euler Projection Step

QGYBJplus.first_projection_step!Function
first_projection_step!(S, G, par, plans; a, dealias_mask=nothing, workspace=nothing, N2_profile=nothing)

Forward Euler initialization step for the leapfrog time stepper.

Purpose

The leapfrog scheme requires values at two time levels (n and n-1). This function takes the initial state and advances it by one Forward Euler step, providing the needed second time level.

Algorithm

  1. Compute tendencies at time n:

    • Advection of q and B by geostrophic flow
    • Wave refraction by vorticity
    • Vertical diffusion
  2. Apply physics switches:

    • linear: Zero nonlinear advection
    • inviscid: Zero dissipation
    • passive_scalar: Zero dispersion and refraction
    • fixed_flow: Mean flow doesn't evolve
  3. Forward Euler update: For each spectral mode:

    q^(n+1) = [q^n - dt × tendency_q + dt × diffusion] × exp(-λ_q × dt)
    B^(n+1) = [B^n - dt × tendency_B] × exp(-λ_B × dt)

    where λ is the hyperdiffusion factor.

  4. Wave feedback (optional):

    q* = q - qʷ
  5. Diagnostic inversions:

    • q → ψ (elliptic inversion)
    • B → A, C (YBJ+ inversion)
    • ψ → u, v (velocity computation)

Arguments

  • S::State: State to advance (modified in place)
  • G::Grid: Grid struct
  • par::QGParams: Model parameters
  • plans: FFT plans
  • a: Elliptic coefficient array a_ell(z) = f²/N²
  • dealias_mask: Optional 2/3 dealiasing mask (nx × ny)
  • workspace: Optional pre-allocated workspace for 2D decomposition
  • N2_profile: Optional N²(z) profile for vertical velocity computation

Returns

Modified state S at time n+1.

Fortran Correspondence

This matches the projection step in main_waqg.f90.

Example

# Initialize and run projection step
state = init_state(grid)
init_random_psi!(state, grid)
a = a_ell_ut(params, grid)
L = dealias_mask(grid)
first_projection_step!(state, grid, params, plans; a=a, dealias_mask=L)
source

Purpose: Initialize the leapfrog scheme by providing values at times n and n-1.

Algorithm:

  1. Compute tendencies at time n (advection, refraction, diffusion)
  2. Apply physics switches (linear, inviscid, etc.)
  3. Forward Euler update with integrating factors
  4. Wave feedback (optional)
  5. Diagnostic inversions (q → ψ → u, v)

Usage:

# Serial mode
first_projection_step!(state, grid, params, plans; a=a_ell, dealias_mask=L)

# Parallel mode (2D decomposition)
first_projection_step!(state, grid, params, plans; a=a_ell, dealias_mask=L, workspace=workspace)

Leapfrog Step with Robert-Asselin Filter

QGYBJplus.leapfrog_step!Function
leapfrog_step!(Snp1, Sn, Snm1, G, par, plans; a, dealias_mask=nothing, workspace=nothing, N2_profile=nothing)

Advance the solution by one leapfrog time step with Robert-Asselin filtering.

Algorithm

1. Compute tendencies at time n:

F_q^n = J(ψ^n, q^n) - νz∂²q^(n-1)/∂z²
F_B^n = J(ψ^n, B^n) + dispersion + refraction

2. Leapfrog update with integrating factors: For each spectral mode (k):

q^(n+1) = q^(n-1) × e^(-2λdt) + 2dt × [-J(ψ,q)^n + diff^n] × e^(-λdt)
B^(n+1) = B^(n-1) × e^(-2λdt) + 2dt × [-J(ψ,B)^n + dispersion + refraction] × e^(-λdt)

Note: All tendencies are evaluated at time n and scaled by e^(-λdt) for second-order accuracy.

3. Robert-Asselin filter:

q̃^n = q^n + γ(q^(n-1) - 2q^n + q^(n+1))
B̃^n = B^n + γ(B^(n-1) - 2B^n + B^(n+1))

The filtered values are stored in Sn (which becomes Snm1 after rotation).

4. Wave feedback (if enabled):

q*^(n+1) = q^(n+1) - qʷ^(n+1)

5. Diagnostic inversions:

  • q^(n+1) → ψ^(n+1)
  • B^(n+1) → A^(n+1), C^(n+1)
  • ψ^(n+1) → u^(n+1), v^(n+1)

Arguments

  • Snp1::State: State at time n+1 (output)
  • Sn::State: State at time n (input, filter applied to Snm1)
  • Snm1::State: State at time n-1 (input, receives filtered values)
  • G::Grid: Grid struct
  • par::QGParams: Model parameters
  • plans: FFT plans
  • a: Elliptic coefficient array
  • dealias_mask: Optional dealiasing mask
  • workspace: Optional pre-allocated workspace for 2D decomposition
  • N2_profile: Optional N²(z) profile for vertical velocity computation

Returns

Modified Snp1 with solution at time n+1.

Time Level Management

After this call:

  • Snp1 contains fields at n+1
  • Sn contains filtered fields at n (becomes new n-1 after rotation)
  • Snm1 is unchanged (will be overwritten after rotation)

Typical loop structure:

for iter in 1:nsteps
    leapfrog_step!(Snp1, Sn, Snm1, G, par, plans; a=a)
    # Rotate: Snm1 ← Sn, Sn ← Snp1
    Snm1, Sn, Snp1 = Sn, Snp1, Snm1
end

Fortran Correspondence

This matches the main leapfrog loop in main_waqg.f90.

Example

# After projection step, run leapfrog
for iter in 1:1000
    leapfrog_step!(Snp1, Sn, Snm1, grid, params, plans; a=a, dealias_mask=L)
    Snm1, Sn, Snp1 = Sn, Snp1, Snm1
end
source

The Leapfrog scheme:

\[\phi^{n+1} = \phi^{n-1} + 2\Delta t \times F^n\]

Robert-Asselin filter (damps computational mode):

\[\tilde{\phi}^n = \phi^n + \gamma(\phi^{n-1} - 2\phi^n + \phi^{n+1})\]

With integrating factor for hyperdiffusion:

\[\phi^{n+1} = \phi^{n-1} \times e^{-2\lambda\Delta t} + 2\Delta t \times F^n \times e^{-\lambda\Delta t}\]

Usage:

# Serial mode
leapfrog_step!(Snp1, Sn, Snm1, grid, params, plans; a=a_ell, dealias_mask=L)

# Parallel mode (2D decomposition)
leapfrog_step!(Snp1, Sn, Snm1, grid, params, plans; a=a_ell, dealias_mask=L, workspace=workspace)

# Time level rotation after each step
Snm1, Sn, Snp1 = Sn, Snp1, Snm1

Forward Euler Update

Used for the first (projection) step:

\[\phi^{n+1} = \left[\phi^n - \Delta t \times F\right] \times e^{-\lambda\Delta t}\]

The integrating factor e^{-λΔt} handles hyperdiffusion exactly.

Tendency Computation

Each time step computes the following tendencies:

QG Potential Vorticity:

\[F_q = -J(\psi, q) + \nu_z \frac{\partial^2 q}{\partial z^2}\]

Wave Envelope (complex form, YBJ+):

\[F_B = -J(\psi, B) + i\,\alpha_{\text{disp}}\,k_h^2 A - \frac{i}{2}\zeta B\]

Equivalent real/imaginary parts:

\[F_{BR} = -J(\psi, BR) - \frac{k_h^2}{2}A_I + \frac{1}{2}BI \times \zeta\]

\[F_{BI} = -J(\psi, BI) + \frac{k_h^2}{2}A_R - \frac{1}{2}BR \times \zeta\]

Nonlinear Terms

QGYBJplus.Nonlinear.convol_waqg!Function
convol_waqg!(nqk, nBRk, nBIk, u, v, qk, BRk, BIk, G, plans; Lmask=nothing)

Compute advection terms in divergence form, matching Fortran convol_waqg.

Mathematical Form

Uses the divergence form of the Jacobian:

J(ψ, q) = ∂(uq)/∂x + ∂(vq)/∂y

where u, v are the geostrophic velocities (in real space).

Output

  • nqk: Ĵ(ψ, q) - advection of QGPV
  • nBRk: Ĵ(ψ, BR) - advection of wave real part
  • nBIk: Ĵ(ψ, BI) - advection of wave imaginary part

Arguments

  • nqk, nBRk, nBIk: Output arrays (spectral)
  • u, v: Real-space velocity arrays (precomputed)
  • qk, BRk, BIk: Input fields (spectral)
  • G::Grid: Grid struct
  • plans: FFT plans
  • Lmask: Dealiasing mask (true = keep mode, false = zero)

Algorithm

For each field χ ∈ {q, BR, BI}:

  1. Transform χ̂ → χ (inverse FFT)
  2. Compute uχ and vχ (pointwise in real space)
  3. Transform back: (ûχ), (v̂χ)
  4. Compute divergence: ikₓ(ûχ) + ikᵧ(v̂χ)
  5. Apply dealiasing mask

Fortran Correspondence

This matches convol_waqg in derivatives.f90.

Note

The velocities u, v should be precomputed and passed in real space.

source
QGYBJplus.Nonlinear.refraction_waqg!Function
refraction_waqg!(rBRk, rBIk, BRk, BIk, psik, G, plans; Lmask=nothing)

Compute wave refraction term: B × ζ where ζ = ∇²ψ is relative vorticity.

Physical Interpretation

Near-inertial waves are refracted by vorticity gradients:

  • Anticyclones (ζ < 0): Wave focusing, amplitude increase
  • Cyclones (ζ > 0): Wave defocusing, amplitude decrease

This is the "wave capture" mechanism that traps NIWs in anticyclonic eddies.

Mathematical Form

refraction = B × ζ

where ζ = ∇²ψ = -kₕ²ψ̂ in spectral space.

Output

  • rBRk: Real part of refraction term (spectral)
  • rBIk: Imaginary part of refraction term (spectral)

Algorithm

  1. Compute ζ̂ = -kₕ²ψ̂ (spectral)
  2. Transform ζ̂, B̂R, B̂I to real space
  3. Compute products: rBR = ζ × BR, rBI = ζ × BI
  4. Transform back and apply dealiasing

Fortran Correspondence

This matches refraction_waqg in derivatives.f90.

Example

refraction_waqg!(rBR, rBI, BR, BI, psi, grid, plans; Lmask=L)
# rBR, rBI now contain the refraction tendencies
source
QGYBJplus.Nonlinear.compute_qw!Function
compute_qw!(qwk, BRk, BIk, par, G, plans; Lmask=nothing)

Compute wave feedback on mean flow: qʷ from wave field B.

Physical Interpretation

The wave feedback qʷ represents how near-inertial waves modify the quasi-geostrophic flow. This is a key component of wave-mean flow interaction in the QG-YBJ+ model.

Mathematical Form (Xie & Vanneste 2015)

For dimensional equations where B has velocity units [m/s]:

qʷ = (i/2f)J(B*, B) + (1/4f)∇²|B|²

where:

  • B* is the complex conjugate of B
  • J(B, B) = BₓBᵧ - B*ᵧBₓ is the Jacobian
  • |B|² = BR² + BI² is the wave energy density

No W2F scaling is applied since B already has its actual dimensional amplitude.

Decomposition

Let B = BR + i×BI. Then:

  • J(B*, B) = 2i(BRₓBIᵧ - BRᵧBIₓ) [purely imaginary]
  • ∇²|B|² = ∇²(BR² + BI²)

The final qʷ is real-valued after combining terms.

Arguments

  • qwk: Output array for q̂ʷ (spectral)
  • BRk, BIk: Wave field components (spectral)
  • par: QGParams
  • G::Grid: Grid struct
  • plans: FFT plans
  • Lmask: Dealiasing mask (true = keep mode, false = zero)

Example

compute_qw!(qw, BR, BI, params, grid, plans; Lmask=L)
# qw now contains wave feedback term
source

Advection:

  • convol_waqg_q! computes J(ψ, q)
  • convol_waqg_L⁺A! computes J(ψ, L⁺A) for the complex YBJ+ envelope
  • convol_waqg! computes J(ψ, q), J(ψ, (L⁺A)R), J(ψ, (L⁺A)I) for explicit real/imag decomposition

Refraction:

  • refraction_waqg_L⁺A! computes ζ × L⁺A for complex L⁺A
  • refraction_waqg! computes (L⁺A)R × ζ and (L⁺A)I × ζ for the decomposed form

Wave feedback:

  • compute_qw_complex! computes qʷ directly from complex L⁺A
  • compute_qw! computes qʷ from (L⁺A)R/(L⁺A)I

Vertical Diffusion

QGYBJplus.Nonlinear.dissipation_q_nv!Function
dissipation_q_nv!(dqk, qok, par, G; workspace=nothing)

Compute vertical diffusion of q with Neumann boundary conditions.

Mathematical Form

D = νz ∂²q/∂z²

with ∂q/∂z = 0 at z = -Lz and z = 0.

Discretization

Interior points (1 < k < nz): D[k] = νz (q[k+1] - 2q[k] + q[k-1]) / dz²

Boundary points (Neumann): D[1] = νz (q[2] - q[1]) / dz² D[nz] = νz (q[nz-1] - q[nz]) / dz²

Arguments

  • dqk: Output array for diffusion term
  • qok: Input q field at time n-1 (for leapfrog)
  • par: QGParams (for nuz coefficient)
  • G::Grid: Grid struct
  • workspace: Optional pre-allocated workspace for 2D decomposition

Note

This operates on spectral q but the vertical derivative is in physical space, so the operation is the same for each (kx, ky) mode.

Fortran Correspondence

This matches dissipation_q_nv in derivatives.f90.

source

Computes νz ∂²q/∂z² using the tridiagonal solver. Automatically handles 2D decomposition transposes.

Integrating Factors

Purpose

For stiff hyperdiffusion terms, we use an integrating factor approach:

\[\tilde{\phi} = \phi \times e^{\nu k^{2p} t}\]

This allows exact treatment of the linear diffusion while using explicit time stepping.

Function

QGYBJplus.Nonlinear.int_factorFunction
int_factor(kx, ky, par; waves=false)

Compute hyperdiffusion integrating factor for given wavenumber.

Mathematical Background

The hyperdiffusion operator is:

D = -ν₁(-∇²)^n₁ - ν₂(-∇²)^n₂

In spectral space, this becomes multiplication by:

λ = ν₁|k|^(2n₁) + ν₂|k|^(2n₂)

The integrating factor for one time step is: exp(-λ×dt)

For efficiency, we return just λ×dt (the exponent).

Arguments

  • kx, ky: Horizontal wavenumber components
  • par: QGParams (contains ν₁, ν₂, n₁, n₂)
  • waves::Bool: If true, use wave hyperdiffusion (nuh1w, ilap1w, etc.)

Returns

λ×dt = dt × [ν₁(kx² + ky²)^n₁ + ν₂(kx² + ky²)^n₂] = dt × [ν₁ kₕ^(2n₁) + ν₂ kₕ^(2n₂)]

Note: Uses isotropic form (kx² + ky²)^n for proper damping of diagonal modes.

Usage in Time Stepping

# After computing tendency
factor = exp(-int_factor(kx, ky, par))
q_new = factor * q_tendency

Fortran Correspondence

This matches the integrating factor computation in the main loop of main_waqg.f90.

Example

# Get integrating factor for wavenumber (3, 4)
lambda_dt = int_factor(3.0, 4.0, params)
factor = exp(-lambda_dt)  # Multiply solution by this
source

Usage:

# Compute factor for a spectral mode
If = int_factor(kx, ky, params; waves=false)   # For mean flow (q)
Ifw = int_factor(kx, ky, params; waves=true)   # For waves (B)

# Apply in time stepping
q_new = q_old * exp(-2*If) - 2*dt * tendency * exp(-If)   # Leapfrog
q_new = q_old * exp(-If) - dt * tendency                    # Euler

Complete Simulation Loop

Setup and Run

using QGYBJplus

# Initialize
params = default_params(Lx=500e3, Ly=500e3, Lz=4000.0, nx=64, ny=64, nz=32)
grid = init_grid(params)
plans = plan_transforms!(grid)
a_ell = a_ell_ut(params, grid)
L = dealias_mask(grid)

# Create three state arrays for leapfrog
Snm1 = init_state(grid)  # n-1
Sn = init_state(grid)    # n
Snp1 = init_state(grid)  # n+1

# Initialize with random fields
init_random_psi!(Sn, grid, params, plans; a=a_ell)

# Projection step (Forward Euler initialization)
first_projection_step!(Sn, grid, params, plans; a=a_ell, dealias_mask=L)

# Copy for n-1 state
copy_state!(Snm1, Sn)

# Main time loop
for iter in 1:nsteps
    leapfrog_step!(Snp1, Sn, Snm1, grid, params, plans; a=a_ell, dealias_mask=L)

    # Rotate time levels
    Snm1, Sn, Snp1 = Sn, Snp1, Snm1
end

Parallel Mode (2D Decomposition)

using MPI, PencilArrays, PencilFFTs, QGYBJplus

MPI.Init()
mpi_config = QGYBJplus.setup_mpi_environment()

# Initialize with MPI
params = default_params(Lx=1000e3, Ly=1000e3, Lz=5000.0, nx=256, ny=256, nz=128)
grid = QGYBJplus.init_mpi_grid(params, mpi_config)
plans = QGYBJplus.plan_mpi_transforms(grid, mpi_config)
workspace = QGYBJplus.init_mpi_workspace(grid, mpi_config)

a_ell = a_ell_ut(params, grid)
L = dealias_mask(grid)

# Create states
Snm1 = QGYBJplus.init_mpi_state(grid, plans, mpi_config)
Sn = QGYBJplus.init_mpi_state(grid, plans, mpi_config)
Snp1 = QGYBJplus.init_mpi_state(grid, plans, mpi_config)

# Initialize
init_random_psi!(Sn, grid, params, plans; a=a_ell)
first_projection_step!(Sn, grid, params, plans; a=a_ell, dealias_mask=L, workspace=workspace)
copy_state!(Snm1, Sn)

# Main loop with workspace for 2D decomposition
for iter in 1:nsteps
    leapfrog_step!(Snp1, Sn, Snm1, grid, params, plans;
                   a=a_ell, dealias_mask=L, workspace=workspace)
    Snm1, Sn, Snp1 = Sn, Snp1, Snm1
end

MPI.Finalize()

CFL Condition

Stability Constraint

function compute_cfl(state, grid, dt)
    u_max = maximum(abs.(state.u))
    v_max = maximum(abs.(state.v))
    return dt * max(u_max/grid.dx, v_max/grid.dy)
end

For stability, CFL < 1 is required. Recommended: CFL ≈ 0.5.

Adaptive Time Stepping

function adaptive_dt(state, grid; cfl_target=0.5, dt_max=0.01)
    u_max = maximum(abs.(state.u)) + 1e-10  # Avoid division by zero
    v_max = maximum(abs.(state.v)) + 1e-10

    dt = cfl_target * min(grid.dx/u_max, grid.dy/v_max)
    return min(dt, dt_max)
end

Robert-Asselin Filter Parameter

The filter coefficient γ (γ in QGParams) controls damping of the computational mode:

  • Too large (γ > 0.01): Excessive damping, accuracy loss
  • Too small (γ < 0.0001): Computational mode growth
  • Recommended: γ ≈ 0.001 (default)
params = default_params(Lx=500e3, Ly=500e3, Lz=4000.0, nx=64, ny=64, nz=32, γ=0.001)

Note: Type \gamma<tab> in the Julia REPL to enter γ.

Time Level Management

The leapfrog scheme requires three time levels:

VariableDescriptionUsage
Snm1State at n-1Input, receives filtered n values
SnState at nInput (unchanged)
Snp1State at n+1Output

After each step, rotate the pointers:

Snm1, Sn, Snp1 = Sn, Snp1, Snm1

This avoids data copying by just swapping references.

Physics Switches

The time stepping respects these QGParams switches:

SwitchEffect
linearZero nonlinear advection J(ψ, q), J(ψ, B)
inviscidZero vertical diffusion νz ∂²q/∂z²
passive_scalarWaves as passive tracers (no dispersion/refraction)
no_dispersionZero wave dispersion (A = 0)
fixed_flowMean flow doesn't evolve (q unchanged)
no_wave_feedbackNo qʷ feedback term

Performance

Timing Breakdown

Typical distribution: | Component | Fraction | |:–––––|:––––-| | FFTs | 40-50% | | Elliptic solves | 20-30% | | Array operations | 15-25% | | Transpose operations (2D) | 5-10% |

2D Decomposition Notes

When using 2D decomposition:

  • Pass workspace argument to avoid repeated allocation
  • Vertical operations (inversions, diffusion) use automatic transposes
  • The workspace contains pre-allocated z-pencil arrays
# Pre-allocate workspace (once)
workspace = QGYBJplus.init_mpi_workspace(grid, mpi_config)

# Reuse for all steps
for step in 1:nsteps
    leapfrog_step!(Snp1, Sn, Snm1, G, par, plans;
                   a=a, dealias_mask=L, workspace=workspace)
    Snm1, Sn, Snp1 = Sn, Snp1, Snm1
end

IMEX Crank-Nicolson Scheme

The IMEX-CNAB scheme (Crank-Nicolson for implicit terms, Adams-Bashforth 2 for explicit terms) treats dispersion implicitly for unconditional stability, allowing timesteps ~10x larger than leapfrog.

Why IMEX-CNAB?

The YBJ+ wave equation has three terms with different timescales:

  • Advection: J(ψ,B) - slow, O(hours)
  • Refraction: (i/2)ζB - moderate, O(minutes)
  • Dispersion: i·αdisp·kₕ²·A - fast, CFL-limited to dt ≤ 2f/N² (~2s)

IMEX-CNAB treats dispersion implicitly with Crank-Nicolson, removing the stiff CFL constraint.

Algorithm: Strang Splitting + IMEX-CNAB

The scheme uses Strang splitting for refraction (second-order accurate) combined with IMEX-CNAB for advection/dispersion:

Stage 1 (First Half-Refraction - Strang):

\[B^* = B^n \times \exp\left(-i \frac{\Delta t}{2} \frac{\zeta}{2}\right)\]

Applied in physical space with exact integrating factor (energy-preserving).

Stage 2 (IMEX-CNAB for Advection + Dispersion):

\[B^{**} = B^* + \frac{\Delta t}{2}[D^* + D^{**}] + \Delta t \left[\frac{3}{2}N^n - \frac{1}{2}N^{n-1}\right]\]

where:

  • D = i·αdisp·kₕ²·A (dispersion, treated implicitly with Crank-Nicolson)
  • N = -J(ψ,B) (advection, treated explicitly with Adams-Bashforth 2)

Stage 3 (Second Half-Refraction - Strang):

\[B^{n+1} = B^{**} \times \exp\left(-i \frac{\Delta t}{2} \frac{\zeta}{2}\right)\]

The symmetric half-steps ensure second-order accuracy in time.

Functions

QGYBJplus.init_imex_workspaceFunction
init_imex_workspace(S, G; nthreads=Threads.maxthreadid())

Initialize workspace for IMEX time stepping with threading support.

NOTE: All work arrays are pre-allocated here to avoid heap corruption from repeated allocation/deallocation in tight loops during time stepping. Per-thread workspaces are created for the tridiagonal solves to enable parallel processing of horizontal modes.

The workspace includes storage for Adams-Bashforth 2 (previous tendency) and Strang splitting (intermediate B* state).

source
QGYBJplus.imex_cn_step!Function
imex_cn_step!(Snp1, Sn, G, par, plans, imex_ws; a, dealias_mask=nothing, workspace=nothing, N2_profile=nothing)

Second-order IMEX-CNAB time step for YBJ+ equation with Strang splitting for refraction.

Uses Strang splitting with Adams-Bashforth 2 for advection (waves and mean flow) and Crank-Nicolson for linear PV diffusion/hyperdiffusion:

  1. Stage 1 (First Half-Refraction): B* = B^n × exp(-i·(dt/2)·ζ/2)
  2. Stage 2 (IMEX-CNAB for Advection + Dispersion):
    • Advection (AB2): (3/2)N^n - (1/2)N^{n-1} where N = -J(ψ,L⁺A)
    • Dispersion (CN): (1/2)[L(B*) + L(B^{n+1})]
  3. Stage 3 (Second Half-Refraction): B^{n+1} = B** × exp(-i·(dt/2)·ζ/2) using ψ^{n+1} predictor

This achieves second-order temporal accuracy through:

  • Strang splitting (second-order) instead of Lie splitting (first-order)
  • Adams-Bashforth 2 (second-order) for advection (q and B)
  • Crank-Nicolson for dispersion (second-order)

Algorithm

  1. Compute q advection at time n: Q^n = -J(ψ^n, q^n)
  2. Apply first half-refraction: B* = B^n × exp(-i·(dt/2)·ζ/2)
  3. Compute B advection using B: N^n = -J(ψ^n, B)
  4. For each spectral mode (kx, ky) solve IMEX-CNAB for B: a. Compute A* = (L⁺)⁻¹B* (essential for IMEX-CN consistency!) b. Build RHS = B* + (dt/2)·i·αdisp·kₕ²·A* + (3dt/2)·N^n - (dt/2)·N^{n-1} c. Solve modified elliptic: (L⁺ - β)·A^{n+1} = RHS where β = (dt/2)·i·αdisp·kₕ² d. Recover B** = RHS + β·A^{n+1}
  5. Update q with CNAB: (I - dt/2·L)·q^{n+1} = (I + dt/2·L)·q^n + dt·[ (3/2)Q^n - (1/2)Q^{n-1} ] where L = νz∂zz - λ_h (vertical diffusion + hyperdiffusion)
  6. Compute ψ^{n+1} predictor from q^{n+1} (and q^w predictor when enabled)
  7. Apply second half-refraction using ψ^{n+1} predictor
  8. Store tendencies for next step (AB2)

Arguments

  • Snp1::State: State at time n+1 (output)
  • Sn::State: State at time n (input)
  • G::Grid: Grid
  • par::QGParams: Parameters
  • plans: FFT plans
  • imex_ws::IMEXWorkspace: Pre-allocated workspace (stores N^{n-1} for AB2)
  • a: Elliptic coefficient a_ell = f²/N²
  • dealias_mask: Dealiasing mask
  • workspace: Additional workspace for elliptic solvers
  • N2_profile: N²(z) profile

Temporal Accuracy

  • Overall: Second-order in time
  • First step: Falls back to forward Euler for advection (AB2 bootstrap)

Stability

  • Refraction: Exactly energy-preserving (integrating factor)
  • Dispersion: Unconditionally stable (implicit CN)
  • Advection CFL: dt < dx/U_max ≈ 1600s for this problem
source
QGYBJplus.first_imex_step!Function
first_imex_step!(S, G, par, plans, imex_ws; a, dealias_mask=nothing, workspace=nothing, N2_profile=nothing)

First-order forward Euler step to initialize IMEX time stepping.

This function:

  1. Performs a first-order forward Euler step (same as firstprojectionstep!)
  2. Initializes the AB2 state by computing and storing the q and B advection tendencies

After this function is called, subsequent calls to imexcnstep! will use second-order Adams-Bashforth 2 for advection.

source

Usage

# Initialize IMEX workspace
imex_ws = init_imex_workspace(state, grid)
Snp1 = copy_state(state)

# Time stepping loop
for step in 1:nsteps
    imex_cn_step!(Snp1, Sn, grid, params, plans, imex_ws;
                  a=a_ell, dealias_mask=L, workspace=workspace,
                  N2_profile=N2_profile)

    # Copy for next step (only need 2 time levels, not 3 like leapfrog)
    parent(Sn.L⁺A) .= parent(Snp1.L⁺A)
    parent(Sn.A) .= parent(Snp1.A)
    parent(Sn.q) .= parent(Snp1.q)
    parent(Sn.psi) .= parent(Snp1.psi)
end

Stability Properties

TermTreatmentStability
RefractionExact integrating factor (Strang split)Unconditionally stable
DispersionImplicit Crank-NicolsonUnconditionally stable
AdvectionExplicit Adams-Bashforth 2CFL: dt < dx/U_max

For U = 0.335 m/s and dx ≈ 273m (256 grid, 70km domain): dt_max ≈ 800s

This allows dt = 20s (vs dt = 2s for leapfrog), a 10x speedup.

Key Implementation Details

  1. Strang Splitting: Refraction is split symmetrically with two half-steps: exp(-i·(dt/2)·ζ/2) before and after the IMEX solve
  2. Adams-Bashforth 2: Advection uses (3/2)N^n - (1/2)N^{n-1} extrapolation for both q and B (bootstraps with forward Euler on first step)
  3. **Consistent A***: After first half-refraction, A* = (L⁺)⁻¹B* is computed (critical for IMEX-CN consistency)
  4. Modified Elliptic Solve: Each mode solves (L⁺ - β)·A^{n+1} = RHS where β = (dt/2)·i·αdisp·kₕ²
  5. Updated Refraction: The second refraction half-step uses a ψ^{n+1} predictor (with q^w when enabled) to keep the coupled system second-order

When to Use

  • Use IMEX-CN when dispersion CFL is limiting (typical oceanographic simulations)
  • Use Leapfrog for academic tests requiring exact 2nd-order temporal accuracy

API Summary

All time stepping functions documented above:

Leapfrog:

  • first_projection_step! - Forward Euler initialization step
  • leapfrog_step! - Main leapfrog integration with Robert-Asselin filter

IMEX-CN:

  • init_imex_workspace - Allocate IMEX workspace arrays
  • imex_cn_step! - IMEX Crank-Nicolson step with operator splitting
  • first_imex_step! - First-order forward Euler initialization

Common:

  • convol_waqg_q! / convol_waqg_L⁺A! - Complex-form advection for q and L⁺A
  • refraction_waqg_L⁺A! - Complex-form wave refraction term
  • convol_waqg! / refraction_waqg! - Real/imag-decomposed advection/refraction
  • dissipation_q_nv! - Vertical diffusion
  • int_factor - Integrating factor for hyperdiffusion
  • compute_qw_complex! / compute_qw! - Wave feedback term (see Physics API)