Inertia-gravity waves
This example simulates the propagation of inertia-gravity waves in a stably stratified atmosphere, following the classical benchmark test case described by Skamarock and Klemp (1994). This test evaluates the accuracy of numerical pressure solvers by introducing a small-amplitude temperature perturbation into a stratified environment with constant Brunt-Väisälä frequency, triggering propagating inertia-gravity waves.
The test case is particularly useful for validating anelastic and compressible solvers, as discussed at the CM1 inertia-gravity wave test page.
Physical setup
The background state is a stably stratified atmosphere with constant Brunt-Väisälä frequency $N$, which gives a potential temperature profile
\[θ^{\rm bg}(z) = θ_0 \exp\left( \frac{N^2 z}{g} \right)\]
where $θ_0 = 300 \, {\rm K}$ is the surface potential temperature and $g$ is the gravitational acceleration.
The initial perturbation is a localized temperature anomaly centered at $x = x_0$:
\[θ'(x, z) = Δθ \frac{\sin(π z / L_z)}{1 + (x - x_0)^2 / a^2}\]
with amplitude $Δθ = 0.01 \, {\rm K}$, half-width parameter $a = 5000 \, {\rm m}$, and perturbation center $x_0 = L_x / 3$. A uniform mean wind $U = 20 \, {\rm m \, s^{-1}}$ advects the waves.
using Breeze
using Oceananigans.Units
using Statistics
using Printf
using CairoMakieProblem parameters
We define the thermodynamic base state and mean wind following Skamarock and Klemp (1994):
p₀ = 100000 # Pa - surface pressure
θ₀ = 300 # K - reference potential temperature
U = 20 # m s⁻¹ - mean wind
N = 0.01 # s⁻¹ - Brunt-Väisälä frequency
N² = N^20.0001Grid configuration
The domain is 300 km × 10 km with 300 × 10 grid points, matching the nonhydrostatic case configuration in the paper by Skamarock and Klemp (1994).
Nx, Nz = 300, 10
Lx, Lz = 300kilometers, 10kilometers
grid = RectilinearGrid(CPU(), size = (Nx, Nz), halo = (5, 5),
x = (0, Lx), z = (0, Lz),
topology = (Periodic, Flat, Bounded))300×1×10 RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Flat, Oceananigans.Grids.Bounded} on Oceananigans.Architectures.CPU with 5×0×5 halo
├── Periodic x ∈ [0.0, 300000.0) regularly spaced with Δx=1000.0
├── Flat y
└── Bounded z ∈ [0.0, 10000.0] regularly spaced with Δz=1000.0Atmosphere model setup
We use the anelastic formulation with liquid-ice potential temperature thermodynamics:
constants = ThermodynamicConstants()
reference_state = ReferenceState(grid, constants; surface_pressure=p₀, potential_temperature=θ₀)
dynamics = AnelasticDynamics(reference_state)
advection = WENO(minimum_buffer_upwind_order=3)
model = AtmosphereModel(grid; dynamics, advection)AtmosphereModel{CPU, RectilinearGrid}(time = 0 seconds, iteration = 0)
├── grid: 300×1×10 RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Flat, Oceananigans.Grids.Bounded} on Oceananigans.Architectures.CPU with 5×0×5 halo
├── dynamics: AnelasticDynamics(p₀=100000.0, θ₀=300.0)
├── formulation: LiquidIcePotentialTemperatureFormulation
├── timestepper: RungeKutta3TimeStepper
├── advection scheme:
│ ├── momentum: WENO{3, Float64, Float32}(order=5)
│ ├── ρθ: WENO{3, Float64, Float32}(order=5)
│ └── ρqᵗ: WENO{3, Float64, Float32}(order=5)
├── tracers: ()
├── coriolis: Nothing
└── microphysics: NothingInitial conditions
The perturbation parameters from the paper by Skamarock and Klemp (1994):
Δθ = 0.01 # K - perturbation amplitude
a = 5000 # m - perturbation half-width parameter
x₀ = Lx / 3 # m - perturbation center in x100000.0The background potential temperature profile with a constant Brunt-Väisälä frequency:
g = model.thermodynamic_constants.gravitational_acceleration
θᵇᵍ(z) = θ₀ * exp(N² * z / g)θᵇᵍ (generic function with 1 method)The initial condition combines the background profile with the localized perturbation:
θᵢ(x, z) = θᵇᵍ(z) + Δθ * sin(π * z / Lz) / (1 + (x - x₀)^2 / a^2)
set!(model, θ=θᵢ, u=U)Simulation
We run for 3000 seconds with a fixed time step equal 24 seconds, matching the simulation time in (Skamarock and Klemp, 1994):
simulation = Simulation(model; Δt=24, stop_time=3000)Simulation of AtmosphereModel{CPU, RectilinearGrid}(time = 0 seconds, iteration = 0)
├── Next time step: 24 seconds
├── run_wall_time: 0 seconds
├── run_wall_time / iteration: NaN days
├── stop_time: 50 minutes
├── stop_iteration: Inf
├── wall_time_limit: Inf
├── minimum_relative_step: 0.0
├── callbacks: OrderedDict with 4 entries:
│ ├── stop_time_exceeded => Callback of stop_time_exceeded on IterationInterval(1)
│ ├── stop_iteration_exceeded => Callback of stop_iteration_exceeded on IterationInterval(1)
│ ├── wall_time_limit_exceeded => Callback of wall_time_limit_exceeded on IterationInterval(1)
│ └── nan_checker => Callback of NaNChecker for ρu on IterationInterval(100)
└── output_writers: OrderedDict with no entriesProgress callback:
θ = PotentialTemperature(model)
θᵇᵍf = CenterField(grid)
set!(θᵇᵍf, (x, z) -> θᵇᵍ(z))
θ′ = θ - θᵇᵍf
function progress(sim)
u, v, w = sim.model.velocities
msg = @sprintf("Iter: % 4d, t: % 14s, max(θ′): %.4e, max|w|: %.4f",
iteration(sim), prettytime(sim), maximum(θ′), maximum(abs, w))
@info msg
return nothing
end
add_callback!(simulation, progress, IterationInterval(20))Output
We save the potential temperature for visualization, including an animation of the wave propagation:
outputs = merge(model.velocities, (; θ′))
filename = "inertia_gravity_wave.jld2"
simulation.output_writers[:jld2] = JLD2Writer(model, outputs; filename,
schedule = TimeInterval(100),
overwrite_existing = true)
run!(simulation)[ Info: Initializing simulation...
[ Info: Iter: 0, t: 0 seconds, max(θ′): 9.7791e-03, max|w|: 0.0000
[ Info: ... simulation initialization complete (33.350 seconds)
[ Info: Executing initial time step...
[ Info: ... initial time step complete (3.811 seconds).
[ Info: Iter: 20, t: 6.667 minutes, max(θ′): 4.5377e-03, max|w|: 0.0060
[ Info: Iter: 40, t: 13.333 minutes, max(θ′): 3.8578e-03, max|w|: 0.0049
[ Info: Iter: 60, t: 20 minutes, max(θ′): 3.5220e-03, max|w|: 0.0042
[ Info: Iter: 80, t: 26.667 minutes, max(θ′): 3.2983e-03, max|w|: 0.0036
[ Info: Iter: 100, t: 33.333 minutes, max(θ′): 3.1264e-03, max|w|: 0.0032
[ Info: Iter: 120, t: 40 minutes, max(θ′): 2.9939e-03, max|w|: 0.0029
[ Info: Iter: 140, t: 46.667 minutes, max(θ′): 2.8867e-03, max|w|: 0.0027
[ Info: Simulation is stopping after running for 39.006 seconds.
[ Info: Simulation time 50 minutes equals or exceeds stop time 50 minutes.
Results: potential temperature perturbation
Following Skamarock and Klemp (1994), we visualize the potential temperature perturbation $θ' = θ - θ^{\rm bg}$. The final state at $t = 3000 \, {\rm s}$ can be compared directly to Figure 3b in the paper by Skamarock and Klemp (1994), which shows the analytic solution for incompressible flow.
The CM1 model test page provides additional comparisons between compressible, anelastic, and incompressible solvers.
θ′t = FieldTimeSeries(filename, "θ′")
times = θ′t.times
Nt = length(times)31Plot the final potential temperature perturbation (compare to Figure 3b by Skamarock and Klemp (1994)):
θ′N = θ′t[Nt]
fig = Figure(size=(800, 300))
ax = Axis(fig[1, 1], xlabel = "x (m)", ylabel = "z (m)",
title = "Potential temperature perturbation θ′ at t = $(prettytime(times[Nt]))")
levels = range(-Δθ/2, stop=Δθ/2, length=20)
contourf!(ax, θ′N, colormap=:balance; levels)
figAnimation of wave propagation
The animation shows the evolution of the potential temperature perturbation as the inertia-gravity waves propagate away from the initial disturbance:
n = Observable(1)
θ′n = @lift θ′t[$n]
fig = Figure(size=(800, 300))
ax = Axis(fig[1, 1], xlabel = "x (m)", ylabel = "z (m)")
title = @lift "Potential temperature perturbation θ′ at t = $(prettytime(times[$n]))"
fig[0, :] = Label(fig, title, fontsize=16, tellwidth=false)
heatmap!(ax, θ′n, colormap = :balance, colorrange = (-Δθ/2, Δθ/2))
record(fig, "inertia_gravity_wave.mp4", 1:Nt, framerate=8) do nn
n[] = nn
endJulia version and environment information
This example was executed with the following version of Julia:
using InteractiveUtils: versioninfo
versioninfo()Julia Version 1.12.2
Commit ca9b6662be4 (2025-11-20 16:25 UTC)
Build Info:
Official https://julialang.org release
Platform Info:
OS: Linux (x86_64-linux-gnu)
CPU: 8 × AMD EPYC 7R13 Processor
WORD_SIZE: 64
LLVM: libLLVM-18.1.7 (ORCJIT, znver3)
GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 8 virtual cores)
Environment:
JULIA_GPG = 3673DF529D9049477F76B37566E3C7DC03D6E495
JULIA_LOAD_PATH = :@breeze
JULIA_VERSION = 1.12.2
JULIA_DEPOT_PATH = /usr/local/share/julia:
JULIA_PATH = /usr/local/julia
JULIA_PROJECT = @breeze
These were the top-level packages installed in the environment:
import Pkg
Pkg.status()Status `/__w/Breeze.jl/Breeze.jl/docs/Project.toml`
[86bc3604] AtmosphericProfilesLibrary v0.1.7
[660aa2fb] Breeze v0.2.1 `.`
[052768ef] CUDA v5.9.5
[13f3f980] CairoMakie v0.15.8
[6a9e3e04] CloudMicrophysics v0.29.1
[e30172f5] Documenter v1.16.1
[daee34ce] DocumenterCitations v1.4.1
[98b081ad] Literate v2.21.0
⌅ [9e8cae18] Oceananigans v0.102.5
[a01a1ee8] RRTMGP v0.21.6
[b77e0a4c] InteractiveUtils v1.11.0
[44cfe95a] Pkg v1.12.0
[9a3f8284] Random v1.11.0
Info Packages marked with ⌅ have new versions available but compatibility constraints restrict them from upgrading. To see why use `status --outdated`
This page was generated using Literate.jl.