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 (8.102 seconds)
[ Info: Executing initial time step...
[ Info: ... initial time step complete (2.491 seconds).
Iter: 50, sim time: 0.544, wall time: 10.729 seconds, Δt: 0.0121, advective CFL: 4.38e-03, diffusive CFL: 1.40e-02
Iter: 100, sim time: 1.145, wall time: 10.945 seconds, Δt: 0.0133, advective CFL: 1.66e-02, diffusive CFL: 1.54e-02
Iter: 150, sim time: 1.806, wall time: 11.100 seconds, Δt: 0.0146, advective CFL: 3.74e-02, diffusive CFL: 1.70e-02
Iter: 200, sim time: 2.515, wall time: 11.258 seconds, Δt: 0.0161, advective CFL: 6.85e-02, diffusive CFL: 1.87e-02
Iter: 250, sim time: 3.306, wall time: 11.403 seconds, Δt: 0.0177, advective CFL: 1.16e-01, diffusive CFL: 2.05e-02
Iter: 300, sim time: 4.177, wall time: 11.600 seconds, Δt: 0.0195, advective CFL: 2.05e-01, diffusive CFL: 2.26e-02
Iter: 350, sim time: 5.136, wall time: 11.803 seconds, Δt: 0.0214, advective CFL: 5.84e-01, diffusive CFL: 2.48e-02
Iter: 400, sim time: 6.193, wall time: 12.043 seconds, Δt: 0.0165, advective CFL: 7.00e-01, diffusive CFL: 1.91e-02
Iter: 450, sim time: 7.000, wall time: 12.208 seconds, Δt: 0.0160, advective CFL: 7.00e-01, diffusive CFL: 1.85e-02
Iter: 500, sim time: 7.788, wall time: 12.378 seconds, Δt: 0.0157, advective CFL: 7.00e-01, diffusive CFL: 1.82e-02
Iter: 550, sim time: 8.563, wall time: 12.579 seconds, Δt: 0.0155, advective CFL: 7.00e-01, diffusive CFL: 1.80e-02
Iter: 600, sim time: 9.326, wall time: 12.777 seconds, Δt: 0.0167, advective CFL: 7.00e-01, diffusive CFL: 1.93e-02
Iter: 650, sim time: 10.150, wall time: 12.992 seconds, Δt: 0.0155, advective CFL: 7.00e-01, diffusive CFL: 1.80e-02
Iter: 700, sim time: 10.920, wall time: 13.190 seconds, Δt: 0.0159, advective CFL: 7.00e-01, diffusive CFL: 1.85e-02
Iter: 750, sim time: 11.691, wall time: 13.351 seconds, Δt: 0.0174, advective CFL: 7.00e-01, diffusive CFL: 2.02e-02
Iter: 800, sim time: 12.552, wall time: 13.583 seconds, Δt: 0.0179, advective CFL: 7.00e-01, diffusive CFL: 2.07e-02
Iter: 850, sim time: 13.429, wall time: 13.765 seconds, Δt: 0.0181, advective CFL: 7.00e-01, diffusive CFL: 2.09e-02
Iter: 900, sim time: 14.325, wall time: 13.928 seconds, Δt: 0.0185, advective CFL: 7.00e-01, diffusive CFL: 2.15e-02
Iter: 950, sim time: 15.241, wall time: 14.089 seconds, Δt: 0.0200, advective CFL: 7.00e-01, diffusive CFL: 2.31e-02
Iter: 1000, sim time: 16.220, wall time: 14.236 seconds, Δt: 0.0199, advective CFL: 7.00e-01, diffusive CFL: 2.31e-02
Iter: 1050, sim time: 17.179, wall time: 14.365 seconds, Δt: 0.0199, advective CFL: 7.00e-01, diffusive CFL: 2.31e-02
Iter: 1100, sim time: 18.139, wall time: 14.488 seconds, Δt: 0.0203, advective CFL: 7.00e-01, diffusive CFL: 2.35e-02
Iter: 1150, sim time: 19.142, wall time: 14.702 seconds, Δt: 0.0211, advective CFL: 7.00e-01, diffusive CFL: 2.45e-02
Iter: 1200, sim time: 20.190, wall time: 14.849 seconds, Δt: 0.0223, advective CFL: 7.00e-01, diffusive CFL: 2.59e-02
Iter: 1250, sim time: 21.290, wall time: 15.003 seconds, Δt: 0.0215, advective CFL: 7.00e-01, diffusive CFL: 2.49e-02
Iter: 1300, sim time: 22.344, wall time: 15.157 seconds, Δt: 0.0202, advective CFL: 7.00e-01, diffusive CFL: 2.34e-02
Iter: 1350, sim time: 23.344, wall time: 15.362 seconds, Δt: 0.0207, advective CFL: 7.00e-01, diffusive CFL: 2.40e-02
Iter: 1400, sim time: 24.352, wall time: 15.590 seconds, Δt: 0.0226, advective CFL: 7.00e-01, diffusive CFL: 2.62e-02
Iter: 1450, sim time: 25.453, wall time: 15.738 seconds, Δt: 0.0241, advective CFL: 7.00e-01, diffusive CFL: 2.79e-02
Iter: 1500, sim time: 26.645, wall time: 15.926 seconds, Δt: 0.0265, advective CFL: 6.86e-01, diffusive CFL: 3.07e-02
Iter: 1550, sim time: 27.951, wall time: 16.082 seconds, Δt: 0.0213, advective CFL: 7.00e-01, diffusive CFL: 2.47e-02
Iter: 1600, sim time: 28.989, wall time: 16.300 seconds, Δt: 0.0171, advective CFL: 7.00e-01, diffusive CFL: 1.98e-02
Iter: 1650, sim time: 29.824, wall time: 16.512 seconds, Δt: 0.0157, advective CFL: 7.00e-01, diffusive CFL: 1.82e-02
Iter: 1700, sim time: 30.594, wall time: 16.732 seconds, Δt: 0.0144, advective CFL: 7.00e-01, diffusive CFL: 1.67e-02
Iter: 1750, sim time: 31.303, wall time: 16.869 seconds, Δt: 0.0157, advective CFL: 7.00e-01, diffusive CFL: 1.82e-02
Iter: 1800, sim time: 32.078, wall time: 17.089 seconds, Δt: 0.0173, advective CFL: 6.43e-01, diffusive CFL: 2.00e-02
Iter: 1850, sim time: 32.932, wall time: 17.253 seconds, Δt: 0.0190, advective CFL: 6.97e-01, diffusive CFL: 2.20e-02
Iter: 1900, sim time: 33.861, wall time: 17.443 seconds, Δt: 0.0207, advective CFL: 7.00e-01, diffusive CFL: 2.40e-02
Iter: 1950, sim time: 34.873, wall time: 17.610 seconds, Δt: 0.0214, advective CFL: 7.00e-01, diffusive CFL: 2.48e-02
Iter: 2000, sim time: 35.928, wall time: 17.822 seconds, Δt: 0.0221, advective CFL: 7.00e-01, diffusive CFL: 2.56e-02
Iter: 2050, sim time: 37.000, wall time: 17.978 seconds, Δt: 0.0226, advective CFL: 7.00e-01, diffusive CFL: 2.61e-02
Iter: 2100, sim time: 38.090, wall time: 18.177 seconds, Δt: 0.0230, advective CFL: 7.00e-01, diffusive CFL: 2.66e-02
Iter: 2150, sim time: 39.230, wall time: 18.319 seconds, Δt: 0.0235, advective CFL: 7.00e-01, diffusive CFL: 2.72e-02
[ Info: Simulation is stopping after running for 18.436 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-34931/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.0
Commit d1c37793dd2 (2026-09-09 19:00 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_LOAD_PATH = @:@v#.#:@stdlib
JULIA_DEPOT_PATH = /var/lib/buildkite-agent/.julia:/var/lib/buildkite-agent/.julia/juliaup/julia-1.13.0+0.x64.linux.gnu/local/share/julia:/var/lib/buildkite-agent/.julia/juliaup/julia-1.13.0+0.x64.linux.gnu/share/julia
JULIA_VERSION_ENZYME = 1.13.0
JULIA_PKG_SERVER_REGISTRY_PREFERENCE = eager
LD_LIBRARY_PATH =
JULIA_MAX_NUM_PRECOMPILE_FILES = 24
JULIA_VERSION = 1.13.0
JULIA_CUDA_USE_COMPAT = false
JULIA_PROJECT = /var/lib/buildkite-agent/Oceananigans.jl-34931/docs/
JULIA_DEBUG = LiterateThese were the top-level packages installed in the environment:
import Pkg
Pkg.status()Status `~/Oceananigans.jl-34931/docs/Project.toml`
[79e6a3ab] Adapt v4.7.4
⌃ [052768ef] CUDA v6.2.2
[13f3f980] CairoMakie v0.15.16
[8e50ac2c] ConservativeRegridding v0.2.16
⌅ [e30172f5] Documenter v1.17.0
[daee34ce] DocumenterCitations v1.5.0
[4710194d] DocumenterVitepress v0.3.8
[7da242da] Enzyme v0.13.216
[033835bb] JLD2 v0.6.7
⌅ [63c18a36] KernelAbstractions v0.9.45
[98b081ad] Literate v2.21.0
[da04e1cc] MPI v0.20.29
[85f8d34a] NCDatasets v0.14.15
[9e8cae18] Oceananigans v0.114.0 `..`
[429524aa] Optim v2.3.2
[f27b6e38] Polynomials v4.1.3
⌅ [3c362404] Reactant v0.2.292
[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.