An interactive Geophysical Fluid Dynamics Textbook Notation

Notation — symbols & sign conventions

NOTATION — symbols & sign conventions

Linked from every chapter. If the printed equation and the code disagree, the code is wrong. Fix conventions here first.

Coordinates & operators

SymbolMeaning
x,y,zx,y,zeastward, northward, upward
u=(u,v,w)\mathbf{u}=(u,v,w)velocity
∇2\nabla^2horizontal Laplacian (spectral: −k2-k^2)
J(A,B)=AxBy−AyBxJ(A,B)=A_xB_y-A_yB_xhorizontal Jacobian

Rotation & stratification

SymbolMeaningConvention
f=f0+βyf=f_0+\beta yCoriolis parameterβ>0\beta>0 (increases northward)
Ω\Omegaplanetary rotation rate
N2=−gρ0∂ρ∂zN^2=-\frac{g}{\rho_0}\frac{\partial\rho}{\partial z}buoyancy freq. squaredN2>0N^2>0 stable
b=−gρ′/ρ0b=-g\rho'/\rho_0buoyancy

Dynamical fields

SymbolMeaningSign
ψ\psistreamfunctionu=(−ψy,ψx)\mathbf{u}=(-\psi_y,\psi_x)
ζ=∇2ψ\zeta=\nabla^2\psirelative vorticity∇2ψ=ζ\nabla^2\psi=\zeta; invert with invert_laplacian(zeta_hat)
qqpotential vorticityErtel / QG per chapter

Streamfunction sign. We use u=z^×∇ψ=(−ψy,ψx)\mathbf u=\hat{\mathbf z}\times\nabla\psi=(-\psi_y,\psi_x) and ζ=∇2ψ\zeta=\nabla^2\psi. In vorticity-form notebooks solve ∇2ψ=ζ\nabla^2\psi=\zeta (so psi_hat = grid.invert_laplacian(zeta_hat)). Keep this consistent across all notebooks.

Geostrophic, hydrostatic & thermal-wind balance

SymbolMeaningConvention
ϕ\phikinematic pressure, p/ρ0p/\rho_0
ug=(ug,vg)\mathbf u_g=(u_g,v_g)geostrophic velocityug=−ϕy/f, vg=ϕx/fu_g=-\phi_y/f,\ v_g=\phi_x/f
hydrostatic balanceϕz=b\phi_z=b
thermal windfug,z=−by,fvg,z=bxfu_{g,z}=-b_y,\quad fv_{g,z}=b_xvertical shear from the horizontal buoyancy gradient

Geostrophic balance is ψ=ϕ/f\psi=\phi/f. ug=−ϕy/f, vg=ϕx/fu_g=-\phi_y/f,\ v_g=\phi_x/f has exactly the streamfunction form above with ψ=ϕ/f\psi=\phi/f — geostrophic flow always has a streamfunction, and ϕ/f\phi/f is it. gfdlib.balance works in a single (y,z)(y,z) cross-section with a closed-form frontal buoyancy field (no discretization): buoyancy_field and its exact yy-derivative dbdy_frontal, and thermal_wind_u, the exact vertical integral of the thermal-wind relation. The buoyancy front’s sign reverses at z=Htropz=H_{trop} (an idealized tropopause), so ug(y,z)u_g(y,z) has an extremum there at every yy — the geostrophic jet core sits where the meridional buoyancy gradient changes sign.

PV inversion & staircases (barotropic, ff- or β\beta-plane)

SymbolMeaningConvention
q=ζ+βyq=\zeta+\beta y(absolute) potential vorticitymaterial conservation: qt+J(ψ,q)=0q_t+J(\psi,q)=0

The invertibility principle. Given q(x,y)q(x,y) everywhere, a balance condition (ζ=∇2ψ\zeta=\nabla^2\psi, i.e. nondivergent flow), and boundary conditions (here: periodic), ψ\psi — and hence u\mathbf u — is uniquely determined: psi_hat = grid.invert_laplacian(zeta_hat) with ζ=q−βy\zeta=q-\beta y. gfdlib.pv.gaussian_blob places a periodic-safe PV anomaly at a point; gfdlib.pv.staircase_pv builds an idealized alternating-band PV staircase (a smoothed square wave in yy). Inverting a staircase shows the jet cores (uu extrema) sit exactly at the sharp PV risers between bands, not within the well-mixed plateaus — verified numerically (local ∣qy∣|q_y| at each jet core is ∼\sim20×\times the domain-mean ∣qy∣|q_y|).

Barotropic instability (Rayleigh-Kuo)

SymbolMeaningConvention
U(y)U(y)basic-state zonal flowchannel, rigid walls ϕ=0\phi=0
c=cr+icic=c_r+ic_icomplex phase speed of a normal mode ϕ(y)eik(x−ct)\phi(y)e^{ik(x-ct)}unstable iff ci>0c_i>0

