```{css, echo=FALSE} .hide { display: none!important; }

/ https://bookdown.org/yihui/rmarkdown-cookbook/html-css.html / .modelScript pre, .modelScript pre.sourceCode code { background-color: #f0f8ff !important; }

```r
library(unittest)
# Redirect ok() output to stderr
options(unittest.output = stderr())
if (nzchar(Sys.getenv('G3_TEST_TMB'))) options(gadget3.tmb.work_dir = gadget3:::vignette_base_dir('work_dir'))

library(gadget3)
set.seed(123)

This vignette walks through a script that will generate a gadget3 model, explaining concepts along the way.

Code blocks that make up the gadget3 script are marked in blue, like this:

wzxhzdk:1

When combined they will form a full model, see the appendix for the entire script.

Gadget3 and the gadget framework

Gadget3 is a marine modelling R package, but it is not in itself an ecosystem model. Instead, it gives you building blocks (or actions) that can be assembled to produce as complex model as your situation requires. This can then be converted into other forms, most importantly a TMB objective function or an R function, which can then be optimised and run to generate reporting.

As the name suggests, it's designed to be a successor to the previous gadget modelling framework. The actions currently available are designed to be very similar, if not identical, to the components present in gadget2. If you are familiar with previous versions of gadget then you will find the naming very similar, and translation of old input files to gadget3 can be done in a rote fashion.

Gadget3 is the core part of what is known as the gadget framework, a set of packages that are designed to work together to produce ecosystem models.

These packages are loosely coupled; you do not need everything installed to create a gadget3 model. However, when they will prove useful it will be mentioned here.

The gadget3 package can be installed via. CRAN:

install.packages('gadget3')

The full set of packages can be installed with:

install.packages('MFDB')
remotes::install_github('gadget-framework/gadgetutils')
remotes::install_github('gadget-framework/gadgetplots')
remotes::install_github('gadget-framework/g3experiments')

Creating a (single species) model

As opposed gadget2 and other modelling frameworks, there is no input data format. Instead, the model configuration is written as an R script. This document will walk through the parts of a model script for a single-species model, introducing concepts along the way.

The first step in any script is to load gadget3. We will also use dplyr when formatting input data:

wzxhzdk:4

Actions

A gadget3 model is defined as a list of actions. Actions are snippets of code that define processes in a model.

To start with, we will add the g3a_time() to our list of actions:

wzxhzdk:5

This acts as timekeeping for our model, starting in year 1990 and progressing until 2023. Each year will have 4 time steps in, of equal length.

As a convention, we build up an actions array of everything required, allowing sections to be added/removed as necessary.

Ultimately, this list will be converted to either R or TMB code with with g3_to_r() or g3_to_tmb() respectively. We can try this already with our time action, and generate a function that will count years & steps:

g3_to_r(actions_time)
g3_to_tmb(actions_time)

Stocks

After actions, the other key concept in a gadget3 model is a stock. These are the means to describe populations within your model. For simpler scenarios such as here, stocks map directly to a species. However, more complicated models may have one stock per-maturation-stage, sex or both.

We define a stock with the g3_stock() and associated g3s_* functions, for example:

wzxhzdk:8

Here we define a stock called "fish" with length bins 10..100, then add an area to live in & 5 age bins.

Ultimately, the stock functions define the structure of the arrays that will hold the state of that stock within gadget3. We can use g3_stock_instance() to see an example of the array used:

# aperm() re-orders dimensions for more compact printing
aperm(g3_stock_instance(fish, 0), c(1,3,2))

For example, the abundance and mean weight of the stock will be stored in one of these arrays within the model.

Stock actions

Now we have a stock, we can add apply population dynamics actions, and save them in the actions array from earlier:

wzxhzdk:10

Each of these g3a_* actions will have a 1:1 parallel with gadget2 stockfile components, so if you are familiar with these config files will do what you expect.

For each action you can click through to the reference to get more information on what it does, but in summary we have defined:

There are more actions available besides these, for instance g3a_spawn() can be used for recruitment dependent on stock size instead of g3a_renewal_normalcv(). For a full list, see ??gadget3::"G3 action" or the package reference index.

Likelihood actions are actions that will sum their output into the model's overall likelihood score, analogous to gadget2's likelihood components.

The order of these actions as we have defined them is not preserved. When a model runs, the steps will not happen in the above order, they will be re-ordered to match the standard action order, see ?g3_action_order.

Model parameters

The definition above should look quite barren, bar maxlengthgroupgrowth we have not provided any figures for the stock dynamics.

The defaults for all actions define model parameters that can be set as fixed values or optimised later, rather than baking them into the model.

For instance, we can see that g3a_naturalmortality() creates a parameter for M by default:

head(g3a_naturalmortality)
head(g3a_naturalmortality_exp)

