Forced secondary circulation

This example demonstrates how to apply RHS forcing and how to create output variables.

With the flow initially at rest we will apply forcing to the RHS of the vorticity equation and generate a secondary circulation. The forcing function will take the form

\[\mathcal{F}(x,z,t) = \mathcal{S}\left(\frac{x}{L},\frac{z}{H}\right)\sigma\left(\frac{t}{\tau}\right)\]

where $L$, $H$ and $\tau$ are horizontal, vertical and temporal scales respectively. The spatial structure is given by a Gaussian,

\[\mathcal{S}(x',z') = \exp\left(-\frac{x'^2}{2} + \frac{z'^2}{2}\right),\]

and the temporal part is given by (minus) the derivative of a Gaussian,

\[\sigma(t') = t'\exp(-t'^2 / 2).\]

Load packages

using CairoMakie
using HDF5
using Printf

using SawyerEliassenSolver

Grid, background flow and domain

We non-dimensionalise by the half-height of the domain and f. The background flow has uniform gradients and thus we require no dealiasing.

const NX, NZ = 2048, 256
const LX::Float64 = 1000
const M²::Float64 = 100
const N²::Float64 = 1e5

grid = Grid(NX, NZ, (-LX / 2, LX / 2), (-1, 1))

background_flow = BackgroundFlow(grid)
background_flow.Bx .= M²
background_flow.Bz .= N²

domain = Domain(grid);

Forcing

$\zeta$ forcing can be applied in four different ways:

Here we implement Pointwise Physical Forcing where we specify the forcing as a function of x, z and t. The function can also optionally accepted parameters as the final arguments. We specify the spatial and temporal parts of the RHS forcing as non-dimensional functions and then provide the length and time scales as parameters.

@inline forcing_spatial(x, z) = exp(-(x^2 + z^2) / 2)
@inline forcing_temporal(t) = t * exp(-t^2 / 2)
@inline forcing(x, z, t, p) = forcing_spatial(x / p.L, z / p.H) * forcing_temporal(t / p.τ)
forcing (generic function with 1 method)

We now construct the RHS forcing for the domain by passing in the function and parameters. We choose a timescale of 3 inertial periods.

const parameters = (; L=20.0, H=0.05, τ=6π)
physical_forcing = PointwisePhysicalForcing(domain, forcing, parameters)
PointwisePhysicalForcing:
  ├──── domain: Domain with eltype Float64 and physical size (2048, 256)
  └──── params: (L = 20.0, H = 0.05, τ = 18.84955592153876)

Time dependence of the forcing

fig = fig = Figure(; size=(1200, 400))
Label(fig[1, :], "Forcing time dependence"; tellwidth=false)
ax = Axis(
    fig[2, :];
    xlabel=L"t \ [\text{inertial periods}]",
    ylabel=L"\sigma\left(\frac{t}{\tau}\right)",
)
time = 0.0:0.1:10.0
lines!(ax, time, forcing_temporal.(time / 3))
xlims!(ax, 0, 10)
ylims!(ax, 0, nothing)

current_figure() # hide
fig

Setting up the problem

Now we pass in the forcing as a keyword argument when constructing the problem.

problem = Problem(domain, background_flow; ζ_forcing=physical_forcing)
Problem:
  ├─────────── domain: Domain with eltype Float64 and physical size (2048, 256)
  ├─────── background: BackgroundFlow with f = 1 and Vx,Bx,Bz = 2048×256 Matrix{Float64}
  ├──────── ζ_forcing: PointwisePhysicalForcing{Float64, @NamedTuple{L::Float64, H::Float64, τ::Float64}}
  ├──────── v_forcing: NoForcing{Float64}
  ├──────── b_forcing: NoForcing{Float64}
  └──────────── state: ζ,ζₜ = 1025×256 FSVariable{Float64}, v,b = 2048×256 XZVariable{Float64}, clock = Clock(t = 0, iteration = 0)

We can also pass in PointwisePhysicalForcing or PointwiseSpectralForcing as keyword arguments v_forcing and b_forcing for v and b respectively.

We don't need to set any initial conditions but we'll use a preconditioner.

preconditioner = DiagonalQuadraticPreconditioner(domain, 1.0, N²)
ts = Timestepper(problem, 2π / 20, preconditioner)
Timestepper:
  ├───────── problem: Problem{Float64, PointwisePhysicalForcing{Float64, @NamedTuple{L::Float64, H::Float64, τ::Float64}}, NoForcing{Float64}, NoForcing{Float64}}
  ├──────── timestep: h = 0.31416
  ├─────────────── 𝓒: SawyerEliassenSolver.Timesteppers.DIRKNCoefficients{Float64}
  ├───────────── cgs: SawyerEliassenSolver.Timesteppers.ConjugateGradientSolver{Float64}
  └─────────────── 𝓟: DiagonalQuadraticPreconditioner with ω₀² = 1, ω₁² = 1e+05

Output

Let's save u and w to visualise the overturning circulation that we're forcing. Also compute and save the background buoyancy.

output_writer = OutputWriter(problem, "forced_secondary_circulation.h5"; overwrite=true)
add_output_variables!(
    output_writer; u=OutputVariables.u(problem), w=OutputVariables.w(problem)
)
write_attributes!(
    output_writer; f=1.0, M²=M², N²=N², L=parameters.L, H=parameters.H, tau=parameters.τ
)
write_background_buoyancy!(output_writer)
write!(output_writer)

Run the simulation.

for ii in 1:50
    advance!(ts, 4)
    write!(output_writer)
end

Movie

output = h5open("forced_secondary_circulation.h5", "r") do h5
    (
        u=read_dataset(h5, "u"),
        w=read_dataset(h5, "w"),
        time=read_dataset(h5, "time"),
        x=read_dataset(h5, "x"),
        z=read_dataset(h5, "z"),
        B=read_dataset(h5, "B"),
    )
end;

n = Observable(1)
uₙ = @lift output[:u][:, :, $n]
wₙ = @lift output[:w][:, :, $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 = Axis(fig[2, 1]; ylabel="z")
ax_w = Axis(fig[2, 2]; xlabel="x", ylabel="z")
linkaxes!(ax_u, ax_w)

cf_u = heatmap!(
    ax_u, output[:x], output[:z], uₙ; colormap=:balance, colorrange=(-0.01, 0.01)
)
Colorbar(fig[3, 1], cf_u; vertical=false, label=L"u", labelpadding=10)
contour!(ax_u, output[:x], output[:z], output[:B]; color=:black)

cf_w = heatmap!(
    ax_w, output[:x], output[:z], wₙ; colormap=:balance, colorrange=(-1e-4, 1e-4)
)
Colorbar(fig[3, 2], cf_w; vertical=false, label=L"w", labelpadding=10)
contour!(ax_w, output[:x], output[:z], output[:B]; color=:black)

record(fig, "forced_secondary_circulation.mp4", 1:length(output[:time]); framerate=8) do i
    n[] = i
end
"forced_secondary_circulation.mp4"


This page was generated using Literate.jl.