Skip to content

Commit f826a11

Browse files
committed
Merge branch 'master' into fix-925-handle-tuple-and-complex-types
2 parents 270970a + 801b2b4 commit f826a11

3 files changed

Lines changed: 154 additions & 92 deletions

File tree

DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ Authors@R:
2020
person(given = "Jacob", family = "Socolar", role = "ctb"),
2121
person(given = "Martin", family = "Modrák", role = "ctb"),
2222
person(given = "Ven", family = "Popov", role = "ctb"),
23-
person("Visruth", "Srimath Kandali", role = "ctb")
23+
person("Visruth", "Srimath Kandali", role = "ctb"),
24+
person("Aki", "Vehtari", role = "ctb")
2425
)
2526
Description: A lightweight interface to 'Stan' <https://mc-stan.org>.
2627
The 'CmdStanR' interface is an alternative to 'RStan' that calls the command

R/args.R

Lines changed: 105 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ CmdStanArgs <- R6::R6Class(
8181
init <- process_init(init, num_inits, model_variables)
8282
self$init <- init
8383
self$opencl_ids <- opencl_ids
84-
self$num_threads = NULL
84+
self$num_threads <- NULL
8585
self$method_args$validate(num_procs = length(self$proc_ids))
8686
if (is.logical(self$save_cmdstan_config)) {
8787
self$save_cmdstan_config <- as.integer(self$save_cmdstan_config)
@@ -984,20 +984,20 @@ validate_pathfinder_args <- function(self) {
984984
self$num_elbo_draws <- as.integer(self$num_elbo_draws)
985985
}
986986
if (!is.null(self$save_single_paths) && is.logical(self$save_single_paths)) {
987-
self$save_single_paths = as.integer(self$save_single_paths)
987+
self$save_single_paths <- as.integer(self$save_single_paths)
988988
}
989989
checkmate::assert_integerish(self$save_single_paths, null.ok = TRUE,
990990
lower = 0, upper = 1, len = 1)
991991
if (!is.null(self$save_single_paths)) {
992992
self$save_single_paths <- 0
993993
}
994994
if (!is.null(self$psis_resample) && is.logical(self$psis_resample)) {
995-
self$psis_resample = as.integer(self$psis_resample)
995+
self$psis_resample <- as.integer(self$psis_resample)
996996
}
997997
checkmate::assert_integerish(self$psis_resample, null.ok = TRUE,
998998
lower = 0, upper = 1, len = 1)
999999
if (!is.null(self$calculate_lp) && is.logical(self$calculate_lp)) {
1000-
self$calculate_lp = as.integer(self$calculate_lp)
1000+
self$calculate_lp <- as.integer(self$calculate_lp)
10011001
}
10021002
checkmate::assert_integerish(self$calculate_lp, null.ok = TRUE,
10031003
lower = 0, upper = 1, len = 1)
@@ -1018,23 +1018,45 @@ validate_pathfinder_args <- function(self) {
10181018
}
10191019

10201020

1021-
# Validation helpers ------------------------------------------------------
10221021

1023-
#' Validate exe file exists
1022+
# Init helpers ------------------------------------------------------------
1023+
1024+
#' Build a model_variables structure from a draws object
1025+
#'
1026+
#' When a model has been created without a Stan file,
1027+
#' `model$variables()` is unavailable. This helper infers parameter
1028+
#' names and dimensions from `posterior::variables(as_draws_df(...))`.
1029+
#' In that representation containers are expanded (e.g. `beta[1]`,
1030+
#' `gamma[1,2]`) while scalars appear as bare names (e.g. `sigma`).
1031+
#' The number of dimensions is inferred from the index pattern:
1032+
#' `mu[1]` has 1, `mu[1,2]` has 2, etc.
1033+
#'
10241034
#' @noRd
1025-
#' @param exe_file Path to executable.
1026-
#' @return Either throws an error or returns `invisible(TRUE)`
1027-
validate_exe_file <- function(exe_file) {
1028-
if (!length(exe_file) ||
1029-
!nzchar(exe_file) ||
1030-
!file.exists(exe_file)) {
1031-
stop("Model not compiled. Try running the compile() method first.",
1032-
call. = FALSE)
1033-
}
1034-
invisible(TRUE)
1035+
#' @param draws A draws object (any format supported by posterior).
1036+
#' @return A list with a `parameters` element in the same format as
1037+
#' `model$variables()`.
1038+
model_variables_from_draws <- function(draws) {
1039+
df_vars <- posterior::variables(posterior::as_draws_df(draws))
1040+
df_vars <- df_vars[!grepl("__$", df_vars)]
1041+
has_bracket <- grepl("\\[", df_vars)
1042+
scalars <- df_vars[!has_bracket]
1043+
# For containers, extract base name and count dimensions from the
1044+
# index pattern of the first occurrence (e.g. "mu[1,2]" -> 2 dims)
1045+
container_vars <- df_vars[has_bracket]
1046+
container_names <- sub("\\[.*", "", container_vars)
1047+
container_indices <- sub("^[^\\[]*\\[(.*)\\]$", "\\1", container_vars)
1048+
parameters <- list()
1049+
for (var_name in scalars) {
1050+
parameters[[var_name]] <- list(type = "real", dimensions = 0L)
1051+
}
1052+
for (var_name in unique(container_names)) {
1053+
idx <- match(var_name, container_names)
1054+
ndims <- length(strsplit(container_indices[idx], ",")[[1]])
1055+
parameters[[var_name]] <- list(type = "real", dimensions = ndims)
1056+
}
1057+
list(parameters = parameters)
10351058
}
10361059

1037-
10381060
#' Generic for processing inits
10391061
#' @noRd
10401062
process_init <- function(init, ...) {
@@ -1080,12 +1102,11 @@ process_init.default <- function(init, ...) {
10801102
process_init.draws <- function(init, num_procs, model_variables = NULL,
10811103
warn_partial = getOption("cmdstanr_warn_inits", TRUE),
10821104
...) {
1083-
if (!is.null(model_variables)) {
1084-
variable_names = names(model_variables$parameters)
1085-
} else {
1086-
variable_names = colnames(draws)[!grepl("__", colnames(draws))]
1087-
}
10881105
draws <- posterior::as_draws_df(init)
1106+
if (is.null(model_variables)) {
1107+
model_variables <- model_variables_from_draws(draws)
1108+
}
1109+
variable_names <- names(model_variables$parameters)
10891110
# Since all other process_init functions return `num_proc` inits
10901111
# This will only happen if a raw draws object is passed
10911112
if (nrow(draws) < num_procs) {
@@ -1095,7 +1116,7 @@ process_init.draws <- function(init, num_procs, model_variables = NULL,
10951116
draws <- posterior::resample_draws(draws, ndraws = num_procs,
10961117
method ="simple_no_replace")
10971118
}
1098-
draws_rvar = posterior::as_draws_rvars(draws)
1119+
draws_rvar <- posterior::as_draws_rvars(draws)
10991120

11001121
# Separate tuple and non-tuple parameters. Tuple parameters use leaf names
11011122
# in draws (e.g., "b_tuple:1:1") rather than the Stan-level name ("b_tuple"),
@@ -1121,19 +1142,19 @@ process_init.draws <- function(init, num_procs, model_variables = NULL,
11211142

11221143
all_names <- c(scalar_names, tuple_names)
11231144

1124-
if (length(scalar_names) > 0) {
1145+
if (length(all_names) > 0) {
11251146
draws_rvar <- posterior::subset_draws(
11261147
draws_rvar,
11271148
variable = expand_stan_params_to_leaves(all_names, rvar_names)
11281149
)
11291150
}
11301151

1131-
inits = lapply(1:num_procs, function(draw_iter) {
1152+
inits <- lapply(1:num_procs, function(draw_iter) {
11321153
bad_names <- character(0)
11331154

11341155
# Extract non-tuple parameters
1135-
init_i = lapply(scalar_names, function(var_name) {
1136-
x = .extract_draw_value(var_name, draws_rvar, draw_iter)
1156+
init_i <- lapply(scalar_names, function(var_name) {
1157+
x <- .extract_draw_value(var_name, draws_rvar, draw_iter)
11371158
if (any(is.infinite(x)) || any(is.na(x))) {
11381159
bad_names[[length(bad_names) + 1L]] <<- var_name
11391160
}
@@ -1158,11 +1179,11 @@ process_init.draws <- function(init, num_procs, model_variables = NULL,
11581179
}
11591180

11601181
if (length(bad_names) > 0) {
1161-
err_msg = paste0(paste(bad_names, collapse = ", "), " contains NA or Inf values!")
1182+
err_msg <- paste0(paste(bad_names, collapse = ", "), " contains NA or Inf values!")
11621183
if (length(bad_names) > 1) {
1163-
err_msg = paste0("Variables: ", err_msg)
1184+
err_msg <- paste0("Variables: ", err_msg)
11641185
} else {
1165-
err_msg = paste0("Variable: ", err_msg)
1186+
err_msg <- paste0("Variable: ", err_msg)
11661187
}
11671188
stop(err_msg)
11681189
}
@@ -1281,8 +1302,7 @@ process_init.function <- function(init, num_procs, model_variables = NULL,
12811302

12821303
#' Validate a fit is a valid init
12831304
#' @noRd
1284-
validate_fit_init = function(init, model_variables) {
1285-
# Convert from data.table to data.frame
1305+
validate_fit_init <- function(init, model_variables) {
12861306
if (all(init$return_codes() == 1)) {
12871307
stop("We are unable to create initial values from a model with no samples. Please check the results of the model used for inits before continuing.")
12881308
} else if (!is.null(model_variables) && !any(stan_param_has_leaf(names(model_variables$parameters), init$metadata()$stan_variables))) {
@@ -1305,13 +1325,10 @@ process_init.CmdStanMCMC <- function(init, num_procs, model_variables = NULL,
13051325
warn_partial = getOption("cmdstanr_warn_inits", TRUE),
13061326
...) {
13071327
validate_fit_init(init, model_variables)
1308-
draws_df = init$draws(format = "df")
1309-
if (is.null(model_variables)) {
1310-
model_variables = list(parameters = colnames(draws_df)[2:(length(colnames(draws_df)) - 3)])
1311-
}
1312-
init_draws_df = posterior::resample_draws(draws_df, ndraws = num_procs,
1328+
draws_df <- init$draws(format = "df")
1329+
init_draws_df <- posterior::resample_draws(draws_df, ndraws = num_procs,
13131330
method = "simple_no_replace")
1314-
init_draws_lst = process_init(init_draws_df,
1331+
init_draws_lst <- process_init(init_draws_df,
13151332
num_procs = num_procs, model_variables = model_variables)
13161333
return(init_draws_lst)
13171334
}
@@ -1332,47 +1349,45 @@ process_init_approx <- function(init, num_procs, model_variables = NULL,
13321349
...) {
13331350
validate_fit_init(init, model_variables)
13341351
# Convert from data.table to data.frame
1335-
draws_df = init$draws(format = "df")
1336-
if (is.null(model_variables)) {
1337-
model_variables = list(parameters = colnames(draws_df)[3:(length(colnames(draws_df)) - 3)])
1338-
}
1339-
draws_df$lw = draws_df$lp__ - draws_df$lp_approx__
1352+
draws_df <- init$draws(format = "df")
1353+
draws_df$lw <- draws_df$lp__ - draws_df$lp_approx__
13401354
# Replace NaN and Inf with -Inf
13411355
draws_df$lw[!is.finite(draws_df$lw)] <- -Inf
13421356
# Calculate unique draws based on 'lw' using base R functions
1343-
unique_draws = length(unique(draws_df$lw))
1357+
unique_draws <- length(unique(draws_df$lw))
13441358
if (num_procs > unique_draws) {
13451359
if (inherits(init, "CmdStanPathfinder")) {
1346-
algo_name = " Pathfinder "
1347-
extra_msg = " Try running Pathfinder with psis_resample=FALSE."
1360+
algo_name <- " Pathfinder "
1361+
extra_msg <- " Try running Pathfinder with psis_resample=FALSE."
13481362
} else if (inherits(init, "CmdStanVB")) {
1349-
algo_name = " CmdStanVB "
1350-
extra_msg = ""
1363+
algo_name <- " CmdStanVB "
1364+
extra_msg <- ""
13511365
} else if (inherits(init, "CmdStanLaplace")) {
1352-
algo_name = " CmdStanLaplace "
1353-
extra_msg = ""
1366+
algo_name <- " CmdStanLaplace "
1367+
extra_msg <- ""
13541368
} else {
1355-
algo_name = ""
1356-
extra_msg = ""
1369+
algo_name <- ""
1370+
extra_msg <- ""
13571371
}
13581372
stop(paste0("Not enough distinct draws (", num_procs, ") in", algo_name ,
13591373
"fit to create inits.", extra_msg))
13601374
}
13611375
if (unique_draws < (0.95 * nrow(draws_df))) {
1362-
temp_df = stats::aggregate(.draw ~ lw, data = draws_df, FUN = min)
1363-
draws_df = posterior::as_draws_df(merge(temp_df, draws_df, by = 'lw'))
1364-
draws_df$weight = exp(draws_df$lw - max(draws_df$lw))
1376+
temp_df <- stats::aggregate(.draw ~ lw, data = draws_df, FUN = min)
1377+
draws_df <- posterior::as_draws_df(merge(temp_df, draws_df, by = 'lw'))
1378+
draws_df$weight <- exp(draws_df$lw - max(draws_df$lw))
13651379
} else {
1366-
draws_df$weight = posterior::pareto_smooth(
1380+
draws_df$weight <- posterior::pareto_smooth(
13671381
exp(draws_df$lw - max(draws_df$lw)), tail = "right", r_eff=1, return_k=FALSE)
13681382
}
1369-
init_draws_df = posterior::resample_draws(draws_df, ndraws = num_procs,
1370-
weights = draws_df$weight, method = "simple_no_replace")
1371-
init_draws_lst = process_init(init_draws_df,
1372-
num_procs = num_procs, model_variables = model_variables, warn_partial)
1373-
return(init_draws_lst)
1374-
}
1375-
1383+
init_draws_df <- posterior::resample_draws(draws_df, ndraws = num_procs,
1384+
weights = draws_df$weight, method = "simple_no_replace")
1385+
init_draws_df <- posterior::subset_draws(init_draws_df,
1386+
variable = setdiff(posterior::variables(init_draws_df), c("lw", "weight")))
1387+
init_draws_lst <- process_init(init_draws_df,
1388+
num_procs = num_procs, model_variables = model_variables, warn_partial)
1389+
return(init_draws_lst)
1390+
}
13761391

13771392
#' Write initial values to files if provided as a `CmdStanPathfinder` class
13781393
#' @noRd
@@ -1391,14 +1406,13 @@ process_init.CmdStanPathfinder <- function(init, num_procs, model_variables = NU
13911406
if (!init$metadata()$calculate_lp) {
13921407
validate_fit_init(init, model_variables)
13931408
# Convert from data.table to data.frame
1394-
draws_df = init$draws(format = "df")
1395-
if (is.null(model_variables)) {
1396-
model_variables = list(parameters = colnames(draws_df)[3:(length(colnames(draws_df)) - 3)])
1397-
}
1398-
draws_df$weight = rep(1.0, nrow(draws_df))
1399-
init_draws_df = posterior::resample_draws(draws_df, ndraws = num_procs,
1409+
draws_df <- init$draws(format = "df")
1410+
draws_df$weight <- rep(1.0, nrow(draws_df))
1411+
init_draws_df <- posterior::resample_draws(draws_df, ndraws = num_procs,
14001412
weights = draws_df$weight, method = "simple_no_replace")
1401-
init_draws_lst = process_init(init_draws_df,
1413+
init_draws_df <- posterior::subset_draws(init_draws_df,
1414+
variable = setdiff(posterior::variables(init_draws_df), "weight"))
1415+
init_draws_lst <- process_init(init_draws_df,
14021416
num_procs = num_procs, model_variables = model_variables, warn_partial)
14031417
return(init_draws_lst)
14041418
} else {
@@ -1457,16 +1471,31 @@ process_init.CmdStanMLE <- function(init, num_procs, model_variables = NULL,
14571471
...) {
14581472
# Convert from data.table to data.frame
14591473
validate_fit_init(init, model_variables)
1460-
draws_df = init$draws(format = "df")
1461-
if (is.null(model_variables)) {
1462-
model_variables = list(parameters = colnames(draws_df)[2:(length(colnames(draws_df)) - 3)])
1463-
}
1464-
init_draws_df = draws_df[rep(1, num_procs),]
1465-
init_draws_lst_lst = process_init(init_draws_df,
1474+
draws_df <- init$draws(format = "df")
1475+
init_draws_df <- draws_df[rep(1, num_procs),]
1476+
init_draws_lst_lst <- process_init(init_draws_df,
14661477
num_procs = num_procs, model_variables = model_variables, warn_partial)
14671478
return(init_draws_lst_lst)
14681479
}
14691480

1481+
1482+
# Validation helpers ------------------------------------------------------
1483+
1484+
#' Validate exe file exists
1485+
#' @noRd
1486+
#' @param exe_file Path to executable.
1487+
#' @return Either throws an error or returns `invisible(TRUE)`
1488+
validate_exe_file <- function(exe_file) {
1489+
if (!length(exe_file) ||
1490+
!nzchar(exe_file) ||
1491+
!file.exists(exe_file)) {
1492+
stop("Model not compiled. Try running the compile() method first.",
1493+
call. = FALSE)
1494+
}
1495+
invisible(TRUE)
1496+
}
1497+
1498+
14701499
#' Validate initial values
14711500
#'
14721501
#' For CmdStan `init` must be `NULL`, a single real number >= 0, or paths to

0 commit comments

Comments
 (0)