A 5-step recipe for tuning the model steady state

Getting a steady state for your mizer model that agrees with observations is in principle a hard chicken and egg problem. I present the trick that makes it surprisingly easy, with a 5-step recipe. I’ll save tips on what to do when the recipe fails for later blog posts.

Gustav Delius true
08-20-2021

What we want to do

In this blog post we will describe stage 2 of the process of building a mizer model. The stages are:

  1. Collect information about the important species in your ecosystem and how they are fished. This includes physiological parameters for the species as they might be found on fishbase, but also information about how abundant the species are and how they are being fished.

  2. Create a mizer model that in its steady state reproduces the time-averaged observed state of your fish community. Of course your real system never is in a perfect steady state. It is continuously changing. There is much fluctuation from year to year. We will however assume that if we average observations over a number of years we obtain something that is close to the steady state. Without some such assumption it would be impossible for us to get started.

  3. Tune the model parameters further to also reproduce time-series observations that capture some of the system’s sensitivity to perturbations, like changes in fishing pressure.

This blog post is only about the second stage. We will present a 5 step recipe for that stage. When the recipe works, it will only take a couple of minutes! So I hope you will try the recipe for your own model. Of course in practice there are all kinds of things that can (and will) go wrong. So I hope this blog post will lead to some exchange of experiences with the recipe.

The recipe is based on an important trick. I call it the constant reproduction trick. The trick is obvious once you see it, but I must admit that I struggled for a long time with mizer model building until I stumbled upon the trick.

Example we will use

To make this concrete, we will consider a model for the North Sea involving 12 species. We will short-circuit stage 1 of the model-building process by basing our example on the North Sea species parameter data frame NS_species_params that comes with mizer.

Some of the functions we will be using are still in active development in the mizerExperimental package. Therefore we will always want to make sure we are loading the latest version of the package with

remotes::install_github("sizespectrum/mizerExperimental")
library(mizerExperimental)

This blog post was compiled with mizer version 2.2.1.9006 and mizerExperimental version 2.2.1.9003

Here is the species parameter data frame that we will be using:

Show code
# Here is how I obtained the example species_params:
species_params <- NS_species_params
species_params$R_max <- NULL
species_params$a <- c(0.007, 0.001, 0.009, 0.002, 0.010, 0.006, 0.008, 0.004,
                         0.007, 0.005, 0.005, 0.007)
species_params$b <- c(3.014, 3.320, 2.941, 3.429, 2.986, 3.080, 3.019, 3.198,
                         3.101, 3.160, 3.173, 3.075)

years <- getTimes(NS_sim) >= 1990 & getTimes(NS_sim) <= 2010
# Average biomass over those 21 years
bm_hist <- getBiomass(NS_sim)[years, ]
species_params$biomass_observed <-  colSums(bm_hist) / 21

library(knitr)
kable(species_params, row.names = FALSE)
species w_inf w_mat beta sigma k_vb a b biomass_observed
Sprat 33.0 13 51076 0.8 0.681 0.007 3.014 3.325300e+10
Sandeel 36.0 4 398849 1.9 1.000 0.001 3.320 1.115636e+12
N.pout 100.0 23 22 1.5 0.849 0.009 2.941 3.047268e+11
Herring 334.0 99 280540 3.2 0.606 0.002 3.429 4.286172e+11
Dab 324.0 21 191 1.9 0.536 0.010 2.986 1.723425e+10
Whiting 1192.0 75 22 1.5 0.323 0.006 3.080 2.299098e+11
Sole 866.0 78 381 1.9 0.284 0.008 3.019 1.078201e+11
Gurnard 668.0 39 283 1.8 0.266 0.004 3.198 1.277013e+11
Plaice 2976.0 105 113 1.6 0.122 0.007 3.101 2.197107e+12
Haddock 4316.5 165 558 2.1 0.271 0.005 3.160 6.953041e+11
Cod 39851.3 1606 66 1.3 0.216 0.005 3.173 3.054369e+11
Saithe 39658.6 1076 40 1.1 0.175 0.007 3.075 8.099535e+11

For each species we are specifying its name and some parameters characteristic of the species: its asymptotic size w_inf and maturity size w_mat, the parameters beta and sigma for its feeding kernel (we are using the default lognormal kernel for all species), the von Bertalanffy growth parameter k_vb and the parameters a and b in the allometric length-weight relationship \(w = a l^b\).

In addition, we also specify information that is specific to our ecosystem, namely the average abundance of each species, in the biomass_observed column. This is measured in grams. Because for the purpose of this blog post it is not important, we did not bother to look up real biomass estimates but instead we simply used the average over the years 1990 to 2010 in the simulated data in the NS_sim object included in the mizer package. You are invited to re-run the analysis with proper data.

The observed system is being fished. We need to give mizer information about how it is being fished. We do this via the gear_params data frame.

Show code
# Average fishing mortality
f_location <- system.file("extdata", "NS_f_history.csv", package = "mizer")
f_history <- as(read.csv(f_location, row.names = 1), "matrix")[years, ]
f <- colSums(f_history) / 12

