Examples 2D

The following is a collection of example model setups, starting with an easy setup of the Barotropic vorticity equation and continuing with the shallow water model.

See also Examples 3D for examples with the primitive equation models.

2D turbulence on a non-rotating sphere

Setup script to copy and paste
using SpeedyWeather
spectral_grid = SpectralGrid(trunc=63, nlayers=1)
still_earth = Earth(spectral_grid, rotation=0)
initial_conditions = StartWithRandomVorticity()
model = BarotropicModel(; spectral_grid, initial_conditions, planet=still_earth)
simulation = initialize!(model)
run!(simulation, period=Day(20))

We want to use the barotropic model to simulate some free-decaying 2D turbulence on the sphere without rotation. We start by defining the SpectralGrid object. To have a resolution of about 200km, we choose a spectral resolution of T63 (see Available horizontal resolutions) and nlayers=1 vertical levels. The SpectralGrid object will provide us with some more information

using SpeedyWeather
spectral_grid = SpectralGrid(trunc=63, nlayers=1)
SpectralGrid:
├ Spectral:   T63 LowerTriangularMatrix{Complex{Float32}}, radius = 6.371e6 m
├ Grid:       96-ring OctahedralGaussianGrid{Float32}, 10944 grid points
├ Resolution: 216km (average)
├ Vertical:   1-layer SigmaCoordinates
└ Device:     CPU using Array

Next step we create a planet that's like Earth but not rotating. As a convention, we always pass on the spectral grid object as the first argument to every other model component we create.

still_earth = Earth(spectral_grid, rotation=0)
Earth{Float32} <: SpeedyWeather.AbstractPlanet
├ rotation::Float32 = 0.0
├ gravity::Float32 = 9.81
├ daily_cycle::Bool = true
├ length_of_day::Second = 86400 seconds
├ seasonal_cycle::Bool = true
├ length_of_year::Second = 31557600 seconds
├ equinox::DateTime = 2000-03-20T00:00:00
├ axial_tilt::Float32 = 23.4
└ solar_constant::Float32 = 1365.0

There are other options to create a planet but they are irrelevant for the barotropic vorticity equations. We also want to specify the initial conditions, randomly distributed vorticity is already defined

initial_conditions = StartWithRandomVorticity()
StartWithRandomVorticity <: SpeedyWeather.AbstractInitialConditions
├ power::Float64 = -3.0
└ amplitude::Float64 = 1.0e-5

By default, the power of vorticity is spectrally distributed with $k^{-3}$, $k$ being the horizontal wavenumber, and the amplitude is $10^{-5}\text{s}^{-1}$.

Now we want to construct a BarotropicModel with these

model = BarotropicModel(; spectral_grid, initial_conditions, planet=still_earth)

The model contains all the parameters, but isn't initialized yet, which we can do with and then run it. The run! command will always return a unicode plot (via UnicodePlots.jl) of the surface relative vorticity. This is just to get a quick idea of what was simulated. The resolution of the plot is not necessarily representative.

simulation = initialize!(model)
run!(simulation, period=Day(20))
                      Surface relative vorticity [1/s]                     
       ┌────────────────────────────────────────────────────────────┐ 4e⁻⁶ 
    90 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ┌──┐ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
    ˚N ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
   -90 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ └──┘ 
       └────────────────────────────────────────────────────────────┘ -4e⁻⁶
        0                           ˚E                           360       

Woohoo! Something is moving! You could pick up where this simulation stopped by simply doing run!(simulation, period=Day(50)) again. We didn't store any output, which you can do by run!(simulation, output=true), which will switch on NetCDF output with default settings. More options on output in NetCDF output.

Shallow water with mountains

Setup script to copy and past
using SpeedyWeather
spectral_grid = SpectralGrid(trunc=63, nlayers=1)
orography = NoOrography(spectral_grid)
initial_conditions = ZonalJet()
model = ShallowWaterModel(; spectral_grid, orography, initial_conditions)
simulation = initialize!(model)
run!(simulation, period=Day(6))

As a second example, let's investigate the Galewsky et al.[G04] test case for the shallow water equations with and without mountains. As the shallow water system has also only one level, we can reuse the SpectralGrid from Example 1.

