Home / Software / swesphere

swesphere

Shallow-water equations on the rotating sphere: a geophysical flow small enough to experiment with, documented enough to compare against, and reproducible from a container.

Zonal wind, meridional wind and depth anomaly of the two_jets regime on the sphere

Above: a state of the two_jets record. Zonal wind, meridional wind, and the departure of the depth from its zonal mean drawn as relief (exaggerated). The polar caps under the sponge are grey.

What it is

The shallow-water equations on a sphere are the smallest system that carries the ingredients of large-scale atmospheric flow: a balance relation between wind and mass, fast gravity waves alongside slow balanced motion, and barotropic instability that sustains turbulence. swesphere solves them with vector-invariant momentum equations and a flux-form continuity equation, on a regular latitude-longitude grid with an exact zonal derivative by FFT, second-order centred meridional differences and fourth-order Runge-Kutta stepping. Dissipation is an exponential filter in spherical harmonics plus a sponge on the wind at the polar caps.

At the default truncation the grid is 66 × 132 and the state has 26,136 variables, which integrates in about two seconds per model day on one core: small enough for hundreds of forecasts on a workstation, large enough to behave like the atmosphere.

How it is put together

One object composes five small modules and exposes the state as a vector. Presets configure it, helper modules measure it, and anything outside drives it through the same three calls.

Anything that drives the model assimilation libraries (the PyTEDA adapter) · your own scripts and notebooks · the experiments of the paper presets waves() one_jet() two_jets() make() → (model, x0) SWEModel x = [u, v, h] propagate(x, [t0, t1]) pack / unpack / var_blocks interior_mask() measuring it climatology diagnostics records, scales, invariants grid lat-lon, exact zonal FFT, centred d/dφ dynamics vector-invariant momentum, flux-form continuity integrators registry of steppers RK4 shipped, yours plugs in here dissipation spectral filter (pyshtools), polar sponge forcing relaxation of the zonal mean (optional)

The amber box is the one you are meant to replace: register a stepper in INTEGRATORS and it runs with the same grid, dissipation and forcing.

The interface

The state is a plain vector x = [u, v, h] and the model is a function that advances it, so an external program drives it without knowing anything about the discretization.

from swesphere import presets, climatology, diagnostics

model, x0 = presets.two_jets()        # 66 x 132, dt = 120 s
x1 = model.propagate(x0, [0.0, 6*3600.0])          # 6 h
u, v, h = model.unpack(x1)            # or model.var_blocks
mask = model.interior_mask()          # rows outside the polar sponge

S = climatology.build_climatology(    # 200 states, 2 days apart
        model, x0, spinup_days=40,
        n_snapshots=200, every_days=2)
scales, per_field = climatology.anomaly_scales(model, S)
print(diagnostics.invariants(model, x1))   # mass, energy, enstrophy

Plugging in your own integrator

The right-hand side is published and the time stepper is looked up in a registry, so a new scheme runs with the same grid, filter, sponge and forcing as the shipped Runge-Kutta, and is measured by the same experiments: order of convergence, conservation of the invariants, largest stable step, cost per model day and the eddy amplitude it sustains.

from swesphere.integrators import INTEGRATORS

def euler(rhs, u, v, h, dt):     # rhs(u,v,h) -> tendencies
    du, dv, dh = rhs(u, v, h)
    return u + dt*du, v + dt*dv, h + dt*dh

INTEGRATORS["euler"] = euler
model, x0 = presets.two_jets(scheme="euler", dt=30.0)

swesphere.dynamics.rhs(u, v, h, grid) is the physics alone; model.rhs(u, v, h) adds the forcing of the preset.

API

Everything is importable and callable directly; the Docker services are only how the experiments of the paper are reproduced. These are the entry points.

