Diurnal cycle of radiative convection
This example simulates a dramatic diurnal cycle of moist convection over a tropical land surface. During the day, the sun heats the ground, driving vigorous boundary-layer convection that lofts moisture into towering cumulus clouds. At night, the surface cools rapidly by longwave emission, stabilizing the boundary layer and shutting off convection.
The key ingredient is a time-varying surface temperature that follows the sun: peaking in the early afternoon and dipping well below the air temperature at night. This creates a strong diurnal contrast — afternoon thunderstorms that die at sunset and don't return until the next morning.
Interactive all-sky radiation with the ecCKD gas optics of NumericalRadiation.jl computes spectrally-resolved shortwave and longwave fluxes, with cloud scattering tables mapped onto the same spectral g-points. Saturation-adjustment microphysics diagnoses cloud liquid water that feeds back on the radiation. A stretched vertical grid resolves the cloud layer (100 m spacing below 3 km) while extending to 15 km, and the radiation model continues the column above the grid top through a standard-atmosphere stratosphere to 65 km. A sponge layer above 8 km prevents spurious temperature drift in the coarse upper cells.
using Breezeusing Oceananigansusing Oceananigans.Unitsusing Dates: DateTimeusing Printf, Random, Statisticsusing CairoMakieusing NCDatasets # Required for the ecCKD lookup tablesusing NumericalRadiation: NumericalRadiationusing CUDARandom.seed!(2025)if CUDA.functional() CUDA.seed!(2025)endGrid
We use a 2D vertical slice (x-z) that is periodic in x and bounded in z. The vertical grid is stretched: fine 100 m cells resolve the cloud layer below 3 km, then a smooth transition to 1 km cells carries the column up to 15 km. The radiation model's OverlyingAtmosphereColumn adds the stratosphere above the grid top, so the total cell count stays modest.
Nx = 128Lx = 12800 # 12.8 kmarch = GPU()Oceananigans.defaults.FloatType = Float32z = PiecewiseStretchedDiscretization( z = [0, 3000, 8000, 15000], Δz = [100, 100, 1000, 1000])Nz = length(z) - 1grid = RectilinearGrid(arch; size = (Nx, Nz), x = (0, Lx), z, halo = (5, 5), topology = (Periodic, Flat, Bounded))128×1×51 RectilinearGrid{Float32, Periodic, Flat, Bounded} on CUDAGPU with 5×0×5 halo
├── Periodic x ∈ [0.0, 12800.0) regularly spaced with Δx=100.0
├── Flat y
└── Bounded z ∈ [0.0, 15000.0] variably spaced with min(Δz)=100.0, max(Δz)=1000.0Reference state
p₀ = 101325 # Surface pressure [Pa]θ₀ = 300 # Reference potential temperature [K]constants = ThermodynamicConstants()reference_state = ReferenceState(grid, constants; base_pressure = p₀, potential_temperature = θ₀, vapor_mass_fraction = 0)dynamics = AnelasticDynamics(reference_state)AnelasticDynamics(p₀=101325.0, θ₀=300.0)
├── pressure_solver_float_type: Float64
└── pressure_anomaly: not materializedBackground atmosphere
The ecCKD gas optics require trace gas concentrations to compute spectral absorption and emission. We specify well-mixed greenhouse gas concentrations and a tropical ozone profile that transitions from low tropospheric values to a stratospheric peak near 25 km. The peak lies above the 15 km grid top, but the radiation model evaluates the ozone profile in its overlying atmosphere too, so the stratospheric ozone still absorbs sunlight before it reaches the grid.
@inline function tropical_ozone(z) FT = float(typeof(z)) # in the grid's precision, which the radiation kernels evaluate it in troposphere_O₃ = FT(30e-9) * (1 + FT(0.5) * z / 10_000) zˢᵗ = FT(25e3) Hˢᵗ = FT(5e3) stratosphere_O₃ = FT(8e-6) * exp(-((z - zˢᵗ) / Hˢᵗ)^2) χˢᵗ = 1 / (1 + exp(-(z - FT(15e3)) / 2)) return troposphere_O₃ * (1 - χˢᵗ) + stratosphere_O₃ * χˢᵗendbackground_atmosphere = BackgroundAtmosphere( CO₂ = 348e-6, CH₄ = 1650e-9, N₂O = 306e-9, O₃ = tropical_ozone)BackgroundAtmosphere with 6 active gases:
N₂ = 0.78084
O₂ = 0.20946
CO₂ = 348.0 ppm
CH₄ = 1.65 ppm
N₂O = 306.0 ppb
O₃ = tropical_ozone (generic function with 1 method)Diurnal surface temperature
Over land, the surface temperature swings dramatically with the sun. We model this as a sinusoidal cycle peaking 2 hours after solar noon: Tₛ(t) = T̄ₛ + ΔTₛ cos(2π(t - t_peak) / 24h), with T̄ₛ = 300 K (mean) and ΔTₛ = 10 K (amplitude). This gives 310 K in early afternoon and 290 K at night — a 20 K diurnal range, typical of tropical semi-arid land.
We start at midnight (t = 0), so the surface starts cold (290 K), warms through the morning, peaks at t = 14 h (2 pm local), and cools at night. A Field stores the surface temperature and a callback updates it each time step, keeping both the bulk fluxes and the radiation in sync.
T̄ₛ = 300 # Mean surface temperature [K]ΔTₛ = 20 # Diurnal amplitude [K]Tₛ = Field{Center, Center, Nothing}(grid)set!(Tₛ, T̄ₛ - ΔTₛ) # Start at midnight minimum128×1×1 Field{Center, Center, Nothing} reduced over dims = (3,) on RectilinearGrid on CUDAGPU
├── grid: 128×1×51 RectilinearGrid{Float32, Periodic, Flat, Bounded} on CUDAGPU with 5×0×5 halo
├── boundary conditions: FieldBoundaryConditions
│ └── west: Periodic, east: Periodic, south: Nothing, north: Nothing, bottom: Nothing, top: Nothing, immersed: Nothing
└── data: 138×1×1 StaticStridesOffsetArray(::CUDACore.CuArray{Float32, 3, CUDACore.DeviceMemory}, -4:133, 1:1, 1:1) with eltype Float32 with indices -4:133×1:1×1:1
└── max=280.0, min=280.0, mean=280.0Radiation with a diurnal cycle
We place the domain at 15°N latitude on the prime meridian and start at midnight on the spring equinox (March 20). The sun rises at t ≈ 6 h, reaches noon at t ≈ 12 h, and sets at t ≈ 18 h.
latitude = 15 # 15°Noptics = EcCKDOptics(clouds = CloudScatteringTables())radiation = RadiativeTransferModel(grid, optics, constants; surface_temperature = Tₛ, surface_albedo = 0.20, surface_emissivity = 0.95, solar_constant = 1361, background_atmosphere, solar_position = ApparentSolarPosition(coordinate = (0, latitude), epoch = DateTime(2020, 3, 20, 0, 0, 0)), schedule = TimeInterval(5minutes), liquid_effective_radius = ConstantRadiusParticles(10e-6), ice_effective_radius = ConstantRadiusParticles(30e-6))RadiativeTransferModel
├── solar_constant: 1361.0 W m⁻²
├── solar_position: ApparentSolarPosition(coordinate=(0, 15), epoch=2020-03-20T00:00:00)
├── surface_temperature: 128×1×1 Field{Center, Center, Nothing} reduced over dims = (3,) on RectilinearGrid on CUDAGPU
├── surface_emissivity: ConstantField(0.95)
├── direct_surface_albedo: ConstantField(0.2)
├── liquid_effective_radius: Breeze.AtmosphereModels.ConstantRadiusParticles{Float32}(1.0f-5)
├── ice_effective_radius: Breeze.AtmosphereModels.ConstantRadiusParticles{Float32}(3.0f-5)
├── diffuse_surface_albedo: ConstantField(0.2)
├── optics: EcCKDOptics with 32 longwave and 32 shortwave g-points, all sky
└── overlying_atmosphere: 40 layers from 15000.0 m to 65000.0 mSurface fluxes
Bulk aerodynamic formulae provide surface sensible heat, moisture, and momentum fluxes driven by the time-varying surface temperature. During the day Tₛ > Tair drives strong upward fluxes; at night Tₛ < Tair can produce downward fluxes that cool the boundary layer.
Cᴰ = Cᵀ = 1e-3Cᵛ = 1.2e-3Uᵍ = 1 # Gustiness [m/s]ρθ_flux = BulkSensibleHeatFlux(coefficient=Cᵀ, gustiness=Uᵍ, surface_temperature=Tₛ)ρqᵗ_flux = BulkVaporFlux(coefficient=Cᵛ, gustiness=Uᵍ, surface_temperature=Tₛ)ρθ_bcs = FieldBoundaryConditions(bottom=ρθ_flux)ρqᵗ_bcs = FieldBoundaryConditions(bottom=ρqᵗ_flux)ρu_bcs = FieldBoundaryConditions(bottom=Breeze.BulkDrag(coefficient=Cᴰ, gustiness=Uᵍ))Oceananigans.FieldBoundaryConditions, with boundary conditions
├── west: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── east: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── south: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── north: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── bottom: FluxBoundaryCondition: BulkDragFunction(direction=Nothing, coefficient=0.001, gustiness=1)
├── top: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
└── immersed: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)Microphysics
Warm-phase saturation adjustment instantly converts supersaturated moisture to cloud liquid water.
microphysics = SaturationAdjustment(equilibrium=WarmPhaseEquilibrium())Breeze.Microphysics.SaturationAdjustment{Breeze.Thermodynamics.WarmPhaseEquilibrium, Breeze.Solvers.SecantSolver{Float32}}(Breeze.Thermodynamics.WarmPhaseEquilibrium(), SecantSolver(reltol=0.0, abstol=0.0001, maxiter=20))Stratospheric sponge
The domain extends to 15 km, but the initial upper troposphere and lower stratosphere aren't in radiative equilibrium: ozone absorbs shortwave radiation and the coarse upper cells respond strongly. A Newtonian relaxation of temperature toward the initial profile above 8 km keeps the upper cells anchored without affecting the tropospheric dynamics. We apply this as an energy forcing on ρE, which Breeze automatically converts to a ρθ tendency.
Tᵣ = reference_state.temperatureρᵣ = reference_state.densitycᵖᵈ = constants.dry_air.heat_capacity # J/(kg·K)τ_sponge = 6hours@inline function stratospheric_relaxation(i, j, k, grid, clock, model_fields, p) @inbounds T = model_fields.T[i, j, k] @inbounds Tᵣ = p.Tᵣ[i, j, k] @inbounds ρ = p.ρᵣ[i, j, k] z = znode(i, j, k, grid, Center(), Center(), Center()) α = clamp((z - 8000) / 4000, 0, 1) ∂T∂t = -α * (T - Tᵣ) / p.τ return ρ * p.cᵖᵈ * ∂T∂tendstratospheric_relaxation (generic function with 1 method)The parameters take the grid's float type, as the kernels compute in it
FT = eltype(grid)sponge = Forcing(stratospheric_relaxation; discrete_form=true, parameters=(; Tᵣ, ρᵣ, cᵖᵈ=convert(FT, cᵖᵈ), τ=convert(FT, τ_sponge)))forcing = (; ρE=sponge)(ρE = DiscreteForcing{@NamedTuple{Tᵣ::Oceananigans.Fields.Field{Nothing, Nothing, Oceananigans.Grids.Center, Nothing, Oceananigans.Grids.RectilinearGrid{Float32, Oceananigans.Grids.Periodic, Oceananigans.Grids.Flat, Oceananigans.Grids.Bounded, Oceananigans.Grids.StaticVerticalDiscretization{Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (62,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (61,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (62,), (-6,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (61,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}}, Float32, Float32, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (138,), (-5,), StepRangeLen{Float32, Float64, Float64, Int64}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (138,), (-5,), StepRangeLen{Float32, Float64, Float64, Int64}}, Float32, Float32, Nothing, Nothing, Oceananigans.Architectures.GPU{CUDACore.CUDAKernels.CUDABackend}, Oceananigans.Grids.GridSize{128, 1, 51, 5, 0, 5}}, Tuple{Colon, Colon, Colon}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 3, (1, 1, 61), (0, 0, -5), CUDACore.CuArray{Float32, 3, CUDACore.DeviceMemory}}, Float32, Oceananigans.BoundaryConditions.FieldBoundaryConditions{Nothing, Nothing, Nothing, Nothing, Oceananigans.BoundaryConditions.NoFluxBoundaryCondition{Oceananigans.BoundaryConditions.Flux{Oceananigans.Utils.ExplicitTimeDiscretization}}, Oceananigans.BoundaryConditions.NoFluxBoundaryCondition{Oceananigans.BoundaryConditions.Flux{Oceananigans.Utils.ExplicitTimeDiscretization}}, Nothing, @NamedTuple{south_and_north::Nothing, west_and_east::Nothing, bottom_and_top::KernelAbstractions.Kernel{CUDACore.CUDAKernels.CUDABackend, KernelAbstractions.NDIteration.StaticSize{(1, 1)}, Oceananigans.Utils.OffsetStaticSize{(1:1, 1:1)}, typeof(Oceananigans.BoundaryConditions.gpu__fill_bottom_and_top_halo!)}}, @NamedTuple{south_and_north::Tuple{Nothing, Nothing}, west_and_east::Tuple{Nothing, Nothing}, bottom_and_top::Tuple{Oceananigans.BoundaryConditions.NoFluxBoundaryCondition{Oceananigans.BoundaryConditions.Flux{Oceananigans.Utils.ExplicitTimeDiscretization}}, Oceananigans.BoundaryConditions.NoFluxBoundaryCondition{Oceananigans.BoundaryConditions.Flux{Oceananigans.Utils.ExplicitTimeDiscretization}}}}}, Nothing, Nothing}, ρᵣ::Oceananigans.Fields.Field{Nothing, Nothing, Oceananigans.Grids.Center, Nothing, Oceananigans.Grids.RectilinearGrid{Float32, Oceananigans.Grids.Periodic, Oceananigans.Grids.Flat, Oceananigans.Grids.Bounded, Oceananigans.Grids.StaticVerticalDiscretization{Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (62,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (61,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (62,), (-6,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (61,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}}, Float32, Float32, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (138,), (-5,), StepRangeLen{Float32, Float64, Float64, Int64}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (138,), (-5,), StepRangeLen{Float32, Float64, Float64, Int64}}, Float32, Float32, Nothing, Nothing, Oceananigans.Architectures.GPU{CUDACore.CUDAKernels.CUDABackend}, Oceananigans.Grids.GridSize{128, 1, 51, 5, 0, 5}}, Tuple{Colon, Colon, Colon}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 3, (1, 1, 61), (0, 0, -5), CUDACore.CuArray{Float32, 3, CUDACore.DeviceMemory}}, Float32, Oceananigans.BoundaryConditions.FieldBoundaryConditions{Nothing, Nothing, Nothing, Nothing, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Value{Nothing}, Oceananigans.Fields.Field{Oceananigans.Grids.Center, Oceananigans.Grids.Center, Nothing, Nothing, Oceananigans.Grids.RectilinearGrid{Float32, Oceananigans.Grids.Periodic, Oceananigans.Grids.Flat, Oceananigans.Grids.Bounded, Oceananigans.Grids.StaticVerticalDiscretization{Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (62,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (61,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (62,), (-6,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (61,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}}, Float32, Float32, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (138,), (-5,), StepRangeLen{Float32, Float64, Float64, Int64}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (138,), (-5,), StepRangeLen{Float32, Float64, Float64, Int64}}, Float32, Float32, Nothing, Nothing, Oceananigans.Architectures.GPU{CUDACore.CUDAKernels.CUDABackend}, Oceananigans.Grids.GridSize{128, 1, 51, 5, 0, 5}}, Tuple{Colon, Colon, Colon}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 3, (138, 1, 1), (-5, 0, 0), CUDACore.CuArray{Float32, 3, CUDACore.DeviceMemory}}, Float32, Oceananigans.BoundaryConditions.FieldBoundaryConditions{Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}, Nothing, Nothing, Nothing, Nothing, Nothing, @NamedTuple{bottom_and_top::Nothing, south_and_north::Nothing, west_and_east::Oceananigans.BoundaryConditions.PeriodicFillHalo{KernelAbstractions.Kernel{CUDACore.CUDAKernels.CUDABackend, KernelAbstractions.NDIteration.StaticSize{(1, 1)}, Oceananigans.Utils.OffsetStaticSize{(1:1, 1:1)}, typeof(Oceananigans.BoundaryConditions.gpu__fill_periodic_west_and_east_halo!)}, 128, 5}}, @NamedTuple{bottom_and_top::Tuple{Nothing, Nothing}, south_and_north::Tuple{Nothing, Nothing}, west_and_east::Tuple{Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}}}}, Nothing, Nothing}}, Oceananigans.BoundaryConditions.NoFluxBoundaryCondition{Oceananigans.BoundaryConditions.Flux{Oceananigans.Utils.ExplicitTimeDiscretization}}, Nothing, @NamedTuple{south_and_north::Nothing, west_and_east::Nothing, bottom_and_top::KernelAbstractions.Kernel{CUDACore.CUDAKernels.CUDABackend, KernelAbstractions.NDIteration.StaticSize{(1, 1)}, Oceananigans.Utils.OffsetStaticSize{(1:1, 1:1)}, typeof(Oceananigans.BoundaryConditions.gpu__fill_bottom_and_top_halo!)}}, @NamedTuple{south_and_north::Tuple{Nothing, Nothing}, west_and_east::Tuple{Nothing, Nothing}, bottom_and_top::Tuple{Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Value{Nothing}, Oceananigans.Fields.Field{Oceananigans.Grids.Center, Oceananigans.Grids.Center, Nothing, Nothing, Oceananigans.Grids.RectilinearGrid{Float32, Oceananigans.Grids.Periodic, Oceananigans.Grids.Flat, Oceananigans.Grids.Bounded, Oceananigans.Grids.StaticVerticalDiscretization{Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (62,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (61,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (62,), (-6,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (61,), (-5,), CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}}, Float32, Float32, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (138,), (-5,), StepRangeLen{Float32, Float64, Float64, Int64}}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 1, (138,), (-5,), StepRangeLen{Float32, Float64, Float64, Int64}}, Float32, Float32, Nothing, Nothing, Oceananigans.Architectures.GPU{CUDACore.CUDAKernels.CUDABackend}, Oceananigans.Grids.GridSize{128, 1, 51, 5, 0, 5}}, Tuple{Colon, Colon, Colon}, Oceananigans.Utils.StaticStridesOffsetArray{Float32, 3, (138, 1, 1), (-5, 0, 0), CUDACore.CuArray{Float32, 3, CUDACore.DeviceMemory}}, Float32, Oceananigans.BoundaryConditions.FieldBoundaryConditions{Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}, Nothing, Nothing, Nothing, Nothing, Nothing, @NamedTuple{bottom_and_top::Nothing, south_and_north::Nothing, west_and_east::Oceananigans.BoundaryConditions.PeriodicFillHalo{KernelAbstractions.Kernel{CUDACore.CUDAKernels.CUDABackend, KernelAbstractions.NDIteration.StaticSize{(1, 1)}, Oceananigans.Utils.OffsetStaticSize{(1:1, 1:1)}, typeof(Oceananigans.BoundaryConditions.gpu__fill_periodic_west_and_east_halo!)}, 128, 5}}, @NamedTuple{bottom_and_top::Tuple{Nothing, Nothing}, south_and_north::Tuple{Nothing, Nothing}, west_and_east::Tuple{Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}}}}, Nothing, Nothing}}, Oceananigans.BoundaryConditions.NoFluxBoundaryCondition{Oceananigans.BoundaryConditions.Flux{Oceananigans.Utils.ExplicitTimeDiscretization}}}}}, Nothing, Nothing}, cᵖᵈ::Float32, τ::Float32}}
├── func: stratospheric_relaxation (generic function with 1 method)
└── parameters: (Tᵣ = 1×1×51 Field{Nothing, Nothing, Center} reduced over dims = (1, 2) on RectilinearGrid on CUDAGPU
├── grid: 128×1×51 RectilinearGrid{Float32, Periodic, Flat, Bounded} on CUDAGPU with 5×0×5 halo
├── boundary conditions: FieldBoundaryConditions
│ └── west: Nothing, east: Nothing, south: Nothing, north: Nothing, bottom: ZeroFlux, top: ZeroFlux, immersed: Nothing
└── data: 1×1×61 StaticStridesOffsetArray(::CUDACore.CuArray{Float32, 3, CUDACore.DeviceMemory}, 1:1, 1:1, -4:56) with eltype Float32 with indices 1:1×1:1×-4:56
└── max=300.642, min=159.593, mean=264.678, ρᵣ = 1×1×51 Field{Nothing, Nothing, Center} reduced over dims = (1, 2) on RectilinearGrid on CUDAGPU
├── grid: 128×1×51 RectilinearGrid{Float32, Periodic, Flat, Bounded} on CUDAGPU with 5×0×5 halo
├── boundary conditions: FieldBoundaryConditions
│ └── west: Nothing, east: Nothing, south: Nothing, north: Nothing, bottom: Value, top: ZeroFlux, immersed: Nothing
└── data: 1×1×61 StaticStridesOffsetArray(::CUDACore.CuArray{Float32, 3, CUDACore.DeviceMemory}, 1:1, 1:1, -4:56) with eltype Float32 with indices 1:1×1:1×-4:56
└── max=1.16766, min=0.239471, mean=0.876104, cᵖᵈ = 1005.0f0, τ = 21600.0f0),)Model assembly
coriolis = FPlane(; latitude)boundary_conditions = (ρθ=ρθ_bcs, ρqᵗ=ρqᵗ_bcs, ρu=ρu_bcs)weno_order = 5momentum_advection = WENO(order=weno_order)scalar_advection = (ρθ = WENO(order=weno_order), ρqᵉ = WENO(order=weno_order, bounds=(0, 1)))model = AtmosphereModel(grid; dynamics, microphysics, radiation, forcing, momentum_advection, scalar_advection, boundary_conditions, coriolis)AtmosphereModel{GPU, RectilinearGrid}(time = 0 seconds, iteration = 0)
├── grid: 128×1×51 RectilinearGrid{Float32, Periodic, Flat, Bounded} on CUDAGPU with 5×0×5 halo
├── dynamics: AnelasticDynamics(p₀=101325.0, θ₀=300.0)
├── formulation: LiquidIcePotentialTemperatureFormulation
├── thermodynamic_constants: ThermodynamicConstants{Float32}
├── timestepper: SSPRungeKutta3
├── advection scheme:
│ ├── momentum: WENO{3, Float32, Oceananigans.Utils.BackendOptimizedDivision}(order=5)
│ ├── ρθ: WENO{3, Float32, Oceananigans.Utils.BackendOptimizedDivision}(order=5)
│ └── ρqᵉ: WENO{3, Float32, Oceananigans.Utils.BackendOptimizedDivision}(order=5, bounds=(0.0, 1.0))
├── forcing: ρE=>DiscreteForcing
├── tracers: ()
├── coriolis: FPlane{Float32}(f=3.77468e-5)
└── microphysics: SaturationAdjustmentInitial conditions
The sounding has a dry-adiabatic sub-cloud layer (0–1 km) capped by a conditionally unstable troposphere (5 K/km lapse rate) that transitions to an isothermal stratosphere at 210 K. Moisture is 20 g/kg at the surface with a 2.5 km scale height, typical of the tropical maritime boundary layer.
function Tᵇᵍ(z) T₀ = 300 Tˢᵗ = 210 # stratosphere temperature T = T₀ - 1e-3 * max(z, 1000) - 5e-3 * max(0, z - 1000) return max(T, Tˢᵗ)enduᵢ(x, z) = -5 * max(1 - z / 3000, 0)uᵢ (generic function with 1 method)Random perturbations in the lowest 1 km trigger convection.
δT = 2δℋ = 1e-2zδ = 1000ϵ() = rand() - 0.5Tᵢ(x, z) = Tᵇᵍ(z) + δT * ϵ() * (z < zδ)ℋᵢ(x, z) = (0.5 + δℋ * ϵ()) * (z < zδ)ℋᵢ (generic function with 1 method)After setting initial conditions, we recompute the reference state from the horizontally-averaged model state. This is important for Float32 accuracy in tall domains: the default dry-adiabat reference state diverges from the actual stratospheric profile, causing large density errors that overwhelm Float32 precision. set_to_mean! adjusts ρᵣ to match the current state while rescaling density-weighted prognostic fields to preserve specific quantities.
set!(model; T=Tᵢ, ℋ=ℋᵢ, u=uᵢ)reference_state = model.dynamics.reference_stateset_to_mean!(reference_state, model, rescale_densities=true)T = model.temperatureqᵗ = specific_prognostic_moisture(model)u, w = model.velocities.u, model.velocities.wqˡ = model.microphysical_fields.qˡ@info "Diurnal Radiative Convection (2D)"@info "Grid: $(Nx) × $(Nz) (stretched), domain: $(Lx/1000) km × 15 km"@info "Initial T range: $(minimum(T)) – $(maximum(T)) K"@info "Initial qᵗ range: $(minimum(qᵗ)*1000) – $(maximum(qᵗ)*1000) g/kg"[ Info: Diurnal Radiative Convection (2D)
[ Info: Grid: 128 × 51 (stretched), domain: 12.8 km × 15 km
[ Info: Initial T range: 238.17137 – 300.11438 K
[ Info: Initial qᵗ range: 0.0 – 12.133494 g/kg
Simulation
We run for two full diurnal cycles (48 hours) starting at midnight, so the on/off pattern of convection repeats convincingly.
simulation = Simulation(model; Δt=1, stop_time=3days)conjure_time_step_wizard!(simulation, cfl=0.7)Oceananigans.Diagnostics.erroring_NaNChecker!(simulation)Surface temperature callback
At each time step we update the surface temperature field following a cosine curve that peaks 2 hours after solar noon (t = 14 h local). The period is 24 hours and the simulation starts at midnight.
function update_surface_temperature!(sim) t = time(sim) t_peak = 14hour # peak at 14:00 local (2 pm) Tₛ_now = T̄ₛ + ΔTₛ * cos(2π * (t - t_peak) / day) set!(Tₛ, Tₛ_now) return nothingendadd_callback!(simulation, update_surface_temperature!, TimeInterval(1minute))wall_clock = Ref(time_ns())function progress(sim) elapsed = 1e-9 * (time_ns() - wall_clock[]) wmax = maximum(abs, w) Tmin, Tmax = extrema(T) qˡmax = maximum(qˡ) Tₛ_now = T̄ₛ + ΔTₛ * cos(2π * (time(sim) - 14hours) / 24hours) OLR = mean(view(radiation.upwelling_longwave_flux, :, 1, Nz+1)) msg = @sprintf("Iter: %5d, t: %8s, Δt: %5.1fs, wall: %8s", iteration(sim), prettytime(sim), sim.Δt, prettytime(elapsed)) msg *= @sprintf(", max|w|: %5.2f m/s, T: [%5.1f, %5.1f] K, max(qˡ): %.2e", wmax, Tmin, Tmax, qˡmax) msg *= @sprintf(", Tₛ: %.1f K, OLR: %.1f W/m²", Tₛ_now, OLR) @info msg wall_clock[] = time_ns() return nothingendadd_callback!(simulation, progress, IterationInterval(1000))Output
Horizontally-averaged profiles are saved every hour (time-averaged) and 2D slices every 10 minutes for animation.
qᵛ = model.microphysical_fields.qᵛFᴿ = radiation.flux_divergenceoutputs = (; u, w, T, qˡ, qᵛ, Fᴿ)avg_outputs = NamedTuple(name => Average(outputs[name], dims=1) for name in keys(outputs))filename = "radiative_convection"averages_filename = filename * "_averages.jld2"slices_filename = filename * "_slices.jld2"simulation.output_writers[:averages] = JLD2Writer(model, avg_outputs; filename = averages_filename, schedule = AveragedTimeInterval(1hour), overwrite_files = true)slice_outputs = (; w, qᵛ, T)simulation.output_writers[:slices] = JLD2Writer(model, slice_outputs; filename = slices_filename, schedule = TimeInterval(10minutes), overwrite_files = true)@info "Starting simulation..."run!(simulation)@info "Simulation completed!"[ Info: Starting simulation...
[ Info: Initializing simulation...
[ Info: Iter: 0, t: 0 seconds, Δt: 1.1s, wall: 52.542 seconds, max|w|: 0.00 m/s, T: [238.2, 300.1] K, max(qˡ): 0.00e+00, Tₛ: 282.7 K, OLR: 373.9 W/m²
[ Info: ... simulation initialization complete (40.428 seconds)
[ Info: Executing initial time step...
[ Info: ... initial time step complete (10.398 seconds).
[ Info: Iter: 1000, t: 2.762 hours, Δt: 14.1s, wall: 20.569 seconds, max|w|: 1.76 m/s, T: [230.5, 299.6] K, max(qˡ): 0.00e+00, Tₛ: 280.4 K, OLR: 359.9 W/m²
[ Info: Iter: 2000, t: 6.095 hours, Δt: 14.3s, wall: 4.368 seconds, max|w|: 2.27 m/s, T: [225.1, 299.3] K, max(qˡ): 0.00e+00, Tₛ: 290.4 K, OLR: 365.1 W/m²
[ Info: Iter: 3000, t: 10.490 hours, Δt: 26.4s, wall: 5.168 seconds, max|w|: 1.47 m/s, T: [221.3, 298.9] K, max(qˡ): 1.02e-03, Tₛ: 312.1 K, OLR: 394.3 W/m²
[ Info: Iter: 4000, t: 14.000 hours, Δt: 8.4s, wall: 4.423 seconds, max|w|: 5.23 m/s, T: [219.9, 304.2] K, max(qˡ): 0.00e+00, Tₛ: 320.0 K, OLR: 406.1 W/m²
[ Info: Iter: 5000, t: 15.856 hours, Δt: 5.9s, wall: 3.574 seconds, max|w|: 7.05 m/s, T: [219.1, 305.9] K, max(qˡ): 0.00e+00, Tₛ: 317.7 K, OLR: 396.9 W/m²
[ Info: Iter: 6000, t: 17.481 hours, Δt: 5.1s, wall: 3.653 seconds, max|w|: 6.59 m/s, T: [218.7, 305.7] K, max(qˡ): 0.00e+00, Tₛ: 312.3 K, OLR: 385.3 W/m²
[ Info: Iter: 7000, t: 19.262 hours, Δt: 7.1s, wall: 4.121 seconds, max|w|: 5.10 m/s, T: [218.4, 305.2] K, max(qˡ): 0.00e+00, Tₛ: 303.8 K, OLR: 368.3 W/m²
[ Info: Iter: 8000, t: 21.555 hours, Δt: 9.3s, wall: 6.079 seconds, max|w|: 3.03 m/s, T: [218.3, 304.8] K, max(qˡ): 0.00e+00, Tₛ: 292.1 K, OLR: 348.6 W/m²
[ Info: Iter: 9000, t: 1.003 days, Δt: 10.0s, wall: 3.636 seconds, max|w|: 2.73 m/s, T: [218.2, 304.7] K, max(qˡ): 0.00e+00, Tₛ: 282.5 K, OLR: 332.9 W/m²
[ Info: Iter: 10000, t: 1.108 days, Δt: 9.7s, wall: 3.928 seconds, max|w|: 3.90 m/s, T: [218.1, 303.7] K, max(qˡ): 0.00e+00, Tₛ: 280.2 K, OLR: 326.8 W/m²
[ Info: Iter: 11000, t: 1.219 days, Δt: 10.9s, wall: 3.862 seconds, max|w|: 4.01 m/s, T: [217.9, 302.8] K, max(qˡ): 0.00e+00, Tₛ: 286.8 K, OLR: 331.2 W/m²
[ Info: Iter: 12000, t: 1.344 days, Δt: 14.0s, wall: 4.223 seconds, max|w|: 2.66 m/s, T: [217.9, 302.6] K, max(qˡ): 6.82e-05, Tₛ: 301.3 K, OLR: 347.4 W/m²
[ Info: Iter: 13000, t: 1.548 days, Δt: 9.4s, wall: 5.167 seconds, max|w|: 5.76 m/s, T: [218.0, 304.2] K, max(qˡ): 3.07e-04, Tₛ: 319.5 K, OLR: 355.7 W/m²
[ Info: Iter: 14000, t: 1.626 days, Δt: 6.3s, wall: 3.752 seconds, max|w|: 8.88 m/s, T: [217.7, 305.7] K, max(qˡ): 9.28e-04, Tₛ: 319.3 K, OLR: 343.5 W/m²
[ Info: Iter: 15000, t: 1.695 days, Δt: 6.8s, wall: 3.929 seconds, max|w|: 6.04 m/s, T: [217.7, 305.5] K, max(qˡ): 8.58e-04, Tₛ: 315.3 K, OLR: 348.1 W/m²
[ Info: Iter: 16000, t: 1.773 days, Δt: 8.6s, wall: 3.963 seconds, max|w|: 4.48 m/s, T: [217.6, 305.2] K, max(qˡ): 5.20e-04, Tₛ: 307.4 K, OLR: 338.6 W/m²
[ Info: Iter: 17000, t: 1.895 days, Δt: 16.3s, wall: 4.728 seconds, max|w|: 1.46 m/s, T: [217.7, 304.1] K, max(qˡ): 0.00e+00, Tₛ: 292.4 K, OLR: 329.5 W/m²
[ Info: Iter: 18000, t: 2.079 days, Δt: 20.9s, wall: 5.884 seconds, max|w|: 0.85 m/s, T: [217.8, 303.2] K, max(qˡ): 1.02e-04, Tₛ: 280.0 K, OLR: 312.4 W/m²
[ Info: Iter: 19000, t: 2.306 days, Δt: 46.0s, wall: 8.105 seconds, max|w|: 0.61 m/s, T: [217.9, 301.4] K, max(qˡ): 0.00e+00, Tₛ: 296.6 K, OLR: 326.0 W/m²
[ Info: Iter: 20000, t: 2.597 days, Δt: 7.3s, wall: 9.010 seconds, max|w|: 4.73 m/s, T: [218.0, 304.2] K, max(qˡ): 0.00e+00, Tₛ: 319.9 K, OLR: 356.4 W/m²
[ Info: Iter: 21000, t: 2.670 days, Δt: 8.0s, wall: 7.158 seconds, max|w|: 6.82 m/s, T: [217.0, 305.3] K, max(qˡ): 1.09e-03, Tₛ: 317.1 K, OLR: 311.3 W/m²
[ Info: Iter: 22000, t: 2.757 days, Δt: 7.9s, wall: 3.653 seconds, max|w|: 5.44 m/s, T: [217.5, 305.2] K, max(qˡ): 2.37e-04, Tₛ: 309.2 K, OLR: 331.3 W/m²
[ Info: Iter: 23000, t: 2.861 days, Δt: 10.8s, wall: 3.735 seconds, max|w|: 3.18 m/s, T: [217.7, 304.6] K, max(qˡ): 0.00e+00, Tₛ: 296.6 K, OLR: 318.3 W/m²
[ Info: Simulation is stopping after running for 2.761 minutes.
[ Info: Simulation time 3 days equals or exceeds stop time 3 days.
[ Info: Simulation completed!
Hovmöller diagrams of mean profile evolution
Time–height Hovmöller diagrams show how the horizontally-averaged temperature, cloud liquid water, and radiative flux divergence evolve over two diurnal cycles. The vertical axis is zoomed to the lowest 6 km where the convective dynamics live.
Tts = FieldTimeSeries(averages_filename, "T")qᵛts = FieldTimeSeries(averages_filename, "qᵛ")Fᴿts = FieldTimeSeries(averages_filename, "Fᴿ")times = Tts.timesNt = length(times)z = Oceananigans.Grids.znodes(Tts.grid, Center())t_hours = times ./ 36000.0:1.0:72.0Build time–height matrices from the averaged profiles.
T_zt = permutedims(interior(Tts, 1, 1, :, :))T_zt .-= mean(T_zt, dims=1)qᵛ_zt = permutedims(interior(qᵛts, 1, 1, :, :))Fᴿ_zt = permutedims(interior(Fᴿts, 1, 1, :, :))fig = Figure(size=(900, 800), fontsize=14)zmax = 12000axT = Axis(fig[1, 1]; ylabel="z (m)", title="Temperature anomaly (K)", limits=((t_hours[1], t_hours[end]), (0, zmax)))axqᵛ = Axis(fig[2, 1]; ylabel="z (m)", title="Specific humidity (kg/kg)", limits=((t_hours[1], t_hours[end]), (0, zmax)))axF = Axis(fig[3, 1]; ylabel="z (m)", xlabel="time (hours)", title="Radiative flux divergence (W/m³)", limits=((t_hours[1], t_hours[end]), (0, zmax)))hidexdecorations!(axT; grid=false)hidexdecorations!(axqᵛ; grid=false)hmT = heatmap!(axT, t_hours, z, T_zt; colormap=:balance, colorrange=(-2, 2))hmqᵛ = heatmap!(axqᵛ, t_hours, z, qᵛ_zt; colormap=:dense, colorrange=(0, 1e-2))hmF = heatmap!(axF, t_hours, z, Fᴿ_zt; colormap=:balance, colorrange=(-0.1, 0.1))Colorbar(fig[1, 2], hmT; label="T′ (K)")Colorbar(fig[2, 2], hmqᵛ; label="qᵛ (kg/kg)")Colorbar(fig[3, 2], hmF; label="Fᴿ (W/m³)")figAnimation of cloud structure
We animate xz slices of vertical velocity and cloud liquid water, zoomed to the lowest 5 km where the convective dynamics and clouds live.
wts = FieldTimeSeries(slices_filename, "w")qᵛts = FieldTimeSeries(slices_filename, "qᵛ")times = wts.timesNt = length(times)wlim = maximum(abs, wts) / 4qᵛlim = maximum(qᵛts) / 2fig = Figure(size=(1000, 600), fontsize=14)n = Observable(Nt)axw = Axis(fig[1, 1]; xlabel="x (km)", ylabel="z (km)", title="w (m/s)", limits=(nothing, (0, 5e3)))axqᵛ = Axis(fig[1, 2]; xlabel="x (km)", ylabel="z (km)", title="qᵛ (kg/kg)", limits=(nothing, (0, 5e3)))title = @lift "Diurnal Radiative Convection at t = " * prettytime(times[$n])fig[0, :] = Label(fig, title, fontsize=16, tellwidth=false)wn = @lift wts[$n]qᵛn = @lift qᵛts[$n]hmw = heatmap!(axw, wn; colormap=:balance, colorrange=(-wlim, wlim))hmqᵛ = heatmap!(axqᵛ, qᵛn; colormap=:dense, colorrange=(0, qᵛlim))Colorbar(fig[2, 1], hmw; vertical=false, label="w (m/s)")Colorbar(fig[2, 2], hmqᵛ; vertical=false, label="qᵛ (g/kg)")hideydecorations!(axqᵛ; grid=false)CairoMakie.record(fig, "radiative_convection.mp4", 1:Nt; framerate = 12, compression = 23) do nn n[] = nnendJulia version and environment information
This example was executed with the following version of Julia:
using InteractiveUtils: versioninfoversioninfo()Julia Version 1.13.2
Commit 45b6d15e34b (2026-10-14 20:33 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-20.1.8 (ORCJIT, znver3)
GC: Built with stock GC
Threads: 1 default, 1 interactive, 5 GC (on 8 virtual cores)
Environment:
JULIA_NUM_GC_THREADS = 4,1
JULIA_GPG = 64B779A570972FFF7BFC2B54EAD471E1A1F2C10A
JULIA_LOAD_PATH = :@breeze
JULIA_PKG_SERVER_REGISTRY_PREFERENCE = eager
JULIA_VERSION = 1.13.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 PkgPkg.status()Status `/__w/Breeze.jl/Breeze.jl/docs/Project.toml`
⌃ [86bc3604] AtmosphericProfilesLibrary v0.1.8
[660aa2fb] Breeze v0.15.1 `.`
⌃ [052768ef] CUDA v6.4.1
[13f3f980] CairoMakie v0.15.17
[6a9e3e04] CloudMicrophysics v0.49.1
[e30172f5] Documenter v1.20.0
[daee34ce] DocumenterCitations v1.5.0
[b6400b83] DocumenterCodeBlocks v1.5.1
[1c52b33b] DocumenterLandingPage v0.2.2
[7da242da] Enzyme v0.13.219
⌅ [46192b85] GPUArraysCore v0.2.1
⌅ [63c18a36] KernelAbstractions v0.9.47
[98b081ad] Literate v2.21.1
[85f8d34a] NCDatasets v0.14.15
[cd8119b0] NumericalRadiation v0.2.1
[9e8cae18] Oceananigans v0.118.1
[a01a1ee8] RRTMGP v1.1.1
[3c362404] Reactant v0.2.308
[276daf66] SpecialFunctions v2.9.2
[10745b16] Statistics v1.11.5
[b77e0a4c] InteractiveUtils v1.11.0
[44cfe95a] Pkg v1.13.0
[9a3f8284] Random v1.11.0
Info Packages marked with ⌃ and ⌅ have new versions available. Those with ⌃ may be upgradable, but those with ⌅ are restricted by compatibility constraints from upgrading. To see why use `status --outdated`
This page was generated using Literate.jl.