::install_github("r4ss/r4ss")
remoteslibrary(r4ss)
Notes on Richards growth in Stock Synthesis
Introduction
The equations used in the Stock Synthesis implementation of the Richards curve, as found in the ADMB source code, are different from the equations in the Stock Synthesis User Manual (Methot et al. 2024). This vignette (1) shows that the equations are equivalent and (2) demonstrates the various shapes of the Richards curve using different values for the \(k\) and \(b\) parameters.
Methods
Install and load the r4ss
package, so we can import Stock Synthesis model output.
Parameter definitions
The Richards growth model has four parameters:
\(L_1\): length at age \(A_1\)
\(L_2\): length at age \(A_2\)
\(k\): growth coefficient
\(b\): shape parameter
and three input variables:
\(t\): age
\(A_1\): young age selected by user, for the estimation of \(L_1\)
\(A_2\): old age selected by user, for the estimation of \(L_2\)
SS3 Code
The following R function replicates the Richards growth implementation from the Stock Synthesis ADMB source code.
<- function(t, L1, L2, k, b, A1, A2)
ss3_code
{<- L1^b
LminR <- L2^b
LmaxR <- LminR + (LmaxR - LminR) / (1 - exp(-k*(A2-A1)))
LinfR <- LinfR + (LminR - LinfR) * exp(-k*(t-A1))
temp ^(1/b)
temp }
Schnute
The Schnute parametrization of the Richards curve is from Schnute (1981) (Eq. 15, case 1).
<- function(t, L1, L2, k, b, A1, A2)
schnute
{^b + (L2^b-L1^b) * (1-exp(-k*(t-A1))) / (1-exp(-k*(A2-A1))))^(1/b)
(L1 }
von Bertalanffy
The reparametrized von Bertalanffy function is from Schnute and Fournier (1980) (Eq. 7).
<- function(t, L1, L2, k, A1, A2)
von_bertalanffy
{+ (L2-L1) * (1-exp(-k*(t-A1))) / (1-exp(-k*(A2-A1)))
L1 }
Gompertz
Finally, a standard Gompertz function is found in Schnute (1981) (Eq. 2).
<- function(t, Linf, tau)
gompertz
{* exp(-exp(-k*(t-tau)))
Linf }
Results
Stock Synthesis model output
Read estimated growth parameters from a fitted Stock Synthesis model.
# dir = the file path to the directory above where the example files are downloaded
<- r4ss::SS_output(file.path(dir, "simple_small_richards"))
model <- subset(model$Growth_Parameters, Sex==1, A1, drop=TRUE)
A1 <- subset(model$Growth_Parameters, Sex==1, A2, drop=TRUE)
A2 <- subset(model$Growth_Parameters, Sex==1, L_a_A1, drop=TRUE)
L1 <- subset(model$Growth_Parameters, Sex==1, L_a_A2, drop=TRUE)
L2 <- subset(model$Growth_Parameters, Sex==1, K, drop=TRUE)
k <- model$parameters["Richards_Fem_GP_1", "Value"]
b <- subset(model$Growth_Parameters, Sex==1, Linf, drop=TRUE) Linf
Compare three growth curves: length-at-age reported in the Stock Synthesis output, predicted lengths using the ss3_code()
function, and predicted lengths using the schnute()
function.
<- 0:20
t plot(Len_Beg~Age_Beg, model$endgrowth, subset=Sex==1, ylim=c(0,80),
xlab="age", ylab="length", type="l", lwd=1.5, col=2)
points(t, ss3_code(t, L1, L2, k, b, A1, A2))
points(t, schnute(t, L1, L2, k, b, A1, A2), pch=3)
legend("bottomright", c("ss_output", "ss3_code", "schnute"),
pch=c(NA, 1, 3), lty=c(1, NA, NA), lwd=c(1.5, NA, NA),
col=c(2, 1, 1), bty="n", inset=0.02, y.intersp=1.5)
The predicted length from the ss3_code()
and schnute()
functions are in perfect agreement with each other. They are the same predicted lengths as reported in the Stock Synthesis output, except for the oldest age which is treated as a plus group inside the model, containing age 20 and older.
<- data.frame(
comparison age = t,
ss3_output = subset(model$endgrowth, Sex==1, Len_Beg, drop=TRUE),
ss3_code = ss3_code(t, L1, L2, k, b, A1, A2),
schnute = schnute(t, L1, L2, k, b, A1, A2)
)print(comparison, row.names=FALSE)
age ss3_output ss3_code schnute
0 22.7690 22.76900 22.76900
1 39.3016 39.30161 39.30161
2 46.8948 46.89477 46.89477
3 51.9159 51.91588 51.91588
4 55.5968 55.59675 55.59675
5 58.4357 58.43566 58.43566
6 60.6924 60.69238 60.69238
7 62.5224 62.52241 62.52241
8 64.0274 64.02738 64.02738
9 65.2779 65.27789 65.27789
10 66.3251 66.32514 66.32514
11 67.2075 67.20754 67.20754
12 67.9547 67.95467 67.95467
13 68.5897 68.58973 68.58973
14 69.1313 69.13126 69.13126
15 69.5942 69.59425 69.59425
16 69.9909 69.99095 69.99095
17 70.3315 70.33147 70.33147
18 70.6242 70.62422 70.62422
19 70.8762 70.87622 70.87622
20 71.3850 71.09338 71.09338
Special cases
Special cases of the Richards curve include the von Bertalanffy (\(b\!=\!1\)) and Gompertz (\(b\!\to\!0\)) models. In the plot below, the lines are drawn using the actual von Bertalanffy and Gompertz functions, and the circles are drawn using the Richards curve with the appropriate values for the \(b\) parameter.
plot(NA, xlim=range(t), ylim=c(0,80), xlab="age", ylab="length")
lines(t, von_bertalanffy(t, L1, L2, k, A1, A2), ylim=c(0,80), lwd=2, col=2)
points(t, schnute(t, L1, L2, k, 1, A1, A2), col=2)
lines(t, gompertz(t, Linf, k), lwd=2, col=4)
<- gompertz(A1, Linf, k)
Lg1 <- gompertz(A2, Linf, k)
Lg2 points(t, schnute(t, Lg1, Lg2, k, 0.0001, A1, A2), col=4)
legend("topleft", c("von Bertalanffy (b=1)","Gompertz (b near 0)"),
lwd=2, col=c(2,4), bty="n", inset=0.02, y.intersp=1.5)
Richards growth curves with different \(k\) and \(b\) combinations
<- seq(0, 20, by=0.1)
x <- 5
L1 <- 80
L2 <- 0
A1 <- 20
A2
plot(NA, xlim=range(x), ylim=c(0, 80), xlab="age", ylab="length")
<- rich.colors.short(6)
z
lines(x, schnute(x, L1, L2, k=0.8, b=-3, A1, A2), lwd=2, col=z[1])
lines(x, schnute(x, L1, L2, k=0.8, b=-2, A1, A2), lwd=2, col=z[1])
lines(x, schnute(x, L1, L2, k=0.8, b=-1, A1, A2), lwd=2, col=z[1])
lines(x, schnute(x, L1, L2, k=0.4, b=-1, A1, A2), lwd=2, col=z[1])
lines(x, schnute(x, L1, L2, k=0.4, b=0.0001, A1, A2), lwd=2, col=z[2])
lines(x, schnute(x, L1, L2, k=0.2, b=0.0001, A1, A2), lwd=2, col=z[2])
lines(x, schnute(x, L1, L2, k=0.4, b=1, A1, A2), lwd=2, col=z[3])
lines(x, schnute(x, L1, L2, k=0.2, b=1, A1, A2), lwd=2, col=z[3])
lines(x, schnute(x, L1, L2, k=0.1, b=1, A1, A2), lwd=2, col=z[3])
lines(x, schnute(x, L1, L2, k=0.2, b=2, A1, A2), lwd=2, col=z[4])
lines(x, schnute(x, L1, L2, k=0.1, b=2, A1, A2), lwd=2, col=z[4])
lines(x, schnute(x, L1, L2, k=0.0001, b=2, A1, A2), lwd=2, col=z[4])
lines(x, schnute(x, L1, L2, k=0.2, b=3, A1, A2), lwd=2, col=z[5])
lines(x, schnute(x, L1, L2, k=0.1, b=3, A1, A2), lwd=2, col=z[5])
lines(x, schnute(x, L1, L2, k=0.0001, b=3, A1, A2), lwd=2, col=z[5])
lines(x, schnute(x, L1, L2, k=0.1, b=4, A1, A2), lwd=2, col=z[6])
lines(x, schnute(x, L1, L2, k=0.0001, b=4, A1, A2), lwd=2, col=z[6])
lines(x, schnute(x, L1, L2, k=0.0001, b=5, A1, A2), lwd=2, col=z[6])
lines(x, schnute(x, L1, L2, k=0.0001, b=6, A1, A2), lwd=2, col=z[6])
legend("bottomright", c("b: 4 to 6", "b: 3", "b: 2", "b: 1", "b: 0.001",
"b: -1 to -3"), lwd=2.5, col=rev(z), bty="n", inset=0.02, y.intersp=1.2)
Discussion
Range of permissible \(b\) values
The Schnute parametrization of the Richards curve used in Stock Synthesis can produce a wide variety of shapes, based on the value of the \(b\) parameter, as demonstrated in Section 3.3. When \(A_1\) is greater than the youngest age in the model, some combinations of Richards growth parameters can lead to undefined (NaN
) predicted length for the younger ages. The choice of \(A_1\) and \(A_2\) will affect the possible growth curve shapes.
The one value of \(b\) that is not allowed when using the Richards growth curve in Stock Synthesis is \(b\!=\!0\). This also holds for the R functions ss3_code()
and schnute()
used in this vignette. When estimating \(b\) as a floating-point number, there is effectively no risk of the parameter becoming precisely zero during estimation, as long as the initial value is non-zero. To use a Gompertz growth curve, the \(b\) parameter can be fixed at a small value such as 0.0001, as demonstrated in Section 3.2.
Proof of ss3_code()
being equivalent to schnute()
The above demonstrations comparing the Stock Synthesis output, ss3_code()
and schnute()
indicate that they are mathematically equivalent, meaning that the same parameter input produces the same growth curve. The equivalence can also be proven algebraically.
In Stock Synthesis, the Richards growth curve is implemented as
<- function(t, L1, L2, k, b, A1, A2)
ss3_code
{<- L1^b
LminR <- L2^b
LmaxR <- LminR + (LmaxR - LminR) / (1 - exp(-k*(A2-A1)))
LinfR <- LinfR + (LminR - LinfR) * exp(-k*(t-A1))
temp ^(1/b)
temp }
\[ L_t ~=~ \Big[\,L_{\infty R} \;+\; (L_1^b-L_{\infty R})\, e^{-k(t-A_1)}\,\Big]^{1/b} \]
where:
\[L_{\infty R} ~=~ L_1^b \;+\; \frac{L_2^b\,-\,L_1^b}{1-e^{-k(A_2-A_1)}}\]
We proceed by replacing \(L_{\infty R}\) with its definition and then gradually simplify the equation:
\[\begin{eqnarray*} L_t &=& \bigg[\,L_1^b \;+\; \frac{L_2^b-L_1^b}{1-e^{-k(A_2-A_1)}} \;+\; \left(L_1^b\,-\,\Big[L_1^b \,+\, \frac{L_2^b-L_1^b}{1-e^{-k(A_2-A_1)}}\Big]\right) e^{-k(t-A_1)}\,\bigg]^{1/b}\\[4ex] L_t &=& \bigg[\,L_1^b \;+\; \frac{L_2^b-L_1^b}{1-e^{-k(A_2-A_1)}} \;+\; \left(L_1^b\,-\, L_1^b \,-\, \frac{L_2^b-L_1^b}{1-e^{-k(A_2-A_1)}} \right) e^{-k(t-A_1)}\,\bigg]^{1/b}\\[4ex] L_t &=& \bigg[\,L_1^b \;+\; \frac{L_2^b-L_1^b}{1-e^{-k(A_2-A_1)}} \;-\; \frac{L_2^b-L_1^b}{1-e^{-k(A_2-A_1)}}\, e^{-k(t-A_1)}\,\bigg]^{1/b}\\[4ex] L_t &=& \bigg[\,L_1^b \;+\; (L_2^b-L_1^b)\,\frac{1}{1-e^{-k(A_2-A_1)}} \;-\; (L_2^b-L_1^b)\,\frac{e^{-k(t-A_1)}}{1-e^{-k(A_2-A_1)}}\,\bigg]^{1/b}\\[4ex] L_t &=& \left[\:L_1^b\;+\;(L_2^b-L_1^b)\, \frac{1-e^{-k(t-A_1)}}{1-e^{-k(A_2-A_1)}}\,\right]^{1/b} \end{eqnarray*}\]
<- function(t, L1, L2, k, b, A1, A2)
schnute
{^b + (L2^b-L1^b) * (1-exp(-k*(t-A1))) / (1-exp(-k*(A2-A1))))^(1/b)
(L1 }
FSA package
The FSA package on CRAN (Ogle et al. 2023) is a useful reference to navigate through the plethora of alternative parametrizations of different growth curves. Among the FSA function calls that are relevant to this vignette are:
library(FSA)
Schnute(t, case=1, ...)
Schnute(t, case=2, ...)
Schnute case 1 (Schnute 1981, Eq. 15) is the same as schnute()
used in this vignette.
Schnute case 2 (Schnute 1981, Eq. 16) is a Gompertz growth model parametrized with \(L_1\), \(L_2\), and \(k\), similar to ss3_code()
and schnute()
when \(b\) is fixed at nearly 0.