Abstract
Task We reanalyse the “estimating the mass of the Milky Way” example from Bilodeau, Stringer, and Tang (2021).
Originally Alex used the ipoptr
package.
However, it is now not possible, or at least very difficult, to install it (as of 01/04/2023).
We will here use the nloptr
package instead.
data(gcdatalist, package = "aghq")
precompile()
## Removing: libTMB.cpp libTMB.o libTMBdbg.cpp libTMBomp.cpp
## Precompilation sources generated
set.seed(4365789)
globalpath <- normalizePath(tempdir(), winslash = "/")
plotpath <- normalizePath(file.path(globalpath, "astro"), winslash = "/", mustWork = FALSE)
if (!dir.exists(plotpath)) dir.create(plotpath)
Get the TMB template from the aghq
package, the compile and load it:
file.copy(normalizePath(system.file("extsrc/01_astro.cpp", package = "aghq"), winslash = "/"), globalpath)
## [1] FALSE
compile(normalizePath(file.path(globalpath, "01_astro.cpp"), winslash = "/"))
## [1] 0
dyn.load(normalizePath(dynlib(file.path(globalpath, "01_astro")), winslash = "/"))
Parameter transformations:
parambounds <- list(
Psi0 = c(1, 200),
gamma = c(0.3, 0.7),
alpha = c(3.0, 3.7),
beta = c(-0.5, 1)
)
get_Psi0 <- function(theta) {
# theta = -log( (Psi0 - 1) / (200 - 1) )
(parambounds$Psi0[2] - parambounds$Psi0[1]) * exp(-exp(theta)) + parambounds$Psi0[1]
}
get_theta1 <- function(Psi0) {
log(-log((Psi0 - parambounds$Psi0[1]) / (parambounds$Psi0[2] - parambounds$Psi0[1])))
}
get_gamma <- function(theta) {
# theta = -log( (gamma - 0.3) / (0.7 - 0.3) )
(parambounds$gamma[2] - parambounds$gamma[1]) * exp(-exp(theta)) + parambounds$gamma[1]
}
get_theta2 <- function(gamma) {
log(-log((gamma - parambounds$gamma[1]) / (parambounds$gamma[2] - parambounds$gamma[1])))
}
get_alpha <- function(theta) {
# theta = log(alpha - 3)
exp(theta) + parambounds$alpha[1]
}
get_theta3 <- function(alpha) {
log(alpha - parambounds$alpha[1])
}
get_beta <- function(theta) {
# theta = -log( (beta - (-0.5)) / (1 - (-0.5)) )
(parambounds$beta[2] - parambounds$beta[1]) * exp(-exp(theta)) + parambounds$beta[1]
}
get_theta4 <- function(beta) {
log(-log((beta - parambounds$beta[1]) / (parambounds$beta[2] - parambounds$beta[1])))
}
Optimisation using IPOPT replaced by optimisation using nloptr
:
nloptr_objective <- function(theta) ff$fn(theta)
nloptr_objective_gradient <- function(theta) ff$gr(theta)
We use box constraints, to improve stability of optimization:
lowerbounds <- c(
get_theta1(parambounds$Psi0[2] - 0.001),
get_theta2(parambounds$gamma[2] - 0.001),
get_theta3(parambounds$alpha[1] + 0.001),
get_theta4(parambounds$beta[2] - 0.001)
)
upperbounds <- c(
get_theta1(parambounds$Psi0[1] + 1),
get_theta2(parambounds$gamma[1] + 0.01),
get_theta3(parambounds$alpha[2] - 0.01),
get_theta4(parambounds$beta[1] + 0.01)
)
Start optimisation at the midpoint:
thetastart <- (upperbounds + lowerbounds) / 2
thetastart_list <- list(theta1 = thetastart[1], theta2 = thetastart[2], theta3 = thetastart[3], theta4 = thetastart[4])
data.frame(
"param" = paste0("theta", 1:4),
lowerbound = lowerbounds,
upperbound = upperbounds,
thetastart = thetastart
) %>%
ggplot(aes(ymin = lowerbound, y = thetastart, ymax = upperbound, x = param)) +
geom_pointrange() +
coord_flip() +
labs(x = "", y = "") +
theme_minimal()
Nonlinear constraints, specified as a function:
nloptr_nonlinear_constraints <- function(theta) -1 * Es$fn(theta)
nloptr_nonlinear_constraints_jacobian <- function(theta) {
J <- -1 * Es$gr(theta)
as.matrix(J)
}
nonlinear_lowerbound <- rep(0, nrow(gcdata) + 2)
nonlinear_upperbound <- rep(Inf, nrow(gcdata) + 2)
Objective function and its derivatives:
ff <- MakeADFun(
data = gcdatalist,
parameters = thetastart_list,
DLL = "01_astro",
ADreport = FALSE,
silent = TRUE
)
Nonlinear constraints and their Jacobian:
Es <- MakeADFun(
data = gcdatalist,
parameters = thetastart_list,
DLL = "01_astro",
ADreport = TRUE,
silent = TRUE
)
Check starting value satisfies constraints.
Note that constraints in nloptr
are of the form \(g(x) \leq 0\).
stopifnot(all(nloptr_nonlinear_constraints(thetastart) <= 0))
tm <- Sys.time()
tmp <- capture.output(
nloptr_result <- nloptr::nloptr(
x0 = thetastart,
eval_f = nloptr_objective,
eval_grad_f = nloptr_objective_gradient,
lb = lowerbounds,
ub = upperbounds,
eval_g_ineq = nloptr_nonlinear_constraints,
eval_jac_g_ineq = nloptr_nonlinear_constraints_jacobian,
opts = list("algorithm" = "NLOPT_LD_MMA", obj_scaling_factor = 1, "xtol_rel" = 1.0e-3)
)
)
optruntime <- difftime(Sys.time(), tm, units = "secs")
cat("Run time for mass model optimisation:", optruntime, "seconds.\n")
## Run time for mass model optimisation: 0.09713817 seconds.
Check that the optimisation results look right (based on Figure 2 of Bilodeau, Stringer, and Tang (2021)).
get_Psi0(nloptr_result$solution[1])
## [1] 32.43971
get_gamma(nloptr_result$solution[2])
## [1] 0.3164915
get_alpha(nloptr_result$solution[3])
## [1] 3.00865
get_beta(nloptr_result$solution[4])
## [1] 0.03624215
Create the template for AGHQ:
ffa <- list(
fn = function(x) -1 * ff$fn(x),
gr = function(x) -1 * ff$gr(x),
he = function(x) -1 * ff$he(x)
)
useropt <- list(
ff = ffa,
mode = nloptr_result$solution,
hessian = ff$he(nloptr_result$solution)
)
#' Fit it with "reuse" first, then correct marginals later
cntrl <- aghq::default_control(negate = TRUE)
#' Do the quadrature
tm <- Sys.time()
k_dense <- 5
astroquad <- aghq::aghq(ff, k = k_dense, thetastart, optresults = useropt, control = cntrl)
#' Correct the marginals except for beta, which gave NA due to the constraints
for (j in 1:3) astroquad$marginals[[j]] <- marginal_posterior(astroquad, j, method = "correct")
quadruntime <- difftime(Sys.time(), tm, units = "secs")
cat("Run time for mass model quadrature:", quadruntime ,"seconds.\n")
## Run time for mass model quadrature: 0.2340021 seconds.
#' Total runtime for AGHQ
optruntime + quadruntime
## Time difference of 0.3311403 secs
tm <- Sys.time()
k_eb <- 1
astroebquad <- aghq::aghq(ff, k = k_eb, thetastart, optresults = useropt, control = cntrl)
#' Correct the marginals except for beta, which gave NA due to the constraints
for (j in 1:3) astroebquad$marginals[[j]] <- marginal_posterior(astroebquad, j, method = "correct")
quadebruntime <- difftime(Sys.time(), tm, units = "secs")
cat("Run time for mass model quadrature:", quadebruntime ,"seconds.\n")
## Run time for mass model quadrature: 0.05084991 seconds.
#' Total runtime for EB
optruntime + quadebruntime
## Time difference of 0.1479881 secs
C <- Matrix::forceSymmetric(solve(useropt$hessian))
eigenC <- eigen(C)
lambda <- eigenC$values
data.frame(
n = 1:length(lambda),
tv = cumsum(lambda / sum(lambda))
) %>% ggplot(aes(x = n, y = tv)) +
geom_point() +
geom_hline(yintercept = 0.9, col = "grey", linetype = "dashed") +
annotate("text", x = 3, y = 0.89, label = "90% of total variation explained", col = "grey") +
scale_y_continuous(labels = scales::percent) +
labs(x = "PCA dimensions included", y = "Total variation explained") +
theme_minimal()
k_pca <- 5
s <- 1 #' Chosen based on the Scree plot
m <- length(useropt$mode)
levels <- c(rep(k_pca, s), rep(1, m - s))
pca_grid <- mvQuad::createNIGrid(dim = m, type = "GHe", level = levels)
mvQuad::rescale(pca_grid, m = useropt$mode, C = C, dec.type = 1)
#' Check that the PCA grid has the correct number of nodes
stopifnot(nrow(mvQuad::getNodes(pca_grid)) == k_pca^s)
tm <- Sys.time()
astropcaquad <- local_aghq(ff, k_pca, thetastart, optresults = useropt, basegrid = pca_grid, adapt = FALSE, control = cntrl)
#' Correct the marginals except for beta, which gave NA due to the constraints
for (j in 1:3) astropcaquad$marginals[[j]] <- marginal_posterior(astropcaquad, j, method = "correct")
pcaquadruntime <- difftime(Sys.time(), tm, units = "secs")
cat("Run time for mass model quadrature:", pcaquadruntime ,"seconds.\n")
## Run time for mass model quadrature: 0.0270288 seconds.
#' Total runtime for PCA-AGHQ
optruntime + pcaquadruntime
## Time difference of 0.124167 secs
stanmod <- tmbstan(
ff,
chains = 4,
cores = 4,
iter = 1e04,
warmup = 1e03,
init = nloptr_result$solution,
seed = 48645,
algorithm = "NUTS"
)
# Time
get_elapsed_time(stanmod)
## warmup sample
## chain:1 0.705935 4.01523
## chain:2 0.619971 3.93486
## chain:3 0.620559 4.11472
## chain:4 0.614505 3.79424
standata <- as.data.frame(stanmod)
standata$Psi0 <- get_Psi0(standata[ ,1])
standata$gamma <- get_gamma(standata[ ,2])
standata$alpha <- get_alpha(standata[ ,3])
standata$beta <- get_beta(standata[ ,4])
#' Manual plots
parambounds <- list(
Psi0 = c(1, 200),
gamma = c(0.3, 0.7),
alpha = c(3.0, 3.7),
beta = c(-0.5, 1)
)
#' Add small buffers for stability
get_theta2_robust <- function(gamma) {
log(-log((gamma - parambounds$gamma[1] + 1e-03) / (parambounds$gamma[2] - parambounds$gamma[1] + 1e-03)))
}
get_theta3_robust <- function(alpha) log(alpha - parambounds$alpha[1] + 1e-03)
#' Transformed PDFs
translist1 <- make_transformation(totheta = get_theta1, fromtheta = get_Psi0)
translist2 <- make_transformation(totheta = get_theta2_robust, fromtheta = get_gamma)
translist3 <- make_transformation(totheta = get_theta3_robust, fromtheta = get_alpha)
translist4 <- make_transformation(totheta = get_theta4, fromtheta = get_beta)
Psi0pdf <- compute_pdf_and_cdf(astroquad$marginals[[1]], translist1)
gammapdf <- compute_pdf_and_cdf(astroquad$marginals[[2]], translist2)
alphapdf <- compute_pdf_and_cdf(astroquad$marginals[[3]], translist3, interpolation = "polynomial")
betapdf <- compute_pdf_and_cdf(astroquad$marginals[[4]], translist4)
Psi0ebpdf <- compute_pdf_and_cdf(astroebquad$marginals[[1]], translist1)
gammaebpdf <- compute_pdf_and_cdf(astroebquad$marginals[[2]], translist2)
alphaebpdf <- compute_pdf_and_cdf(astroebquad$marginals[[3]], translist3, interpolation = "polynomial")
betaebpdf <- compute_pdf_and_cdf(astroebquad$marginals[[4]], translist4)
Psi0pcapdf <- compute_pdf_and_cdf(astropcaquad$marginals[[1]], translist1)
gammapcapdf <- compute_pdf_and_cdf(astropcaquad$marginals[[2]], translist2)
alphapcapdf <- compute_pdf_and_cdf(astropcaquad$marginals[[3]], translist3, interpolation = "polynomial")
betapcapdf <- compute_pdf_and_cdf(astropcaquad$marginals[[4]], translist4)
Psi0prior <- function(Psi0) dunif(Psi0, parambounds$Psi0[1], parambounds$Psi0[2], log = FALSE)
gammaprior <- function(gamma) dunif(gamma, parambounds$gamma[1], parambounds$gamma[2], log = FALSE)
alphaprior <- function(alpha) dgamma(alpha - parambounds$alpha[1], shape = 1, rate = 4.6, log = FALSE)
betaprior <- function(beta) dunif(beta, parambounds$beta[1], parambounds$beta[2], log = FALSE)
(Psi0_postplot <- Psi0pdf %>%
ggplot(aes(x = transparam, y = pdf_transparam)) +
geom_histogram(
data = standata,
mapping = aes(x = Psi0, y = after_stat(density)),
bins = 50,
colour = "white",
fill = "grey"
) +
geom_line() +
geom_line(
data = Psi0pcapdf,
inherit.aes = FALSE,
mapping = aes(x = transparam, y = pdf_transparam),
colour = "blue",
) +
stat_function(fun = Psi0prior, linetype = "dashed") +
labs(title = "", x = "", y = "Density") +
scale_x_continuous(breaks = seq(10, 60, by = 5)) +
coord_cartesian(xlim = c(24, 43)) +
theme_minimal())
(gamma_postplot <- gammapdf %>%
ggplot(aes(x = transparam, y = pdf_transparam)) +
geom_histogram(
data = standata,
mapping = aes(x = gamma, y = after_stat(density)),
bins = 100,
colour = "white",
fill = "grey"
) +
geom_line() +
geom_line(
data = gammapcapdf,
mapping = aes(x = transparam, y = pdf_transparam),
colour = "blue",
) +
stat_function(fun = gammaprior, linetype = "dashed") +
labs(title = "", x = "", y = "Density") +
scale_x_continuous(breaks = seq(0, 0.44, by = 0.02)) +
coord_cartesian(xlim = c(0.3, 0.4)) +
theme_minimal())
(alpha_postplot <- alphapdf %>%
ggplot(aes(x = transparam, y = pdf_transparam)) +
geom_histogram(
data = standata,
mapping = aes(x = alpha, y = after_stat(density)),
bins = 2000,
colour = "white",
fill = "grey"
) +
geom_line() +
geom_line(
data = alphapcapdf,
mapping = aes(x = transparam, y = pdf_transparam),
colour = "blue",
) +
stat_function(fun = alphaprior, linetype = "dashed") +
labs(title = "",x = "",y = "Density") +
scale_x_continuous(breaks = seq(3, 4, by = 0.02)) +
coord_cartesian(xlim = c(3, 3.05)) +
theme_minimal())
(beta_postplot <- betapdf %>%
ggplot(aes(x = transparam, y = pdf_transparam)) +
geom_histogram(
data = standata,
mapping = aes(x = beta, y = after_stat(density)),
bins = 50,
colour = "white",
fill = "grey"
) +
geom_line() +
geom_line(
data = betapcapdf,
mapping = aes(x = transparam, y = pdf_transparam),
colour = "blue",
) +
stat_function(fun = betaprior, linetype = "dashed") +
labs(title = "",x = "",y = "Density") +
scale_x_continuous(breaks = seq(-0.5, 1, by = 0.1)) +
coord_cartesian(xlim = c(-0.3, 0.4)) +
theme_minimal())
data.frame(
method = c("AGHQ", "PCA-AGHQ", "EB"),
lognormconst = c(astroquad$normalized_posterior$lognormconst, astropcaquad$normalized_posterior$lognormconst, astroebquad$normalized_posterior$lognormconst)
) %>%
gt::gt()
method | lognormconst |
---|---|
AGHQ | -706.2578 |
PCA-AGHQ | -706.2804 |
EB | -706.3330 |
kstable <- function(quad) {
aghqpostsamps_spline <- sample_marginal(quad, nrow(standata), interpolation = "spline")
aghqpostsamps_poly <- sample_marginal(quad, nrow(standata), interpolation = "polynomial")
suppressWarnings({
kstable <- data.frame(
Psi0 = ks.test(standata[[1]], aghqpostsamps_poly[[1]])$statistic,
gamma = ks.test(standata[[2]], aghqpostsamps_poly[[2]])$statistic,
alpha = ks.test(standata[[3]], aghqpostsamps_poly[[3]])$statistic,
beta = ks.test(standata[[4]], aghqpostsamps_poly[[4]])$statistic
)
})
return(kstable)
}
kstable_astroquad <- kstable(astroquad) %>%
pivot_longer(cols = everything()) %>%
mutate(method = "AGHQ", k = k_dense, s = NA, total_points = k^4)
#' Not working (yet?)
# kstable_astropcaquad <- kstable(astropcaquad) %>%
# pivot_longer(cols = everything()) %>%
# mutate(method = "AGHQ-PCA", k = k_pca, s = s, total_points = k^s)
bind_rows(kstable_astroquad) %>%
gt::gt()
name | value | method | k | s | total_points |
---|---|---|---|---|---|
Psi0 | 0.008277778 | AGHQ | 5 | NA | 625 |
gamma | 0.010861111 | AGHQ | 5 | NA | 625 |
alpha | 0.037861111 | AGHQ | 5 | NA | 625 |
beta | 0.007166667 | AGHQ | 5 | NA | 625 |
bind_rows(kstable_astroquad) %>%
ggplot(aes(x = name, y = value, col = method)) +
geom_jitter(height = 0, width = 0.05) +
scale_y_continuous(limits = c(0, 0.05)) +
scale_colour_manual(values = multi.utils::cbpalette()) +
labs(x = "Parameter", y = "KS(method, NUTS)", col = "Method") +
theme_minimal()
sessionInfo()
## R version 4.2.0 (2022-04-22)
## Platform: x86_64-apple-darwin17.0 (64-bit)
## Running under: macOS 13.3.1
##
## Matrix products: default
## LAPACK: /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRlapack.dylib
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] aghq_0.4.1 nloptr_2.0.3 naomi_2.8.5 sf_1.0-9 tmbstan_1.0.4 rstan_2.21.5 StanHeaders_2.21.0-7 TMB_1.9.2 Matrix_1.5-4.1
## [10] rmarkdown_2.18 multi.utils_0.1.0 patchwork_1.1.2 forcats_0.5.2 stringr_1.5.0 dplyr_1.0.10 purrr_1.0.1 readr_2.1.3 tidyr_1.3.0
## [19] tibble_3.2.1 ggplot2_3.4.0 tidyverse_1.3.1
##
## loaded via a namespace (and not attached):
## [1] colorspace_2.0-3 ellipsis_0.3.2 class_7.3-20 rprojroot_2.0.3 fs_1.6.1 rstudioapi_0.14 proxy_0.4-27 farver_2.1.1 bit64_4.0.5 fansi_1.0.4
## [11] lubridate_1.8.0 xml2_1.3.3 splines_4.2.0 codetools_0.2-18 cachem_1.0.6 knitr_1.41 polynom_1.4-1 jsonlite_1.8.4 gt_0.8.0 broom_0.8.0
## [21] dbplyr_2.1.1 compiler_4.2.0 httr_1.4.4 backports_1.4.1 assertthat_0.2.1 fastmap_1.1.0 cli_3.6.1 htmltools_0.5.3 prettyunits_1.1.1 tools_4.2.0
## [31] gtable_0.3.1 glue_1.6.2 tinytex_0.39.1 V8_4.2.2 Rcpp_1.0.10 jquerylib_0.1.4 cellranger_1.1.0 vctrs_0.6.1 mvQuad_1.0-6 xfun_0.37
## [41] ps_1.7.3 rvest_1.0.2 lifecycle_1.0.3 statmod_1.4.36 orderly_1.4.3 ids_1.0.1 scales_1.2.1 vroom_1.6.0 hms_1.1.2 parallel_4.2.0
## [51] inline_0.3.19 rticles_0.23.6 yaml_2.3.7 curl_5.0.0 memoise_2.0.1 gridExtra_2.3 sass_0.4.4 loo_2.5.1 stringi_1.7.8 RSQLite_2.2.14
## [61] highr_0.9 e1071_1.7-12 pkgbuild_1.3.1 rlang_1.1.0 pkgconfig_2.0.3 matrixStats_0.62.0 evaluate_0.20 lattice_0.20-45 labeling_0.4.2 bit_4.0.5
## [71] tidyselect_1.2.0 processx_3.8.0 traduire_0.0.6 bookdown_0.26 magrittr_2.0.3 R6_2.5.1 generics_0.1.3 DBI_1.1.3 pillar_1.9.0 haven_2.5.0
## [81] withr_2.5.0 units_0.8-0 modelr_0.1.8 crayon_1.5.2 uuid_1.1-0 KernSmooth_2.23-20 utf8_1.2.3 tzdb_0.3.0 grid_4.2.0 readxl_1.4.0
## [91] isoband_0.2.6 data.table_1.14.6 blob_1.2.3 callr_3.7.3 reprex_2.0.1 digest_0.6.31 classInt_0.4-8 numDeriv_2016.8-1.1 openssl_2.0.5 RcppParallel_5.1.5
## [101] stats4_4.2.0 munsell_0.5.0 bslib_0.4.1 askpass_1.1