Without any arguments, we use g3a_naturalmortality_exp(), which sets M to be g3_parameterized("M", by_stock = TRUE, by_age = TRUE). This tells gadget3 that a parameter M should be expected by the model, that will both be broken down by stock (i.e. will include the name of our stock), and each age within that stock.

We can define a model with just g3a_time() and g3a_initialconditions_normalcv() to see the end result. To be able to run the function you need to provide a list of parameter values, the format of this list is defined by the attached parameter template:

simple_actions <- list(
    g3a_time(1990, 1991),
    g3a_initialconditions_normalcv(fish))
simple_fn <- g3_to_r(c(simple_actions, list(
    g3a_report_detail(simple_actions) )))

params <- attr(simple_fn, 'parameter_template')
unlist(params)

g3a_report_detail() adds standard reporting to our model, we will cover it's use later.

ok(ut_cmp_identical(sort(names(params), method = "radix"), c(
    "fish.K",
    "fish.Linf",
    "fish.M.1", "fish.M.2", "fish.M.3", "fish.M.4", "fish.M.5",
    "fish.init.1", "fish.init.2", "fish.init.3", "fish.init.4", "fish.init.5",
    "fish.init.scalar",
    "fish.lencv",
    "fish.t0",
    "fish.walpha",
    "fish.wbeta",
    "init.F",
    "project_years",
    "recage",
    "report_detail",
    "retro_years")), "parameter_template names match expected")

We can fill in these values and run the model:

params$fish.init.scalar <- 10
params$fish.init.1 <- 10
params$fish.init.2 <- 10
params$fish.init.3 <- 10
params$fish.init.4 <- 10
params$fish.init.5 <- 10
params$fish.M.1 <- 0.15
params$fish.M.2 <- 0.15
params$fish.M.3 <- 0.15
params$fish.M.4 <- 0.15
params$fish.M.5 <- 0.15
params$init.F <- 0.5
params$recage <- 0
params$fish.Linf <- max(g3_stock_def(fish, "midlen"))
params$fish.K <- 0.3
params$fish.t0 <- g3_stock_def(fish, "minage") - 0.8
params$fish.lencv <- 0.1
params$report_detail <- 1

# Run model and pull out final abundance from the result
g3_array_plot(g3_array_agg(
    attr(simple_fn(params), "dstart_fish__num"),
    c('age', 'length'),
    time = "1990-01"))

Altering K results in corresponding changes to the stock structure:

params$fish.K <- 0.9
g3_array_plot(g3_array_agg(
    attr(simple_fn(params), "dstart_fish__num"),
    c('age', 'length'),
    time = "1990-01"))
# Make reporting is working
abund <- g3_array_agg(attr(simple_fn(params), "dstart_fish__num"), c('age', 'length'), time = "1990-01")
ok(ut_cmp_identical(dimnames(abund), list(
    length = c("10:20", "20:30", "30:40", "40:50", "50:60", "60:70", "70:80", "80:90", "90:100", "100:Inf"),
    age = c("age1", "age2", "age3", "age4", "age5"))), "abund dimnames() structure")

Fleet actions

Fleets in gadget3 are modelled as stock objects, which predate on their target stocks.

To define a fleet, we need to introduce historical data into the model. In our case we will generate random data, but the aggretation steps would apply regardless.

Landings data

wzxhzdk:17

Here we use expand.grid() to generate a data.frame() with all possible year/step/area cominations. We then use dplyr::mutate() to add a weight column to this table, using rnorm() to generate random numbers distributed about a mean.

The identity() function is a do-nothing function that passes through the input. We use this to move the assignment onto it's own line.

The end result is a data.frame() of total biomass figures:

landings_f_surv
plot(landings_f_surv[c('year', 'weight')], ylim = c(0, 2000), col = "red")

Note that we haven't provided data for all years/steps, as we'll assume this fleet only works in spring. For more information on how this works, see vignette("incorporating-observation-data").

Length distribution data

Next we generate some length-distribution data:

wzxhzdk:19

As before, expand.grid() and mutate() generate a table with random lengths distributed about the mean. This is our unaggregated data, which we save using assign() so we can see the end result:

head(ldist_f_surv.raw)

We next use group_by() and cut() to aggregate by year, step & length bins. cut() is responsible for binning continuous data. We can see what it does by running for single values:

cut(c(50), breaks = c(seq(0, 80, 20), Inf), right = FALSE)

Note that we add Inf to the end of the list of breaks, to create a plus-group. We also specify right = FALSE so that the groups are closed on the left.

Also note that our breaks aren't the same as our stock definition, this is allowed and gadget3 will re-aggregate the model data to match. For more information on how this works, see vignette("incorporating-observation-data").

Finally, summarise counts the number in each group and puts the result into a number column. The end result looks like:

