Tropical Cyclone World (Cronin and Chavas, 2019)
This example implements the rotating radiative-convective equilibrium (RCE) experiment from Cronin and Chavas (2019). The experiment demonstrates that tropical cyclones can form and persist even in completely dry atmospheres, challenging the conventional wisdom that moisture is essential for TC dynamics.
The key innovation is the surface wetness parameter $β$, which controls the transition from completely dry ($β = 0$, no evaporation) to fully moist ($β = 1$) conditions. Cronin and Chavas (2019) found that TCs form in both limits, with a "no-storms-land" at intermediate $β$ where spontaneous genesis does not occur. This script defaults to $β = 1$ (moist), which produces robust spontaneous TC genesis at moderate resolution. The simulation approximates the paper's 100-day nonrotating RCE spinup with an equilibrated initial temperature profile (dry adiabat in the troposphere, isothermal stratosphere) and uses warm-phase saturation adjustment microphysics for the moist case.
using Breezeusing Breeze.Thermodynamics: compute_reference_state!using Oceananigans: Oceananigansusing Oceananigans.Unitsusing CairoMakieusing CUDAusing Printfusing RandomRandom.seed!(2019)if CUDA.functional() CUDA.seed!(2019)endOceananigans.defaults.FloatType = Float32Domain and grid
Cronin and Chavas (2019) used a 1152 km × 1152 km domain with 2 km horizontal resolution. To reduce computational costs for the purpose of this example, we use a 288 km × 288 km domain – 4x smaller in both horizontal directions – with a 2x coarser 4 km horizontal resolution. We keep the 28 km domain top, but with 40 m spacing in the lowest kilometers rather than ~16 m, and 1000 m spacing above 3.5 km rather than 500 m (and a smooth transition in between).
arch = GPU()paper_Lx = 1152kilometerspaper_Nx = 576Lx = Ly = paper_Lx / 4Nx = Ny = paper_Nx / 8 |> IntH = 28kilometersΔz_fine = 40 # mΔz_coarse = 1000 # mz = PiecewiseStretchedDiscretization( z = [0, 1000, 3500, H], Δz = [Δz_fine, Δz_fine, Δz_coarse, Δz_coarse])Nz = length(z) - 1grid = RectilinearGrid(arch; size = (Nx, Ny, Nz), halo = (5, 5, 5), x = (0, Lx), y = (0, Ly), z, topology = (Periodic, Periodic, Bounded))72×72×60 RectilinearGrid{Float32, Periodic, Periodic, Bounded} on CUDAGPU with 5×5×5 halo
├── Periodic x ∈ [0.0, 288000.0) regularly spaced with Δx=4000.0
├── Periodic y ∈ [0.0, 288000.0) regularly spaced with Δy=4000.0
└── Bounded z ∈ [0.0, 28000.0] variably spaced with min(Δz)=40.0, max(Δz)=1000.0Reference state and dynamics
We use the anelastic formulation with a reference state initialized from the surface potential temperature $T_0 = 300$ K and standard surface pressure. The reference state is then adjusted to match the initial temperature and moisture profiles. This adjustment is critical for tall domains: without it, the constant-$θ$ adiabat reference state diverges from the actual atmosphere in the stratosphere ($T_{ref} ≈ 26$ K vs $T_{actual} = 210$ K at 28 km), producing catastrophic buoyancy forces.
T₀ = 300 # Kp₀ = 101325 # Paconstants = ThermodynamicConstants()reference_state = ReferenceState(grid, constants; surface_pressure = p₀, potential_temperature = T₀, vapor_mass_fraction = 0)ReferenceState{Float32}(p₀=101325.0, θ₀=300.0, pˢᵗ=100000.0)Define equilibrium temperature and moisture profiles for adjustment and initialization
Tᵗˢ = 210cᵖᵈ = constants.dry_air.heat_capacityg = constants.gravitational_accelerationRᵈ = Breeze.Thermodynamics.dry_air_gas_constant(constants)κ = Rᵈ / cᵖᵈpˢᵗ = reference_state.standard_pressureΠ₀ = (p₀ / pˢᵗ)^κ1.0037661f0Analytical Exner function for a hydrostatic constant-$θ$ atmosphere
Π(z) = Π₀ - g * z / (cᵖᵈ * T₀)β = 1q₀ = 15e-3 # surface specific humidity (kg/kg)Hq = 3000 # moisture scale height (m)Tᵇᵍ(z) = max(Tᵗˢ, T₀ * Π(z))qᵇᵍ(z) = max(0, β * q₀ * exp(-z / Hq))qᵇᵍ (generic function with 1 method)Adjust reference state to match actual profiles
compute_reference_state!(reference_state, Tᵇᵍ, qᵇᵍ, constants)dynamics = AnelasticDynamics(reference_state)coriolis = FPlane(f = 3e-4)FPlane{Float32}(f=0.0003)Surface fluxes
Following the paper's bulk formulas (Eqs. 2-4), with drag coefficient $C^D = 1.5 × 10^{-3}$ and gustiness $U^g = 1$ m/s. The surface wetness parameter $β$ scales the moisture flux coefficient.
Cᴰ = Cᵀ = 1.5e-3Uᵍ = 1ρu_bcs = FieldBoundaryConditions(bottom = BulkDrag(coefficient = Cᴰ, gustiness = Uᵍ))ρv_bcs = FieldBoundaryConditions(bottom = BulkDrag(coefficient = Cᴰ, gustiness = Uᵍ))ρe_bcs = FieldBoundaryConditions(bottom = BulkSensibleHeatFlux(coefficient = Cᵀ, gustiness = Uᵍ, surface_temperature = T₀))ρqᵉ_bcs = FieldBoundaryConditions(bottom = BulkVaporFlux(coefficient = β*Cᵀ, gustiness = Uᵍ, surface_temperature = T₀))boundary_conditions = (; ρu=ρu_bcs, ρv=ρv_bcs, ρe=ρe_bcs, ρqᵉ=ρqᵉ_bcs)Radiative forcing
The paper (Eq. 1) prescribes a piecewise radiative tendency: constant cooling at $Ṫ = 1$ K/day for $T > T^{ts}$ (troposphere), and Newtonian relaxation toward $T^{ts}$ with timescale $τ_r = 20$ days for $T ≤ T^{ts}$ (stratosphere). We apply this as an energy forcing on $ρe$, so that Breeze handles the conversion to $ρθ$ tendency.
Ṫ = 1 / dayτᵣ = 20daysρᵣ = reference_state.densityparameters = (; Tᵗˢ, Ṫ, τᵣ, ρᵣ, cᵖᵈ)@inline function ρe_forcing_func(i, j, k, grid, clock, model_fields, p) @inbounds T = model_fields.T[i, j, k] @inbounds ρ = p.ρᵣ[i, j, k] ∂t_T = ifelse(T > p.Tᵗˢ, -p.Ṫ, (p.Tᵗˢ - T) / p.τᵣ) return ρ * p.cᵖᵈ * ∂t_Tendρe_forcing = Forcing(ρe_forcing_func; discrete_form=true, parameters)DiscreteForcing{@NamedTuple{Tᵗˢ::Int64, Ṫ::Float64, τᵣ::Float64, ρᵣ::Oceananigans.Fields.Field{Nothing, Nothing, Oceananigans.Grids.Center, Nothing, Oceananigans.Grids.RectilinearGrid{Float32, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded, Oceananigans.Grids.StaticVerticalDiscretization{OffsetArrays.OffsetVector{Float32, CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, OffsetArrays.OffsetVector{Float32, CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, OffsetArrays.OffsetVector{Float32, CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}, OffsetArrays.OffsetVector{Float32, CUDACore.CuArray{Float32, 1, CUDACore.DeviceMemory}}}, Float32, Float32, OffsetArrays.OffsetVector{Float32, StepRangeLen{Float32, Float64, Float64, Int64}}, OffsetArrays.OffsetVector{Float32, StepRangeLen{Float32, Float64, Float64, Int64}}, Oceananigans.Architectures.GPU{CUDACore.CUDAKernels.CUDABackend}, Oceananigans.Grids.GridSize{72, 72, 60, 5, 5, 5}}, Tuple{Colon, Colon, Colon}, OffsetArrays.OffsetArray{Float32, 3, CUDACore.CuArray{Float32, 3, CUDACore.DeviceMemory}}, Float32, Oceananigans.BoundaryConditions.FieldBoundaryConditions{Nothing, Nothing, Nothing, Nothing, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Value{Nothing}, Float32}, Oceananigans.BoundaryConditions.NoFluxBoundaryCondition, 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}, Float32}, Oceananigans.BoundaryConditions.NoFluxBoundaryCondition}}}, Nothing, Nothing}, cᵖᵈ::Float32}}
├── func: ρe_forcing_func (generic function with 1 method)
└── parameters: (Tᵗˢ = 210, Ṫ = 1.1574074074074073e-5, τᵣ = 1.728e6, ρᵣ = 1×1×60 Field{Nothing, Nothing, Oceananigans.Grids.Center} reduced over dims = (1, 2) on Oceananigans.Grids.RectilinearGrid on CUDAGPU
├── grid: 72×72×60 RectilinearGrid{Float32, Periodic, Periodic, Bounded} on CUDAGPU with 5×5×5 halo
├── boundary conditions: FieldBoundaryConditions
│ └── west: Nothing, east: Nothing, south: Nothing, north: Nothing, bottom: Value, top: ZeroFlux, immersed: Nothing
└── data: 1×1×70 OffsetArray(::CUDACore.CuArray{Float32, 3, CUDACore.DeviceMemory}, 1:1, 1:1, -4:65) with eltype Float32 with indices 1:1×1:1×-4:65
└── max=1.16002, min=0.0238631, mean=0.745775, cᵖᵈ = 1005.0f0)Sponge layer
Rayleigh damping with a Gaussian profile centered at 26 km (width 2 km) prevents spurious wave reflections from the rigid lid.
sponge_mask = GaussianMask{:z}(center=26kilometers, width=2kilometers)ρw_sponge = Relaxation(rate=1/30, mask=sponge_mask)forcing = (; ρe=ρe_forcing, ρw=ρw_sponge)Model
We use WENO schemes for advection and warm-phase saturation adjustment microphysics.
momentum_advection = WENO(order=9)scalar_advection = (ρθ = WENO(order=5), ρqᵉ = WENO(order=5, bounds=(0, 1)))microphysics = SaturationAdjustment(equilibrium=WarmPhaseEquilibrium())model = AtmosphereModel(grid; dynamics, coriolis, momentum_advection, scalar_advection, microphysics, forcing, boundary_conditions)AtmosphereModel{GPU, RectilinearGrid}(time = 0 seconds, iteration = 0)
├── grid: 72×72×60 RectilinearGrid{Float32, Periodic, Periodic, Bounded} on CUDAGPU with 5×5×5 halo
├── dynamics: AnelasticDynamics(p₀=101325.0, θ₀=300.0)
├── formulation: LiquidIcePotentialTemperatureFormulation
├── thermodynamic_constants: ThermodynamicConstants{Float32}
├── timestepper: SSPRungeKutta3
├── advection scheme:
│ ├── momentum: WENO{5, Float32, Oceananigans.Utils.BackendOptimizedDivision}(order=9)
│ ├── ρθ: WENO{3, Float32, Oceananigans.Utils.BackendOptimizedDivision}(order=5)
│ └── ρqᵉ: WENO{3, Float32, Oceananigans.Utils.BackendOptimizedDivision}(order=5, bounds=(0.0f0, 1.0f0))
├── forcing: ρw=>Relaxation, ρe=>DiscreteForcing
├── tracers: ()
├── coriolis: FPlane{Float32}(f=0.0003)
└── microphysics: SaturationAdjustmentInitial conditions
We initialize with an equilibrated temperature profile: a dry adiabat in the troposphere transitioning to an isothermal stratosphere at $T^{ts} = 210$ K. This approximates the paper's 100-day nonrotating RCE spinup. Small random perturbations in the lowest kilometer trigger convection.
After compute_reference_state!, we must use set!(model, T=...) rather than set!(model, θ=...). The compute_reference_state! call recomputes the reference pressure, which changes the Exner function used to convert $θ \to T$. Setting $θ$ directly would produce incorrect temperatures in the stratosphere.
δT = 1//2 # perturbation amplitude (K)zδ = 1000 # perturbation depth (m)δq = 1e-4 # moisture perturbation amplitude (kg/kg)Tᵢ(x, y, z) = Tᵇᵍ(z) + δT * (2rand() - 1) * (z < zδ)qᵗᵢ(x, y, z) = max(0, qᵇᵍ(z) + δq * (2rand() - 1) * (z < zδ))set!(model, T = Tᵢ, qᵗ = qᵗᵢ)Simulation
We run for 4 days, which is sufficient for moist TC genesis and intensification.
simulation = Simulation(model; Δt=1, stop_time=4days)conjure_time_step_wizard!(simulation, cfl=0.7)Oceananigans.Diagnostics.erroring_NaNChecker!(simulation)Output and progress
u, v, w = model.velocitiesθ = liquid_ice_potential_temperature(model)s = @at (Center, Center, Center) sqrt(u^2 + v^2)s₀ = Field(s, indices = (:, :, 1))ρqᵉ = model.moisture_densityρe = static_energy_density(model)ℒˡ = Breeze.Thermodynamics.liquid_latent_heat(T₀, constants)𝒬ᵀ = BoundaryConditionOperation(ρe, :bottom, model)Jᵛ = BoundaryConditionOperation(ρqᵉ, :bottom, model)𝒬 = Field(𝒬ᵀ + ℒˡ * Jᵛ)function progress(sim) compute!(s₀) compute!(𝒬) umax = maximum(abs, u) vmax = maximum(abs, v) wmax = maximum(abs, w) s₀max = maximum(s₀) 𝒬max = maximum(𝒬) θmin, θmax = extrema(θ) msg = @sprintf("(%d) t = %s, Δt = %s", iteration(sim), prettytime(sim, false), prettytime(sim.Δt, false)) msg *= @sprintf(", s₀ = %.1f m/s, max(𝒬) = %.1f W/m², max|U| ≈ (%d, %d, %d) m/s, θ ∈ [%d, %d] K", s₀max, 𝒬max, umax, vmax, wmax, floor(θmin), ceil(θmax)) @info msg return nothingendadd_callback!(simulation, progress, IterationInterval(1000))Horizontally-averaged profiles.
qᵛ = specific_humidity(model)ℋ = RelativeHumidity(model)avg_outputs = (θ = Average(θ, dims=(1, 2)), qᵛ = Average(qᵛ, dims=(1, 2)), ℋ = Average(ℋ, dims=(1, 2)), w² = Average(w^2, dims=(1, 2)), wθ = Average(w * θ, dims=(1, 2)), wqᵛ = Average(w * qᵛ, dims=(1, 2)))function save_parameters(file, model) file["parameters/β"] = β file["parameters/T₀"] = T₀ file["parameters/Tᵗˢ"] = Tᵗˢ file["parameters/Ṫ"] = Ṫ file["parameters/f₀"] = coriolis.f file["parameters/Cᴰ"] = Cᴰ file["parameters/Nx"] = Nx file["parameters/Nz"] = Nzendsimulation.output_writers[:profiles] = JLD2Writer(model, avg_outputs; filename = "tc_world_profiles.jld2", schedule = TimeInterval(1day), init = save_parameters, overwrite_existing = true)JLD2Writer scheduled on TimeInterval(1 day):
├── filepath: tc_world_profiles.jld2
├── 6 outputs: (θ, qᵛ, ℋ, w², wθ, wqᵛ)
├── array_type: Array{Float32}
├── including: [:thermodynamic_constants]
├── file_splitting: NoFileSplitting
└── file size: 0 bytes (file not yet created)Surface fields for tracking TC development.
surface_outputs = (; s, 𝒬)simulation.output_writers[:surface] = JLD2Writer(model, surface_outputs; filename = "tc_world_surface.jld2", indices = (:, :, 1), schedule = TimeInterval(30minutes), overwrite_existing = true)JLD2Writer scheduled on TimeInterval(30 minutes):
├── filepath: tc_world_surface.jld2
├── 2 outputs: (s, 𝒬)
├── array_type: Array{Float32}
├── including: [:thermodynamic_constants]
├── file_splitting: NoFileSplitting
└── file size: 0 bytes (file not yet created)Run
run!(simulation)[ Info: Initializing simulation...
[ Info: (0) t = 0 s, Δt = 1.100 s, s₀ = 0.0 m/s, max(𝒬) = 28.4 W/m², max|U| ≈ (0, 0, 0) m/s, θ ∈ [299, 706] K
[ Info: ... simulation initialization complete (1.083 minutes)
[ Info: Executing initial time step...
[ Info: ... initial time step complete (5.035 seconds).
[ Info: (1000) t = 45.688 m, Δt = 4.051 s, s₀ = 34.3 m/s, max(𝒬) = 2415.2 W/m², max|U| ≈ (35, 28, 24) m/s, θ ∈ [296, 708] K
[ Info: (2000) t = 4.490 hrs, Δt = 13.513 s, s₀ = 7.7 m/s, max(𝒬) = 542.6 W/m², max|U| ≈ (21, 21, 5) m/s, θ ∈ [299, 657] K
[ Info: (3000) t = 8.989 hrs, Δt = 17.418 s, s₀ = 6.1 m/s, max(𝒬) = 418.3 W/m², max|U| ≈ (25, 24, 7) m/s, θ ∈ [299, 633] K
[ Info: (4000) t = 13.793 hrs, Δt = 16.363 s, s₀ = 5.3 m/s, max(𝒬) = 307.3 W/m², max|U| ≈ (18, 19, 7) m/s, θ ∈ [299, 618] K
[ Info: (5000) t = 18.800 hrs, Δt = 18.872 s, s₀ = 5.1 m/s, max(𝒬) = 317.0 W/m², max|U| ≈ (18, 20, 8) m/s, θ ∈ [299, 610] K
[ Info: (6000) t = 1.003 d, Δt = 16.781 s, s₀ = 5.6 m/s, max(𝒬) = 293.6 W/m², max|U| ≈ (17, 16, 11) m/s, θ ∈ [299, 604] K
[ Info: (7000) t = 1.211 d, Δt = 15.836 s, s₀ = 7.3 m/s, max(𝒬) = 309.5 W/m², max|U| ≈ (15, 14, 17) m/s, θ ∈ [298, 601] K
[ Info: (8000) t = 1.421 d, Δt = 15.172 s, s₀ = 9.5 m/s, max(𝒬) = 279.3 W/m², max|U| ≈ (10, 10, 13) m/s, θ ∈ [298, 600] K
[ Info: (9000) t = 1.606 d, Δt = 14.689 s, s₀ = 8.8 m/s, max(𝒬) = 403.8 W/m², max|U| ≈ (12, 12, 14) m/s, θ ∈ [298, 599] K
[ Info: (10000) t = 1.766 d, Δt = 12.266 s, s₀ = 12.4 m/s, max(𝒬) = 382.7 W/m², max|U| ≈ (15, 14, 17) m/s, θ ∈ [297, 600] K
[ Info: (11000) t = 1.920 d, Δt = 15.362 s, s₀ = 9.6 m/s, max(𝒬) = 398.2 W/m², max|U| ≈ (13, 13, 17) m/s, θ ∈ [298, 603] K
[ Info: (12000) t = 2.055 d, Δt = 11.080 s, s₀ = 13.3 m/s, max(𝒬) = 404.1 W/m², max|U| ≈ (13, 12, 16) m/s, θ ∈ [298, 605] K
[ Info: (13000) t = 2.197 d, Δt = 12.555 s, s₀ = 13.7 m/s, max(𝒬) = 517.8 W/m², max|U| ≈ (15, 15, 13) m/s, θ ∈ [298, 608] K
[ Info: (14000) t = 2.330 d, Δt = 9.333 s, s₀ = 15.7 m/s, max(𝒬) = 497.8 W/m², max|U| ≈ (15, 16, 19) m/s, θ ∈ [298, 612] K
[ Info: (15000) t = 2.449 d, Δt = 9.891 s, s₀ = 18.4 m/s, max(𝒬) = 426.8 W/m², max|U| ≈ (20, 19, 22) m/s, θ ∈ [297, 616] K
[ Info: (16000) t = 2.563 d, Δt = 9.627 s, s₀ = 19.3 m/s, max(𝒬) = 481.8 W/m², max|U| ≈ (20, 20, 27) m/s, θ ∈ [297, 617] K
[ Info: (17000) t = 2.685 d, Δt = 12.202 s, s₀ = 22.6 m/s, max(𝒬) = 602.1 W/m², max|U| ≈ (22, 23, 19) m/s, θ ∈ [298, 622] K
[ Info: (18000) t = 2.813 d, Δt = 10.409 s, s₀ = 21.4 m/s, max(𝒬) = 536.2 W/m², max|U| ≈ (22, 22, 26) m/s, θ ∈ [294, 625] K
[ Info: (19000) t = 2.932 d, Δt = 11.619 s, s₀ = 21.9 m/s, max(𝒬) = 520.8 W/m², max|U| ≈ (22, 20, 21) m/s, θ ∈ [297, 627] K
[ Info: (20000) t = 3.049 d, Δt = 11.581 s, s₀ = 19.5 m/s, max(𝒬) = 628.0 W/m², max|U| ≈ (18, 19, 27) m/s, θ ∈ [298, 627] K
[ Info: (21000) t = 3.157 d, Δt = 10.202 s, s₀ = 23.3 m/s, max(𝒬) = 495.1 W/m², max|U| ≈ (21, 22, 14) m/s, θ ∈ [297, 633] K
[ Info: (22000) t = 3.273 d, Δt = 10.614 s, s₀ = 24.5 m/s, max(𝒬) = 547.1 W/m², max|U| ≈ (22, 21, 17) m/s, θ ∈ [298, 629] K
[ Info: (23000) t = 3.386 d, Δt = 8.210 s, s₀ = 22.3 m/s, max(𝒬) = 648.5 W/m², max|U| ≈ (25, 22, 19) m/s, θ ∈ [298, 635] K
[ Info: (24000) t = 3.491 d, Δt = 9.467 s, s₀ = 28.9 m/s, max(𝒬) = 706.9 W/m², max|U| ≈ (26, 29, 22) m/s, θ ∈ [297, 633] K
[ Info: (25000) t = 3.598 d, Δt = 9.155 s, s₀ = 28.6 m/s, max(𝒬) = 499.0 W/m², max|U| ≈ (26, 27, 21) m/s, θ ∈ [296, 640] K
[ Info: (26000) t = 3.706 d, Δt = 8.558 s, s₀ = 25.4 m/s, max(𝒬) = 540.0 W/m², max|U| ≈ (23, 24, 16) m/s, θ ∈ [297, 639] K
[ Info: (27000) t = 3.803 d, Δt = 8.071 s, s₀ = 26.1 m/s, max(𝒬) = 634.5 W/m², max|U| ≈ (26, 30, 34) m/s, θ ∈ [297, 634] K
[ Info: (28000) t = 3.892 d, Δt = 8.508 s, s₀ = 26.3 m/s, max(𝒬) = 594.0 W/m², max|U| ≈ (24, 25, 18) m/s, θ ∈ [297, 635] K
[ Info: (29000) t = 3.988 d, Δt = 9.885 s, s₀ = 28.7 m/s, max(𝒬) = 686.3 W/m², max|U| ≈ (27, 25, 18) m/s, θ ∈ [297, 638] K
[ Info: Simulation is stopping after running for 8.094 minutes.
[ Info: Simulation time 4 days equals or exceeds stop time 4 days.
Results: mean profile evolution
Evolution of horizontally-averaged potential temperature, specific and relative humidity, vertical velocity variance, vertical potential temperature flux, and vertical specific humidity flux.
θt = FieldTimeSeries("tc_world_profiles.jld2", "θ")qᵛt = FieldTimeSeries("tc_world_profiles.jld2", "qᵛ")ℋt = FieldTimeSeries("tc_world_profiles.jld2", "ℋ")w²t = FieldTimeSeries("tc_world_profiles.jld2", "w²")wθt = FieldTimeSeries("tc_world_profiles.jld2", "wθ")wqᵛt = FieldTimeSeries("tc_world_profiles.jld2", "wqᵛ")times = θt.timesNt = length(times)fig = Figure(size=(900, 400), fontsize=10)axθ = Axis(fig[1, 1], xlabel="θ (K)", ylabel="z (m)")axqᵛ = Axis(fig[1, 2], xlabel="qᵛ (kg/kg)")axℋ = Axis(fig[1, 3], xlabel="ℋ")axw² = Axis(fig[1, 4], xlabel="w² (m²/s²)")axwθ = Axis(fig[1, 5], xlabel="wθ (m/s K)")axwqᵛ = Axis(fig[1, 6], xlabel="wqᵛ (10⁻⁵ m/s kg/kg)", ylabel="z (m)", yaxisposition=:right)default_colours = Makie.wong_colors()colors = [default_colours[mod1(n, length(default_colours))] for n in 1:Nt]linewidth = 3alpha = 0.6for n in 1:Nt color = colors[n] label = n == 1 ? "initial" : "t = $(prettytime(times[n]))" lines!(axθ, θt[n]; color, linewidth, alpha, label) lines!(axqᵛ, qᵛt[n]; color, linewidth, alpha) lines!(axℋ, ℋt[n]; color, linewidth, alpha) lines!(axw², w²t[n]; color, linewidth, alpha) lines!(axwθ, wθt[n]; color, linewidth, alpha) lines!(axwqᵛ, 1e5 * wqᵛt[n]; color, linewidth, alpha)endfor ax in (axqᵛ, axℋ, axw², axwθ) hideydecorations!(ax, grid=false) hidespines!(ax, :t, :r, :l)endhidespines!(axθ, :t, :r)hidespines!(axwqᵛ, :t, :l)xlims!(axℋ, -0.1, 1.1)Legend(fig[2, :], axθ, labelsize=12, orientation=:horizontal)fig[0, :] = Label(fig, "TC World (β = $β): mean profile evolution", fontsize=16, tellwidth=false)figSurface wind speed snapshots
Snapshots of the surface wind speed field at early, middle, and late times show the evolution of convective organization and TC formation.
st = FieldTimeSeries("tc_world_surface.jld2", "s")𝒬t = FieldTimeSeries("tc_world_surface.jld2", "𝒬")times = st.timesNt = length(times)smax = maximum(st)slim = smax / 2𝒬lim = maximum(𝒬t) / 8fig = Figure(size=(1200, 800), fontsize=12)s_heatmaps = []𝒬_heatmaps = []indices = ceil.(Int, [Nt / 3, 2Nt / 3, Nt])for (i, idx) in enumerate(indices) xlabel = i == 1 ? "x (m)" : "" ylabel = i == 1 ? "y (m)" : "" title = "t = $(prettytime(times[idx]))" axs = Axis(fig[1, i]; aspect = 1, xlabel, ylabel, title) ax𝒬 = Axis(fig[2, i]; aspect = 1, xlabel, ylabel, title) s_hm = heatmap!(axs, st[idx]; colormap=:speed, colorrange=(0, slim)) push!(s_heatmaps, s_hm) 𝒬_hm = heatmap!(ax𝒬, 𝒬t[idx]; colormap=:magma, colorrange=(0, 𝒬lim)) push!(𝒬_heatmaps, 𝒬_hm)endColorbar(fig[1, length(indices) + 1], s_heatmaps[end]; label="Surface wind speed (m/s)")Colorbar(fig[2, length(indices) + 1], 𝒬_heatmaps[end]; label="Surface moisture flux (W/m²)")fig[0, :] = Label(fig, "TC World (β = $β): surface wind and heat flux", fontsize=16, tellwidth=false)figAnimation of surface wind speed
fig = Figure(size=(600, 550), fontsize=14)ax = Axis(fig[1, 1]; xlabel="x (m)", ylabel="y (m)", aspect=1)n = Observable(1)title = @lift "TC World (β = $β) — t = $(prettytime(times[$n]))"sn = @lift st[$n]hm = heatmap!(ax, sn; colormap=:speed, colorrange=(0, slim))Colorbar(fig[1, 2], hm; label="Surface wind speed (m/s)")fig[0, :] = Label(fig, title, fontsize=16, tellwidth=false)CairoMakie.record(fig, "tc_world.mp4", 1:Nt; framerate = 16, compression = 23) do nn n[] = nnendDiscussion
This example demonstrates spontaneous tropical cyclone genesis in a rotating radiative-convective equilibrium setup, following Cronin and Chavas (2019). The surface wetness parameter $β$ controls moisture availability: $β = 1$ (default) produces robust moist TC genesis, while $β = 0$ yields dry TCs.
The radiative forcing is a piecewise temperature tendency: constant cooling at 1 K/day in the troposphere ($T > T^{ts}$) and Newtonian relaxation toward $T^{ts}$ with timescale $τ_r = 20$ days in the stratosphere. Surface fluxes use bulk formulas with drag coefficient $C^D = 1.5 × 10^{-3}$ and gustiness 1 m/s. The $f$-plane Coriolis parameter is $f_0 = 3 × 10^{-4}$ s⁻¹.
Julia version and environment information
This example was executed with the following version of Julia:
using InteractiveUtils: versioninfoversioninfo()Julia Version 1.12.6
Commit 15346901f00 (2026-04-09 19:20 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_PKG_SERVER_REGISTRY_PREFERENCE = eager
JULIA_VERSION = 1.12.6
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.8.0 `.`
[052768ef] CUDA v6.2.1
[13f3f980] CairoMakie v0.15.13
⌅ [6a9e3e04] CloudMicrophysics v0.37.1
[e30172f5] Documenter v1.17.0
[daee34ce] DocumenterCitations v1.4.1
[b6400b83] DocumenterCodeBlocks v1.1.0
[7da242da] Enzyme v0.13.198
[46192b85] GPUArraysCore v0.2.0
[63c18a36] KernelAbstractions v0.9.42
[98b081ad] Literate v2.21.0
[85f8d34a] NCDatasets v0.14.15
[9e8cae18] Oceananigans v0.110.14
⌅ [a01a1ee8] RRTMGP v0.21.9
[3c362404] Reactant v0.2.278
[b77e0a4c] InteractiveUtils v1.11.0
[44cfe95a] Pkg v1.12.1
[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.