Rceattle C++ template
The CEATTLE likelihood: src/TMB/ceattle.cpp and its headers
Loading...
Searching...
No Matches
helper_functions.hpp
Go to the documentation of this file.
1
2/**
3 * @file helper_functions.hpp
4 * @brief Small differentiable (AD-safe) utilities used across the CEATTLE
5 * template: numeric guards and transforms, the Dirichlet-multinomial density,
6 * and the separable year x age x cohort GMRF precision matrix (construct_Q).
7 */
8
9// Max of an IVECTOR, returned as an int.
10template <class Type>
11Type imax(const vector<Type> &x)
12{
13 int res = x[0];
14 for(int i=0; i < x.size(); i++){
15 int res2 = x[i];
16 if(res < res2){
17 res = res2;
18 }
19 }
20 return res;
21}
22
23// Smooth positive constraint: returns x where x >= eps, otherwise a value in
24// (0, eps), and adds a quadratic penalty for the shortfall.
25template<class Type>
26Type posfun(Type x, Type eps, Type &penalty) {
27 Type denom = 2;
28 denom -= x/eps;
29 Type ans = CppAD::CondExpGe(x, eps, x, eps/denom);
30 penalty += CppAD::CondExpGe(x, eps, Type(0), 0.01 * (x - eps) * (x - eps));
31 return ans;
32}
33
34
35// Dirichlet multinomial (from WHAM)
36template<class Type>
37Type ddirmultinom(vector<Type> obs, vector<Type> alpha, int do_log)
38{
39 int dim = obs.size();
40 Type N = obs.sum();
41 Type phi=sum(alpha);
42 Type ll = lgamma(N + 1.0) + lgamma(phi) - lgamma(N + phi);
43 for(int a = 0; a < dim; a++) ll += -lgamma(obs(a) + 1.0) + lgamma(obs(a) + alpha(a)) - lgamma(alpha(a));
44 if(do_log == 1) return ll;
45 else return exp(ll);
46}
47
48// From TMB examples, modified name, constrains between -1 and 1
49template <class Type>
50Type rho_trans(Type x){
51 return Type(2)/(Type(1) + exp(-Type(2) * x)) - Type(1);
52}
53
54
55template<class Type>
56bool isNA(Type x){
57 return R_IsNA(asDouble(x));
58}
59
60
61template<class Type>
62bool isFinite(Type x){
63 return R_finite(asDouble(x));
64}
65
66
67template <class Type> Type square(Type x){return x*x;}
68
69
70/**
71 * @brief Concentrated (analytical) sd of a lognormal observation likelihood.
72 *
73 * Catch and index observations are fit as `log(obs) ~ N(log(pred) - b * sigma^2 / 2,
74 * sigma)`, with `b` the bias-adjustment flag `bias_adjust_obs` (1 = mean-unbiased
75 * predictions, Rceattle's default; 0 = median-unbiased). Minimising that density over
76 * sigma with `S` = mean squared log residual gives
77 *
78 * sigma^2 = 2 * S / (sqrt(1 + b^2 * S) + 1)
79 *
80 * the Walters and Ludwig (1994) estimator `sigma = sqrt(S)` at `b = 0`, shrunk as the
81 * bias term takes up part of the residual. It is the rationalised form of `2 (sqrt(1 +
82 * b^2 S) - 1) / b^2`, which needs no branch at `b = 0` and stays accurate for small `b`,
83 * where that expression cancels to zero.
84 *
85 * @param mean_sq_resid S, the mean of squared `log(obs) - log(pred)`.
86 * @param bias_adjust b, the bias-adjustment flag applied by the density.
87 * @return The sd that minimises the lognormal negative log-likelihood.
88 */
89template <class Type>
90Type concentrated_lognormal_sd(Type mean_sq_resid, Type bias_adjust)
91{
92 return sqrt( Type(2.0) * mean_sq_resid /
93 (sqrt(Type(1.0) + square(bias_adjust) * mean_sq_resid) + Type(1.0)) );
94}
95
96
97template <class Type>
98vector<Type> first_difference(const vector<Type> &x)
99{
100 int length = x.size() - 1;
101
102 if(length <= 0){
103 std::cerr << "Error -- vector size too small in first_difference()" << std::endl;
104 return vector<Type>(0); // no first difference exists below two elements
105 }
106
107 vector<Type> tmp(length);
108
109 for(int i = 0; i < length; i++){
110 tmp(i) = x(i+1) - x(i);
111 }
112
113 return tmp;
114}
115
116
117// Differentiable max of two values.
118// https://groups.google.com/g/tmb-users/c/QAVmgj66OC0
119template <class Type>
120Type max2(Type x, Type y){
121 Type ans = 0.5*(CppAD::abs(x-y)+x+y);
122 return ans;
123}
124
125
126/**
127 * @brief log(mean(exp(x))) over a non-empty vector, computed so it cannot overflow.
128 *
129 * The non-parametric selectivity forms rescale their log-scale coefficients against
130 * log(mean(exp(coefficients))) once a year, holding a curve whose level is otherwise
131 * unidentified at mean selectivity 1. Summed directly, exp() overflows to infinity once a
132 * coefficient passes about 709, the largest argument it takes in double precision, and
133 * the objective is NaN thereafter. Subtracting the largest coefficient first holds every
134 * exponent at or below 0; the shift cancels out of both the result and its derivative.
135 *
136 * @param x non-empty vector of log-scale values.
137 * @return log of the mean of exp(x).
138 */
139template <class Type>
140Type log_mean_exp(const vector<Type> &x)
141{
142 Type mx = x(0);
143 for(int i = 1; i < x.size(); i++) mx = max2(mx, x(i));
144
145 Type total = 0;
146 for(int i = 0; i < x.size(); i++) total += exp(x(i) - mx);
147
148 return mx + log(total / Type(x.size()));
149}
150
151
152/**
153 * @brief Construct a sparse GMRF precision matrix separable along year, age, and cohort.
154 *
155 * Builds Q = (I - B^T) * Omega * (I - B), where B carries the partial correlation to each
156 * neighbouring node and Omega is the diagonal precision (1 / variance). `Var_Param`
157 * selects the conditional (0) or marginal (1) variance parameterization of the GMRF.
158 *
159 * The sole caller reads these names the other way round: ceattle.cpp's 3D-AR1 selectivity
160 * passes its (bin, year, cohort) correlations in and fills `ay_Index` as (bin, year), so
161 * the two inversions cancel. Read the note at that call before renaming anything here.
162 *
163 * @param n_years Number of years.
164 * @param n_ages Number of ages.
165 * @param ay_Index [total_n x 2] age/year index locating each node.
166 * @param rho_y Partial correlation to the node one back in `ay_Index` column 0.
167 * @param rho_a Partial correlation to the node one back in column 1.
168 * @param rho_c Partial correlation to the node one back in both -- the diagonal,
169 * which is the cohort when the columns are filled (age, year).
170 * @param log_sigma2 Log variance parameter governing the GMRF.
171 * @param Var_Param 0 = conditional variance, 1 = marginal variance.
172 * @return Sparse precision matrix [total_n x total_n], total_n = n_years * n_ages.
173 */
174template<class Type>
175Eigen::SparseMatrix<Type> construct_Q(int n_years,
176 int n_ages,
177 matrix<Type> ay_Index,
178 Type rho_y,
179 Type rho_a,
180 Type rho_c,
181 Type log_sigma2,
182 int Var_Param
183) {
184
185 int total_n = n_years * n_ages;
186
187 Eigen::SparseMatrix<Type> B(total_n,total_n);
188 Eigen::SparseMatrix<Type> I(total_n,total_n);
189 I.setIdentity();
190 Eigen::SparseMatrix<Type> Omega(total_n,total_n);
191 Eigen::SparseMatrix<Type> Q_sparse(total_n, total_n);
192
193 for(int n = 0; n < total_n; n++) {
194
195 Type age = ay_Index(n,0);
196 Type year = ay_Index(n,1);
197
198 // B holds the partial correlation from node n to each of its neighbours.
199 if(age > 1) {
200
201 // One step back in ay_Index column 0, same column 1.
202 for(int n1 = 0; n1 < total_n; n1++) {
203 if(ay_Index(n1, 0) == age - 1 && ay_Index(n1, 1) == year)
204 B.coeffRef(n, n1) = rho_y;
205 } // n1 loop
206
207 } // end age > 1
208
209 if(year > 1) {
210
211 // One step back in column 1, same column 0.
212 for(int n1 = 0; n1 < total_n; n1++) {
213 if(ay_Index(n1, 0) == age && ay_Index(n1, 1) == year - 1)
214 B.coeffRef(n, n1) = rho_a;
215 } // n1 loop
216
217 } // if year > 1
218
219 if(year > 1 && age > 1) {
220
221 // One step back in both columns: the diagonal (cohort) neighbour.
222 for(int n1 = 0; n1 < total_n; n1++) {
223 if(ay_Index(n1, 0) == age - 1 && ay_Index(n1, 1) == year - 1)
224 B.coeffRef(n,n1) = rho_c;
225 } // n1 loop
226
227 } // if both year and age > 1
228
229 } // end n loop
230
231 // Omega is diagonal and holds precisions, 1 / variance.
232 if(Var_Param == 0) { // Conditional variance
233
234 for(int i = 0; i < total_n; i++) {
235 for(int j = 0; j < total_n; j++) {
236 if(i == j) Omega.coeffRef(i,j) = 1/exp(log_sigma2);
237 else Omega.coeffRef(i,j) = Type(0.0);
238 } // j loop
239 } // i loop
240
241 } // end if conditional variance
242
243 if(Var_Param == 1) { // Marginal Variance
244
245 matrix<Type> L(total_n, total_n);
246 matrix<Type> tmp_I_B = I-B;
247 L = tmp_I_B.inverse();
248 vector<Type> d(total_n); // conditional variance per node
249
250 for(int n = 0; n < total_n; n++) {
251 if(n == 0) {
252 d(n) = exp(log_sigma2); // marginal variance parameter
253 } else{
254
255 Type cumvar = 0;
256
257 for(int n1 = 0; n1 < n; n1++) {
258 cumvar += L(n, n1) * d(n1) * L(n, n1);
259 } // n1 loop
260
261 // Set d(n) so node n's MARGINAL variance comes out at exp(log_sigma2).
262 d(n) = (exp(log_sigma2) - cumvar) / pow(L(n, n), 2);
263
264 } // else loop
265 } // n loop
266
267 for(int i = 0; i < total_n; i++) {
268 for(int j = 0; j < total_n; j++) {
269 if(i == j) Omega.coeffRef(i,j) = 1/d(i);
270 else Omega.coeffRef(i,j) = Type(0.0);
271 } // j loop
272 } // i loop
273
274 } // end if marginal variance
275
276 Eigen::SparseMatrix<Type> B_transpose = B.transpose();
277
278 Q_sparse = (I - B_transpose) * Omega * (I-B);
279
280 return(Q_sparse);
281
282} // end construct_Q function
283
Type concentrated_lognormal_sd(Type mean_sq_resid, Type bias_adjust)
Concentrated (analytical) sd of a lognormal observation likelihood.
bool isNA(Type x)
Type rho_trans(Type x)
Eigen::SparseMatrix< Type > construct_Q(int n_years, int n_ages, matrix< Type > ay_Index, Type rho_y, Type rho_a, Type rho_c, Type log_sigma2, int Var_Param)
Construct a sparse GMRF precision matrix separable along year, age, and cohort.
bool isFinite(Type x)
Type log_mean_exp(const vector< Type > &x)
log(mean(exp(x))) over a non-empty vector, computed so it cannot overflow.
Type imax(const vector< Type > &x)
Type max2(Type x, Type y)
Type ddirmultinom(vector< Type > obs, vector< Type > alpha, int do_log)
Type square(Type x)
Type posfun(Type x, Type eps, Type &penalty)
vector< Type > first_difference(const vector< Type > &x)