Time steppers

Aether.TimeSteppers.SSPRKType
struct SSPRK{FT, N}

An N-stage explicit strong-stability-preserving Runge–Kutta method in the Ketcheson (2010) two-register form. Each stage l applies

$u_1 \leftarrow u_1 + \delta_l u_0$

$u_0 \leftarrow \gamma_{0,l} u_0 + \gamma_{1,l} u_1 + \beta_l \Delta t\, L(u_0)$

with u1 written (not accumulated) at stage 1, so δ = (1, 0, …) makes the stage-1 accumulation a plain copy. L is the full right-hand side: flux divergence plus source contributions. The struct is a coefficient table; the executor is step!.

  • γ0::NTuple{N, FT} where {FT, N}: multiplies u0 in the stage combination

  • γ1::NTuple{N, FT} where {FT, N}: multiplies u1 in the stage combination

  • β::NTuple{N, FT} where {FT, N}: multiplies Δt L(u0) in the stage combination

  • δ::NTuple{N, FT} where {FT, N}: per-stage u1 accumulation weight; stage 1 writes instead

  • cfl_limit::Any: largest stable Courant number of the scheme

source
Aether.TimeSteppers.IMEXSSPRKType
struct IMEXSSPRK{FT, NE, NI}

An implicit–explicit Runge–Kutta pair: the explicit tableau is an SSPRK in two-register form, the implicit tableau is a diagonally implicit method one stage ahead of the explicit scheme (NI = NE + 1; implicit stage l belongs to the implicit_update! call at stage = l - 1, with stage = 0 before the first explicit stage). Diagonal entries are the constant γ for stages that solve and 0 for stages that don't — IMEX2P's first implicit row is zero, so its pre-stage call is a no-op. The implicit combination weights b are stored explicitly. Stiff terms enter through components with isstiff(component) = true, whose implicit_update! methods consult these tables.

  • explicit::SSPRK: explicit tableau in two-register form

  • a::NTuple{NI, NTuple{NI, FT}} where {FT, NI}: rows of the implicit tableau, zero-padded; a[l][l] is γ or 0

  • b::NTuple{NI, FT} where {FT, NI}: implicit combination weights of the final stage

  • γ::Any: diagonal implicit coefficient

source
Aether.TimeSteppers.RK2Function
RK2() -> SSPRK{Float64, 2}
RK2(FT) -> SSPRK{T, 2} where T

The optimal 2-stage second-order SSP scheme (Heun; Gottlieb 2009, §3).

using Aether

RK2()

# output
SSPRK{Float64}(2 stages, cfl_limit = 1.0)
source
Aether.TimeSteppers.RK3Function
RK3() -> SSPRK{Float64, 3}
RK3(FT) -> SSPRK{T, 3} where T

The optimal 3-stage third-order SSP scheme (Shu & Osher; Gottlieb 2009, §3).

source
Aether.TimeSteppers.RK4Function
RK4() -> SSPRK{Float64, 4}
RK4(FT) -> SSPRK{T, 4} where T

The 4-stage fourth-order low-storage scheme RK4()4[2S] of Ketcheson (2010), Table 2. Mid-cycle δ accumulation is what buys fourth order in two registers; the Courant limit 1.3925 is the first-order-flux constraint of Colella (2011).

source
Aether.TimeSteppers.IMEX2PFunction
IMEX2P(; ...) -> IMEXSSPRK{Float64, 2, 3}
IMEX2P(FT; γ) -> IMEXSSPRK{_A, 2, 3} where _A

The two-solve second-order IMEX pair of Ascher, Ruuth & Spiteri (1997) — IMEX(2,2,2) with δ = 1 - 1/(2γ) — the scheme Krapp et al. (2024) build the multifluid dust drag on ("MDIRK"). The implicit part is a stiffly accurate, L-stable DIRK for either root γ = 1 ± 1/√2 and its first row is zero: there is no implicit solve before the first explicit stage. The default γ = 1 + 1/√2 is the monotone choice, whose explicit part is TVD up to the encoded Courant limit 1/γ ≈ 0.586 (Krapp et al. 2024, §2.2 and Appendix D); γ = 1 - 1/√2 has the smaller truncation error but negative explicit combination weights (no SSP property).

In the two-register form the explicit part is u* = uⁿ + γΔt L(uⁿ) at stage 1 and u0 ← (δ/γ) u0 + (1 - δ/γ) u1 + (1 - δ) Δt L(u0) at stage 2 — verified to reproduce uⁿ⁺¹ = uⁿ + δΔt L(uⁿ) + (1 - δ)Δt L(u*) exactly.

using Aether

IMEX2P()

# output
IMEXSSPRK{Float64}(2 explicit + 3 implicit stages, cfl_limit = 0.585786437626905)
source
Aether.TimeSteppers.ClockType
mutable struct Clock{FT}

Simulation time, current timestep, and cycle count — deliberately nothing else, so the restart payload stays exactly state + clock. The stage index is an executor argument, never clock state: cycles are atomic.

  • t::Any: simulation time

  • dt::Any: current timestep

  • cycle::Int64: completed cycles

using Aether

Clock(Float64)

# output
Clock{Float64}(t = 0.0, dt = 0.0, cycle = 0)
source
Aether.TimeSteppers.step!Function
step!(simulation)

Advance the simulation one cycle: hooks, the drive! injections, the cycle-seam refresh when injections are configured, the stepper's stage loop, the operator-split component updates, then the clock and the next timestep. The slot order is written only here and in the advance_stage! methods — configuration can delete steps (empty tuples, nothing hooks, protocol fallbacks compile to nothing) but never reorder them.

When cycle_seam is set, the final-stage exchange is deferred until before the next stage 1. Ghost values may be stale during that interval; consumers that need them must call refresh_primitives!.

using Aether

mesh = Mesh(CPU(); size = (4, 4, 4), extent = (1, 1, 1))
eos = IdealHydro(2.0, 1e-12, 1e-10)
stepper = RK3()
state = HydroState(mesh, eos, stepper)
state.w0[:, :, :, 1, :] .= 1;    # slot 1 = ρ
state.w0[:, :, :, 5, :] .= 0.5;  # slot 5 = e
primitive_to_conserved!(CPU(), eos, state.w0, state.u0,
                        Meshes.padded_ranges(mesh)..., 1:mesh.nblocks)
clock = Clock(Float64)
simulation = (; mesh, eos, state, reconstruction = PLM(), riemann_solver = HLLE(),
                exchange = BoundaryExchange(mesh; cells = 5), stepper, clock,
                cfl = 1.0, timestep_reduction = nothing, components = (),
                cycle_seam = false, hooks = default_hooks())
clock.dt = new_timestep(simulation)
step!(simulation)
clock

# output
Clock{Float64}(t = 0.25, dt = 0.25, cycle = 1)
source
Aether.TimeSteppers.default_hooksFunction
default_hooks(

) -> @NamedTuple{before_cycle::Nothing, after_update::Nothing, after_stage::Nothing, after_cycle::Nothing}

The hook set with every slot empty. A simulation's hooks are a NamedTuple of nothing-or-callable over exactly these names, called as hook(simulation, stage):

  • before_cycle — before anything else in the cycle (stage = 0)
  • after_update — after each stage's conserved update and sources
  • after_stage — at the end of each stage, primitives and ghosts current
  • after_cycle — after the stage loop and split updates

Custom flooring after every update is hooks = (; after_stage = my_floor!)Simulation merges user hooks over default_hooks(); an absent hook costs zero instructions.

using Aether

default_hooks()

# output
(before_cycle = nothing, after_update = nothing, after_stage = nothing, after_cycle = nothing)
source