gear_params <- 
    data.frame(gear = "All",
               species = NS_species_params$species,
               sel_func = "sigmoid_length",
               l25 =  c(7.6, 9.8, 8.7, 10.1, 11.5, 19.8, 16.4, 19.8, 11.5,
                        19.1, 13.2, 35.3),
               l50 = c(8.1, 11.8, 12.2, 20.8, 17.0, 29.0, 25.8, 29.0, 17.0,
                       24.3, 22.9, 43.6),
               catchability = f)

kable(gear_params, row.names = FALSE)
gear species sel_func l25 l50 catchability
All Sprat sigmoid_length 7.6 8.1 1.3763157
All Sandeel sigmoid_length 9.8 11.8 1.3331618
All N.pout sigmoid_length 8.7 12.2 1.3763157
All Herring sigmoid_length 10.1 20.8 0.8626210
All Dab sigmoid_length 11.5 17.0 0.2059334
All Whiting sigmoid_length 19.8 29.0 1.3429086
All Sole sigmoid_length 16.4 25.8 1.4424019
All Gurnard sigmoid_length 19.8 29.0 0.1342909
All Plaice sigmoid_length 11.5 17.0 1.0296672
All Haddock sigmoid_length 19.1 24.3 1.1690897
All Cod sigmoid_length 13.2 22.9 1.6476887
All Saithe sigmoid_length 35.3 43.6 0.9744912

We are setting up a single gear that we call “All” which catches all species. For each species we set up the selectivity curve of the gear as a sigmoid curve with given l25 and l50 parameters. Finally we set the catchability of each species to the observed fishing mortality, averaged over the years 1990 to 2010. We will then set the fishing effort to 1, because in mizer the fishing mortality is the product of effort, catchability and selectivity.

Our task now is to create a mizer model that describes species with the above characteristics and that has a steady state with the observed biomasses under the given fishing pressure.

Why it is a hard problem

We have a chicken and egg problem. The equilibrium abundance and size distributions of the fish are determined by their size-dependent growth and death rates. These rates in turn are determined by the abundance and size distribution of their prey and their predators. So we can’t determine the size distributions before we have determined the rates and we can’t determine the rates before we have determined the size distributions.

Because every species is a both prey and predator of fish of various species and sizes during their life, this is a highly coupled non-linear problem. If for example we use 100 size bins and 12 species, plus a resource spectrum, then we would have far over a thousand coupled nonlinear equations to solve simultaneously. That is not practical.

Rather than solving the equilibrium equations, another way to find a steady state is to simply evolve the time dynamics until the system settles down to a steady state. The problem with this approach is that the coexistence steady state of a size spectrum model has a very small region of attraction, so unless one starts with an initial state that is already very close to that coexistence steady state one will end up with extinctions.

The reason is a feedback loop: as the spawning stock biomass of a species grows, also its reproduction rate grows, leading to further growth of the spawning stock biomass and so on. Similarly as the spawning stock of another species declines, so does its reproduction rate, leading to further decline. In spite of moderating non-linear effects in the model, the general outcome is extinctions.

We can see the phenomenon in our North Sea example. If we simply run the dynamics, starting with the initial state set up by newMultispeciesParams(), first Sprat goes extinct, and Herring follows soon after. Just click the play button on the animation below.

params <- newMultispeciesParams(species_params = species_params,
                                gear_params = gear_params,
                                initial_effort = 1)
sim <- project(params, t_max = 12)
animateSpectra(sim, power = 2)

The constant reproduction trick

So the trick is to cut the destabilising feedback loop by decoupling the reproductive rate from the spawning stock biomass. We do this by simply keeping the reproduction rate constant. The size spectrum model with constant reproduction turns out to be very stable and quickly approach a steady state, due to the smoothing effect of the feeding kernel. Once the steady state is found, we can simply adjust the reproductive efficiency of each species so that the steady state spawning stock produces the chosen reproduction rate. With that choice of the reproductive efficiency the steady state of the restricted dynamics is also the steady state of the full size spectrum model.

Here is the code that does that. Run the animation that it produces by clicking the play button.

params <- newMultispeciesParams(species_params = species_params,
                                gear_params = gear_params,
                                initial_effort = 1)
# Keep reproduction constant at the initial level
params@species_params$constant_reproduction <- getRDD(params)
params <- setReproduction(params, RDD = "constantRDD")
# Run the dynamics with this constant reproduction
sim <- project(params, t_max = 15)
animateSpectra(sim, power = 2)

Mizer has a function called steady() that does the same as the above code, namely run to steady state with constant reproduction and then adjust the reproduction parameters, and then sets the resulting steady state as the initial state of the MizerParams object.

params <- newMultispeciesParams(species_params = species_params,
                                gear_params = gear_params,
                                initial_effort = 1)
params <- steady(params)
plotlySpectra(params, power = 2)