Eady turbulence example
In this example, we initialize a random velocity field and observe its viscous, turbulent decay in a two-dimensional domain. This example demonstrates:
- How to use a tuple of turbulence closures
- How to use hyperdiffusivity
- How to implement background velocity and tracer distributions
- How to use
ComputedField
s for output
Install dependencies
First let's make sure we have all required packages installed.
using Pkg
pkg"add Oceananigans, JLD2, Plots"
Resolving package versions... No Changes to `/storage7/buildkite-agent/builds/tartarus-mit-edu-6/clima/oceananigans/docs/Project.toml` No Changes to `/storage7/buildkite-agent/builds/tartarus-mit-edu-6/clima/oceananigans/docs/Manifest.toml`
The Eady problem
The "Eady problem" simulates the baroclinic instability problem proposed by Eric Eady in the classic paper "Long waves and cyclone waves," Tellus (1949). The Eady problem is a simple, canonical model for the generation of mid-latitude atmospheric storms and the ocean eddies that permeate the world sea.
In the Eady problem, baroclinic motion and turublence is generated by the interaction between turbulent motions and a stationary, geostrophically-balanced basic state that is unstable to baroclinic instability. In this example, the baroclinic generation of turbulence due to extraction of energy from the geostrophic basic state is balanced by a bottom boundary condition that extracts momentum from turbulent motions and serves as a crude model for the drag associated with an unresolved and small-scale turbulent bottom boundary layer.
The geostrophic basic state
The geostrophic basic state in the Eady problem is represented by the streamfunction,
\[ψ(y, z) = - α y (z + L_z) \, ,\]
where $α$ is the geostrophic shear and $L_z$ is the depth of the domain. The background buoyancy includes both the geostrophic flow component, $f ∂_z ψ$, where $f$ is the Coriolis parameter, and a background stable stratification component, $N^2 z$, where $N$ is the buoyancy frequency:
\[B(y, z) = f ∂_z ψ + N^2 z = - α f y + N^2 z \, .\]
The background velocity field is related to the geostrophic streamfunction via $U = - ∂_y ψ$ such that
\[U(z) = α (z + L_z) \, .\]
Boundary conditions
All fields are periodic in the horizontal directions. We use "insulating", or zero-flux boundary conditions on the buoyancy perturbation at the top and bottom. We thus implicitly assume that the background vertical density gradient, $N^2 z$, is maintained by a process external to our simulation. We use free-slip, or zero-flux boundary conditions on $u$ and $v$ at the surface where $z=0$. At the bottom, we impose a momentum flux that extracts momentum and energy from the flow.
Bottom boundary condition: quadratic bottom drag
We model the effects of a turbulent bottom boundary layer on the eddy momentum budget with quadratic bottom drag. A quadratic cottom drag is introduced by imposing a vertical flux of horizontal momentum that removes momentum from the layer immediately above: in other words, the flux is negative (downwards) when the velocity at the bottom boundary is positive, and positive (upwards) with the velocity at the bottom boundary is negative. This drag term is "quadratic" because the rate at which momentum is removed is proportional to $\bm{u}_h |\bm{u}_h|$, where $\bm{u}_h = u \bm{\hat{x}} + v \bm{\hat{y}}$ is the horizontal velocity.
The $x$-component of the quadratic bottom drag is thus
\[\tau_{xz}(z=L_z) = - c^D u \sqrt{u^2 + v^2} \, ,\]
while the $y$-component is
\[\tau_{yz}(z=L_z) = - c^D v \sqrt{u^2 + v^2} \, ,\]
where $c^D$ is a dimensionless drag coefficient and $\tau_{xz}(z=L_z)$ and $\tau_{yz}(z=L_z)$ denote the flux of $u$ and $v$ momentum at $z = L_z$, the bottom of the domain.
Vertical and horizontal viscosity and diffusivity
Vertical and horizontal viscosties and diffusivities are required to stabilize the Eady problem and can be idealized as modeling the effect of turbulent mixing below the grid scale. For both tracers and velocities we use a Laplacian vertical diffusivity $κ_z ∂_z^2 c$ and a horizontal hyperdiffusivity $ϰ_h (∂_x^4 + ∂_y^4) c$.
Eady problem summary and parameters
To summarize, the Eady problem parameters along with the values we use in this example are
Parameter name | Description | Value | Units |
---|---|---|---|
$f$ | Coriolis parameter | $10^{-4}$ | $\mathrm{s^{-1}}$ |
$N$ | Buoyancy frequency (square root of $\partial_z B$) | $10^{-3}$ | $\mathrm{s^{-1}}$ |
$\alpha$ | Background vertical shear $\partial_z U$ | $10^{-3}$ | $\mathrm{s^{-1}}$ |
$c^D$ | Bottom quadratic drag coefficient | $10^{-4}$ | none |
$κ_z$ | Laplacian vertical diffusivity | $10^{-2}$ | $\mathrm{m^2 s^{-1}}$ |
$ϰ_h$ | Biharmonic horizontal diffusivity | $10^{-2} \times \Delta x^4 / \mathrm{day}$ | $\mathrm{m^4 s^{-1}}$ |
We start off by importing Oceananigans
, Printf
, and some convenient utils for specifying dimensional constants:
using Oceananigans, Oceananigans.Utils, Printf
The grid
We use a three-dimensional grid with a depth of 4000 m and a horizontal extent of 1000 km, appropriate for mesoscale ocean dynamics with characteristic scales of 50-200 km.
grid = RegularCartesianGrid(size=(48, 48, 16), x=(0, 1e6), y=(0, 1e6), z=(-4e3, 0))
RegularCartesianGrid{Float64, Periodic, Periodic, Bounded} domain: x ∈ [0.0, 1.0e6], y ∈ [0.0, 1.0e6], z ∈ [-4000.0, 0.0] topology: (Periodic, Periodic, Bounded) resolution (Nx, Ny, Nz): (48, 48, 16) halo size (Hx, Hy, Hz): (1, 1, 1) grid spacing (Δx, Δy, Δz): (20833.333333333332, 20833.333333333332, 250.0)
Rotation
The classical Eady problem is posed on an $f$-plane. We use a Coriolis parameter typical to mid-latitudes on Earth,
coriolis = FPlane(f=1e-4) # [s⁻¹]
FPlane{Float64}: f = 1.00e-04
The background flow
We build a NamedTuple
of parameters that describe the background flow,
basic_state_parameters = ( α = 10 * coriolis.f, # s⁻¹, geostrophic shear
f = coriolis.f, # s⁻¹, Coriolis parameter
N = 1e-3, # s⁻¹, buoyancy frequency
Lz = grid.Lz) # m, ocean depth
(α = 0.001, f = 0.0001, N = 0.001, Lz = 4000.0)
and then construct the background fields $U$ and $B$
using Oceananigans.Fields: BackgroundField
# Background fields are defined via functions of x, y, z, t, and optional parameters
U(x, y, z, t, p) = + p.α * (z + p.Lz)
B(x, y, z, t, p) = - p.α * p.f * y + p.N^2 * z
U_field = BackgroundField(U, parameters=basic_state_parameters)
B_field = BackgroundField(B, parameters=basic_state_parameters)
BackgroundField{typeof(Main.ex-eady_turbulence.B), NamedTuple{(:α, :f, :N, :Lz),NTuple{4,Float64}}} ├── func: B └── parameters: (α = 0.001, f = 0.0001, N = 0.001, Lz = 4000.0)
Boundary conditions
The boundary conditions prescribe a quadratic drag at the bottom as a flux condition.
cᴰ = 1e-4 # quadratic drag coefficient
@inline drag_u(x, y, t, u, v, cᴰ) = - cᴰ * u * sqrt(u^2 + v^2)
@inline drag_v(x, y, t, u, v, cᴰ) = - cᴰ * v * sqrt(u^2 + v^2)
drag_bc_u = BoundaryCondition(Flux, drag_u, field_dependencies=(:u, :v), parameters=cᴰ)
drag_bc_v = BoundaryCondition(Flux, drag_v, field_dependencies=(:u, :v), parameters=cᴰ)
u_bcs = UVelocityBoundaryConditions(grid, bottom = drag_bc_u)
v_bcs = VVelocityBoundaryConditions(grid, bottom = drag_bc_v)
Oceananigans.FieldBoundaryConditions (NamedTuple{(:x, :y, :z)}), with boundary conditions ├── x: CoordinateBoundaryConditions{BoundaryCondition{Oceananigans.BoundaryConditions.Periodic,Nothing},BoundaryCondition{Oceananigans.BoundaryConditions.Periodic,Nothing}} ├── y: CoordinateBoundaryConditions{BoundaryCondition{Oceananigans.BoundaryConditions.Periodic,Nothing},BoundaryCondition{Oceananigans.BoundaryConditions.Periodic,Nothing}} └── z: CoordinateBoundaryConditions{BoundaryCondition{Flux,Oceananigans.BoundaryConditions.ContinuousBoundaryFunction{Nothing,Nothing,Nothing,Nothing,typeof(Main.ex-eady_turbulence.drag_v),Float64,Tuple{Symbol,Symbol},Nothing,Nothing}},BoundaryCondition{Flux,Nothing}}
Turbulence closures
We use a horizontal hyperdiffusivity and a Laplacian vertical diffusivity to dissipate energy in the Eady problem. To use both of these closures at the same time, we set the keyword argument closure
to a tuple of two closures.
κ₂z = 1e-2 # [m² s⁻¹] Laplacian vertical viscosity and diffusivity
κ₄h = 1e-1 / day * grid.Δx^4 # [m⁴ s⁻¹] horizontal hyperviscosity and hyperdiffusivity
Laplacian_vertical_diffusivity = AnisotropicDiffusivity(νh=0, κh=0, νz=κ₂z, κz=κ₂z)
biharmonic_horizontal_diffusivity = AnisotropicBiharmonicDiffusivity(νh=κ₄h, κh=κ₄h)
AnisotropicBiharmonicDiffusivity{Float64,Float64,Float64,Float64}(2.1803253690129166e11, 2.1803253690129166e11, 0.0, 2.1803253690129166e11, 2.1803253690129166e11, 0.0)
Model instantiation
We instantiate the model with the fifth-order WENO advection scheme, a 3rd order Runge-Kutta time-stepping scheme, and a BuoyancyTracer
.
using Oceananigans.Advection: WENO5
model = IncompressibleModel(
architecture = CPU(),
grid = grid,
advection = WENO5(),
timestepper = :RungeKutta3,
coriolis = coriolis,
tracers = :b,
buoyancy = BuoyancyTracer(),
background_fields = (b=B_field, u=U_field),
closure = (Laplacian_vertical_diffusivity, biharmonic_horizontal_diffusivity),
boundary_conditions = (u=u_bcs, v=v_bcs)
)
IncompressibleModel{CPU, Float64}(time = 0 seconds, iteration = 0) ├── grid: RegularCartesianGrid{Float64, Periodic, Periodic, Bounded}(Nx=48, Ny=48, Nz=16) ├── tracers: (:b,) ├── closure: Tuple{AnisotropicDiffusivity{Float64,Float64,Float64,NamedTuple{(:b,),Tuple{Float64}},NamedTuple{(:b,),Tuple{Float64}},NamedTuple{(:b,),Tuple{Float64}}},AnisotropicBiharmonicDiffusivity{Float64,NamedTuple{(:b,),Tuple{Float64}},NamedTuple{(:b,),Tuple{Float64}},NamedTuple{(:b,),Tuple{Float64}}}} ├── buoyancy: BuoyancyTracer └── coriolis: FPlane{Float64}
Initial conditions
We seed our initial conditions with random noise stimulate the growth of baroclinic instability.
# A noise function, damped at the top and bottom
Ξ(z) = randn() * z/grid.Lz * (z/grid.Lz + 1)
# Scales for the initial velocity and buoyancy
Ũ = 1e-1 * basic_state_parameters.α * grid.Lz
B̃ = 1e-2 * basic_state_parameters.α * coriolis.f
uᵢ(x, y, z) = Ũ * Ξ(z)
vᵢ(x, y, z) = Ũ * Ξ(z)
bᵢ(x, y, z) = B̃ * Ξ(z)
set!(model, u=uᵢ, v=vᵢ, b=bᵢ)
We subtract off any residual mean velocity to avoid exciting domain-scale inertial oscillations. We use a sum
over the entire parent
arrays or data to ensure this operation is efficient on the GPU (set architecture = GPU()
in IncompressibleModel
constructor to run this problem on the GPU if one is available).
ū = sum(model.velocities.u.data.parent) / (grid.Nx * grid.Ny * grid.Nz)
v̄ = sum(model.velocities.v.data.parent) / (grid.Nx * grid.Ny * grid.Nz)
model.velocities.u.data.parent .-= ū
model.velocities.v.data.parent .-= v̄
Simulation set-up
We set up a simulation that runs for 10 days with a JLD2OutputWriter
that saves the vertical vorticity and divergence every 2 hours.
The TimeStepWizard
The TimeStepWizard manages the time-step adaptively, keeping the Courant-Freidrichs-Lewy (CFL) number close to 1.0
while ensuring the time-step does not increase beyond the maximum allowable value for numerical stability given the specified background flow, Coriolis time scales, and diffusion time scales.
# Calculate absolute limit on time-step using diffusivities and
# background velocity.
Ū = basic_state_parameters.α * grid.Lz
max_Δt = min(grid.Δx / Ū, grid.Δx^4 / κ₄h, grid.Δz^2 / κ₂z, 0.2/coriolis.f)
wizard = TimeStepWizard(cfl=1.0, Δt=max_Δt, max_change=1.1, max_Δt=max_Δt)
TimeStepWizard{Float64}(1.0, Inf, 1.1, 0.5, 2000.0, 0.0, 2000.0)
A progress messenger
We write a function that prints out a helpful progress message while the simulation runs.
using Oceananigans.Diagnostics: AdvectiveCFL
CFL = AdvectiveCFL(wizard)
start_time = time_ns()
progress(sim) = @printf("i: % 6d, sim time: % 10s, wall time: % 10s, Δt: % 10s, CFL: %.2e\n",
sim.model.clock.iteration,
prettytime(sim.model.clock.time),
prettytime(1e-9 * (time_ns() - start_time)),
prettytime(sim.Δt.Δt),
CFL(sim.model))
Build the simulation
We're ready to build and run the simulation. We ask for a progress message and time-step update every 20 iterations,
simulation = Simulation(model, Δt = wizard, iteration_interval = 20,
stop_time = 8days,
progress = progress)
Simulation{IncompressibleModel{CPU, Float64}} ├── Model clock: time = 0 seconds, iteration = 0 ├── Next time step (TimeStepWizard{Float64}): 33.333 minutes ├── Iteration interval: 20 ├── Stop criteria: Any[Oceananigans.Simulations.iteration_limit_exceeded, Oceananigans.Simulations.stop_time_exceeded, Oceananigans.Simulations.wall_time_limit_exceeded] ├── Run time: 0 seconds, wall time limit: Inf ├── Stop time: 8 days, stop iteration: Inf ├── Diagnostics: OrderedCollections.OrderedDict with 1 entry: │ └── nan_checker => NaNChecker └── Output writers: OrderedCollections.OrderedDict with no entries
Output
To visualize the baroclinic turbulence ensuing in the Eady problem, we use ComputedField
s to diagnose and output vertical vorticity and divergence. Note that ComputedField
s take "AbstractOperations" on Field
s as input:
using Oceananigans.AbstractOperations
using Oceananigans.Fields: ComputedField
u, v, w = model.velocities # unpack velocity `Field`s
# Vertical vorticity [s⁻¹]
ζ = ComputedField(∂x(v) - ∂y(u))
# Horizontal divergence, or ∂x(u) + ∂y(v) [s⁻¹]
δ = ComputedField(-∂z(w))
ComputedField located at (Cell, Cell, Cell) of UnaryOperation at (Cell, Cell, Cell) ├── data: OffsetArrays.OffsetArray{Float64,3,Array{Float64,3}}, size: (54, 54, 22) ├── grid: RegularCartesianGrid{Float64, Periodic, Periodic, Bounded}(Nx=48, Ny=48, Nz=16) ├── operand: UnaryOperation at (Cell, Cell, Cell) └── status: time=0.0
With the vertical vorticity, ζ
, and the horizontal divergence, δ
in hand, we create a JLD2OutputWriter
that saves ζ
and δ
and add them to simulation
.
using Oceananigans.OutputWriters: JLD2OutputWriter, TimeInterval
simulation.output_writers[:fields] = JLD2OutputWriter(model, (ζ=ζ, δ=δ),
schedule = TimeInterval(4hours),
prefix = "eady_turbulence",
force = true)
All that's left is to press the big red button:
run!(simulation)
i: 20, sim time: 10.222 hours, wall time: 1.093 minutes, Δt: 33.333 minutes, CFL: 4.44e-02 i: 40, sim time: 20 hours, wall time: 1.163 minutes, Δt: 33.333 minutes, CFL: 5.66e-02 i: 60, sim time: 1.259 days, wall time: 1.230 minutes, Δt: 33.333 minutes, CFL: 8.96e-02 i: 80, sim time: 1.667 days, wall time: 1.298 minutes, Δt: 33.333 minutes, CFL: 1.05e-01 i: 100, sim time: 2.093 days, wall time: 1.365 minutes, Δt: 33.333 minutes, CFL: 1.73e-01 i: 120, sim time: 2.500 days, wall time: 1.433 minutes, Δt: 33.333 minutes, CFL: 2.54e-01 i: 140, sim time: 2.926 days, wall time: 1.501 minutes, Δt: 33.333 minutes, CFL: 4.02e-01 i: 160, sim time: 3.333 days, wall time: 1.569 minutes, Δt: 33.333 minutes, CFL: 6.81e-01 i: 180, sim time: 3.759 days, wall time: 1.636 minutes, Δt: 33.333 minutes, CFL: 8.78e-01 i: 200, sim time: 4.167 days, wall time: 1.704 minutes, Δt: 33.333 minutes, CFL: 1.31e+00 i: 220, sim time: 4.500 days, wall time: 1.771 minutes, Δt: 25.459 minutes, CFL: 1.10e+00 i: 240, sim time: 4.811 days, wall time: 1.838 minutes, Δt: 23.082 minutes, CFL: 1.04e+00 i: 260, sim time: 5.092 days, wall time: 1.905 minutes, Δt: 22.156 minutes, CFL: 7.08e-01 i: 280, sim time: 5.418 days, wall time: 1.973 minutes, Δt: 24.371 minutes, CFL: 8.44e-01 i: 300, sim time: 5.760 days, wall time: 2.040 minutes, Δt: 26.808 minutes, CFL: 7.42e-01 i: 320, sim time: 6.143 days, wall time: 2.106 minutes, Δt: 29.489 minutes, CFL: 1.19e+00 i: 340, sim time: 6.471 days, wall time: 2.173 minutes, Δt: 24.819 minutes, CFL: 9.04e-01 i: 360, sim time: 6.833 days, wall time: 2.242 minutes, Δt: 27.301 minutes, CFL: 8.29e-01 i: 380, sim time: 7.250 days, wall time: 2.310 minutes, Δt: 30.031 minutes, CFL: 1.12e+00 i: 400, sim time: 7.611 days, wall time: 2.377 minutes, Δt: 26.726 minutes, CFL: 8.61e-01 i: 420, sim time: 7.997 days, wall time: 2.445 minutes, Δt: 29.398 minutes, CFL: 9.49e-01 i: 421, sim time: 8 days, wall time: 2.449 minutes, Δt: 30.968 minutes, CFL: 1.00e+00 [ Info: Simulation is stopping. Model time 8 days has hit or exceeded simulation stop time 8 days.
Visualizing Eady turbulence
We animate the results by opening the JLD2 file, extracting data for the iterations we ended up saving at, and ploting slices of the saved fields. We prepare for animating the flow by creating coordinate arrays, opening the file, building a vector of the iterations that we saved data at, and defining a function for computing colorbar limits:
using JLD2, Plots
using Oceananigans.Grids: nodes
# Coordinate arrays
xζ, yζ, zζ = nodes(ζ)
xδ, yδ, zδ = nodes(δ)
# Open the file with our data
file = jldopen(simulation.output_writers[:fields].filepath)
# Extract a vector of iterations
iterations = parse.(Int, keys(file["timeseries/t"]))
49-element Array{Int64,1}: 0 8 16 24 32 40 48 56 64 72 ⋮ 351 360 368 376 385 394 403 412 421
This utility is handy for calculating nice contour intervals:
function nice_divergent_levels(c, clim, nlevels=31)
levels = range(-clim, stop=clim, length=nlevels)
cmax = maximum(abs, c)
clim < cmax && (levels = vcat([-cmax], levels, [cmax]))
return levels
end
Now we're ready to animate.
@info "Making an animation from saved data..."
anim = @animate for (i, iter) in enumerate(iterations)
# Load 3D fields from file
t = file["timeseries/t/$iter"]
R_snapshot = file["timeseries/ζ/$iter"] ./ coriolis.f
δ_snapshot = file["timeseries/δ/$iter"]
surface_R = R_snapshot[:, :, grid.Nz]
surface_δ = δ_snapshot[:, :, grid.Nz]
slice_R = R_snapshot[:, 1, :]
slice_δ = δ_snapshot[:, 1, :]
Rlim = 0.5 * maximum(abs, R_snapshot) + 1e-9
δlim = 0.5 * maximum(abs, δ_snapshot) + 1e-9
Rlevels = nice_divergent_levels(R_snapshot, Rlim)
δlevels = nice_divergent_levels(δ_snapshot, δlim)
@info @sprintf("Drawing frame %d from iteration %d: max(ζ̃ / f) = %.3f \n",
i, iter, maximum(abs, surface_R))
xy_kwargs = (xlims = (0, grid.Lx), ylims = (0, grid.Lx),
xlabel = "x (m)", ylabel = "y (m)",
aspectratio = 1,
linewidth = 0, color = :balance, legend = false)
xz_kwargs = (xlims = (0, grid.Lx), ylims = (-grid.Lz, 0),
xlabel = "x (m)", ylabel = "z (m)",
aspectratio = grid.Lx / grid.Lz * 0.5,
linewidth = 0, color = :balance, legend = false)
R_xy = contourf(xζ, yζ, surface_R'; clims=(-Rlim, Rlim), levels=Rlevels, xy_kwargs...)
δ_xy = contourf(xδ, yδ, surface_δ'; clims=(-δlim, δlim), levels=δlevels, xy_kwargs...)
R_xz = contourf(xζ, zζ, slice_R'; clims=(-Rlim, Rlim), levels=Rlevels, xz_kwargs...)
δ_xz = contourf(xδ, zδ, slice_δ'; clims=(-δlim, δlim), levels=δlevels, xz_kwargs...)
plot(R_xy, δ_xy, R_xz, δ_xz,
size = (1000, 800),
link = :x,
layout = Plots.grid(2, 2, heights=[0.5, 0.5, 0.2, 0.2]),
title = [@sprintf("ζ(t=%s) / f", prettytime(t)) @sprintf("δ(t=%s) (s⁻¹)", prettytime(t)) "" ""])
iter == iterations[end] && close(file)
end