using SpeedyWeather
spectral_grid = SpectralGrid(trunc=63, nlayers=1)
SpectralGrid:
├ Spectral:   T63 LowerTriangularMatrix{Complex{Float32}}, radius = 6.371e6 m
├ Grid:       96-ring OctahedralGaussianGrid{Float32}, 10944 grid points
├ Resolution: 216km (average)
├ Vertical:   1-layer SigmaCoordinates
└ Device:     CPU using Array

Now as a first simulation, we want to disable any orography, so we create a NoOrography

orography = NoOrography(spectral_grid)
NoOrography{Float32, OctahedralGaussianGrid{Float32}} <: SpeedyWeather.AbstractOrography
└── arrays: orography, geopot_surf

Although the orography is zero, you have to pass on spectral_grid so that it can still initialize zero-arrays of the correct size and element type. Awesome. This time the initial conditions should be set the the Galewsky et al.[G04] zonal jet, which is already defined as

initial_conditions = ZonalJet()
ZonalJet <: SpeedyWeather.AbstractInitialConditions
├ latitude::Float64 = 45.0
├ width::Float64 = 19.28571428571429
├ umax::Float64 = 80.0
├ perturb_lat::Float64 = 45.0
├ perturb_lon::Float64 = 270.0
├ perturb_xwidth::Float64 = 19.098593171027442
├ perturb_ywidth::Float64 = 3.819718634205488
└ perturb_height::Float64 = 120.0

The jet sits at 45˚N with a maximum velocity of 80m/s and a perturbation as described in their paper. Now we construct a model, but this time a ShallowWaterModel

model = ShallowWaterModel(; spectral_grid, orography, initial_conditions)
simulation = initialize!(model)
run!(simulation, period=Day(6))
                      Surface relative vorticity [1/s]                       
       ┌────────────────────────────────────────────────────────────┐0.0001  
    90 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ┌──┐   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
    ˚N ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄   
   -90 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ └──┘   
       └────────────────────────────────────────────────────────────┘-0.0001 
        0                           ˚E                           360         

Oh yeah. That looks like the wobbly jet in their paper. Let's run it again for another 6 days but this time also store NetCDF output.

run!(simulation, period=Day(6), output=true)
                      Surface relative vorticity [1/s]                     
       ┌────────────────────────────────────────────────────────────┐ 9e⁻⁵ 
    90 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ┌──┐ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
    ˚N ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
   -90 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ └──┘ 
       └────────────────────────────────────────────────────────────┘ -8e⁻⁵
        0                           ˚E                           360       

The progress bar tells us that the simulation run got the identification "0001" (which just counts up, so yours might be higher), meaning that data is stored in the folder /run_0001. In general we can check this also via

id = model.output.id
"0002"

So let's plot that data properly (and not just using UnicodePlots.jl). $id in the following just means that the string is interpolated to run_0001 if this is the first unnamed run in your folder.

using NCDatasets
ds = NCDataset("run_$id/output.nc")
ds["vor"]
vor (192 × 96 × 1 × 25)
  Datatype:    Union{Missing, Float32} (Float32)
  Dimensions:  lon × lat × layer × time
  Attributes:
   units                = s^-1
   long_name            = relative vorticity
   _FillValue           = NaN

Vorticity vor is stored as a lon x lat x vert x time array, we may want to look at the first time step, which is the end of the previous simulation (time = 6days) which we didn't store output for.

t = 1
vor = Matrix{Float32}(ds["vor"][:, :, 1, t]) # convert from Matrix{Union{Missing, Float32}} to Matrix{Float32}
lat = ds["lat"][:]
lon = ds["lon"][:]

using CairoMakie
heatmap(lon, lat, vor)

Galewsky jet

You see that in comparison the unicode plot heavily coarse-grains the simulation, well it's unicode after all! Here, we have unpacked the netCDF file using NCDatasets.jl and then plotted via heatmap(lon, lat, vor). While you can do that to give you more control on the plotting, SpeedyWeather.jl also defines an extension for Makie.jl, see Extensions. Because if our matrix vor here was an AbstractGrid (see RingGrids) then all its geographic information (which grid point is where) would be implicitly known from the type. From the netCDF file, however, you would need to use the longitude and latitude dimensions.

So we can also just do (input_as=Matrix here as all our grids use and expect a horizontal dimension flattened into a vector by default)

vor_grid = FullGaussianGrid(vor, input_as=Matrix)

using CairoMakie    # this will load the extension so that Makie can plot grids directly
heatmap(vor_grid, title="Relative vorticity [1/s]")

Galewsky jet pyplot1

