Time Stepping
This page documents the time integration functions.
Available Time Stepping Schemes
QGYBJ+.jl provides two time stepping methods:
| Method | Description | When to Use |
|---|---|---|
| Leapfrog | Explicit, 2nd order | Default, dt ≤ 2f/N² (~2s) |
| IMEX-CN | Implicit dispersion, explicit advection | Large 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:
first_projection_step!- Forward Euler initializationleapfrog_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
Compute tendencies at time n:
- Advection of q and B by geostrophic flow
- Wave refraction by vorticity
- Vertical diffusion
Apply physics switches:
linear: Zero nonlinear advectioninviscid: Zero dissipationpassive_scalar: Zero dispersion and refractionfixed_flow: Mean flow doesn't evolve
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.
Wave feedback (optional):
q* = q - qʷ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 structpar::QGParams: Model parametersplans: FFT plansa: Elliptic coefficient array a_ell(z) = f²/N²dealias_mask: Optional 2/3 dealiasing mask (nx × ny)workspace: Optional pre-allocated workspace for 2D decompositionN2_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)Purpose: Initialize the leapfrog scheme by providing values at times n and n-1.
Algorithm:
- Compute tendencies at time n (advection, refraction, diffusion)
- Apply physics switches (linear, inviscid, etc.)
- Forward Euler update with integrating factors
- Wave feedback (optional)
- 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 + refraction2. 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 structpar::QGParams: Model parametersplans: FFT plansa: Elliptic coefficient arraydealias_mask: Optional dealiasing maskworkspace: Optional pre-allocated workspace for 2D decompositionN2_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
endFortran 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
endThe 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, Snm1Forward 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)/∂ywhere u, v are the geostrophic velocities (in real space).
Output
nqk: Ĵ(ψ, q) - advection of QGPVnBRk: Ĵ(ψ, BR) - advection of wave real partnBIk: Ĵ(ψ, 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 structplans: FFT plansLmask: Dealiasing mask (true = keep mode, false = zero)
Algorithm
For each field χ ∈ {q, BR, BI}:
- Transform χ̂ → χ (inverse FFT)
- Compute uχ and vχ (pointwise in real space)
- Transform back: (ûχ), (v̂χ)
- Compute divergence: ikₓ(ûχ) + ikᵧ(v̂χ)
- 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.
QGYBJplus.Nonlinear.convol_waqg_q! — Function
convol_waqg_q!(nqk, u, v, qk, G, plans; Lmask=nothing)Compute advection of q using divergence form without splitting wave fields.
QGYBJplus.Nonlinear.convol_waqg_L⁺A! — Function
convol_waqg_L⁺A!(nL⁺Ak, u, v, L⁺Ak, G, plans; Lmask=nothing)Compute advection of complex L⁺A directly (YBJ+ path).
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
- Compute ζ̂ = -kₕ²ψ̂ (spectral)
- Transform ζ̂, B̂R, B̂I to real space
- Compute products: rBR = ζ × BR, rBI = ζ × BI
- 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 tendenciesQGYBJplus.Nonlinear.refraction_waqg_L⁺A! — Function
refraction_waqg_L⁺A!(rL⁺Ak, L⁺Ak, ψₖ, G, plans; Lmask=nothing)Compute wave refraction term ζ*L⁺A directly for complex L⁺A (YBJ+ path).
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: QGParamsG::Grid: Grid structplans: FFT plansLmask: Dealiasing mask (true = keep mode, false = zero)
Example
compute_qw!(qw, BR, BI, params, grid, plans; Lmask=L)
# qw now contains wave feedback termQGYBJplus.Nonlinear.compute_qw_complex! — Function
compute_qw_complex!(qʷₖ, Bk, par, G, plans; Lmask=nothing)Compute wave feedback directly from complex B without spectral BR/BI splitting.
Advection:
convol_waqg_q!computes J(ψ, q)convol_waqg_L⁺A!computes J(ψ, L⁺A) for the complex YBJ+ envelopeconvol_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⁺Arefraction_waqg!computes (L⁺A)R × ζ and (L⁺A)I × ζ for the decomposed form
Wave feedback:
compute_qw_complex!computes qʷ directly from complex L⁺Acompute_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 termqok: Input q field at time n-1 (for leapfrog)par: QGParams (for nuz coefficient)G::Grid: Grid structworkspace: 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.
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_factor — Function
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 componentspar: 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_tendencyFortran 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 thisUsage:
# 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 # EulerComplete 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
endParallel 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)
endFor 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)
endRobert-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:
| Variable | Description | Usage |
|---|---|---|
Snm1 | State at n-1 | Input, receives filtered n values |
Sn | State at n | Input (unchanged) |
Snp1 | State at n+1 | Output |
After each step, rotate the pointers:
Snm1, Sn, Snp1 = Sn, Snp1, Snm1This avoids data copying by just swapping references.
Physics Switches
The time stepping respects these QGParams switches:
| Switch | Effect |
|---|---|
linear | Zero nonlinear advection J(ψ, q), J(ψ, B) |
inviscid | Zero vertical diffusion νz ∂²q/∂z² |
passive_scalar | Waves as passive tracers (no dispersion/refraction) |
no_dispersion | Zero wave dispersion (A = 0) |
fixed_flow | Mean flow doesn't evolve (q unchanged) |
no_wave_feedback | No 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
workspaceargument 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
endIMEX 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_workspace — Function
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).
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:
- Stage 1 (First Half-Refraction): B* = B^n × exp(-i·(dt/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})]
- 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
- Compute q advection at time n: Q^n = -J(ψ^n, q^n)
- Apply first half-refraction: B* = B^n × exp(-i·(dt/2)·ζ/2)
- Compute B advection using B: N^n = -J(ψ^n, B)
- 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}
- 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)
- Compute ψ^{n+1} predictor from q^{n+1} (and q^w predictor when enabled)
- Apply second half-refraction using ψ^{n+1} predictor
- Store tendencies for next step (AB2)
Arguments
Snp1::State: State at time n+1 (output)Sn::State: State at time n (input)G::Grid: Gridpar::QGParams: Parametersplans: FFT plansimex_ws::IMEXWorkspace: Pre-allocated workspace (stores N^{n-1} for AB2)a: Elliptic coefficient a_ell = f²/N²dealias_mask: Dealiasing maskworkspace: Additional workspace for elliptic solversN2_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
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:
- Performs a first-order forward Euler step (same as firstprojectionstep!)
- 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.
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)
endStability Properties
| Term | Treatment | Stability |
|---|---|---|
| Refraction | Exact integrating factor (Strang split) | Unconditionally stable |
| Dispersion | Implicit Crank-Nicolson | Unconditionally stable |
| Advection | Explicit Adams-Bashforth 2 | CFL: 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
- Strang Splitting: Refraction is split symmetrically with two half-steps:
exp(-i·(dt/2)·ζ/2)before and after the IMEX solve - Adams-Bashforth 2: Advection uses
(3/2)N^n - (1/2)N^{n-1}extrapolation for bothqandB(bootstraps with forward Euler on first step) - **Consistent A***: After first half-refraction, A* = (L⁺)⁻¹B* is computed (critical for IMEX-CN consistency)
- Modified Elliptic Solve: Each mode solves (L⁺ - β)·A^{n+1} = RHS where β = (dt/2)·i·αdisp·kₕ²
- 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 stepleapfrog_step!- Main leapfrog integration with Robert-Asselin filter
IMEX-CN:
init_imex_workspace- Allocate IMEX workspace arraysimex_cn_step!- IMEX Crank-Nicolson step with operator splittingfirst_imex_step!- First-order forward Euler initialization
Common:
convol_waqg_q!/convol_waqg_L⁺A!- Complex-form advection for q and L⁺Arefraction_waqg_L⁺A!- Complex-form wave refraction termconvol_waqg!/refraction_waqg!- Real/imag-decomposed advection/refractiondissipation_q_nv!- Vertical diffusionint_factor- Integrating factor for hyperdiffusioncompute_qw_complex!/compute_qw!- Wave feedback term (see Physics API)