The land-sea mask

The following describes how a custom land-sea mask can be defined. SpeedyWeather uses a fractional land-sea mask, i.e. for every grid-point

  • 1 indicates land
  • 0 indicates ocean
  • a value in between indicates a grid-cell partially covered by ocean and land

Setting the land-sea mask to ocean therefore will disable any fluxes that may come from land, and vice versa. However, with an ocean-everywhere land-sea mask you must also define sea surface temperatures everywhere, otherwise the fluxes in those regions will be zero.

For more details, see Surface fluxes and the Land-sea mask section therein.

Manual land-sea mask

You can create the default land-sea mask as follows

using SpeedyWeather
spectral_grid = SpectralGrid(trunc=31, nlayers=8)
land_sea_mask = LandSeaMask(spectral_grid)
LandSeaMask{Float32, OctahedralGaussianGrid{Float32}} <: AbstractLandSeaMask
├ path::String = SpeedyWeather.jl/input_data
├ file::String = land-sea_mask.nc
├ file_Grid::UnionAll = FullClenshawGrid
└── arrays: mask

which will automatically interpolate the land-sea mask onto grid and resolution as defined in spectral_grid at initialization. The actual mask is in land_sea_mask.mask and you can visualise it with

model = PrimitiveWetModel(spectral_grid; land_sea_mask)
simulation = initialize!(model)     # triggers also initialization of model.land_sea_mask

using CairoMakie
heatmap(land_sea_mask.mask, title="Land-sea mask at T31 resolution")

Land-sea mask

Now after initialization (otherwise you reinitialize the mask, overwriting your changes) you could manually change the land-sea mask with the set! function which can take scalars as global constants or functions of two arguments longitude $\lambda$ and $\varphi$. You can use an anonymous function (λ, φ) -> ... but you do not have to, defining function f(λ, φ) and then using land_sea_mask = f works too.

set!(model, land_sea_mask=0)                    # aqua planet
set!(model, land_sea_mask=1)                    # rocky planet
set!(model, land_sea_mask=(λ, φ) -> rand()-1)   # random small islands

# snowball planet with ocean in the tropics between 10˚S and 10˚N
set!(model, land_sea_mask=(λ, φ) -> abs(φ) < 10 ? 0 : 1)

# flood the northern hemisphere only, values are automatically clamped into [0, 1]
initialize!(model.land_sea_mask, model)         # back to Earth's mask
set!(model, land_sea_mask=(λ, φ) -> φ > 0 ? -1 : 0, add=true)

# visualise
heatmap(land_sea_mask.mask, title="Land-sea mask with Northern Hemisphere ocean")
┌ Warning: Land-sea mask was not set to values in [0, 1] but in [-0.9996922, -0.0005770094]. Clamping.
@ SpeedyWeather ~/work/SpeedyWeather.jl/SpeedyWeather.jl/src/physics/land_sea_mask.jl:70
┌ Warning: Land-sea mask was not set to values in [0, 1] but in [-1.0, 1.0]. Clamping.
@ SpeedyWeather ~/work/SpeedyWeather.jl/SpeedyWeather.jl/src/physics/land_sea_mask.jl:70

NH ocean

And now you can run the simulation as usual with run!(simulation).

Earth's land-sea mask

The Earth's LandSeaMask has itself the option to load another land-sea mask from file, but you also have to specify the grid that mask from files comes on. It will then attempt to read it via NCDatasets and interpolate onto the model grid.

AquaPlanetMask

Predefined is also the AquaPlanetMask which can be created as

land_sea_mask = AquaPlanetMask(spectral_grid)
AquaPlanetMask{Float32, OctahedralGaussianGrid{Float32}} <: AbstractLandSeaMask
└── arrays: mask

and is equivalent to using Earth's LandSeaMask but setting the entire mask to zero afterwards land_sea_mask.mask .= 0.

Custom land-sea mask

Every (custom) land-sea mask has to be a subtype of AbstractLandSeaMask. A custom land-sea mask has to be defined as a new type (struct or mutable struct)

CustomMask{NF<:AbstractFloat, Grid<:AbstractGrid{NF}} <: AbstractLandSeaMask{NF, Grid}

and needs to have at least a field called mask::Grid that uses a Grid as defined by the spectral grid object, so of correct size and with the number format NF. All AbstractLandSeaMask have a convenient generator function to be used like mask = CustomMask(spectral_grid, option=argument), but you may add your own or customize by defining CustomMask(args...) which should return an instance of type CustomMask{NF, Grid} with parameters matching the spectral grid. Then the initialize function has to be extended for that new mask

initialize!(mask::CustomMask, model::PrimitiveEquation)

which generally is used to tweak the mask.mask grid as you like, using any other options you have included in CustomMask as fields or anything else (preferably read-only, because this is only to initialize the land-sea mask, nothing else) from model. You can for example read something from file, set some values manually, or use coordinates from model.geometry.

Time-dependent land-sea mask

It is possible to define an intrusive callback to change the land-sea mask during integration. The grid in model.land_sea_mask.mask is mutable, meaning you can change the values of grid points in-place but not replace the entire mask or change its size. If that mask is changed, this will be reflected in all relevant model components. For example, we can define a callback that floods the entire planet at the beginning of the 21st century as

Base.@kwdef struct MilleniumFlood <: SpeedyWeather.AbstractCallback
    schedule::Schedule = Schedule(DateTime(2000,1,1))
end

# initialize the schedule
function SpeedyWeather.initialize!(
    callback::MilleniumFlood,
    progn::PrognosticVariables,
    diagn::DiagnosticVariables,
    model::AbstractModel,
)
    initialize!(callback.schedule, progn.clock)
end

function SpeedyWeather.callback!(
    callback::MilleniumFlood,
    progn::PrognosticVariables,
    diagn::DiagnosticVariables,
    model::AbstractModel,
)
    # escape immediately if not scheduled yet
    isscheduled(callback.schedule, progn.clock) || return nothing

    # otherwise set the entire land-sea mask to ocean
    model.land_sea_mask.mask .= 0
    @info "Everything flooded on $(progn.clock.time)"
end

# nothing needs to be done after simulation is finished
SpeedyWeather.finalize!(::MilleniumFlood, args...) = nothing

Note that the flooding will take place only at the start of the 21st century, last indefinitely, but not if the model integration period does not cover that exact event, see Schedules. Initializing a model a few days earlier would then have this MilleniumFlood take place

land_sea_mask = LandSeaMask(spectral_grid)      # start with Earth's land-sea mask
model = PrimitiveWetModel(spectral_grid; land_sea_mask)
add!(model, MilleniumFlood())   # or MilleniumFlood(::DateTime) for any non-default date

simulation = initialize!(model, time=DateTime(1999,12,29))
run!(simulation, period=Day(5))
heatmap(model.land_sea_mask.mask, title="Land-sea mask after MilleniumFlood callback")
[ Info: Everything flooded on 2000-01-01T00:00:00

Land-sea mask2

And the land-sea mask has successfully been set to ocean everywhere at the start of the 21st century. Note that while we added an @info line into the callback! function, this is here not printed because of how the Documenter works. If you execute this in the REPL you'll see it.