Rayleigh-Kuo equation: (U−c)(ϕ′′−k2ϕ)+(β−U′′)ϕ=0(U-c)(\phi''-k^2\phi)+(\beta-U'')\phi=0 — derived independently in this repo (not copied from a text) and checked against the classical form at β=0\beta=0 (Drazin & Reid). A necessary condition for instability is that β−U′′\beta-U'' changes sign somewhere in the domain (Rayleigh’s inflection-point criterion, generalized to a β\beta-plane by Kuo). gfdlib.instability.growth_rate solves this as a generalized eigenvalue problem Aϕ=cBϕA\phi=cB\phi, rewritten B−1AB^{-1}A (no SciPy needed) and diagonalized with plain numpy.linalg.eigvals. Verified: a profile with no inflection point gives exactly zero growth at every kk tested; the classical tanh⁡(y/δ)\tanh(y/\delta) shear layer’s growth rate peaks at kδ≈0.445k\delta\approx0.445, matching the published value (Michalke 1964, kδ≈0.4446k\delta\approx0.4446) to within grid resolution. gfdlib.instability.double_shear_layer builds the periodic nonlinear initial condition; its evolution reuses spectral.Grid and timestep.ifrk4_step unchanged from ch18.

Shallow water (linearized, beta-plane)

SymbolMeaningConvention
HHmean layer depth
η\etafree-surface height anomalypositive = raised surface
c=gHc=\sqrt{gH}gravity-wave speed
LR=c/f0L_R=c/f_0Rossby deformation radiuslength scale

Nondimensionalized by LRL_R (length), f0−1f_0^{-1} (time), HH (height, so η^=η/H\hat\eta=\eta/H, c^=1\hat c=1), with f^=1+β^(y−y0)\hat f = 1+\hat\beta(y-y_0), β^=βLR/f0\hat\beta=\beta L_R/f_0:

η^t+u^x+v^y=0,u^t−f^v^=−η^x,v^t+f^u^=−η^y.\hat\eta_t+\hat u_x+\hat v_y=0,\qquad \hat u_t-\hat f\hat v=-\hat\eta_x,\qquad \hat v_t+\hat f\hat u=-\hat\eta_y.

Linear PV anomaly q=ζ−η^q=\zeta-\hat\eta (with ζ=vx−uy\zeta=v_x-u_y from the same u^,v^\hat u,\hat v) obeys qt=−β^v^q_t=-\hat\beta\hat v: exactly conserved pointwise on the ff-plane (β^=0\hat\beta=0). gfdlib.shallowwater.potential_vorticity computes it; gfdlib.shallowwater.invert_pv inverts (∇2−1)ηbal=q(\nabla^2-1)\eta_{bal}=q for the geostrophically balanced height field carrying that PV.

Single-layer QG (barotropic PV + deformation-radius stretching)

SymbolMeaningConvention
q=∇2ψ−ψ+βyq=\nabla^2\psi-\psi+\beta yquasi-geostrophic PV (LRL_R nondim to 1)material conservation: qt+J(ψ,q)=0q_t+J(\psi,q)=0

QGPV inversion reuses the shallow-water Helmholtz solve. With LRL_R nondimensionalized to 1, (∇2−1)ψ=q−βy(\nabla^2-1)\psi=q-\beta y is the same operator as the shallow-water PV inversion above — gfdlib.shallowwater. invert_pv computes ψ\psi from the QGPV anomaly directly; no separate QG inversion primitive exists or is needed. The linear Rossby-wave part of the evolution operator changes accordingly: gfdlib.qg.dispersion_omega gives ω=−βkx/(kx2+ky2+1)\omega=-\beta k_x/(k_x^2+k_y^2+1), bounded by ∣ω∣≤β/2|\omega|\le\beta/2 (attained at ∣k∣=1|\mathbf k|=1) — unlike the unbounded barotropic relation in ch18, the deformation radius screens the lowest wavenumbers. An isolated vortex (gfdlib.pv.gaussian_blob) on this operator sheds a trailing Rossby-wave wake and drifts westward — verified numerically.

Baroclinic instability (Eady; 2-layer/Phillips)

