Hydrostatic lock exchange with CATKEVerticalDiffusivity
This example simulates a lock exchange problem on a slope. It demonstrates:
How to set up a 2D grid with a sloping bottom with an immersed boundary
Initializing a hydrostatic free surface model
Including variable density initial conditions
Applying bottom drag boundary conditions on immersed boundaries using
BulkDragSaving outputs of the simulation
Creating an animation with CairoMakie
The Lock Exchange Problem
In a lock exchange, two fluids of different densities (due to temperature, salinity, etc.) are separated by a "lock" at time
Install dependencies
First let's make sure we have all required packages installed.
using Pkg
pkg"add Oceananigans, CairoMakie"Import Required Packages
using Oceananigans
using Oceananigans.Units
using Printf
using CairoMakieSet up a 2D rectilinear grid
Nx = 128
Nz = 64
L = 8kilometers # horizontal length
H = 50meters # depth50.0We wrap z into a MutableVerticalDiscretization. This allows the HydrostaticFreeSurfaceModel (which we construct further down) to use a time-evolving, free-surface-following ZStarCoordinate vertical coordinate.
x = (-L/8, 7L/8)
z = MutableVerticalDiscretization((-H, 0))
underlying_grid = RectilinearGrid(; size=(Nx, Nz), x, z, halo=(5, 5),
topology=(Bounded, Flat, Bounded))128×1×64 RectilinearGrid{Float32, Bounded, Flat, Bounded} on CPU with 5×0×5 halo
├── Bounded x ∈ [-1000.0, 7000.0] regularly spaced with Δx=62.5
├── Flat y
└── Bounded z ∈ [-50.0, 0.0] regularly spaced with mutable Δr=0.78125The bottom slopes upward from
slope = H / 2L
bottom(x) = -H + slope * x
grid = ImmersedBoundaryGrid(underlying_grid, PartialCellBottom(bottom))128×1×64 ImmersedBoundaryGrid{Float32, Bounded, Flat, Bounded} on CPU with 5×0×5 halo:
├── immersed_boundary: PartialCellBottom(mean(zb)=-40.4425, min(zb)=-50.0, max(zb)=-28.2812, ϵ=0.2)
├── underlying_grid: 128×1×64 RectilinearGrid{Float32, Bounded, Flat, Bounded} on CPU with 5×0×5 halo
├── Bounded x ∈ [-1000.0, 7000.0] regularly spaced with Δx=62.5
├── Flat y
└── Bounded z ∈ [-50.0, 0.0] regularly spaced with mutable Δr=0.78125Set up a bottom drag boundary condition on the immersed boundary
To apply drag to the flow, we use BulkDrag, which implements quadratic drag proportional to
Cᴰ = 0.002
drag = BulkDrag(coefficient=Cᴰ)FluxBoundaryCondition: BulkDragFunction(QuadraticFormulation(), Nothing, Cᴰ=0.002)BulkDrag can be applied both to domain boundaries (like the bottom of a Bounded grid) and to immersed boundaries. Here we apply it to the immersed sloping bottom boundary:
u_bcs = FieldBoundaryConditions(bottom=drag, immersed=drag)Oceananigans.FieldBoundaryConditions, with boundary conditions
├── west: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── east: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── south: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── north: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
├── bottom: FluxBoundaryCondition: BulkDragFunction(QuadraticFormulation(), Nothing, Cᴰ=0.002)
├── top: DefaultBoundaryCondition (FluxBoundaryCondition: nothing)
└── immersed: FluxBoundaryCondition: BulkDragFunction(QuadraticFormulation(), Nothing, Cᴰ=0.002)In a 2D simulation with Flat in the
Initialize the model
We use a hydrostatic model because the horizontal scale of the flow is much larger than its vertical scale
Buoyancy
is a tracer that determines the fluid density The vertical closure
CATKEVerticalDiffusivityparameterizes small-scale vertical turbulenceWeighted Essentially Non-Oscillatory (WENO) advection schemes capture sharp changes in density
model = HydrostaticFreeSurfaceModel(grid;
tracers = :b,
buoyancy = BuoyancyTracer(),
closure = CATKEVerticalDiffusivity(),
momentum_advection = WENO(order=5),
tracer_advection = WENO(order=7),
boundary_conditions = (; u=u_bcs),
free_surface = SplitExplicitFreeSurface(grid; substeps=20))HydrostaticFreeSurfaceModel{CPU, ImmersedBoundaryGrid}(time = 0 seconds, iteration = 0)
├── grid: 128×1×64 ImmersedBoundaryGrid{Float32, Bounded, Flat, Bounded} on CPU with 5×0×5 halo
├── timestepper: QuasiAdamsBashforth2TimeStepper
├── tracers: (b, e)
├── closure: CATKEVerticalDiffusivity{VerticallyImplicitTimeDiscretization}
├── buoyancy: BuoyancyTracer with ĝ = NegativeZDirection()
├── free surface: SplitExplicitFreeSurface with gravitational acceleration 9.80665 m s⁻²
│ └── substepping: FixedSubstepNumber(14)
├── advection scheme:
│ ├── momentum: WENO{3, Float32, Oceananigans.Utils.BackendOptimizedDivision}(order=5)
│ ├── b: WENO{4, Float32, Oceananigans.Utils.BackendOptimizedDivision}(order=7)
│ └── e: WENO{4, Float32, Oceananigans.Utils.BackendOptimizedDivision}(order=7)
├── vertical_coordinate: ZStarCoordinate
└── coriolis: NothingSet variable density initial conditions
The lock at
bᵢ(x, z) = x > L/2 ? 0.01 : 0.06
set!(model, b=bᵢ)Construct a Simulation
Fast wave speeds make the equations stiff, so the CFL condition restricts the time step to small values to maintain numerical stability.
simulation = Simulation(model, Δt=1second, stop_time=6hours)Simulation of HydrostaticFreeSurfaceModel{CPU, ImmersedBoundaryGrid}(time = 0 seconds, iteration = 0)
├── Next time step: 1 second
├── run_wall_time: 0 seconds
├── run_wall_time / iteration: NaN days
├── stop_time: 6 hours
├── 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 is incorporated in the simulation via the conjure_time_step_wizard! helper function and it ensures stable time-stepping with a Courant–Friedrichs–Lewy (CFL) number of 0.3.
conjure_time_step_wizard!(simulation, cfl=0.3)Track simulation progress
We add a callback that prints the simulation progress alongside some flow statistics.
function progress(sim)
@info @sprintf("Iter: %6d, time: %s, Δt: %s, wall time: %s, max|w| = %6.3e m s⁻¹",
iteration(sim),
prettytime(sim),
prettytime(sim.Δt),
prettytime(sim.run_wall_time),
maximum(abs, sim.model.velocities.w))
return nothing
end
add_callback!(simulation, progress, IterationInterval(1000))Add output writer
Here, we construct a JLD2Writer to save buoyancy
b = model.tracers.b
e = model.tracers.e
u = model.velocities.u
N² = ∂z(b)
filename = "hydrostatic_lock_exchange.jld2"
simulation.output_writers[:fields] = JLD2Writer(model, (; b, e, u, N²);
filename,
schedule = TimeInterval(2minutes),
overwrite_files = true)JLD2Writer scheduled on TimeInterval(2 minutes):
├── filepath: hydrostatic_lock_exchange.jld2
├── 4 outputs: (b, e, u, N²)
├── array_type: Array{Float32}
├── including: [:coriolis, :buoyancy, :closure]
├── file_splitting: NoFileSplitting
└── file size: 0 bytes (file not yet created)Run the simulation
run!(simulation)
@info "Simulation finished. Output saved to $(filename)"[ Info: Initializing simulation...
[ Info: Iter: 0, time: 0 seconds, Δt: 1.100 seconds, wall time: 0 seconds, max|w| = 0.000e+00 m s⁻¹
[ Info: ... simulation initialization complete (8.767 seconds)
[ Info: Executing initial time step...
[ Info: ... initial time step complete (17.744 seconds).
[ Info: Iter: 1000, time: 32.353 minutes, Δt: 1.716 seconds, wall time: 34.243 seconds, max|w| = 1.356e-01 m s⁻¹
[ Info: Iter: 2000, time: 1.030 hours, Δt: 1.726 seconds, wall time: 42.286 seconds, max|w| = 1.355e-01 m s⁻¹
[ Info: Iter: 3000, time: 1.480 hours, Δt: 1.837 seconds, wall time: 51.552 seconds, max|w| = 1.261e-01 m s⁻¹
[ Info: Iter: 4000, time: 1.900 hours, Δt: 1.430 seconds, wall time: 1.022 minutes, max|w| = 1.636e-01 m s⁻¹
[ Info: Iter: 5000, time: 2.300 hours, Δt: 832.665 ms, wall time: 1.187 minutes, max|w| = 2.813e-01 m s⁻¹
[ Info: Iter: 6000, time: 3.005 hours, Δt: 3.513 seconds, wall time: 1.358 minutes, max|w| = 6.530e-02 m s⁻¹
[ Info: Iter: 7000, time: 3.917 hours, Δt: 2.231 seconds, wall time: 1.532 minutes, max|w| = 1.028e-01 m s⁻¹
[ Info: Iter: 8000, time: 4.420 hours, Δt: 1.627 seconds, wall time: 1.695 minutes, max|w| = 1.429e-01 m s⁻¹
[ Info: Iter: 9000, time: 4.906 hours, Δt: 2.188 seconds, wall time: 1.860 minutes, max|w| = 1.069e-01 m s⁻¹
[ Info: Iter: 10000, time: 5.556 hours, Δt: 2.664 seconds, wall time: 2.005 minutes, max|w| = 8.704e-02 m s⁻¹
[ Info: Simulation is stopping after running for 2.092 minutes.
[ Info: Simulation time 6 hours equals or exceeds stop time 6 hours.
[ Info: Simulation finished. Output saved to hydrostatic_lock_exchange.jld2Load the saved time series
u_timeseries = FieldTimeSeries(filename, "u")
N²_timeseries = FieldTimeSeries(filename, "N²")
b_timeseries = FieldTimeSeries(filename, "b")
e_timeseries = FieldTimeSeries(filename, "e")
times = b_timeseries.times0.0:120.0:21600.0Visualize the simulation output
We use Makie's Observable to animate the data. To dive into how Observables work we refer to Makie.jl's Documentation.
n = Observable(1)
title = @lift "t = " * prettytime(times[$n])
uₙ = @lift u_timeseries[$n]
N²ₙ = @lift N²_timeseries[$n]
bₙ = @lift b_timeseries[$n]
eₙ = @lift e_timeseries[$n]We use the last snapshot to set the color ranges.
umax = maximum(abs, u_timeseries[end])
N²max = maximum(abs, N²_timeseries[end])
bmax = maximum(abs, b_timeseries[end])
emax = maximum(abs, e_timeseries[end])We visualize
nan_color = :grey
axis_kwargs = (xlabel = "x (m)", ylabel = "z (m)",
limits = (x, (-H, 0)), titlesize = 18)
fig = Figure(size = (800, 900))
fig[1, :] = Label(fig, title, fontsize = 24, tellwidth = false)
ax_b = Axis(fig[2, 1]; title = "b (buoyancy)", axis_kwargs...)
hm_b = heatmap!(ax_b, bₙ; nan_color, colorrange = (0, bmax), colormap = :thermal)
Colorbar(fig[2, 2], hm_b, label = "m s⁻²")
ax_e = Axis(fig[3, 1]; title = "e (turbulent kinetic energy)", axis_kwargs...)
hm_e = heatmap!(ax_e, eₙ; nan_color, colorrange = (0, emax), colormap = :magma)
Colorbar(fig[3, 2], hm_e, label = "m² s⁻²")
ax_u = Axis(fig[4, 1]; title = "u (horizontal velocity)", axis_kwargs...)
hm_u = heatmap!(ax_u, uₙ; nan_color, colorrange = (-umax, umax), colormap = :balance)
Colorbar(fig[4, 2], hm_u, label = "m s⁻¹")
ax_N² = Axis(fig[5, 1]; title = "N² (stratification)", axis_kwargs...)
hm_N² = heatmap!(ax_N², N²ₙ; nan_color, colorrange = (-N²max/4, N²max), colormap = :haline)
Colorbar(fig[5, 2], hm_N², label = "s⁻²")
record(fig, "hydrostatic_lock_exchange.mp4", 1:length(times); framerate = 8) do i
n[] = i
endThe visualization shows the time evolution of buoyancy
Julia 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.