Note that here you need to know which grid the data comes on (an error is thrown if FullGaussianGrid(vor) is not size compatible). By default the output will be on the FullGaussianGrid, but if you play around with other grids, you'd need to change this here, see NetCDF output and Output grid.

We did want to showcase the usage of NetCDF output here, but from now on we will use heatmap to plot data on our grids directly, without storing output first. So for our current simulation, that means at time = 12 days, vorticity on the grid is stored in the diagnostic variables and can be visualised with ([:, 1] is horizontal x vertical dimension, so all grid points on the first and only vertical layer)

vor = simulation.diagnostic_variables.grid.vor_grid[:, 1]
heatmap(vor, title="Relative vorticity [1/s]")

Galewsky jet

The jet broke up into many small eddies, but the turbulence is still confined to the northern hemisphere, cool! How this may change when we add mountains (we had NoOrography above!), say Earth's orography, you may ask? Let's try it out! We create an EarthOrography struct like so

orography = EarthOrography(spectral_grid)
EarthOrography{Float32, OctahedralGaussianGrid{Float32}} <: SpeedyWeather.AbstractOrography
├ path::String = SpeedyWeather.jl/input_data
├ file::String = orography.nc
├ file_Grid::UnionAll = FullGaussianGrid
├ scale::Float64 = 1.0
├ smoothing::Bool = true
├ smoothing_power::Float64 = 1.0
├ smoothing_strength::Float64 = 0.1
├ smoothing_fraction::Float64 = 0.05
└── arrays: orography, geopot_surf

It will read the orography from file as shown (only at initialize!(model)), and there are some smoothing options too, but let's not change them. Same as before, create a model, initialize into a simulation, run. This time directly for 12 days so that we can compare with the last plot

model = ShallowWaterModel(; spectral_grid, orography, initial_conditions)
simulation = initialize!(model)
run!(simulation, period=Day(12), output=true)
                      Surface relative vorticity [1/s]                     
       ┌────────────────────────────────────────────────────────────┐ 9e⁻⁵ 
    90 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ┌──┐ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
    ˚N ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
       ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ 
   -90 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ └──┘ 
       └────────────────────────────────────────────────────────────┘ -7e⁻⁵
        0                           ˚E                           360       

This time the run got a new run id, which you see in the progress bar, but can again always check after the run! call (the automatic run id is only determined just before the main time loop starts) with model.output.id, but otherwise we do as before.

id = model.output.id
"0003"
ds = NCDataset("run_$id/output.nc")
Dataset: run_0003/output.nc
Group: /

Dimensions
   time = 49
   lon = 192
   lat = 96
   layer = 1

Variables
  time   (49)
    Datatype:    DateTime (Float64)
    Dimensions:  time
    Attributes:
     units                = hours since 2000-01-01 00:00:0.0
     calendar             = proleptic_gregorian
     long_name            = time
     standard_name        = time

  lon   (192)
    Datatype:    Float64 (Float64)
    Dimensions:  lon
    Attributes:
     units                = degrees_east
     long_name            = longitude

  lat   (96)
    Datatype:    Float64 (Float64)
    Dimensions:  lat
    Attributes:
     units                = degrees_north
     long_name            = latitude

  layer   (1)
    Datatype:    Float32 (Float32)
    Dimensions:  layer
    Attributes:
     units                = 1
     long_name            = sigma layer

  eta   (192 × 96 × 49)
    Datatype:    Union{Missing, Float32} (Float32)
    Dimensions:  lon × lat × time
    Attributes:
     units                = m
     long_name            = interface displacement
     _FillValue           = NaN

  v   (192 × 96 × 1 × 49)
    Datatype:    Union{Missing, Float32} (Float32)
    Dimensions:  lon × lat × layer × time
    Attributes:
     units                = m/s
     long_name            = meridional wind
     _FillValue           = NaN

  u   (192 × 96 × 1 × 49)
    Datatype:    Union{Missing, Float32} (Float32)
    Dimensions:  lon × lat × layer × time
    Attributes:
     units                = m/s
     long_name            = zonal wind
     _FillValue           = NaN

  vor   (192 × 96 × 1 × 49)
    Datatype:    Union{Missing, Float32} (Float32)
    Dimensions:  lon × lat × layer × time
    Attributes:
     units                = s^-1
     long_name            = relative vorticity
     _FillValue           = NaN

