Barotropic zeta refraction
This example demonstrates how to apply dealiasing, explores different variable representations and uses a preconditioner to speed up the iterative solver.
Grid
The first step is to load the packages and then create a grid. Here the grid is non-dimensionalised by the half-width of the domain.
using CairoMakie
using HDF5
using Printf
using SawyerEliassenSolver
using SpecialFunctions: erf
NX, NZ = 128, 512
const LX, LZ = 2, 0.1
grid = Grid(NX, NZ, (-LX / 2, LX / 2), LZ)
Grid{Float64}:
├── NX: 128
├── NZ: 512
├─── x: [-1,1)
└─── z: [-0.1,0]
Background Flow
The background flow is barotropic with constant stratification $N^2 = 200^2f^2$ but spatially varying vorticity. We are modelling a cyclonic Gaussian filament in an otherwise anticyclonic flow. Non-dimensionalising by $f$ the lateral shear is given by
\[V_x = Ro\left(-1 + 2L^{-1}\mathrm{e}^{-\pi x^2/L^2}\right)\]
where $Ro = 0.5$ is the Rossby number of the anticyclonic region and $L = 0.2$ is the half-width of the Gaussian filament.
const N²::Float64 = 200^2
const Ro = 0.5
const L = 0.2
Vx(x) = Ro * (-1 + 2 / L * exp(-π * x^2 / L^2));
We create a background flow with the default $f = 1$.
xgrid, zgrid = gridpoints(grid)
background_flow = BackgroundFlow(grid)
background_flow.Vx .= Vx.(xgrid);
background_flow.Bz .= N²;
Dealiasing
The background flow is spatially varying and so it is a good idea to dealias by zeroing out the highest wavenumbers. This is done when creating the Domain
. Here we drop the highest 10 wavenumbers in the horizontal.
const dealias = 10
domain = Domain(grid; dealias_x=dealias)
Domain:
├─────────── grid: Grid with eltype Float64 and size (128, 512)
├─────── spectral: Spectral domain of size (65, 512) and spectral resolution (54, 511)
└───── transforms: FFTW transforms: rfft, type II DST and type II DCT.
Variables
Let's visualise the background flow in both physical and Fourier space. We can do this by creating an XZVariable
and then transforming in the horizontal.
Vx_physical = XZVariable(domain)
Vx_physical .= background_flow.Vx
Vx_fourier = horizontal_transform(Vx_physical);
summary(Vx_fourier)
"65×512 FZVariable{Float64}"
Notice that Vx_fourier
is an FZVariable
. Variables can be any of six types determined by their horizontal and vertical representations. The six types are
XZVariable
- physical in horizontal and verticalFZVariable
- Fourier in horizontal, physical in verticalXSVariable
- physical in horizontal, sine in verticalFSVariable
- Fourier in horizontal, sine in verticalXCVariable
- physical in horizontal, cosine in verticalFCVariable
- Fourier in horizontal, cosine in vertical
Furthermore, the functions horizontal_transform
, sine_transform
, cosine_transform
, and the corresponding inplace versions horizontal_transform!
, sine_transform!
, cosine_transform!
, can be used to transform between these representations.
Let's plot the background flow as a function of $x$ and its power spectrum as a function of horizontal wavenumber $k_x$. We also indicate where the 10th wavenumber is.
kx = xwavenumbers_full(domain)
fig = Figure(; size=(1200, 600))
Label(fig[1, 1], "Gaussian vorticity filament"; tellwidth=false)
ax_physical = Axis(fig[2, 1]; xlabel=L"x", ylabel=L"V_x")
lines!(ax_physical, xgrid[:, 1], Vx_physical[:, 1]; color=:black)
xlims!(ax_physical, -1, 1)
ax_spectral = Axis(fig[3, 1]; xlabel=L"k_x", ylabel=L"\Phi_{V_x}")
scatterlines!(ax_spectral, kx, abs2.(Vx_fourier[:, 1]); color=:black)
vlines!(ax_spectral, kx[dealias]; color=:red)
xlims!(ax_spectral, 0, 200)
current_figure() # hide
fig
We see that all the power of the background flow is contained in the first 10 wavenumbers and that our dealiasing will be sufficient.
Initial Conditions
We initialise a slab of across-front velocity
\[u_0 = \frac{1}{2}\left(1 + \mathrm{erf}\left(3 + \frac{2 z}{H}\right)\right)\]
where $H = 2\times10^{-3}$.
const H = 2e-3
u₀(x, z) = 0.5 * (1 + erf(3 + 2 * z / H));
problem = Problem(domain, background_flow)
set_ζ!(problem; u=u₀)
Preconditioning
We can speed up the iterative solver by using a preconditioner. Here we use a DiagonalQuadraticPreconditioner
This preconditioner approximates the Sawyer-Eliassen equation in spectral space by
\[\mathcal{L}\zeta \approx \left(\omega_0^2 k_z^2 + \omega_1^2 k_x^2\right) / (k_x^2 + k_z^2) \zeta\]
Here we set $\omega_0^2 = 1$ and $\omega_1^2 = N²$.
ω₀² = 1.0
ω₁² = N²
preconditioner = DiagonalQuadraticPreconditioner(domain, ω₀², ω₁²)
DiagonalQuadraticPreconditioner:
├───────── domain: Domain with eltype Float64 and physical size (128, 512)
├──────────── ω₀²: 1
└──────────── ω₁²: 40000
Then we create a timestepper with timestep $\Delta t = 2\pi / f / 50$.
ts = Timestepper(problem, 2π / 50, preconditioner)
Timestepper:
├───────── problem: Problem{Float64, NoForcing{Float64}, NoForcing{Float64}, NoForcing{Float64}}
├──────── timestep: h = 0.12566
├─────────────── 𝓒: SawyerEliassenSolver.Timesteppers.DIRKNCoefficients{Float64}
├───────────── cgs: SawyerEliassenSolver.Timesteppers.ConjugateGradientSolver{Float64}
└─────────────── 𝓟: DiagonalQuadraticPreconditioner with ω₀² = 1, ω₁² = 40000
Output
Save $u$, $v$, $w$, $b$ and $∂u/∂z$ every 5 timesteps. We'll also save the background flow as ConstantOutputVariable
s using write_background_flow!
.
output_writer = OutputWriter(problem, "barotropic_zeta_refraction.h5"; overwrite=true)
add_output_variables!(
output_writer;
u=OutputVariables.u(problem),
v=OutputVariables.v(problem),
w=OutputVariables.w(problem),
b=OutputVariables.b(problem),
∂u∂z=OutputVariables.∂u∂z(problem),
)
write_background_flow!(output_writer)
write_attributes!(output_writer; Ro=Ro, L=L, H=H)
write!(output_writer)
Run the simulation for 10 inertial periods.
for _ in 1:100
advance!(ts, 5)
write!(output_writer)
end
Visualise the solution
We'll plot the across-filament vertical shear and the wave energy density
\[\mathcal{E} = \frac{1}{2}\left(u^2 + v^2 + w^2 + b^2/N^2\right)\]
in the top 25% of the domain. We'll also contour the background vorticity $V_x$
z_idx = (3 * NZ ÷ 4):NZ
output = h5open("barotropic_zeta_refraction.h5", "r") do h5
u = h5["u"][:, z_idx, :]
v = h5["v"][:, z_idx, :]
w = h5["w"][:, z_idx, :]
b = h5["b"][:, z_idx, :]
E = (u .^ 2 .+ v .^ 2 .+ w .^ 2 .+ b .^ 2 / N²) / 2
Vx = h5["Vx"][:, z_idx]
(
E=E,
∂u∂z=h5["∂u∂z"][:, z_idx, :],
Vx=Vx,
time=read_dataset(h5, "time"),
x=read_dataset(h5, "x"),
z=h5["z"][z_idx],
)
end;
n = Observable(1)
∂u∂zₙ = @lift output[:∂u∂z][:, :, $n]
Eₙ = @lift output[:E][:, :, $n]
title = @lift @sprintf "t = %.2f inertial periods" output[:time][$n] / 2π
fig = Figure(; size=(1200, 400))
Label(fig[1, 1:2], title; tellwidth=false)
ax_∂u∂z = Axis(fig[2, 1]; ylabel="z")
ax_E = Axis(fig[2, 2]; xlabel="x", ylabel="z")
linkaxes!(ax_∂u∂z, ax_E)
contour_levels = 0:1:4
cf_∂u∂z = heatmap!(
ax_∂u∂z, output[:x], output[:z], ∂u∂zₙ; colormap=:balance, colorrange=(-1000, 1000)
)
Colorbar(
fig[3, 1], cf_∂u∂z; vertical=false, label=L"\partial u/\partial z", labelpadding=10
)
contour!(
ax_∂u∂z,
output[:x],
output[:z],
output[:Vx];
levels=contour_levels,
color=:black,
labels=true,
)
cf_E = heatmap!(
ax_E, output[:x], output[:z], Eₙ; colormap=Reverse(:grays), colorrange=(0, 0.5)
)
Colorbar(fig[3, 2], cf_E; vertical=false, label=L"\mathcal{E}", labelpadding=10)
contour!(
ax_E,
output[:x],
output[:z],
output[:Vx];
levels=contour_levels,
color=:black,
labels=true,
)
record(fig, "barotropic_zeta_refraction.mp4", 1:length(output[:time]); framerate=10) do i
n[] = i
end
Performance with and without preconditioning
Let's time how long it takes to run 10 inertial periods with and without preconditioning. First create a clean problem.
function setup_problem()
grid = Grid(NX, NZ, (-LX / 2, LX / 2), LZ)
domain = Domain(grid; dealias_x=dealias)
xgrid, zgrid = gridpoints(grid)
background_flow = BackgroundFlow(grid)
background_flow.Vx .= Vx.(xgrid)
background_flow.Bz .= N²
problem = Problem(domain, background_flow)
set_ζ!(problem; u=u₀)
return domain, problem
end;
Without preconditioning
Create the timestepper and advance 1 step before timing.
domain, problem = setup_problem()
ts = Timestepper(problem, 2π / 50)
advance!(ts)
@time advance!(ts, 500)
199.356834 seconds (4.00 k allocations: 125.000 KiB)
With preconditioning
domain, problem = setup_problem()
preconditioner = DiagonalQuadraticPreconditioner(domain, ω₀², ω₁²)
ts = Timestepper(problem, 2π / 50, preconditioner)
advance!(ts)
@time advance!(ts, 500)
15.332822 seconds (4.00 k allocations: 125.000 KiB)
We see that the preconditioner speeds up the simulation by more than a factor of 10. This is because the preconditioner drastically reduces the number of iterations required by the iterative solver from approximately 150 to 3.
This page was generated using Literate.jl.