Gradient operators
SpeedyTransforms also includes many gradient operators to take derivatives in spherical harmonics. These are in particular $\nabla, \nabla \cdot, \nabla \times, \nabla^2, \nabla^{-2}$. We call them divergence
, curl
, ∇
, ∇²
, ∇⁻²
(as well as their in-place versions with !
) within the limits of unicode characters and Julia syntax. These functions are defined for inputs being spectral coefficients (i.e. LowerTriangularMatrix
) or gridded fields (i.e. <:AbstractGrid
) and also allow as an additional argument a spectral transform object (see SpectralTransform) which avoids recalculating it under the hood.
The gradient operators in SpeedyTransforms generally assume a sphere of radius $R=1$. For the transforms themselves that does not make a difference, but the gradient operators divergence
, curl
, ∇
, ∇²
, ∇⁻²
omit the radius scaling unless you provide the optional keyword radius
(or you can do ./= radius
manually). Also note that meridional derivates in spectral space expect a $\cos^{-1}(\theta)$ scaling. Details are always outlined in the respective docstrings, ?∇
for example.
The actually implemented operators are, in contrast to the mathematical Derivatives in spherical coordinates due to reasons of scaling as follows. Let the implemented operators be $\hat{\nabla}$ etc.
\[\hat{\nabla} A = \left(\frac{\partial A}{\partial \lambda}, \cos(\theta)\frac{\partial A}{\partial \theta} \right) = R\cos(\theta)\nabla A\]
So the zonal derivative omits the radius and the $\cos^{-1}(\theta)$ scaling. The meridional derivative adds a $\cos(\theta)$ due to a recursion relation being defined that way, which, however, is actually convenient because the whole operator is therefore scaled by $R\cos(\theta)$. The curl and divergence operators expect the input velocity fields to be scaled by $\cos^{-1}(\theta)$, i.e.
\[\begin{aligned} \hat{\nabla} \cdot (\cos^{-1}(\theta)\mathbf{u}) &= \frac{\partial u}{\partial \lambda} + \cos\theta\frac{\partial v}{\partial \theta} = R\nabla \cdot \mathbf{u}, \\ \hat{\nabla} \times (\cos^{-1}(\theta)\mathbf{u}) &= \frac{\partial v}{\partial \lambda} - \cos\theta\frac{\partial u}{\partial \theta} = R\nabla \times \mathbf{u}. \end{aligned}\]
And the Laplace operators omit a $R^2$ (radius $R$) scaling, i.e.
\[\hat{\nabla}^{-2}A = \frac{1}{R^2}\nabla^{-2}A , \quad \hat{\nabla}^{2}A = R^2\nabla^{2}A\]
Gradient ∇
We illustrate the usage of the gradient function ∇
. Let us create some fake data G
on the grid first
using SpeedyWeather, CairoMakie
# create some data with wave numbers 0,1,2,3,4
trunc = 64 # 1-based maximum degree of spherical harmonics
L = randn(LowerTriangularMatrix{ComplexF32}, trunc, trunc)
spectral_truncation!(L, 5) # remove higher wave numbers
G = transform(L)
heatmap(G, title="Some fake data G") # requires `using CairoMakie`
Now we can take the gradient as follows
dGdx, dGdy = ∇(G)
this transforms internally back to spectral space takes the gradients in zonal and meridional direction, transforms to grid-point space again und unscales the coslat-scaling on the fly but assumes a radius of 1 as the keyword argument radius
was not provided. Use ∇(G, radius=6.371e6)
for a gradient on Earth in units of "data unit" divided by meters.
heatmap(dGdx, title="dG/dx on the unit sphere")
heatmap(dGdy, title="dG/dy on the unit sphere")
Geostrophy
Now, we want to use the following example to illustrate a more complex use of the gradient operators: We have $u, v$ and want to calculate $\eta$ in the shallow water system from it following geostrophy. Analytically we have
\[-fv = -g\partial_\lambda \eta, \quad fu = -g\partial_\theta \eta\]
which becomes, if you take the divergence of these two equations
\[\zeta = \frac{g}{f}\nabla^2 \eta\]
Meaning that if we start with $u, v$ we can obtain the relative vorticity $\zeta$ and, using Coriolis parameter $f$ and gravity $g$, invert the Laplace operator to obtain displacement $\eta$. How to do this with SpeedyTransforms?
Let us start by generating some data
spectral_grid = SpectralGrid(trunc=31, nlayers=1)
forcing = SpeedyWeather.JetStreamForcing(spectral_grid)
drag = QuadraticDrag(spectral_grid)
model = ShallowWaterModel(spectral_grid; forcing, drag)
simulation = initialize!(model);
run!(simulation, period=Day(30))
Now pretend you only have u, v
to get vorticity (which is actually the prognostic variable in the model, so calculated anyway...).
u = simulation.diagnostic_variables.grid.u_grid[:, 1] # [:, 1] for 1st layer
v = simulation.diagnostic_variables.grid.v_grid[:, 1]
vor = curl(u, v, radius = spectral_grid.radius)
Here, u, v
are the grid-point velocity fields, and the function curl
takes in either LowerTriangularMatrix
s (no transform needed as all gradient operators act in spectral space), or, as shown here, arrays of the same grid and size. In this case, the function actually runs through the following steps
RingGrids.scale_coslat⁻¹!(u)
RingGrids.scale_coslat⁻¹!(v)
S = SpectralTransform(u, one_more_degree=true)
us = transform(u, S)
vs = transform(v, S)
vor = curl(us, vs, radius = spectral_grid.radius)
560-element, 33x32 LowerTriangularMatrix{ComplexF32}
0.0+0.0im 0.0+0.0im … 0.0+0.0im
4.12608f-7+0.0im 2.11622f-8+4.98253f-8im 0.0+0.0im
4.44341f-6+0.0im 1.7195f-7-1.1708f-7im 0.0+0.0im
2.17306f-5+0.0im 2.79259f-6+2.15968f-6im 0.0+0.0im
1.67976f-5+0.0im 5.93419f-6+1.99527f-6im 0.0+0.0im
3.17986f-6+0.0im 5.86513f-6+4.69897f-6im … 0.0+0.0im
8.55055f-6+0.0im 5.01191f-6+4.0124f-6im 0.0+0.0im
-3.10526f-5+0.0im -2.00665f-6-3.21207f-6im 0.0+0.0im
-4.58005f-6+0.0im -1.9162f-6-5.74668f-6im 0.0+0.0im
3.08854f-6+0.0im -5.36323f-6+9.84665f-7im 0.0+0.0im
⋮ ⋱
-1.34046f-7+0.0im 4.8528f-7+5.02978f-7im 0.0+0.0im
5.23277f-7+0.0im 2.25738f-7+5.30718f-7im … 0.0+0.0im
-6.03135f-7+0.0im 2.62626f-7-2.56767f-7im 0.0+0.0im
-2.24041f-7+0.0im -1.18713f-7-2.87092f-7im 0.0+0.0im
-2.54579f-7+0.0im 1.18004f-8+3.52475f-8im 0.0+0.0im
6.81236f-8+0.0im -1.62242f-7-4.38346f-8im 0.0+0.0im
1.37445f-7+0.0im 8.56346f-9+1.88593f-8im … 0.0+0.0im
-5.6527f-8+0.0im -4.76852f-9-6.20656f-8im -1.12937f-9+9.70778f-9im
0.0+0.0im 0.0+0.0im 0.0+0.0im
(Copies of) the velocity fields are unscaled by the cosine of latitude (see above), then transformed into spectral space, and the curl
has the keyword argument radius
to divide internally by the radius (if not provided it assumes a unit sphere). We always unscale vector fields by the cosine of latitude if they are provided to curl
or divergence
in spectral as you can only do this scaling effectively in grid-point space. The methods accepting arguments as grids generally do this for you. If in doubt, check the docstrings, ?∇
for example.
One more degree for spectral fields
The SpectralTransform
in general takes a one_more_degree
keyword argument, if otherwise the returned LowerTriangularMatrix
would be of size 32x32, setting this to true would return 33x32. The reason is that while most people would expect square lower triangular matrices for a triangular spectral truncation, all vector quantities always need one more degree (= one more row) because of a recursion relation in the meridional gradient. So as we want to take the curl of us, vs
here, they need this additional degree, but in the returned lower triangular matrix this row is set to zero.
All gradient operators expect the input lower triangular matrices of shape $(N+1) \times N$. This one more degree of the spherical harmonics is required for the meridional derivative. Scalar quantities contain this degree too for size compatibility but they should not make use of it. Use spectral_truncation
to add or remove this degree manually.
You may also generally assume that a SpectralTransform
struct precomputed for some truncation, say $l_{max} = m_{max} = T$ could also be used for smaller lower triangular matrices. While this is mathematically true, this does not work here in practice because LowerTriangularMatrices
are implemented as a vector. So always use a SpectralTransform
struct that fits matches your resolution exactly (otherwise an error will be thrown).
Example: Geostrophy (continued)
Now we transfer vor
into grid-point space, but specify that we want it on the grid that we also used in spectral_grid
. The Coriolis parameter for a grid like vor_grid
is obtained, and we do the following for $f\zeta/g$.
vor_grid = transform(vor, Grid=spectral_grid.Grid)
f = coriolis(vor_grid) # create Coriolis parameter f on same grid with default rotation
g = model.planet.gravity
fζ_g = @. vor_grid * f / g # in-place and element-wise
Now we need to apply the inverse Laplace operator to $f\zeta/g$ which we do as follows
fζ_g_spectral = transform(fζ_g, one_more_degree=true)
R = spectral_grid.radius
η = SpeedyTransforms.∇⁻²(fζ_g_spectral) * R^2
η_grid = transform(η, Grid=spectral_grid.Grid)
Note the manual scaling with the radius $R^2$ here. We now compare the results
using CairoMakie
heatmap(η_grid, title="Geostrophic interface displacement η [m]")
Which is the interface displacement assuming geostrophy. The actual interface displacement contains also ageostrophy
η_grid2 = simulation.diagnostic_variables.grid.pres_grid
heatmap(η_grid2, title="Interface displacement η [m] with ageostrophy")
Strikingly similar! The remaining differences are the ageostrophic motions but also note that the mean can be off. This is because geostrophy only use/defines the gradient of $\eta$ not the absolute values itself. Our geostrophic $\eta_g$ has by construction a mean of zero (that is how we define the inverse Laplace operator) but the actual $\eta$ can be higher or lower depending on the mass/volume in the shallow water system, see Mass conservation.