You could plot the NetCDF output now as before, but we'll be plotting directly from the current state of the simulation

vor = simulation.diagnostic_variables.grid.vor_grid[:, 1]   # 1 to index surface
heatmap(vor, title="Relative vorticity [1/s]")

Galewsky jet

Interesting! One can clearly see some imprint of the orography on vorticity and there is especially more vorticity in the southern hemisphere. You can spot the coastline of Antarctica; the Andes and Greenland are somewhat visible too. Mountains also completely changed the flow after 12 days, probably not surprising!

Polar jet streams in shallow water

Setup script to copy and paste:

using SpeedyWeather
spectral_grid = SpectralGrid(trunc=63, nlayers=1)

forcing = JetStreamForcing(spectral_grid, latitude=60)
drag = QuadraticDrag(spectral_grid)

model = ShallowWaterModel(; spectral_grid, drag, forcing)
simulation = initialize!(model)
run!(simulation, period=Day(40))

We want to simulate polar jet streams in the shallow water model. We add a JetStreamForcing that adds momentum at 60˚N and 60˚S an to inject kinetic energy into the model. This energy needs to be removed (the diffusion is likely not sufficient) through a drag, we have implemented a QuadraticDrag and use the default drag coefficient. Then visualize zonal wind after 40 days with

using CairoMakie

u = simulation.diagnostic_variables.grid.u_grid[:, 1]
heatmap(u, title="Zonal wind [m/s]")

Polar jets pyplot

Gravity waves on the sphere

Setup script to copy and paste:

using SpeedyWeather
spectral_grid = SpectralGrid(trunc=127, nlayers=1)

# model components
time_stepping = SpeedyWeather.Leapfrog(spectral_grid, Δt_at_T31=Minute(30))
implicit = SpeedyWeather.ImplicitShallowWater(spectral_grid, α=0.5)
orography = EarthOrography(spectral_grid, smoothing=false)
initial_conditions = SpeedyWeather.RandomWaves()

# construct, initialize, run
model = ShallowWaterModel(; spectral_grid, orography, initial_conditions, implicit, time_stepping)
simulation = initialize!(model)
run!(simulation, period=Day(2))

How are gravity waves propagating around the globe? We want to use the shallow water model to start with some random perturbations of the interface displacement (the "sea surface height") but zero velocity and let them propagate around the globe. We set the $\alpha$ parameter of the semi-implicit time integration to $0.5$ to have a centred implicit scheme which dampens the gravity waves less than a backward implicit scheme would do. But we also want to keep orography, and particularly no smoothing on it, to have the orography as rough as possible. The initial conditions are set to RandomWaves which set the spherical harmonic coefficients of $\eta$ to between given wavenumbers to some random values

SpeedyWeather.RandomWaves()
RandomWaves <: SpeedyWeather.AbstractInitialConditions
├ A::Float64 = 2000.0
├ lmin::Int64 = 10
└ lmax::Int64 = 20

so that the amplitude A is as desired, here 2000m. Our layer thickness in meters is by default

model.atmosphere.layer_thickness
8500.0f0

so those waves are with an amplitude of 2000m quite strong. But the semi-implicit time integration can handle that even with fairly large time steps of

model.time_stepping.Δt_sec
450.0f0

seconds. Note that the gravity wave speed here is $\sqrt{gH}$ so almost 300m/s, given the speed of gravity waves we don't have to integrate for long. Visualise the dynamic layer thickness $h = \eta + H + H_b$ (interface displacement $\eta$, layer thickness at rest $H$ and orography $H_b$) with

using CairoMakie

H = model.atmosphere.layer_thickness
Hb = model.orography.orography
η = simulation.diagnostic_variables.grid.pres_grid
h = @. η + H - Hb   # @. to broadcast grid + scalar - grid

heatmap(h, title="Dynamic layer thickness h", colormap=:oslo)

Gravity waves pyplot

Mountains like the Himalayas or the Andes are quite obvious because the atmospheric layer is much thinner there. The pressure gradient is relative to $z=0$ so in a fluid at rest the mountains would just "reach into" the fluid, thinning the layer the higher the mountain. As the atmosphere here is not at rest the layer thickness is not perfectly (anti-)correlated with orography but almost so.

References

  • G04Galewsky, Scott, Polvani, 2004. An initial-value problem for testing numerical models of the global shallow-water equations, Tellus A. DOI: 10.3402/tellusa.v56i5.14436