CallWhat it gives you
presets.waves()
presets.one_jet(tau_days=5, h0=1e4)
presets.two_jets(...)
presets.make(name, **overrides)
a configured model and its initial state, (model, x0). Any keyword overrides the defaults: LMAX, dt, filter_every, scheme, n_pole_rows.
model.propagate(x0, [t0, t1])integrate the state vector from t0 to t1 (seconds). just_final_state=False returns every step.
model.pack(u, v, h)
model.unpack(x)
between the three fields and the state vector.
model.var_blocks
model.field_size, model.dim
model.Nlat, model.Nlon
the slice of each field in the vector, and the sizes.
model.interior_mask()boolean mask of the rows outside the polar sponge: the part of the domain meant to be used.
model.initial_condition(kind, seed=0, **kw)'rossby' (add perturbed=True for a seeded perturbation), 'tc2', 'galewsky'.
model.rhs(u, v, h)
dynamics.rhs(u, v, h, grid)
the tendencies with and without the forcing of the preset.
dynamics.relative_vorticity(u, v, grid)
dynamics.divergence(u, v, grid)
diagnostics on the fields.
climatology.build_climatology(model, x0, spinup_days, n_snapshots, every_days)a record of states as a (n, dim) array.
climatology.anomaly_scales(model, S)
climatology.lag_correlation(model, S, lag=1, field='h')
the scale of the departures from the record mean, and how fast the record decorrelates.
diagnostics.invariants(model, x, interior=False)global mass, total energy and potential enstrophy.
diagnostics.error_norms(model, x, x_ref, field='h')normalized l1, l2 and l-infinity errors.
INTEGRATORS[name] = stepperregister a time-stepping scheme; pass scheme=name to a preset.

The state vector is x = [u, v, h], each field flattened row-major from north to south on a grid of 2(LMAX+1) by 4(LMAX+1) points. Time is in seconds. Depths are in metres, winds in metres per second.

Three regimes

Each preset is a function that returns a configured model and its initial state, so an experiment names its regime in one line. The statistics below were measured on 200-state records with the default settings.

PresetWhat it isEddy std of h|U|maxRegime
wavesRossby waves on a zonal flow, unforced8.8 m (decaying)42 m/squasi-linear; the filter is the only sink
one_jetGalewsky jet at 45°N + relaxation of the zonal mean124 ± 13 m76 m/sstationary, fairly regular eddies in the north
two_jetsGalewsky jets at ±45° + the same relaxation156 ± 13 m76 m/sstationary irregular turbulence in both hemispheres

The three regimes of swesphere on the sphere

The three presets, same fields as above. Note the scales: the meridional wind and the depth anomaly of waves are an order of magnitude weaker than those of the forced presets.

Verified, and its limits measured

On the steady zonal flow of Williamson et al.’s test case 2 the normalized l2 error of the depth after five days is 2.2×10-4 at the default truncation and 5.5×10-5 at twice the resolution: second order, as expected from the centred meridional differences. The Galewsky barotropic instability reproduces the published reference solution, with the global mass conserved to 10-6 and the total energy to 10-4 over ten days.

Two limits are documented rather than hidden. The polar sponge, not the discretization, dominates the error of the default configuration on tests whose flow extends to the poles. And because the filter acts every few steps rather than every few seconds, the eddy amplitude of a regime is set by the interval between filter applications, not by the time step: runs that share an interval and differ by a factor of six in the time step agree within the natural variability of the flow.

Reproducing the experiments

Each experiment is a Docker Compose service that writes its results as CSV after every case; the figures are regenerated from those files.

docker compose build test
docker compose run --rm test
docker compose run -d verification   # independent of each other
docker compose run -d stability
docker compose run -d regimes
docker compose run -d footprint      # after regimes
docker compose run --rm figures      # after all
Package
swesphere
Type
Model · Python · NumPy, SciPy, pyshtools
State
26,136 variables (66 × 132, three fields)
Cost
about 2 s per model day, one core
Regimes
waves, one_jet, two_jets
Author
Elias D. Nino-Ruiz
Code
github.com/enino84/swesphere
License
MIT

GitHub