Vertical Modes
Vertical mode decomposition and coupling for multi-layer quasi-geostrophic models.
Coupling Matrix
finitevolx.build_coupling_matrix(H, g_prime)
Build the tridiagonal vertical coupling (stratification) matrix A.
A encodes the buoyancy coupling between layers via layer thicknesses and
reduced gravities. For positive H and g_prime, A is tridiagonal
and diagonally similar to a symmetric positive-definite matrix; it
becomes exactly symmetric when all layer thicknesses are equal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
H
|
Float[Array, 'nl']
|
Layer resting thicknesses [m]. All values must be positive. |
required |
g_prime
|
Float[Array, 'nl']
|
Reduced gravities [m s⁻²]. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'nl nl']
|
Tridiagonal coupling matrix A of shape |
Examples:
Single-layer case:
>>> import jax.numpy as jnp
>>> H = jnp.array([1000.0])
>>> g_prime = jnp.array([9.81])
>>> A = build_coupling_matrix(H, g_prime)
>>> A.shape
(1, 1)
Two-layer case with equal thicknesses:
>>> H = jnp.array([500.0, 500.0])
>>> g_prime = jnp.array([0.02, 0.02])
>>> A = build_coupling_matrix(H, g_prime)
>>> A.shape
(2, 2)
Source code in finitevolx/_src/vertical/vertical_modes.py
Mode Decomposition
finitevolx.decompose_vertical_modes(A, f0)
Eigendecompose the coupling matrix A to get Rossby radii and transforms.
A is not symmetric unless all layer thicknesses are equal: it is
diag(1/H) @ B for a symmetric B, so A[i, i+1] / A[i+1, i] =
H[i+1] / H[i]. Handing it straight to a symmetric eigensolver would
silently decompose a different matrix — the one built from a single
triangle — whose eigenvectors do not diagonalize A, leaving the modes
coupled and the deformation radii wrong.
So A is diagonalized through its symmetric similarity
S = diag(d) @ A @ diag(1/d), where d[i] ∝ sqrt(H[i]) is recovered
from A's own off-diagonals. S shares A's real, non-negative eigenvalues,
and A's eigenvectors are r = v / d for S's orthonormal v. This
keeps jnp.linalg.eigh — stable, JIT-compatible, and sorted real
output — rather than reaching for a general eigensolver, and reduces to
the plain symmetric case (d constant) for equal thicknesses::
Cl2m = vᵀ diag(d)
Cm2l = diag(1/d) v
Cl2m @ Cm2l = vᵀ v = I
The mode basis is scaled by d[0] = 1 rather than to unit column norm.
A vertical mode is only defined up to a scale, the pair inverts and
diagonalizes the same either way, and the choice cancels in a
layer → mode → layer round trip.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Float[Array, 'nl nl']
|
Vertical coupling matrix (from :func: |
required |
f0
|
float
|
Reference Coriolis parameter [s⁻¹]. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
rossby_radii |
Float[Array, 'nl']
|
Rossby deformation radii [m] for each vertical mode. A rigid lid
makes the gravest eigenvalue zero and its radius infinite; with a
free surface (a finite |
Cl2m |
Float[Array, 'nl nl']
|
Layer-to-mode transform matrix. |
Cm2l |
Float[Array, 'nl nl']
|
Mode-to-layer transform matrix. |
Examples:
Two-layer decomposition:
>>> import jax.numpy as jnp
>>> H = jnp.array([500.0, 500.0])
>>> g_prime = jnp.array([0.02, 0.02])
>>> A = build_coupling_matrix(H, g_prime)
>>> radii, Cl2m, Cm2l = decompose_vertical_modes(A, f0=1e-4)
>>> radii.shape
(2,)
>>> Cl2m.shape
(2, 2)
Source code in finitevolx/_src/vertical/vertical_modes.py
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | |
Layer ↔ Mode Transforms
finitevolx.layer_to_mode(field, Cl2m)
Transform a field from layer space to mode space.
Applies the layer-to-mode transform matrix along the leading (layer) axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
field
|
Float[Array, 'nl *rest']
|
Field in layer space. The first dimension is the layer index; any
trailing dimensions (e.g. |
required |
Cl2m
|
Float[Array, 'nl nl']
|
Layer-to-mode transform matrix (from :func: |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'nl *rest']
|
Field in mode space, same shape as input. |
Examples:
>>> import jax.numpy as jnp
>>> field = jnp.ones((3,)) # 3-layer 0-D
>>> Cl2m = jnp.eye(3)
>>> layer_to_mode(field, Cl2m)
Array([1., 1., 1.], dtype=float32)
Source code in finitevolx/_src/vertical/vertical_modes.py
finitevolx.mode_to_layer(field, Cm2l)
Transform a field from mode space to layer space.
Applies the mode-to-layer transform matrix along the leading (mode) axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
field
|
Float[Array, 'nl *rest']
|
Field in mode space. The first dimension is the mode index; any trailing dimensions are preserved. |
required |
Cm2l
|
Float[Array, 'nl nl']
|
Mode-to-layer transform matrix (from :func: |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'nl *rest']
|
Field in layer space, same shape as input. |
Examples:
>>> import jax.numpy as jnp
>>> field = jnp.ones((3,)) # 3-mode 0-D
>>> Cm2l = jnp.eye(3)
>>> mode_to_layer(field, Cm2l)
Array([1., 1., 1.], dtype=float32)