Baroclinic instability of a 2D front based on Eady (1949)
Introduction
Eady (1949) (Eady, 1949) showed that in a uniformly sheared, stratified layer between two rigid lids on an $f$-plane, two counter-propagating Rossby edge waves can phase lock and convert available potential energy into kinetic energy, producing baroclinic eddies that grow fastest at wavelengths about four deformation radii and on timescales of a few days.
Basic state
The basic state is given by
\[\begin{align} B(y, z) &= \text{Ri}\, z - y, \\ U(y, z) &= z - {1}/{2}, \end{align}\]
where $\text{Ri}$ is the Richardson number, and $N^2 = \text{Ri}$ is the stratification.
Governing equations
The non-dimensional form of the linearized version of the QG PV perturbation equation under the $f$-plane approximation can be expressed as,
\[\begin{align} \frac{\partial q^\text{qg}}{\partial t} + U \frac{\partial q^\text{qg}}{\partial x} + \frac{\partial \psi}{\partial x} \frac{\partial Q^\text{qg}}{\partial y} = \text{E} \, \nabla_h^2 q^\text{qg}, \,\,\,\,\,\,\ \text{for} \,\,\, 0 < z <1, \end{align}\]
where $q^\text{qg}$ is the perturbation QG PV, and it is defined as
\[\begin{align} q^\text{qg} = \nabla_h^2 \psi^\text{qg} + \frac{\partial}{\partial z} \left(\frac{1}{N^2} \frac{\partial \psi^\text{qg}}{\partial z}\right), \end{align}\]
The variable $\psi^\text{qg}$ describes the QG perturbation streamfunction with $u^\text{qg}=-\partial_y \psi^\text{qg}$ and $v^\text{qg}=\partial_x \psi^\text{qg}$. The variable $Q^\text{qg}$ describes the QG PV of the basic state, which is defined as (Pedlosky, 2013)
\[\begin{align} Q^\text{qg} = -\frac{\partial U}{\partial y} + \frac{\partial}{\partial z}\left(\frac{B}{N^2} \right), \end{align}\]
and the cross-front gradient of $Q^\text{qg}$ is defined as
\[\begin{align} \frac{\partial Q^\text{qg}}{\partial y} = - \frac{\partial}{\partial z}\left(\frac{\partial_z U}{N^2} \right). \end{align}\]
The linearized perturbation buoyancy equation at the top and the bottom boundary is
\[\begin{align} \frac{\partial b^\text{qg}}{\partial t} + U \frac{\partial b^\text{qg}}{\partial x} + \frac{\partial \psi^\text{qg}}{\partial x} \frac{\partial B}{\partial y} = 0, \,\,\,\,\,\,\ \text{at} \, z=0 \,\ \text{and} \,\, 1, \end{align}\]
where $b^\text{qg}=\partial_z \psi^\text{qg}$.
Normal-mode solutions
Next, we seek normal-mode solutions for $\psi^\text{qg}$ and $q^\text{qg}$ in the form of
\[\begin{align} [\psi^\text{qg}, q^\text{qg}] = \mathfrak{R}\big([\widetilde{\psi}^\text{qg}, \widetilde{q}^\text{qg}] \big)(y, z) e^{i kx-\sigma t}, \end{align}\]
where $\widetilde{\psi}^\text{qg}$, $\widetilde{q}^\text{qg}$ are the eigenfunctions of $\psi^\text{qg}$ and $q^\text{qg}$, respectively. In terms of streamfunction $\psi^\text{qg}$,
\[\begin{align} [(\sigma + i k U) - \text{E}] \mathscr{L}\widetilde{\psi}^\text{qg} + i k \partial_y Q^\text{qg} \widetilde{\psi}^\text{qg} &= 0, \,\,\,\,\ \text{for} \,\, 0 < z <1, \\ (\sigma + i k U_{-})\partial_z \widetilde{\psi}^\text{qg}_{-} + i k \partial_y B_{-} \widetilde{\psi}^\text{qg}_{-} &= 0, \,\,\,\,\, \text{at} \,\, z = 0, \\ (\sigma + i k U_{+})\partial_z \widetilde{\psi}^\text{qg}_{+} + i k \partial_y B_{+} \widetilde{\psi}^\text{qg}_{+} &= 0, \,\,\,\,\, \text{at} \,\, z = 1, \end{align}\]
where $\mathscr{L}$ is a linear operator, and is defined as $\mathscr{L} \equiv \mathcal{D}_h^2 + 1/N^2 \partial_z^2$, where $\mathcal{D}_h^2 = (\partial_y^2 - k^2)$. The subscripts $-,+$ denote the values of the fields at $z=0$ and $z=1$, respectively.
Generalized eigenvalue problem
The above set of equations can be cast into a generalized eigenvalue problem
\[\begin{align} AX= \lambda BX, \end{align}\]
where $\lambda$ is the eigenvalue, and $X$ is the eigenvector.
Implementation using the BiGSTARS equation DSL
The DSL allows us to write the equations in physical-space notation. The dx() operator is automatically converted to im*k multiplication, and the discretization is performed once with k-separated caching for efficient wavenumber sweeps.
Load required packages
using BiGSTARS
using Printf1. Domain
x is the along-front direction (Fourier-transformed → wavenumber k) y is the cross-front direction (Fourier, periodic) z is the vertical direction (Chebyshev, rigid lids at 0 and 1)
domain = Domain(
x = FourierTransformed(),
y = Fourier(60, [0, 1]),
z = Chebyshev(30, [0, 1])
)Domain(x=FourierTransformed(), y=Fourier(N=60, [0.0,1.0]), z=Chebyshev(N=30, [0.0,1.0]))2. Problem
prob = EVP(domain, variables=[:psi], eigenvalue=:sigma)EVP Problem
Domain: Domain(x=FourierTransformed(), y=Fourier(N=60, [0.0,1.0]), z=Chebyshev(N=30, [0.0,1.0]))
Variables: [:psi]
Eigenvalue: sigma
Parameters:
Equations: 0
BCs: 0 (0 dynamic)
Substitutions:
3. Parameters and background state
Y, Z = gridpoints(domain, :y, :z)
H = 1.0
Ri = 1.0
prob[:U] = Z .- 0.5 * H # along-front velocity: U(z) = z - 1/2
prob[:Ri] = Ri # Richardson number
prob[:E] = 1e-12 # Ekman number (hyperviscosity)
prob[:dBdy] = -ones(length(Z)) # dB/dy = -1 for Eady basic state30-element Vector{Float64}:
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0
-1.0Background PV gradient (zero for the Eady problem)
prob[:dQdy] = zeros(length(Z))30-element Vector{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
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.04. Substitutions
QG PV operator: Lap = dx² + dy² + (1/Ri)dz² For Eady with constant N² = Ri: Lap(A) = dx(dx(A)) + dy(dy(A)) + (1/Ri)dz(dz(A))
@substitution Lap(A) = dx(dx(A)) + dy(dy(A)) + Ri * dz(dz(A))BiGSTARS.Substitution(:Lap, [:A], ((dx(dx(A)) + dy(dy(A))) + (Ri * dz(dz(A)))))5. Governing equation (interior QG PV)
sigma * Lap(psi) = U * dx(Lap(psi)) + dQdy * dx(psi) - E * Lap(Lap(psi))
@equation sigma * Lap(psi) = U * dx(Lap(psi)) + dQdy * dx(psi) - E * Lap(Lap(psi))1-element Vector{BiGSTARS.Equation}:
BiGSTARS.Equation((sigma * Lap(psi)), (((U * dx(Lap(psi))) + (dQdy * dx(psi))) - (E * Lap(Lap(psi)))))6. Boundary conditions
Eady BCs: linearized buoyancy equation at z=0 and z=1 sigma * dz(psi) + U * dx(dz(psi)) + dBdy * dx(psi) = 0 These are eigenvalue-dependent (dynamic) BCs — the DSL detects sigma and puts the sigma side into the B matrix, the rest into A.
@bc left(sigma * dz(psi) + U * dx(dz(psi)) + dBdy * dx(psi)) = 0
@bc right(sigma * dz(psi) + U * dx(dz(psi)) + dBdy * dx(psi)) = 02-element Vector{BiGSTARS.BoundaryCondition}:
BiGSTARS.BoundaryCondition(:left, :z, (((sigma * dz(psi)) + (U * dx(dz(psi)))) + (dBdy * dx(psi))), 0.0, true)
BiGSTARS.BoundaryCondition(:right, :z, (((sigma * dz(psi)) + (U * dx(dz(psi)))) + (dBdy * dx(psi))), 0.0, true)7. Discretize
cache = discretize(prob)DiscretizationCache
System size: 1800 x 1800 (1 variables, 1800 per var)
A components (k-powers): [0, 1, 2, 3, 4]
B components (k-powers): [0, 1, 2]
Total nnz: 81206 (1.25% dense)
8. Solve over wavenumbers with adaptive shift
function solve_eady(cache, k_values, Ri)
growth_rates = zeros(length(k_values))
sigma_shift = 0.02
for (i, k_val) in enumerate(k_values)
A, B = assemble(cache, Float64(k_val))
solver = EigenSolver(A, B; σ₀=sigma_shift, method=:Krylov, nev=1, which=:LR, sortby=:R)
try
solve!(solver; verbose=false)
λ, _ = get_results(solver)
growth_rates[i] = real(λ[1])
sigma_shift = real(λ[1]) ## adapt shift for next wavenumber
catch
growth_rates[i] = NaN
end
end
# Results
valid_idx = findall(!isnan, growth_rates)
if !isempty(valid_idx)
idx_max = valid_idx[argmax(growth_rates[valid_idx])]
@printf "Most unstable wavenumber: k = %.3f\n" k_values[idx_max]
@printf "Maximum growth rate: sigma = %.6f\n" growth_rates[idx_max]
else
println("No wavenumber converged.")
end
# Compare with analytical Eady solution
println("\nComparison with Eady (1949) analytical solution:")
for k_test in [0.1, 0.5, 1.0, 1.5]
mu = k_test * sqrt(Ri)
arg1 = coth(0.5mu) - 0.5mu
arg2 = 0.5mu - tanh(0.5mu)
sigma_theory = arg1 * arg2 > 0 ? (1.0 / sqrt(Ri)) * sqrt(arg1 * arg2) : 0.0
idx = argmin(abs.(collect(k_values) .- k_test))
@printf " k=%.1f: numerical=%.6f, analytical=%.6f\n" k_test growth_rates[idx] sigma_theory
end
return growth_rates
end
k_values = [0.1]
growth_rates = solve_eady(cache, k_values, Ri)1-element Vector{Float64}:
0.028829034425800273This page was generated using Literate.jl.