Near-inertial waves
This simple example demonstrates how to set up and run a simulation with SawyerEliassenSolver
.
Setting up a problem
First load the SawyerEliassenSolver and the other packages we need.
using CairoMakie
using HDF5
using Printf
using Statistics
using SawyerEliassenSolver
Grid
The first step is to create a grid. The grids are periodic in $x$ and bounded in $z$. Let's simulate some near-inertial waves in a domain of depth 1 km and width 250 km.
NX, NZ = 128, 64
const LX, LZ = 250, 1
grid = Grid(NX, NZ, (-LX / 2, LX / 2), LZ)
Grid{Float64}:
├── NX: 128
├── NZ: 64
├─── x: [-125,125)
└─── z: [-1,0]
Some remarks on the grid:
- Here we use the default floating point precision of
Float64
. - Alternatively, we could use
Float32
by creating the grid withgrid = Grid(Float32, NX, NZ, (-LX/2,LX/2), LZ)
. - The choice of precision here propagates through the entire simulation.
- We specified the $x$ extent with a tuple of bounds but the $z$ extent with a single depth value. Passing a the width / depth is equivalent to passing a tuple of
(0, LX)
/(-LZ, 0)
for $x$ and $z$ respectively.
Background Flow
With a grid we can now create a background flow. When creating the background flow let's set the Coriolis frequency to a typical midlatitude value of $10^{-4}\mathrm{s}^{-1}$.
const f = 1e-4
background_flow = BackgroundFlow(grid, f)
BackgroundFlow:
├─── f: 0.0001
├── Vx: 128×64 Matrix{Float64}
│ [0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0]
├── Bx: 128×64 Matrix{Float64}
│ [0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0]
└── Bz: 128×64 Matrix{Float64}
[0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0]
The background flow is initialised to zeros. For our simple example, let's use a constant background stratification of $10^{-4}\mathrm{s}^{-2}$
const N² = 1e-4
background_flow.Bz .= N²;
Domain
Now we create a Domain
. The Domain
object contains not just the grid but also details of the spectral domain and the transforms between the different representations. When creating a Domain
we can specify the number of high wavenumbers to zero-out inorder to dealias the products with the background flow. In this example the background flow is constant and so we do not need to worry about dealiasing.
domain = Domain(grid)
Domain:
├─────────── grid: Grid with eltype Float64 and size (128, 64)
├─────── spectral: Spectral domain of size (65, 64) and spectral resolution (64, 63)
└───── transforms: FFTW transforms: rfft, type II DST and type II DCT.
Problem
The next step is to define a Problem
. A problem consists of a domain, background flow and forcing. We leave forcing for a later example and thus create the problem with
problem = Problem(domain, background_flow)
Problem:
├─────────── domain: Domain with eltype Float64 and physical size (128, 64)
├─────── background: BackgroundFlow with f = 0.0001 and Vx,Bx,Bz = 128×64 Matrix{Float64}
├──────── ζ_forcing: NoForcing{Float64}
├──────── v_forcing: NoForcing{Float64}
├──────── b_forcing: NoForcing{Float64}
└──────────── state: ζ,ζₜ = 65×64 FSVariable{Float64}, v,b = 128×64 XZVariable{Float64}, clock = Clock(t = 0, iteration = 0)
The problem object also contains the current state of the simulation.
Initial conditions
Here we simulate a pair of waves propagating horizontally in different directions. If the streamfunction of a wave is $\psi = a \cos(k x - \omega t + \phi) \sin m z$ then the polarisation relations give
\[\begin{align*} u & = -m a \cos(k x - \omega t + \phi) \cos m z \\ v & = -(f / \omega) m a \sin(k x - \omega t + \phi) \cos m z \\ w & = -k a \sin(k x - \omega t + \phi) \sin m z \\ b & = (N^2 / \omega) k a \cos(k x - \omega t + \phi) \sin m z \end{align*}\]
ω(k, m) = √(f^2 * m^2 + N² * k^2) / √(k^2 + m^2)
function u(x, z, t, W)
return -W.m * W.a * cos(W.k * x - ω(W.k, W.m) * t + W.φ) * cos(W.m * z)
end
function v(x, z, t, W)
return -(f / ω(W.k, W.m)) *
W.m *
W.a *
sin(W.k * x - ω(W.k, W.m) * t + W.φ) *
cos(W.m * z)
end
function w(x, z, t, W)
return -W.k * W.a * sin(W.k * x - ω(W.k, W.m) * t + W.φ) * sin(W.m * z)
end
function b(x, z, t, W)
return N² / ω(W.k, W.m) *
W.k *
W.a *
cos(W.k * x - ω(W.k, W.m) * t + W.φ) *
sin(W.m * z)
end
W₁ = (; k=2 * π / LX, m=π / LZ, a=0.1, φ=0)
W₂ = (; k=-12 * π / LX, m=4 * π / LZ, a=0.01, φ=π / 2);
ω(W₁.k, W₁.m) / f, ω(W₂.k, W₂.m) / f
(1.2805838694583849, 1.5619374797310408)
A couple of utility functions allow us to set initial conditions from the velocity and buoyancy. First we specify the initial conditions as functions of x
and z
.
u₀(x, z) = u(x, z, 0, W₁) + u(x, z, 0, W₂)
v₀(x, z) = v(x, z, 0, W₁) + v(x, z, 0, W₂)
w₀(x, z) = w(x, z, 0, W₁) + w(x, z, 0, W₂)
b₀(x, z) = b(x, z, 0, W₁) + b(x, z, 0, W₂);
Then we use set_ζ!
and set_vb!
to set the initial conditions.
set_ζ!(problem; u=u₀, w=w₀)
set_vb!(problem; v=v₀, b=b₀)
Note that set_vb!
also sets the state variable ζₜ
using $\zeta_t = b_x - fv_z$. The functions set_v!
and set_b!
can be used to set v
and b
separately without computing ζₜ
. Furthermore, with v
and b
specified the function compute_ζₜ!
can be used to compute and set ζₜ
.
Timestepper
The next step is to create a timestepper. We use a timestep of Δt = 2πf⁻¹/100
.
Δt = 2π / f / 100
ts = Timestepper(problem, Δt)
Timestepper:
├───────── problem: Problem{Float64, NoForcing{Float64}, NoForcing{Float64}, NoForcing{Float64}}
├──────── timestep: h = 628.32
├─────────────── 𝓒: SawyerEliassenSolver.Timesteppers.DIRKNCoefficients{Float64}
├───────────── cgs: SawyerEliassenSolver.Timesteppers.ConjugateGradientSolver{Float64}
└─────────────── 𝓟: IdentityPreconditioner{Float64}
Output
We can write output to an HDF5 file using an OutputWriter
.
output_writer = OutputWriter(problem, "near_inertial_waves.h5"; overwrite=true)
OutputWriter:
├─────── filepath: near_inertial_waves.h5
├──────── problem: Problem{Float64, NoForcing{Float64}, NoForcing{Float64}, NoForcing{Float64}}
├──── coordinates: x, z
└────── variables:
Here we'll just save the u
component of the velocity. Common output variables are defined in the submodule OutputVariables
u_output = OutputVariables.u(problem)
add_output_variables!(output_writer; u=u_output)
We can also write some attributes to the output file.
write_attributes!(output_writer; f=f, N²=N²)
To save the current state of the simulation to the output file we use the write!
function. i.e. to save the initial conditions
write!(output_writer)
Running the simulation
Finally, it is time to run the simulation. We use advance!
to timestep the simulation. Let's run the simulation for 1000 timesteps saving output every 10 timesteps.
for _ in 1:100
advance!(ts, 10)
write!(output_writer)
end
Visualise the solution
Read in the output we saved.
output = h5open("near_inertial_waves.h5", "r") do h5
(
u=read_dataset(h5, "u"),
time=read_dataset(h5, "time"),
x=read_dataset(h5, "x"),
z=read_dataset(h5, "z"),
)
end;
Make a video of u
.
n = Observable(1)
uₙ = @lift output[:u][:, :, $n]
title = @lift @sprintf "t = %.2f inertial periods" output[:time][$n] * f / 2π
fig = Figure(; size=(1200, 400))
Label(fig[1, 1:2], title; tellwidth=false)
ax = Axis(fig[2, 1]; xlabel="x [km]", ylabel="z [km]")
cf = heatmap!(ax, output[:x], output[:z], uₙ; colormap=:balance, colorrange=(-0.5, 0.5))
Colorbar(fig[2, 2], cf; label="u [ms⁻¹]", labelpadding=10)
record(fig, "near-inertial_waves.mp4", 1:length(output[:time]); framerate=10) do i
n[] = i
end
We have a mode 1 wave propagating to the right and a mode 4 wave propagating to the left.
Error analysis
We can compute the root mean square error between the theoretical solution and the simulated solution after 1 inertial period
x_grid, z_grid = gridpoints(domain)
u_theory = [
u(x, z, 2π / f, W₁) + u(x, z, 2π / f, W₂) for
x in xgridpoints(domain), z in zgridpoints(domain)
];
function run_one_inertial_period(problem, nsteps::Int)
set_ζ!(problem; u=u₀, w=w₀)
set_vb!(problem; v=v₀, b=b₀)
Δt = 2π / f / nsteps
ts = Timestepper(problem, Δt)
for _ in 1:nsteps
advance!(ts)
end
output = OutputVariables.u(problem)
compute!(output)
return output
end
function rms_error(simulated, theory)
squared_error = (simulated .- theory) .^ 2
return sqrt(mean(squared_error))
end;
Make a plot of the root mean square error as a function of the timestep. Our time stepping scheme is third-order accurate and we see that the error scales as Δt³
. The lower bound for the error is determined by the tolerance of the iterative solver.
fig = Figure(; size=(1200, 400))
ax = Axis(
fig[1, 1];
xlabel="Δt [Inertial periods]",
ylabel="RMS Error",
xscale=log10,
yscale=log10,
)
number_of_steps = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000]
for nsteps in number_of_steps
problem = Problem(domain, background_flow)
u_sim = run_one_inertial_period(problem, nsteps)
rms = rms_error(u_sim, u_theory)
scatter!(ax, 1 / nsteps, rms; color=:red, marker=:cross)
end
lines!(
ax,
1 ./ number_of_steps,
1e3 ./ number_of_steps .^ 3;
color=:black,
label="Δt³",
linewidth=2,
)
axislegend(; position=:lt)
fig
This page was generated using Literate.jl.