summary(ldist_f_surv)
years <- 1990:1994
par(mfrow=c(2, ceiling(length(years) / 2)))
for (y in years) g3_array_plot(xtabs(number ~ length + year, ldist_f_surv)[,as.character(y)])

Age-length distribution data

Finally, we can apply the same techniques to generate and aggregate age-length data:

wzxhzdk:23

The end result is a data.frame() with year/step/age/length/number:

summary(aldist_f_surv)
years <- 1990:1994
ages <- unique(aldist_f_surv$age)

par(mfrow=c(ceiling(length(years) / 2), 2), mar = c(2,2,1,0))
for (y in years) g3_array_plot(xtabs(number ~ length + age + year, aldist_f_surv)[,,as.character(y)])

Fleet definition

A fleet, f_surv, is defined in much the same way as our stock above, however with a different set of actions:

wzxhzdk:25

We define the stock with g3_fleet() instead of g3_stock(), as a fleet isn't divided into length or age bins. Simiarly, g3s_age() to divide into age bins isn't relevant.

wzxhzdk:26

The only action of f_surv is to predate fish. We define this with g3a_predate_fleet(), setting:

For other possible settings, follow the links to the function definitions.

Finally we define likelihood actions using g3l_catchdistribution(), to compare modelled catch against our length & age-length distribution data generated above.

wzxhzdk:27

Note that the only difference between the 2 likelihood actions is the structure of the inputted data. aldist_f_surv unlike ldist_f_surv has an age column, gadget3 will detect this and group the modelled catch accordingly. Similarly neither data.frame has the full range of years, so comparisons will be made outside those ranges. For more detail on what can be done here, see vignette("incorporating-observation-data").

function_f defines the method of comparison between modelled catch & observation data, once aggregation has been done. g3l_distribution_sumofsquares() in this case compares the sum of squared difference. For more options on what to use here, follow the links to the reference.

To add futher fleets to your model, just repeat the same code with a different fleet name.

Survey indices

Measures of abudnance, such as commercial CPUE data, can be added as observation data by adding g3l_abundancedistribution() likelihood actions:

wzxhzdk:28

We create a data.frame() with year/step/area/weight columns, and input this into a likelihood action as before with our fleet.

The key differences between the catch distribution above are:

Creating model functions and Parameterization

At this point, we are ready to convert our model into code:

wzxhzdk:29

g3_to_tmb() will take our list of actions and convert it into C++ code suitable for use with TMB.

g3a_report_detail() and g3l_bounds_penalty() add further actions to the model, based on the actions already within it:

To be able to run this model, we need to provide values (or initial guesses) for parameters. Earlier we used g3_to_r() and saw the resultant parameter template. With g3_to_tmb() we can do the same, however the template is more complex:

simple_code <- g3_to_tmb(list(
    g3a_time(1990, 1991),
    g3a_naturalmortality(fish) ))
attr(simple_code, 'parameter_template')

The TMB parameter template has the following columns:

The model is expecting 5 parameters, fish.M.1 to fish.M.5, for each age group. We can either fix these to known values, or configure bounds to optimise within.

Filling in individual values can be tedious. The helper, g3_init_val(), will assist in filling in these values for you. Instead of setting individual values we can assign values using wildcard characters *, # (numeric), | (or):

attr(simple_code, "parameter_template") |>
    g3_init_val("*.M.#", 0.1) |>
    g3_init_val("*.M.3", 0.5) |>
    g3_init_val("*.M.2|4", 0.2)

Setting lower & upper bounds automatically turns on optimise, and fills in parscale:

attr(simple_code, "parameter_template") |>
    g3_init_val("*.M.#", 0.15, lower = 0.001, upper = 1)

We can also use spread as a shorthand for lower = value * (1 - spread), upper = value * (1 + spread):

attr(simple_code, "parameter_template") |>
    g3_init_val("*.M.#", 0.15, spread = 0.5)

This allows us to fill in parameters without worrying too much about stock/fleet naming:

wzxhzdk:34

Finally we are ready for optimisation runs. g3_tmb_adfun() is a wrapper around TMB::MakeADFun() and TMB::compile, producing a TMB objective function. gadgetutils::g3_iterative() then optimises based on iterative reweighting

wzxhzdk:35

Once this has finished, we can view the output using gadgetplots::gadget_plots().

wzxhzdk:36

Once finished, you can view the output in your web browser:

wzxhzdk:37

Appendix: Full model script

For convenience, here is all the sections of the model script above joined together:

```{js, echo=FALSE} document.write( '

' +
    Array.from(document.querySelectorAll('.modelScript pre')).map((x) => x.innerHTML).join("\n\n") +
    '
');

```r
gadget3:::vignette_test_output("introduction-single-stock", model_code, params.out)


gadget-framework/gadget3 documentation built on June 13, 2025, 5:06 a.m.