SymbolMeaningConvention
μ=kNH/f0\mu=kNH/f_0Eady nondimensional horizontal wavenumbergrowth rate σ=μci\sigma=\mu c_i
F=f02/(g′H)F=f_0^2/(g'H)layer coupling (“stretching”) parameter, 2-layer model
U1,U2U_1,U_2upper/lower layer mean flowΔU=U1−U2\Delta U=U_1-U_2 drives the instability
rrbottom Ekman drag coefficientacts on layer 2’s relative vorticity only

Both growth-rate problems are solved as small eigenvalue problems derived here, not copied from a closed-form formula. gfdlib. baroclinic.eady_growth_rate matches the boundary-value problem’s interior solution to the linearized thermal boundary condition at z=0,Hz=0,H; verified against the published benchmark (μmax≈1.61\mu_{max}\approx1.61, σmax≈0.31\sigma_{max}\approx0.31, cutoff μc≈2.399\mu_c\approx2.399; Vallis/Pedlosky) to within grid resolution. gfdlib.baroclinic.invert_2layer is the 2-layer QGPV Helmholtz solve (closed-form 2×22\times2 inverse); rhs_2layer is spectral-space, for use as the nonlinear RHS in gfdlib.timestep.ifrk4_step with hyperviscosity as the (scalar, both-layers-identical) linear operator — the mean-flow and β\beta terms stay in the nonlinear part since they couple the two layers (2×22\times2 per wavenumber, not a scalar). Cross-checked against an independently-derived linear eigenvalue: a small-amplitude single-wavenumber perturbation’s measured growth rate converges to the predicted value to <0.1%<0.1\% once the mode purifies (a coincidental choice of parameters that placed an integer wavenumber exactly at the marginal-stability cutoff was caught and avoided during this check).

A fixed mean shear with no drag is an unlimited energy source. Nonlinearly, rhs_2layer without Ekman drag (r=0r=0) was found to grow without bound regardless of how much hyperviscosity was added — energy conservation was verified exact for the pure nonlinear (Jacobian) terms in isolation, so the runaway traced to the mean-flow/β\beta terms having no saturating mechanism, not a sign error. Adding bottom drag on the lower layer’s relative vorticity (r>0, the standard ingredient in Phillips’ original 1954 model) produces the expected life cycle — exponential growth, a peak, decay, and equilibration — confirmed by a multi-hundred-time-unit run at the notebook’s actual resolution.

Barotropic/baroclinic mode decomposition (ch. 10). ψbt=(ψ1+ψ2)/2\psi_{bt}=(\psi_1+\psi_2)/2, ψbc=(ψ1−ψ2)/2\psi_{bc}=(\psi_1-\psi_2)/2 decouple the two-layer PV equations EXACTLY: summing/differencing q1=∇2ψ1+F(ψ2−ψ1)q_1=\nabla^2\psi_1+F(\psi_2-\psi_1), q2=∇2ψ2+F(ψ1−ψ2)q_2=\nabla^2\psi_2+F(\psi_1-\psi_2) cancels the FF terms in the sum and doubles them in the difference, giving qbt=∇2ψbtq_{bt}=\nabla^2\psi_{bt} (exactly ch. 7/18’s barotropic PV, no FF at all) and qbc=∇2ψbc−2Fψbcq_{bc}=\nabla^2\psi_{bc}-2F\psi_{bc} (exactly ch. 8’s single-layer QGPV, with deformation parameter 2F2F). gfdlib.baroclinic. to_modes/from_modes implement the (trivial, linear) change of variables; pv_2layer is the forward operator (exact inverse of invert_2layer). Verified directly, not just asserted from the algebra: to_modes applied to pv_2layer’s output matches the two decoupled single-layer operators to 10−810^{-8}. The decoupling holds only for the LINEAR operator — the Jacobian nonlinearity in rhs_2layer still couples the two modes, which is exactly what ch. 10’s notebook demonstrates (a disturbance seeded purely in qbcq_{bc} measurably leaks barotropic KE through nonlinear self-advection alone).

Symmetric, inertial & Kelvin-Helmholtz instability (survey)

SymbolMeaningConvention
Ro=−∂U/∂yf\mathrm{Ro}=-\dfrac{\partial U/\partial y}{f}cross-front Rossby numberf(1+Ro)f(1+\mathrm{Ro}) = absolute vertical vorticity
Ri=N2/(∂U/∂z)2\mathrm{Ri}=N^2/(\partial U/\partial z)^2gradient Richardson numberthermal-wind shear (symmetric); imposed shear (KH)
JJHazel (1972) profile’s bulk/minimum Richardson numberU=tanh⁡zU=\tanh z, N2=J sech2zN^2=J\,\mathrm{sech}^2 z

Symmetric & inertial instability: one Ertel-PV sign criterion. For a zonal thermal-wind front, gfdlib.symmetric derives (not looks up) the Ertel PV q=(f−∂U/∂y)N2−f(∂U/∂z)2q=(f-\partial U/\partial y)N^2-f(\partial U/\partial z)^2 and shows the front is unstable to slantwise (symmetric) displacements iff qf<0qf<0, i.e. (1+Ro)<1/Ri(1+\mathrm{Ro})<1/\mathrm{Ri}. Two independently-checkable limits confirm the sign convention: at Ro=0\mathrm{Ro}=0 the marginal curve reduces to the textbook value Ri=1\mathrm{Ri}=1; for (1+Ro)<0(1+\mathrm{Ro})<0 (absolute vorticity changes sign) the front is unconditionally unstable at every Ri>0\mathrm{Ri}>0 — pure inertial instability, no stratification dependence. gfdlib.symmetric.classify labels any (Ro,>Ri)(\mathrm{Ro}, > \mathrm{Ri}) point stable / symmetric / inertial / gravitational (Ri<0\mathrm{Ri}<0) for the Ri-Ro stability map.

Kelvin-Helmholtz: the Taylor-Goldstein equation, and a real numerical pitfall caught along the way. gfdlib.instability. taylor_goldstein_growth_rate solves (U−c)2(ϕ′′−k2ϕ)−U′′(U−c)>ϕ+N2ϕ=0(U-c)^2(\phi''-k^2\phi)-U''(U-c) > \phi+N^2\phi=0 as a natively linear 2n×2n2n\times2n generalized eigenvalue problem (keeping the buoyancy perturbation as an independent unknown rather than eliminating it via division by (U−c)(U-c), which gives a mathematically equivalent but quadratic-in-cc pencil). At N2=0N^2=0 this reduces exactly (to 10−1310^{-13}) to gfdlib.instability.growth_rate — a strong internal cross-check, not an external one. For the classic Hazel (1972) profile, growth was initially found NOT to vanish cleanly above the rigorous Miles-Howard threshold Ri≥1/4\mathrm{Ri}\geq1/4 — a hard theorem violation is impossible, so this was root-caused, not dismissed: the spurious growth scaled with grid spacing Δz\Delta z alone (matched Δz\Delta z across different nn and domain sizes gave matched spurious magnitude) and shrank as Δz→0\Delta z\to0, confirming genuine 2nd-order truncation error — worst exactly at the delicate Ri=1/4\mathrm{Ri}=1/4 point, where the equation’s critical-layer indicial roots coalesce, a classically hard point for finite differences (professional codes use spectral methods here). Fixed by tightening the domain/resolution and applying a documented noise floor calibrated to this investigation (_TG_NOISE_FLOOR); the resulting curve decreases smoothly and reaches zero by J≈0.4J\approx0.4, correctly reproducing the theorem’s qualitative Ri≥1/4\geq1/4 cutoff (not a knife-edge at exactly 1/41/4, an honest limitation of a simple solver on this classically delicate problem). gfdlib.instability.hazel_profile builds U,N2U,N^2 for this test case.

Rossby-wave ray tracing (sphere, WKB)

SymbolMeaningConvention
λ,ϕ\lambda,\philongitude, latitude
n,mn,mzonal/meridional wavenumber indices, conjugate to λ,ϕ\lambda,\phiphysical wavenumbers kx=n/(acos⁡ϕ), ky=m/ak_x=n/(a\cos\phi),\ k_y=m/a
Ωs\Omega_sbackground solid-body rotation rate (added to Ω\Omega)U(ϕ)=Ωsacos⁡ϕU(\phi)=\Omega_s a\cos\phi

Great circles are a theorem here, not a plotting choice. For a background flow U=Ωsacos⁡ϕU=\Omega_s a\cos\phi, both UU and the meridional gradient of absolute vorticity scale by (Ω+Ωs)(\Omega+\Omega_s) — the same factor — so the stationary wavenumber Ks2=2(Ω+Ωs)/(Ωs>a2)K_s^2=2(\Omega+\Omega_s)/(\Omega_s > a^2) is independent of latitude, and Hamilton’s ray equations (gfdlib.rossby.ray_rhs, via central differences of dispersion_omega — deliberately not hand-derived symbolically) integrate to exact great circles: great_circle_deviation checks this directly (fit the plane through the ray’s first two sampled points on the unit sphere, then check every later point lies in it) and finds deviations at the 10−910^{-9} level, i.e. floating-point roundoff, not approximation error.

Rayleigh-Benard convection (vertical xx–zz channel)

SymbolMeaningConvention
ψ(x,z)\psi(x,z)channel streamfunctionu=−ψz, w=ψxu=-\psi_z,\ w=\psi_x
ζ=∇2ψ\zeta=\nabla^2\psivorticitywx−uz=ψxx+ψzz=∇2ψw_x-u_z=\psi_{xx}+\psi_{zz}=\nabla^2\psi; same sign as the horizontal case above
θ\thetatemperature deviation from the linear conduction profile
Rac=27π4/4Ra_c=27\pi^4/4critical Rayleigh number (free-slip, both boundaries)onset at kc=π/2k_c=\pi/\sqrt2

A genuine bug to avoid re-introducing. With u=−ψz, w=ψxu=-\psi_z,\ w=\psi_x, vorticity is ζ=wx−uz=∇2ψ\zeta=w_x-u_z=\nabla^2\psi — the same sign as the horizontal streamfunction convention above, not ∇2ψ=−ζ\nabla^2\psi=-\zeta. Getting this backward flips the sign of the buoyancy-vorticity feedback and the instability never grows at any RaRa: verified numerically (see ch14 PR) that the wrong sign gives pure decay for all RaRa, while the correct sign reproduces subcritical decay (Ra<RacRa<Ra_c) and supercritical growth into finite-amplitude convection (Ra>RacRa>Ra_c) as expected. gfdlib.convection.ChannelGrid (periodic xx, Dirichlet zz) and rhs_boussinesq implement the corrected convention; lorenz_rhs is the (X,Y,Z)(X,Y,Z) truncation, sharing gfdlib.timestep.rk4 with every other chapter.

Buoyancy-driven overturning (abyssal recipes; horizontal convection)

SymbolMeaningConvention
wwupwelling velocity (Munk’s abyssal-recipe balance)positive = upward
κ\kappadiapycnal diffusivityboundary-layer thickness ∼κ/w\sim\kappa/w
Q(x,z)Q(x,z)differential surface heating∝cos⁡(2πx/Lx)\propto\cos(2\pi x/L_x), warm at x=0x=0, cool at x=Lx/2x=L_x/2

Munk’s (1966) abyssal recipe w ∂T/∂z=κ >∂2T/∂z2w\,\partial T/\partial z=\kappa\, > \partial^2T/\partial z^2 is solved exactly (gfdlib.overturning. abyssal_profile): a linear 2nd-order ODE with constant coefficients, general solution T=A+Bewz/κT=A+Be^{wz/\kappa}, with A,BA,B fixed by the two BCs via a 2×22\times2 numpy.linalg.solve. Large w/κw/\kappa sweeps the bottom value through most of the water column, compressing the transition to the surface value into a thin layer near z=Hz=H — the real ocean’s near-uniform abyssal temperature capped by a thin thermocline; verified numerically, not just plotted, against both this limit and the w→0w\to0 pure-diffusion (linear) profile.

The overturning cell reuses ch14’s Boussinesq machinery unchanged. gfdlib.overturning.rhs_overturning has the exact same structure as convection.rhs_boussinesq, but replaces the fixed background- stratification advection term (+ψx+\psi_x, uniform bottom heating) with an explicit, horizontally-varying heating field Q(x,z)Q(x,z) (gfdlib.overturning.surface_heating) — warm at one end of the (periodic) domain, cool at the other, concentrated near the surface. This is Rossby’s (1965) “horizontal convection” mechanism: differential heating along a boundary organizes into a single overturning cell (rising at the heated column, sinking at the cooled one) rather than the many small convective rolls of uniform bottom heating — verified directly, not just asserted: the vertical velocity w=ψxw=\psi_x comes out positive under the heated column and negative under the cooled one in every run tested.

Internal gravity waves (vertical xx–zz plane)

SymbolMeaningConvention
ψ(x,z)\psi(x,z)vertical-plane streamfunctionu=ψz, w=−ψxu=\psi_z,\ w=-\psi_x
q=∇2ψq=\nabla^2\psias above, but in the (x,z)(x,z) plane∂t2q=−(N2(z)ψxx+f2ψzz)+S\partial_t^2q=-(N^2(z)\psi_{xx}+f^2\psi_{zz})+S
θ\thetawavevector angle from verticalcos⁡θ=(ω2−f2)/(N2−f2)\cos\theta=\sqrt{(\omega^2-f^2)/(N^2-f^2)}; equals the energy beam’s angle from horizontal

A different ψ\psi than the horizontal chapters. This (u,w)=(ψz,−ψx)(u,w)=(\psi_z,-\psi_x) sign convention is for a vertical plane and is unrelated to the horizontal (x,y)(x,y) streamfunction above — the two never appear in the same notebook. gfdlib.internalwaves reuses spectral.Grid with its yy-axis standing in for zz; dispersion_omega/beam_angle give ω(k,m)\omega(k,m) and θ\theta; step_leapfrog integrates the qq equation and accepts a zz-dependent N2N^2 directly (no assumption of constant stratification). Nondimensionalized by a reference buoyancy frequency N0N_0 (time) and the domain width (length): N^=N/N0, f^=f/N0, ω^=ω/N0\hat N=N/N_0,\ \hat f=f/N_0,\ \hat\omega=\omega/N_0.

Eddy transport & mixing (passive tracer in 2D turbulence)

SymbolMeaningConvention
Γ=dCˉ/dy\Gamma=d\bar C/dyimposed mean tracer gradientCtotal=Γy+c′(x,y,t)C_{total}=\Gamma y+c'(x,y,t), c′c' periodic
Keff=−v′c′‾/ΓK_{eff}=-\overline{v'c'}/\Gammaeffective (eddy) diffusivitydomain average; Keff>0K_{eff}>0 = down-gradient

The mean-gradient trick. A passive tracer with an unbounded mean gradient is handled the same way ch16/ch18 handle a mean shear or βy\beta y: split into an imposed linear part and a periodic perturbation c′c', so gfdlib.mixing.rhs_coupled can evolve c′c' on the same doubly- periodic spectral.Grid as the vorticity that stirs it, with the mean gradient appearing as an ordinary source term −Γv-\Gamma v (the tracer is passive: it’s advected by, but does not feed back on, the vorticity).

KeffK_{eff} is intrinsically Γ\Gamma-independent — verified, not assumed. The c′c' equation is linear in c′c', so c′∝Γc'\propto\Gamma for a fixed flow realization and Keff=−v′c′‾/ΓK_{eff}=-\overline{v'c'}/\Gamma must be exactly independent of Γ\Gamma. Checked numerically: two runs on the identical vorticity history with Γ\Gamma differing by a factor of 2 give KeffK_{eff} agreeing to machine precision (relative difference 10−1610^{-16}), and a control run with the velocity field identically zero gives Keff=0K_{eff}=0 exactly (no possible eddy flux without a flow). For freely- decaying 2D turbulence, KeffK_{eff} comes out positive (down-gradient) and of the same order as a mixing-length estimate urms×u_{rms}\times(domain scale) — a real emergent result of the simulation, not tuned to match.

Wind-driven circulation (Stommel; Munk)

SymbolMeaningConvention
ε\varepsilonStommel nondimensional bottom-drag parameterboundary layer width ∼ε\sim\varepsilon
δ\deltaMunk nondimensional lateral-friction parameterboundary layer width ∼δ\sim\delta
f(x)f(x)west-east profile, ψ=f(x)sin⁡(πy)\psi=f(x)\sin(\pi y)f(0)=0f(0)=0 always; f′(0)=0f'(0)=0 too for Munk (no-slip)

A genuinely new domain shape. Every earlier chapter’s domain was doubly-periodic or a channel with two boundaries; this is the first bounded rectangular basin, four walls. gfdlib.circulation solves the Stommel and Munk gyre problems two independent ways: an exact analytic solution (separating ψ=f(x)sin⁡(πy)\psi=f(x)\sin(\pi y) reduces the PDE to a linear ODE for f(x)f(x) with constant coefficients, solved via its characteristic polynomial’s roots — quadratic formula for Stommel, numpy.roots for Munk’s quartic — and a small numpy.linalg.solve for the boundary constants), and a general 2D finite-difference direct solve (Kronecker- sum operators, numpy.linalg.solve, no SciPy) that works for arbitrary forcing, cross-validated against the analytic solution with clean 2nd-order convergence (gfdlib tests).

Munk’s biharmonic operator is handled by keeping ζ=∇2ψ\zeta=\nabla^2\psi as an independent field (the same pattern as ch16/17/19’s coupled states) rather than a hand-derived 4th-order stencil, closed with Thom’s (1933) wall-vorticity formula ζwall=2ψadjacent/d2\zeta_{wall}=2\psi_{adjacent}/d^2 (derived by Taylor-expanding ψ\psi from a ψ=ψn=0\psi=\psi_n=0 wall) for no-slip on the east/west walls, with free-slip (ζ=0\zeta=0, no correction needed) on the north/south walls — the standard textbook simplification that keeps the problem separable.

A real bug, caught by a convergence test, not an eyeball check. An earlier version enforced no-slip on all four walls (matching physical intuition, but not the separable-solution assumption). It disagreed with the exact analytic solution by a stubborn ∼30%\sim30\% — but critically, that error did not shrink with grid refinement at any boundary-layer width tested (0.08 to 0.4), immediately distinguishing a genuine discretization error (which must vanish as Δx→0\Delta x\to0) from a boundary-condition mismatch (which does not). The fix — free-slip on north/south, matching the classical Pedlosky/Vallis treatment — restored clean 2nd-order convergence.

Kinematics: strain, vorticity & the Okubo-Weiss parameter

SymbolMeaningConvention
Sn=ux−vyS_n=u_x-v_ynormal strain
Ss=vx+uyS_s=v_x+u_yshear strain
W=Sn2+Ss2−ζ2W=S_n^2+S_s^2-\zeta^2Okubo-Weiss parameterW<0W<0 vorticity-dominated, W>0W>0 strain-dominated

A steady strain + smooth vortex, differentiated by finite differences, not by hand. gfdlib.kinematics.velocity_field is a Lamb-Oseen-like vortex superposed on uniform strain (both individually divergence-free); okubo_weiss gets Sn,Ss,ζS_n,S_s,\zeta from central differences of that velocity field, the same safety pattern gfdlib.rossby uses for its ray equations (correctness then rests only on velocity_field itself, not on a hand-differentiated closed form for the smoothed vortex profile). Verified against two independently-derivable limits: with Γ=0\Gamma=0 the flow is exactly linear strain, so central differences are exact (W=4\alpha^2 to floating-point precision, any hh); at the vortex center with α=0\alpha=0 the flow is locally solid-body rotation with zero strain, so W=−ζcenter2W=-\zeta_{center}^2 with ζcenter=Γ/(2πσ2)\zeta_{center}=\Gamma/(2\pi\sigma^2) from the vortex’s small-rr Taylor expansion.

Inertial oscillations & the Taylor-Proudman limit

SymbolMeaningConvention
w=u+ivw=u+ivcomplex velocitydw/dt=−ifwdw/dt=-ifw on an unforced ff-plane
T=2π/fT=2\pi/finertial period

Taylor-Proudman is not a separate solver — it is b≡0b\equiv0 in Ch. 5’s thermal wind. gfdlib.rotation handles only the inertial-oscillation half (inertial_rhs for RK4 integration, inertial_trajectory for the closed-form circle w(t)=w0e−iftw(t)=w_0e^{-ift}, radius ∣w0∣/f|w_0|/f, period 2π/f2\pi/f); the Taylor-Proudman half of ch03 reuses gfdlib.balance.thermal_wind_u directly with the buoyancy contrast swept to zero, since f ug,z=−by≡0⇒ug,z=0f\,u_{g,z}=-b_y\equiv0\Rightarrow u_{g,z}=0 requires no new code, only a new reading of code that already existed. Verified: the numerically RK4-integrated (x,y,u,v)(x,y,u,v) trajectory matches inertial_trajectory to 10−610^{-6}; the closed-form trajectory’s radius and speed are exactly constant (a genuine circle) to 10−1010^{-10}; the thermal-wind shear range is exactly linear in Δb\Delta b (inherited directly from thermal_wind_u’s own linearity — no fitting).

Vertical normal modes (Sturm-Liouville, rigid lid)

SymbolMeaningConvention
Φn(z)\Phi_n(z)nn-th vertical mode shapeΦn′(0)=Φn′(H)=0\Phi_n'(0)=\Phi_n'(H)=0; normalized Φn(0)=1\Phi_n(0)=1
cnc_nnn-th mode speedLn=cn/f0L_n=c_n/f_0 the nn-th deformation radius

The equation is d/dz[(1/N2) dΦ/dz]+(1/c2)Φ=0d/dz[(1/N^2)\,d\Phi/dz]+(1/c^2)\Phi=0 — no f0f_0 in the eigenvalue problem itself. Derived from scratch (separate the linear hydrostatic equations in zz, track ww’s structure through continuity, tie it back to Φ′\Phi' through the buoyancy equation) and checked twice: for constant NN, Φn=cos⁡(nπz/H)\Phi_n=\cos(n\pi z/H), cn=NH/(nπ)c_n=NH/(n\pi) solves it exactly by direct substitution (no f0f_0 anywhere), and mode n=0n=0 (Φ≡\Phi\equivconst) is trivially a solution with c0=∞c_0=\infty — the CONTINUOUS analog of gfdlib.baroclinic’s barotropic mode, no deformation-radius constraint at all.

A real, instructive bug: equal-weight ghost points broke matrix symmetry. A first implementation of gfdlib.stratification. vertical_modes used naive ghost-point differencing for the Neumann BCs; it gave eigenvalues that LOOKED nearly correct (within 0.3% of the constant-NN benchmark) but mode SHAPES that were badly, visibly wrong once plotted (values 100x too large in the interior) — because the boundary row’s coefficient came out exactly double its neighboring interior row’s reference to it, making the matrix asymmetric and numpy.linalg.eigh invalid on it (silently: eigh doesn’t check symmetry, it just uses the lower/upper triangle and returns garbage for the rest). Fixed with the standard finite-VOLUME treatment (half-cell control volumes at the two boundaries) symmetrized via a diagonal congruence transform before calling eigh — verified: mode shapes now match the exact constant-NN cosines to 10−1210^{-12}, not just the eigenvalues to a few tenths of a percent. The lesson generalizes: for any self-adjoint eigenvalue problem, checking eigenVALUES against a benchmark is not sufficient — check eigenVECTORS too.

Wave-mean-flow interaction (Stokes drift; Rossby critical layers)

SymbolMeaningConvention
uS=A2k/(2ω)u_S=A^2k/(2\omega)Stokes drift of u′=Acos⁡(kx−ωt)u'=A\cos(kx-\omega t)positive = with phase propagation
ycy_ccritical layerU(yc)=ω/kU(y_c)=\omega/k, the Doppler-shifted phase speed

Stokes drift is not a separate force — it’s evaluating u′u' at the parcel’s true position instead of its mean one. gfdlib.wavemean. stokes_drift is derived from scratch in three lines (displacement ξ=(A/ω)sin⁡(ωt−kx0)\xi=(A/\omega)\sin(\omega t-kx_0), then ⟨ξ ∂xu′>⟩\langle\xi\,\partial_xu' > \rangle) and verified two ways: against direct RK4 particle advection in wave_velocity (Ch. 1’s dx/dt=ud\mathbf x/dt=\mathbf u machinery, now genuinely time-dependent) sampled stroboscopically once per period, and by sign (positive for k,ωk,\omega same sign — with phase propagation, matching the classical surface-wave result).

Rossby-wave critical layers extend ch. 9’s ray tracing to a Doppler-shifted medium. gfdlib.wavemean.rossby_shear_dispersion adds a background shear U(y)U(y) to the barotropic dispersion relation; ray_rhs_shear is its EXACT symbolic ray-equation derivative (safe to hand-differentiate here, unlike ch. 9’s central-difference approach, since the formula is simple). Verified directly by integration, not just asserted from the algebra: ω\omega stays conserved along the ray to 10−610^{-6} even as yy and ll both change substantially; the ray approaches but never crosses the analytically-predicted critical layer yc=ω0/(kΛ)y_c=\omega_0/(k\Lambda) (linear-shear case), with ∣l∣→∞|l|\to\infty and the meridional group velocity dy/dt→0dy/dt\to0 as it does — the mathematical signature of a genuine singularity in linear ray theory, not a numerical artifact.

Point vortices (Kirchhoff Hamiltonian system)

SymbolMeaningConvention
Γi\Gamma_icirculation of vortex iisigned
H=−14π∑i<jΓiΓjln⁡rij2H=-\frac{1}{4\pi}\sum_{i<j}\Gamma_i\Gamma_j\ln r_{ij}^2interaction HamiltonianΓix˙i=∂H/∂yi\Gamma_i\dot x_i=\partial H/\partial y_i, Γiy˙i=−∂H/∂xi\Gamma_i\dot y_i=-\partial H/\partial x_i
Q,P,LQ,P,L∑Γixi\sum\Gamma_ix_i, ∑Γiyi\sum\Gamma_iy_i, ∑Γi(xi2+yi2)\sum\Gamma_i(x_i^2+y_i^2)Noether charges of translation & rotation

Positions are the conjugate pair — phase space is physical space. gfdlib.vortex implements the mutual Biot-Savart RHS, the Hamiltonian and invariants, the symplectic implicit-midpoint step (step_midpoint, fixed-point iteration — leapfrog is unavailable because Kirchhoff’s HH is non-separable) and a same-order non-symplectic control (step_heun). Verified against exact solutions: co-rotating pair period T=2π2d2/ΓT=2\pi^2d^2/\Gamma to 10−810^{-8} and dipole speed Γ/(2πd)\Gamma/(2\pi d) to 10−610^{-6} (both derived by hand, not fitted). The chapter’s punchline is a measured fact, not an assertion: over a 20,000-step chaotic 4-vortex run at the same Δt\Delta t, Heun’s energy error drifts secularly to ∼10−3\sim10^{-3} while midpoint’s stays bounded at ∼5×10−6\sim5\times10^{-6} (tests enforce a 30x separation). Finer structure verified too: linear invariants Q,PQ,P are conserved to machine precision by EVERY Runge-Kutta method, while the quadratic LL drifts under RK4 (∼10−8\sim10^{-8}) but is exact under implicit midpoint (a Gauss method) up to the fixed-point iteration tolerance — 3×10−143\times10^{-14} at 12 sweeps, measured.

Wave activity & non-acceleration (barotropic)

SymbolMeaningConvention
A=q′2‾/(2qˉy)A=\overline{q'^2}/(2\bar q_y)small-amplitude pseudomomentum densityq′q' = deviation from zonal mean; qˉy=β−Uˉ′′\bar q_y=\beta-\bar U''
non-acceleration∂t(uˉ+A)=0\partial_t(\bar u+A)=0conservative, small-amplitude waves; pointwise in yy

The theorem is three one-line derivations chained (linearized PV conservation ⇒At=−v′q′‾\Rightarrow A_t=-\overline{v'q'}; the Taylor identity v′q′‾=−∂yu′v′‾\overline{v'q'}=-\partial_y\overline{u'v'}; the zonal-mean momentum equation uˉt=−∂yu′v′‾\bar u_t=-\partial_y\overline{u'v'}) — see ch. 23. gfdlib.wavemean.wave_activity computes (uˉ,A,qˉy)(\bar u, A, \bar q_y) from a vorticity snapshot; AA requires qˉy\bar q_y bounded away from zero (design experiments with β>max⁡∣Uˉ′′∣\beta>\max|\bar U''|). Verified against the FULL nonlinear solver, not the linear theory it comes from: a small packet on U0cos⁡yU_0\cos y shear produces Δuˉ\Delta\bar u and ΔA\Delta A each ∼10−5\sim10^{-5} that cancel to <1%<1\% of themselves, pointwise in yy (0.5% measured at a=0.05a=0.05, n=128n=128; the residual is the expected O(a3)O(a^3) correction, and switching on hyperviscosity breaks the cancellation for the stated physical reason — dissipation destroys pseudomomentum without repaying the mean flow).

Slow manifold & balanced initialization (shallow water)

SymbolMeaningConvention
δ=ux+vy\delta=u_x+v_yhorizontal divergenceslow (geostrophic) mode: δ≡0\delta\equiv0; IG waves are made of it
α\alphabalance fraction of initial winds(u,v)0=α(−η0y,η0x)(u,v)_0=\alpha(-\eta_{0y},\eta_{0x})

RMS divergence meters the distance from the slow manifold. gfdlib.shallowwater.divergence is the one-line spectral diagnostic; verified against an analytic case to 10−1010^{-10} and exactly zero (to 10−1210^{-12}) on a geostrophically balanced state. Ch. 24’s measured facts: a fully balanced initialization holds RMS δ\delta at machine zero for the whole run (the linear slow subspace is exactly invariant on the ff-plane); partial balance rings with amplitude exactly ∝(1−α)\propto(1-\alpha) (measured: α=0.5\alpha=0.5 gives precisely half the α=0\alpha=0 ringing — linearity, not coincidence); and pointwise PV drift is at machine precision in every run regardless of α\alpha — balance is entirely about not exciting the fast modes; the slow (PV) content was never at risk.

Dimensionless numbers

NumberDefinitionRegime
Rossby RoRoU/(f0L)U/(f_0 L)≪1\ll1 rotation-dominated
Burger BuBu(NH/f0L)2=(LR/L)2(NH/f_0L)^2=(L_R/L)^2balance vs. wave scale
Ekman EkEkν/(f0H2)\nu/(f_0H^2)friction vs. rotation
Froude FrFrU/(NH)U/(NH)stratification
Rayleigh RaRagαΔTH3/(νκ)g\alpha\Delta T H^3/(\nu\kappa)convective onset
Reynolds ReReUL/νUL/\nuinertia vs. viscosity
Richardson RiRiN2/(∂zu)2N^2/(\partial_z u)^2shear stability

gfdlib.scaling turns the table above into testable functions (rossby_number, ekman_number, froude_number, reynolds_number, burger_number, deformation_radius, coriolis_parameter) plus a coarse three-region classify_regime(Ro, Bu) used by ch04’s regime map: Ro>1Ro>1 unbalanced; Ro<1,Bu>1Ro<1,Bu>1 (L<LRL<L_R) QG-baroclinic (Ch. 16 territory); Ro<1,Bu<1Ro<1,Bu<1 (L>LRL>L_R) QG-barotropic (Taylor-Proudman-like, Ch. 3/18 territory). A pedagogical simplification of the classical Ro-Bu regime diagram — real transitions are smooth, not the sharp lines drawn on the map — but the two boundaries (Ro=1Ro=1, Bu=1Bu=1) are exactly the numbers' own definitions, not fitted thresholds.