Horizontal convection example
In "horizontal convection", a non-uniform buoyancy is imposed on top of an initially resting fluid.
This example demonstrates:
How to use computed
Fields for output.How to post-process saved output using
FieldTimeSeries.
Install dependencies
First let's make sure we have all required packages installed.
using Pkg
pkg"add Oceananigans, CairoMakie"Horizontal convection
We consider two-dimensional horizontal convection of an incompressible flow
We start by importing Oceananigans and Printf.
using Oceananigans
using PrintfThe grid
H = 1 # vertical domain extent
Lx = 2H # horizontal domain extent
Nx, Nz = 128, 64 # horizontal, vertical resolution
grid = RectilinearGrid(size = (Nx, Nz),
x = (-Lx/2, Lx/2),
z = (-H, 0),
topology = (Bounded, Flat, Bounded))128×1×64 RectilinearGrid{Float64, Bounded, Flat, Bounded} on CPU with 3×0×3 halo
├── Bounded x ∈ [-1.0, 1.0] regularly spaced with Δx=0.015625
├── Flat y
└── Bounded z ∈ [-1.0, 0.0] regularly spaced with Δz=0.015625Boundary conditions
At the surface, the imposed buoyancy is
while zero-flux boundary conditions are imposed on all other boundaries. We use free-slip boundary conditions on
b★ = 1
@inline bˢ(x, t, p) = - p.b★ * cos(2π * x / p.Lx)
b_bcs = FieldBoundaryConditions(top = ValueBoundaryCondition(bˢ, parameters=(; b★, Lx)))Oceananigans.FieldBoundaryConditions, with boundary conditions
├── west: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── east: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── south: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── north: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── bottom: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── top: ValueBoundaryCondition: ContinuousBoundaryFunction bˢ at (Nothing, Nothing, Nothing)
└── immersed: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)Non-dimensional control parameters and Turbulence closure
The problem is characterized by three non-dimensional parameters. The first is the domain's aspect ratio,
The Prandtl number expresses the ratio of momentum over heat diffusion; the Rayleigh number is a measure of the relative importance of gravity over viscosity in the momentum equation.
For a domain with a given extent, the nondimensional values of
We use isotropic viscosity and diffusivity, ν and κ, whose values are obtained from the prescribed
Pr = 1
Ra = 1e8
ν = sqrt(Pr * b★ * Lx^3 / Ra)
κ = sqrt(b★ * Lx^3 / (Pr * Ra))Model instantiation
We instantiate the model with the fifth-order WENO advection scheme, a 3rd order Runge-Kutta time-stepping scheme, and a BuoyancyTracer.
model = NonhydrostaticModel(grid;
advection = WENO(),
timestepper = :RungeKutta3,
tracers = :b,
buoyancy = BuoyancyTracer(),
closure = ScalarDiffusivity(; ν, κ),
boundary_conditions = (; b=b_bcs))NonhydrostaticModel{CPU, RectilinearGrid}(time = 0 seconds, iteration = 0)
├── grid: 128×1×64 RectilinearGrid{Float64, Bounded, Flat, Bounded} on CPU with 3×0×3 halo
├── timestepper: RungeKutta3TimeStepper
├── advection scheme:
│ ├── momentum: WENO{3, Float64, Oceananigans.Utils.BackendOptimizedDivision}(order=5)
│ └── b: WENO{3, Float64, Oceananigans.Utils.BackendOptimizedDivision}(order=5)
├── tracers: b
├── closure: ScalarDiffusivity{ExplicitTimeDiscretization}(ν=0.000282843, κ=(b=0.000282843,))
├── buoyancy: BuoyancyTracer with ĝ = NegativeZDirection()
└── coriolis: NothingSimulation set-up
We set up a simulation that runs up to JLD2Writer that saves the flow speed,
simulation = Simulation(model, Δt=1e-2, stop_time=40)Simulation of NonhydrostaticModel{CPU, RectilinearGrid}(time = 0 seconds, iteration = 0)
├── Next time step: 10 ms
├── run_wall_time: 0 seconds
├── run_wall_time / iteration: NaN days
├── stop_time: 40 seconds
├── 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 entriesThe TimeStepWizard
The TimeStepWizard manages the time-step adaptively, keeping the Courant-Friedrichs-Lewy (CFL) number close to 0.7.
conjure_time_step_wizard!(simulation, IterationInterval(50), cfl=0.7, max_Δt=1e-1)A progress messenger
We write a function that prints out a helpful progress message while the simulation runs.
progress(sim) = @printf("Iter: % 6d, sim time: % 1.3f, wall time: % 10s, Δt: % 1.4f, advective CFL: %.2e, diffusive CFL: %.2e\n",
iteration(sim), time(sim), prettytime(sim.run_wall_time),
sim.Δt, AdvectiveCFL(sim.Δt)(sim.model), DiffusiveCFL(sim.Δt)(sim.model))
simulation.callbacks[:progress] = Callback(progress, IterationInterval(50))Callback of progress on IterationInterval(50)Output
We use computed Fields to diagnose and output the total flow speed, the vorticity, Fields take "AbstractOperations" on Fields as input:
u, v, w = model.velocities
b = model.tracers.b
s = @at (Center, Center, Center) sqrt(u^2 + w^2)
ζ = ∂z(u) - ∂x(w)We add to the simulation a JLD2Writer that saves the speed, the buoyancy, and the vorticity. We want to post-process buoyancy to compute the buoyancy variance dissipation (which is proportional to JLD2Writer saves the fields' halos, so when we load the buoyancy back it comes with the proper boundary conditions for computing gradients.
filename = "horizontal_convection.jld2"
simulation.output_writers[:fields] = JLD2Writer(model, (; s, b, ζ),
schedule = TimeInterval(0.5),
filename = filename,
overwrite_files = true)Ready to press the big red button:
run!(simulation)[ Info: Initializing simulation...
Iter: 0, sim time: 0.000, wall time: 0 seconds, Δt: 0.0110, advective CFL: 0.00e+00, diffusive CFL: 1.27e-02
[ Info: ... simulation initialization complete (2.704 seconds)
[ Info: Executing initial time step...
[ Info: ... initial time step complete (1.113 seconds).
Iter: 50, sim time: 0.544, wall time: 3.930 seconds, Δt: 0.0121, advective CFL: 4.38e-03, diffusive CFL: 1.40e-02
Iter: 100, sim time: 1.145, wall time: 4.046 seconds, Δt: 0.0133, advective CFL: 1.66e-02, diffusive CFL: 1.54e-02
Iter: 150, sim time: 1.806, wall time: 4.143 seconds, Δt: 0.0146, advective CFL: 3.74e-02, diffusive CFL: 1.70e-02
Iter: 200, sim time: 2.515, wall time: 4.245 seconds, Δt: 0.0161, advective CFL: 6.85e-02, diffusive CFL: 1.87e-02
Iter: 250, sim time: 3.306, wall time: 4.342 seconds, Δt: 0.0177, advective CFL: 1.16e-01, diffusive CFL: 2.05e-02
Iter: 300, sim time: 4.177, wall time: 4.445 seconds, Δt: 0.0195, advective CFL: 2.05e-01, diffusive CFL: 2.26e-02
Iter: 350, sim time: 5.136, wall time: 4.568 seconds, Δt: 0.0214, advective CFL: 5.86e-01, diffusive CFL: 2.48e-02
Iter: 400, sim time: 6.193, wall time: 4.676 seconds, Δt: 0.0165, advective CFL: 7.00e-01, diffusive CFL: 1.91e-02
Iter: 450, sim time: 7.000, wall time: 4.775 seconds, Δt: 0.0160, advective CFL: 7.00e-01, diffusive CFL: 1.85e-02
Iter: 500, sim time: 7.788, wall time: 4.881 seconds, Δt: 0.0157, advective CFL: 7.00e-01, diffusive CFL: 1.82e-02
Iter: 550, sim time: 8.563, wall time: 4.988 seconds, Δt: 0.0155, advective CFL: 7.00e-01, diffusive CFL: 1.80e-02
Iter: 600, sim time: 9.326, wall time: 5.103 seconds, Δt: 0.0166, advective CFL: 7.00e-01, diffusive CFL: 1.93e-02
Iter: 650, sim time: 10.133, wall time: 5.211 seconds, Δt: 0.0155, advective CFL: 7.00e-01, diffusive CFL: 1.80e-02
Iter: 700, sim time: 10.904, wall time: 5.317 seconds, Δt: 0.0159, advective CFL: 7.00e-01, diffusive CFL: 1.85e-02
Iter: 750, sim time: 11.675, wall time: 5.429 seconds, Δt: 0.0174, advective CFL: 7.00e-01, diffusive CFL: 2.01e-02
Iter: 800, sim time: 12.535, wall time: 5.534 seconds, Δt: 0.0179, advective CFL: 7.00e-01, diffusive CFL: 2.07e-02
Iter: 850, sim time: 13.430, wall time: 5.642 seconds, Δt: 0.0180, advective CFL: 7.00e-01, diffusive CFL: 2.09e-02
Iter: 900, sim time: 14.325, wall time: 5.756 seconds, Δt: 0.0185, advective CFL: 7.00e-01, diffusive CFL: 2.14e-02
Iter: 950, sim time: 15.222, wall time: 5.860 seconds, Δt: 0.0200, advective CFL: 7.00e-01, diffusive CFL: 2.31e-02
Iter: 1000, sim time: 16.200, wall time: 5.961 seconds, Δt: 0.0199, advective CFL: 7.00e-01, diffusive CFL: 2.31e-02
Iter: 1050, sim time: 17.159, wall time: 6.064 seconds, Δt: 0.0199, advective CFL: 7.00e-01, diffusive CFL: 2.31e-02
Iter: 1100, sim time: 18.119, wall time: 6.167 seconds, Δt: 0.0203, advective CFL: 7.00e-01, diffusive CFL: 2.35e-02
Iter: 1150, sim time: 19.122, wall time: 6.280 seconds, Δt: 0.0211, advective CFL: 7.00e-01, diffusive CFL: 2.45e-02
Iter: 1200, sim time: 20.169, wall time: 6.394 seconds, Δt: 0.0223, advective CFL: 7.00e-01, diffusive CFL: 2.59e-02
Iter: 1250, sim time: 21.268, wall time: 6.500 seconds, Δt: 0.0216, advective CFL: 7.00e-01, diffusive CFL: 2.50e-02
Iter: 1300, sim time: 22.324, wall time: 6.606 seconds, Δt: 0.0203, advective CFL: 7.00e-01, diffusive CFL: 2.35e-02
Iter: 1350, sim time: 23.325, wall time: 6.713 seconds, Δt: 0.0209, advective CFL: 7.00e-01, diffusive CFL: 2.42e-02
Iter: 1400, sim time: 24.355, wall time: 6.824 seconds, Δt: 0.0227, advective CFL: 7.00e-01, diffusive CFL: 2.63e-02
Iter: 1450, sim time: 25.453, wall time: 6.924 seconds, Δt: 0.0243, advective CFL: 7.00e-01, diffusive CFL: 2.81e-02
Iter: 1500, sim time: 26.646, wall time: 7.027 seconds, Δt: 0.0267, advective CFL: 6.88e-01, diffusive CFL: 3.09e-02
Iter: 1550, sim time: 27.954, wall time: 7.120 seconds, Δt: 0.0212, advective CFL: 7.00e-01, diffusive CFL: 2.46e-02
Iter: 1600, sim time: 28.988, wall time: 7.211 seconds, Δt: 0.0171, advective CFL: 7.00e-01, diffusive CFL: 1.98e-02
Iter: 1650, sim time: 29.824, wall time: 7.303 seconds, Δt: 0.0157, advective CFL: 7.00e-01, diffusive CFL: 1.82e-02
Iter: 1700, sim time: 30.594, wall time: 7.395 seconds, Δt: 0.0145, advective CFL: 7.00e-01, diffusive CFL: 1.68e-02
Iter: 1750, sim time: 31.319, wall time: 7.483 seconds, Δt: 0.0158, advective CFL: 7.00e-01, diffusive CFL: 1.83e-02
Iter: 1800, sim time: 32.095, wall time: 7.579 seconds, Δt: 0.0174, advective CFL: 6.48e-01, diffusive CFL: 2.02e-02
Iter: 1850, sim time: 32.953, wall time: 7.675 seconds, Δt: 0.0191, advective CFL: 7.00e-01, diffusive CFL: 2.21e-02
Iter: 1900, sim time: 33.882, wall time: 7.774 seconds, Δt: 0.0208, advective CFL: 7.00e-01, diffusive CFL: 2.41e-02
Iter: 1950, sim time: 34.895, wall time: 7.870 seconds, Δt: 0.0214, advective CFL: 7.00e-01, diffusive CFL: 2.48e-02
Iter: 2000, sim time: 35.950, wall time: 7.969 seconds, Δt: 0.0221, advective CFL: 7.00e-01, diffusive CFL: 2.56e-02
Iter: 2050, sim time: 37.022, wall time: 8.071 seconds, Δt: 0.0226, advective CFL: 7.00e-01, diffusive CFL: 2.62e-02
Iter: 2100, sim time: 38.113, wall time: 8.173 seconds, Δt: 0.0231, advective CFL: 7.00e-01, diffusive CFL: 2.68e-02
Iter: 2150, sim time: 39.254, wall time: 8.276 seconds, Δt: 0.0236, advective CFL: 7.00e-01, diffusive CFL: 2.73e-02
[ Info: Simulation is stopping after running for 8.342 seconds.
[ Info: Simulation time 40 seconds equals or exceeds stop time 40 seconds.Load saved output, process, visualize
We animate the results by loading the saved output and plotting the saved fields. From the saved buoyancy field we compute the buoyancy dissipation,
To start we load the saved fields as FieldTimeSeries.
using CairoMakie
s_timeseries = FieldTimeSeries(filename, "s")
b_timeseries = FieldTimeSeries(filename, "b")
ζ_timeseries = FieldTimeSeries(filename, "ζ")
times = b_timeseries.times
Nt = length(times)
χ_timeseries = deepcopy(b_timeseries)
for n in 1:Nt
b = b_timeseries[n]
χ_timeseries[n] .= @at (Center, Center, Center) κ * (∂x(b)^2 + ∂z(b)^2)
end┌ Warning: Assignment to `b` in soft scope is ambiguous because a global variable by the same name exists: `b` will be treated as a new local. Disambiguate by using `local b` to suppress this warning or `global b` to assign to the existing global variable.
└ @ ~/Oceananigans.jl-36221/docs/src/literated/horizontal_convection.md:13Now we're ready to animate using Makie.
@info "Making an animation from saved data..."
n = Observable(1)
title = @lift @sprintf("t=%1.2f", times[$n])
sn = @lift s_timeseries[$n]
ζn = @lift ζ_timeseries[$n]
bn = @lift b_timeseries[$n]
χn = @lift χ_timeseries[$n]
slim = 0.6
blim = 0.6
ζlim = 9
χlim = 0.025
axis_kwargs = (xlabel = L"x / H",
ylabel = L"z / H",
limits = ((-Lx/2, Lx/2), (-H, 0)),
aspect = Lx / H,
titlesize = 20)
fig = Figure(size = (600, 1100))
ax_s = Axis(fig[2, 1]; title = L"speed, $(u^2+w^2)^{1/2} / (L_x b_*)^{1/2}$", axis_kwargs...)
ax_b = Axis(fig[3, 1]; title = L"buoyancy, $b / b_*$", axis_kwargs...)
ax_ζ = Axis(fig[4, 1]; axis_kwargs...,
title = L"vorticity, $(∂u/∂z - ∂w/∂x) \, (L_x / b_*)^{1/2}$")
ax_χ = Axis(fig[5, 1]; axis_kwargs...,
title = L"buoyancy dissipation, $κ |\mathbf{\nabla}b|^2 \, (L_x / {b_*}^5)^{1/2}$")
fig[1, :] = Label(fig, title, fontsize=24, tellwidth=false)
hm_s = heatmap!(ax_s, sn; colorrange=(0, slim), colormap=:speed)
Colorbar(fig[2, 2], hm_s)
hm_b = heatmap!(ax_b, bn; colorrange=(-blim, blim), colormap=:thermal)
Colorbar(fig[3, 2], hm_b)
hm_ζ = heatmap!(ax_ζ, ζn; colorrange=(-ζlim, ζlim), colormap=:balance)
Colorbar(fig[4, 2], hm_ζ)
hm_χ = heatmap!(ax_χ, χn; colorrange=(0, χlim), colormap=:dense)
Colorbar(fig[5, 2], hm_χ)Colorbar()And, finally, we record a movie.
record(fig, "horizontal_convection.mp4", 1:Nt, framerate=8) do i
n[] = i
endAt higher Rayleigh numbers the flow becomes much more vigorous. See, for example, an animation of the vorticity of the fluid at
The Nusselt number
Often we are interested in how much the flow enhances mixing. This is quantified by the Nusselt number, which compares the mixing achieved by the flow with the mixing that diffusion alone would achieve. The Nusselt number is given by
where angle brackets above denote both a volume and time average and
with the same boundary conditions as our setup. In this case, we can readily find that
where
We use the loaded FieldTimeSeries to compute the instantaneous Nusselt number and the volume-averaged kinetic energy of the fluid.
First we compute the diffusive buoyancy dissipation,
χᵈⁱᶠᶠ = κ * b★^2 * π * tanh(2π * H / Lx) / (Lx * H)Then we compute the volume averages with Average, and plot.
kinetic_energy = [Field(Average(s_timeseries[n]^2 / 2))[1, 1, 1] for n in 1:Nt]
Nu = [Field(Average(χ_timeseries[n]))[1, 1, 1] / χᵈⁱᶠᶠ for n in 1:Nt]
fig = Figure(size = (850, 450))
ax_KE = Axis(fig[1, 1], xlabel = L"t \, (b_* / L_x)^{1/2}", ylabel = L"KE $ / (L_x b_*)$")
lines!(ax_KE, times, kinetic_energy; linewidth = 3)
ax_Nu = Axis(fig[2, 1], xlabel = L"t \, (b_* / L_x)^{1/2}", ylabel = L"Nu")
lines!(ax_Nu, times, Nu; linewidth = 3)
figJulia version and environment information
This example was executed with the following version of Julia:
using InteractiveUtils: versioninfo
versioninfo()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: 128 × AMD EPYC 9374F 32-Core Processor
WORD_SIZE: 64
LLVM: libLLVM-20.1.8 (ORCJIT, znver4)
GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 128 virtual cores)
Environment:
JULIA_VERSION_ENZYME = 1.13.1
JULIA_DEPOT_PATH = /var/lib/buildkite-agent/.julia:/var/lib/buildkite-agent/.julia/juliaup/julia-1.13.2+0.x64.linux.gnu/local/share/julia:/var/lib/buildkite-agent/.julia/juliaup/julia-1.13.2+0.x64.linux.gnu/share/julia
JULIA_VERSION = 1.13.2
JULIA_PKG_SERVER_REGISTRY_PREFERENCE = eager
LD_LIBRARY_PATH =
JULIA_LOAD_PATH = @:@v#.#:@stdlib
JULIA_MAX_NUM_PRECOMPILE_FILES = 24
JULIA_CUDA_USE_COMPAT = false
JULIA_PROJECT = /var/lib/buildkite-agent/Oceananigans.jl-36221/docs/
JULIA_DEBUG = LiterateThese were the top-level packages installed in the environment:
import Pkg
Pkg.status()Status `~/Oceananigans.jl-36221/docs/Project.toml`
[79e6a3ab] Adapt v4.8.0
⌃ [052768ef] CUDA v6.4.1
[13f3f980] CairoMakie v0.15.17
[8e50ac2c] ConservativeRegridding v0.2.21
[e30172f5] Documenter v1.20.0
[daee34ce] DocumenterCitations v1.5.0
[4710194d] DocumenterVitepress v0.3.9
[7da242da] Enzyme v0.13.219
[033835bb] JLD2 v0.6.7
⌅ [63c18a36] KernelAbstractions v0.9.47
[98b081ad] Literate v2.21.1
[da04e1cc] MPI v0.20.29
[85f8d34a] NCDatasets v0.14.15
[9e8cae18] Oceananigans v0.118.1 `..`
[429524aa] Optim v2.3.2
[f27b6e38] Polynomials v4.1.6
[3c362404] Reactant v0.2.308
[6038ab10] Rotations v1.7.1
[d496a93d] SeawaterPolynomials v0.3.11
[09ab397b] StructArrays v0.7.4
[bdfc003b] TimesDates v0.3.3
[0a941bbe] Zarr v0.11.2
[b77e0a4c] InteractiveUtils v1.11.0
[37e2e46d] LinearAlgebra v1.13.0
[44cfe95a] Pkg v1.13.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.