From 7b451825d82288dee4637e2fa21cba3cefbc13d5 Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Sun, 29 Mar 2026 14:40:02 +0300 Subject: [PATCH 01/17] add get_cmdstan_args method --- R/model.R | 196 +++++++++++++++++++ _pkgdown.yml | 1 + man/CmdStanModel.Rd | 1 + man/cmdstanr-package.Rd | 20 +- man/model-method-check_syntax.Rd | 1 + man/model-method-compile.Rd | 1 + man/model-method-diagnose.Rd | 1 + man/model-method-expose_functions.Rd | 1 + man/model-method-format.Rd | 1 + man/model-method-generate-quantities.Rd | 1 + man/model-method-get_cmdstan_args.Rd | 65 ++++++ man/model-method-laplace.Rd | 1 + man/model-method-optimize.Rd | 1 + man/model-method-pathfinder.Rd | 1 + man/model-method-sample.Rd | 1 + man/model-method-sample_mpi.Rd | 1 + man/model-method-variables.Rd | 1 + man/model-method-variational.Rd | 1 + tests/testthat/test-model-get-cmdstan-args.R | 123 ++++++++++++ 19 files changed, 409 insertions(+), 10 deletions(-) create mode 100644 man/model-method-get_cmdstan_args.Rd create mode 100644 tests/testthat/test-model-get-cmdstan-args.R diff --git a/R/model.R b/R/model.R index 79882226..b38cbfe9 100644 --- a/R/model.R +++ b/R/model.R @@ -196,6 +196,7 @@ cmdstan_model <- function(stan_file = NULL, exe_file = NULL, compile = TRUE, ... #' [`$hpp_file()`][model-method-compile] | Return the file path to the `.hpp` file containing the generated C++ code. | #' [`$save_hpp_file()`][model-method-compile] | Save the `.hpp` file containing the generated C++ code. | #' [`$expose_functions()`][model-method-expose_functions] | Expose Stan functions for use in R. | +#' [`$get_cmdstan_args()`][model-method-get_cmdstan_args] | Get CmdStan default argument values for a method. | #' #' ## Diagnostics #' @@ -2209,6 +2210,201 @@ expose_functions = function(global = FALSE, verbose = FALSE) { CmdStanModel$set("public", name = "expose_functions", value = expose_functions) +#' Get CmdStan default argument values +#' +#' @name model-method-get_cmdstan_args +#' @aliases get_cmdstan_args +#' @family CmdStanModel methods +#' +#' @description The `$get_cmdstan_args()` method of a [`CmdStanModel`] +#' object queries the compiled model binary for the default argument +#' values used by a given inference method. The returned list uses +#' cmdstanr-style argument names (e.g., `iter_sampling` instead of +#' CmdStan's `num_samples`). +#' +#' The model must be compiled before calling this method. +#' +#' @param method (string) The inference method whose defaults to +#' retrieve. One of `"sample"`, `"optimize"`, `"variational"`, +#' `"pathfinder"`, or `"laplace"`. +#' @return A named list of default argument values for the specified +#' method, with cmdstanr-style argument names. +#' +#' @template seealso-docs +#' +#' @examples +#' \dontrun{ +#' mod <- cmdstan_model(file.path(cmdstan_path(), +#' "examples/bernoulli/bernoulli.stan")) +#' mod$get_cmdstan_args("sample") +#' mod$get_cmdstan_args("optimize") +#' } +#' +get_cmdstan_args <- function(method = c("sample", "optimize", "variational", + "pathfinder", "laplace")) { + method <- match.arg(method) + if (length(self$exe_file()) == 0 || !file.exists(self$exe_file())) { + stop( + "'$get_cmdstan_args()' requires a compiled model. ", + "Please compile the model first with '$compile()'.", + call. = FALSE + ) + } + parse_cmdstan_args(self$exe_file(), method) +} +CmdStanModel$set("public", name = "get_cmdstan_args", value = get_cmdstan_args) + + +# get_cmdstan_args helpers ------------------------------------------------ + +#' Parse CmdStan default argument values from model binary +#' +#' Runs a CmdStan model binary with `help-all` to extract valid arguments +#' and their default values for a given inference method, returning them +#' with cmdstanr argument names. +#' +#' @noRd +#' @param model_binary Path to the CmdStan model binary. +#' @param method Inference method: `"sample"`, `"optimize"`, +#' `"variational"`, `"pathfinder"`, or `"laplace"`. +#' @return A named list with cmdstanr-style argument names and default +#' values. +parse_cmdstan_args <- function(model_binary, method) { + ret <- wsl_compatible_run( + command = wsl_safe_path(model_binary), + args = c(method, "help-all"), + error_on_status = FALSE + ) + output <- strsplit(ret$stdout, "\n")[[1]] + + arguments <- map_cmdstan_to_cmdstanr(method) + target_args <- vapply(arguments, function(p) { + parts <- strsplit(p, "\\.")[[1]] + parts[length(parts)] + }, FUN.VALUE = character(1), USE.NAMES = TRUE) + + result <- list() + n <- length(output) + + for (i in seq_len(n)) { + line <- output[i] + content <- trimws(line) + + # Match argument lines like "num_samples=" or "t0=" + arg_match <- regmatches(content, regexec("^([a-z_][a-z0-9_]*)=", content))[[1]] + + if (length(arg_match) >= 2) { + arg_name <- arg_match[2] + + # Check if this is one of our target arguments + matches <- which(target_args == arg_name) + + if (length(matches) > 0) { + # Look ahead for "Defaults to" line + default_value <- NULL + for (j in (i + 1):min(i + 5, n)) { + next_content <- trimws(output[j]) + if (grepl("^Defaults to", next_content)) { + default_value <- parse_default_value(next_content) + break + } + # Stop if we hit another argument + if (grepl("^[a-z_][a-z0-9_]*=", next_content)) break + } + + # Add to result for each matching cmdstanr argument name + for (m in matches) { + cmdstanr_name <- names(target_args)[m] + result[[cmdstanr_name]] <- default_value + } + } + } + } + + result +} + +#' Parse default value from "Defaults to ..." line +#' @noRd +parse_default_value <- function(line) { + val_str <- sub("^Defaults to\\s*", "", line) + if (val_str %in% c("true", "false")) return(val_str == "true") + if (grepl("^-?[0-9]+$", val_str)) return(as.integer(val_str)) + if (grepl("^-?[0-9]*\\.?[0-9]+([eE][+-]?[0-9]+)?$", val_str)) return(as.numeric(val_str)) + val_str +} + +#' Map CmdStan argument names to CmdStanR argument names +#' @noRd +map_cmdstan_to_cmdstanr <- function(method) { + switch(method, + sample = c( + iter_sampling = "sample.num_samples", + iter_warmup = "sample.num_warmup", + save_warmup = "sample.save_warmup", + thin = "sample.thin", + adapt_engaged = "sample.adapt.engaged", + adapt_delta = "sample.adapt.delta", + init_buffer = "sample.adapt.init_buffer", + term_buffer = "sample.adapt.term_buffer", + window = "sample.adapt.window", + save_metric = "sample.adapt.save_metric", + max_treedepth = "sample.algorithm.hmc.engine.nuts.max_depth", + metric = "sample.algorithm.hmc.metric", + metric_file = "sample.algorithm.hmc.metric_file", + step_size = "sample.algorithm.hmc.stepsize", + num_chains = "sample.num_chains" + ), + optimize = c( + algorithm = "optimize.algorithm", + jacobian = "optimize.jacobian", + iter = "optimize.iter", + save_iterations = "optimize.save_iterations", + init_alpha = "optimize.algorithm.lbfgs.init_alpha", + tol_obj = "optimize.algorithm.lbfgs.tol_obj", + tol_rel_obj = "optimize.algorithm.lbfgs.tol_rel_obj", + tol_grad = "optimize.algorithm.lbfgs.tol_grad", + tol_rel_grad = "optimize.algorithm.lbfgs.tol_rel_grad", + tol_param = "optimize.algorithm.lbfgs.tol_param", + history_size = "optimize.algorithm.lbfgs.history_size" + ), + variational = c( + algorithm = "variational.algorithm", + iter = "variational.iter", + grad_samples = "variational.grad_samples", + elbo_samples = "variational.elbo_samples", + eta = "variational.eta", + adapt_engaged = "variational.adapt.engaged", + adapt_iter = "variational.adapt.iter", + tol_rel_obj = "variational.tol_rel_obj", + eval_elbo = "variational.eval_elbo", + output_samples = "variational.output_samples" + ), + pathfinder = c( + init_alpha = "pathfinder.init_alpha", + tol_obj = "pathfinder.tol_obj", + tol_rel_obj = "pathfinder.tol_rel_obj", + tol_grad = "pathfinder.tol_grad", + tol_rel_grad = "pathfinder.tol_rel_grad", + tol_param = "pathfinder.tol_param", + history_size = "pathfinder.history_size", + draws = "pathfinder.num_psis_draws", + num_paths = "pathfinder.num_paths", + save_single_paths = "pathfinder.save_single_paths", + psis_resample = "pathfinder.psis_resample", + calculate_lp = "pathfinder.calculate_lp", + max_lbfgs_iters = "pathfinder.max_lbfgs_iters", + single_path_draws = "pathfinder.num_draws", + num_elbo_draws = "pathfinder.num_elbo_draws" + ), + laplace = c( + jacobian = "laplace.jacobian", + draws = "laplace.draws" + ), + character(0) + ) +} + # internal ---------------------------------------------------------------- assert_valid_stanc_options <- function(stanc_options) { diff --git a/_pkgdown.yml b/_pkgdown.yml index 123d2191..af774887 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -95,6 +95,7 @@ reference: - read_cmdstan_csv - write_stan_json - write_stan_file + - print_stan_file - draws_to_csv - as_mcmc.list - as_draws.CmdStanMCMC diff --git a/man/CmdStanModel.Rd b/man/CmdStanModel.Rd index 0b21ac5c..cc64b2b0 100644 --- a/man/CmdStanModel.Rd +++ b/man/CmdStanModel.Rd @@ -30,6 +30,7 @@ methods, many of which have their own (linked) documentation pages: \code{\link[=model-method-compile]{$hpp_file()}} \tab Return the file path to the \code{.hpp} file containing the generated C++ code. \cr \code{\link[=model-method-compile]{$save_hpp_file()}} \tab Save the \code{.hpp} file containing the generated C++ code. \cr \code{\link[=model-method-expose_functions]{$expose_functions()}} \tab Expose Stan functions for use in R. \cr + \code{\link[=model-method-get_cmdstan_args]{$get_cmdstan_args()}} \tab Get CmdStan default argument values for a method. \cr } } diff --git a/man/cmdstanr-package.Rd b/man/cmdstanr-package.Rd index c2ccbb1d..e6479f57 100644 --- a/man/cmdstanr-package.Rd +++ b/man/cmdstanr-package.Rd @@ -34,22 +34,22 @@ algorithms, and writing results to output files. \subsection{Advantages of RStan}{ \itemize{ \item Allows other developers to distribute R packages with \emph{pre-compiled} -Stan programs (like \strong{rstanarm}) on CRAN. (Note: As of 2023, this can -mostly be achieved with CmdStanR as well. See \href{https://mc-stan.org/cmdstanr/articles/cmdstanr-internals.html#developing-using-cmdstanr}{Developing using CmdStanR}.) -\item Avoids use of R6 classes, which may result in more familiar syntax for -many R users. +Stan programs (like \strong{rstanarm}) on CRAN. (Note: As of 2023, this +can mostly be achieved with CmdStanR as well. See \href{https://mc-stan.org/cmdstanr/articles/cmdstanr-internals.html#developing-using-cmdstanr}{Developing using CmdStanR}.) +\item Avoids use of R6 classes, which may result in more familiar syntax +for many R users. \item CRAN binaries available for Mac and Windows. } } \subsection{Advantages of CmdStanR}{ \itemize{ -\item Compatible with latest versions of Stan. Keeping up with Stan releases -is complicated for RStan, often requiring non-trivial changes to the -\strong{rstan} package and new CRAN releases of both \strong{rstan} and -\strong{StanHeaders}. With CmdStanR the latest improvements in Stan will be -available from R immediately after updating CmdStan using -\code{cmdstanr::install_cmdstan()}. +\item Compatible with latest versions of Stan. Keeping up with Stan +releases is complicated for RStan, often requiring non-trivial +changes to the \strong{rstan} package and new CRAN releases of both +\strong{rstan} and \strong{StanHeaders}. With CmdStanR the latest improvements +in Stan will be available from R immediately after updating CmdStan +using \code{cmdstanr::install_cmdstan()}. \item Running Stan via external processes results in fewer unexpected crashes, especially in RStudio. \item Less memory overhead. diff --git a/man/model-method-check_syntax.Rd b/man/model-method-check_syntax.Rd index 68366fb5..1d1126e9 100644 --- a/man/model-method-check_syntax.Rd +++ b/man/model-method-check_syntax.Rd @@ -83,6 +83,7 @@ Other CmdStanModel methods: \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-compile.Rd b/man/model-method-compile.Rd index 14871ef5..1e7249a0 100644 --- a/man/model-method-compile.Rd +++ b/man/model-method-compile.Rd @@ -150,6 +150,7 @@ Other CmdStanModel methods: \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-diagnose.Rd b/man/model-method-diagnose.Rd index c7117ef1..18335822 100644 --- a/man/model-method-diagnose.Rd +++ b/man/model-method-diagnose.Rd @@ -150,6 +150,7 @@ Other CmdStanModel methods: \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-expose_functions.Rd b/man/model-method-expose_functions.Rd index b7d42231..c37cd0f4 100644 --- a/man/model-method-expose_functions.Rd +++ b/man/model-method-expose_functions.Rd @@ -74,6 +74,7 @@ Other CmdStanModel methods: \code{\link{model-method-diagnose}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-format.Rd b/man/model-method-format.Rd index 4a8af83b..56a68b72 100644 --- a/man/model-method-format.Rd +++ b/man/model-method-format.Rd @@ -90,6 +90,7 @@ Other CmdStanModel methods: \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-generate-quantities.Rd b/man/model-method-generate-quantities.Rd index 4f7c87bf..6dbc48ec 100644 --- a/man/model-method-generate-quantities.Rd +++ b/man/model-method-generate-quantities.Rd @@ -188,6 +188,7 @@ Other CmdStanModel methods: \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-get_cmdstan_args.Rd b/man/model-method-get_cmdstan_args.Rd new file mode 100644 index 00000000..d83115f3 --- /dev/null +++ b/man/model-method-get_cmdstan_args.Rd @@ -0,0 +1,65 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model.R +\name{model-method-get_cmdstan_args} +\alias{model-method-get_cmdstan_args} +\alias{get_cmdstan_args} +\title{Get CmdStan default argument values} +\usage{ +get_cmdstan_args( + method = c("sample", "optimize", "variational", "pathfinder", "laplace") +) +} +\arguments{ +\item{method}{(string) The inference method whose defaults to +retrieve. One of \code{"sample"}, \code{"optimize"}, \code{"variational"}, +\code{"pathfinder"}, or \code{"laplace"}.} +} +\value{ +A named list of default argument values for the specified +method, with cmdstanr-style argument names. +} +\description{ +The \verb{$get_cmdstan_args()} method of a \code{\link{CmdStanModel}} +object queries the compiled model binary for the default argument +values used by a given inference method. The returned list uses +cmdstanr-style argument names (e.g., \code{iter_sampling} instead of +CmdStan's \code{num_samples}). + +The model must be compiled before calling this method. +} +\examples{ +\dontrun{ +mod <- cmdstan_model(file.path(cmdstan_path(), + "examples/bernoulli/bernoulli.stan")) +mod$get_cmdstan_args("sample") +mod$get_cmdstan_args("optimize") +} + +} +\seealso{ +The CmdStanR website +(\href{https://mc-stan.org/cmdstanr/}{mc-stan.org/cmdstanr}) for online +documentation and tutorials. + +The Stan and CmdStan documentation: +\itemize{ +\item Stan documentation: \href{https://mc-stan.org/users/documentation/}{mc-stan.org/users/documentation} +\item CmdStan User’s Guide: \href{https://mc-stan.org/docs/cmdstan-guide/}{mc-stan.org/docs/cmdstan-guide} +} + +Other CmdStanModel methods: +\code{\link{model-method-check_syntax}}, +\code{\link{model-method-compile}}, +\code{\link{model-method-diagnose}}, +\code{\link{model-method-expose_functions}}, +\code{\link{model-method-format}}, +\code{\link{model-method-generate-quantities}}, +\code{\link{model-method-laplace}}, +\code{\link{model-method-optimize}}, +\code{\link{model-method-pathfinder}}, +\code{\link{model-method-sample}}, +\code{\link{model-method-sample_mpi}}, +\code{\link{model-method-variables}}, +\code{\link{model-method-variational}} +} +\concept{CmdStanModel methods} diff --git a/man/model-method-laplace.Rd b/man/model-method-laplace.Rd index e94c0f64..aecc1e5c 100644 --- a/man/model-method-laplace.Rd +++ b/man/model-method-laplace.Rd @@ -243,6 +243,7 @@ Other CmdStanModel methods: \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, \code{\link{model-method-sample}}, diff --git a/man/model-method-optimize.Rd b/man/model-method-optimize.Rd index f9fbb5c5..f73971ba 100644 --- a/man/model-method-optimize.Rd +++ b/man/model-method-optimize.Rd @@ -368,6 +368,7 @@ Other CmdStanModel methods: \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-pathfinder}}, \code{\link{model-method-sample}}, diff --git a/man/model-method-pathfinder.Rd b/man/model-method-pathfinder.Rd index a7b3c15e..f726d0a1 100644 --- a/man/model-method-pathfinder.Rd +++ b/man/model-method-pathfinder.Rd @@ -387,6 +387,7 @@ Other CmdStanModel methods: \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-sample}}, diff --git a/man/model-method-sample.Rd b/man/model-method-sample.Rd index dd8a1b43..cc9d2894 100644 --- a/man/model-method-sample.Rd +++ b/man/model-method-sample.Rd @@ -453,6 +453,7 @@ Other CmdStanModel methods: \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-sample_mpi.Rd b/man/model-method-sample_mpi.Rd index b8620ecb..e3327132 100644 --- a/man/model-method-sample_mpi.Rd +++ b/man/model-method-sample_mpi.Rd @@ -348,6 +348,7 @@ Other CmdStanModel methods: \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-variables.Rd b/man/model-method-variables.Rd index 87e9d73e..9afb24f3 100644 --- a/man/model-method-variables.Rd +++ b/man/model-method-variables.Rd @@ -43,6 +43,7 @@ Other CmdStanModel methods: \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-variational.Rd b/man/model-method-variational.Rd index 41fb890c..7c0358b6 100644 --- a/man/model-method-variational.Rd +++ b/man/model-method-variational.Rd @@ -358,6 +358,7 @@ Other CmdStanModel methods: \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, +\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/tests/testthat/test-model-get-cmdstan-args.R b/tests/testthat/test-model-get-cmdstan-args.R new file mode 100644 index 00000000..29a3502c --- /dev/null +++ b/tests/testthat/test-model-get-cmdstan-args.R @@ -0,0 +1,123 @@ +set_cmdstan_path() +mod <- testing_model("bernoulli") + +test_that("get_cmdstan_args() errors for uncompiled model", { + mod_uncompiled <- cmdstan_model( + stan_file = testing_stan_file("bernoulli"), + compile = FALSE + ) + expect_error( + mod_uncompiled$get_cmdstan_args("sample"), + "'$get_cmdstan_args()' requires a compiled model", + fixed = TRUE + ) +}) + +test_that("get_cmdstan_args() errors for invalid method", { + expect_error( + mod$get_cmdstan_args("bogus"), + "'arg' should be one of", + fixed = TRUE + ) +}) + +test_that("get_cmdstan_args() returns named list for sample", { + args <- mod$get_cmdstan_args("sample") + expect_type(args, "list") + expect_named(args) + expected_names <- names(map_cmdstan_to_cmdstanr("sample")) + for (nm in expected_names) { + expect_true(nm %in% names(args), info = paste0("missing: ", nm)) + } +}) + +test_that("get_cmdstan_args() returns expected default types for sample", { + args <- mod$get_cmdstan_args("sample") + expect_type(args$iter_sampling, "integer") + expect_type(args$iter_warmup, "integer") + expect_type(args$thin, "integer") + expect_type(args$adapt_delta, "double") + expect_type(args$save_warmup, "logical") + expect_type(args$max_treedepth, "integer") +}) + +test_that("get_cmdstan_args() works for optimize", { + args <- mod$get_cmdstan_args("optimize") + expect_type(args, "list") + expect_named(args) + expected_names <- names(map_cmdstan_to_cmdstanr("optimize")) + for (nm in expected_names) { + expect_true(nm %in% names(args), info = paste0("missing: ", nm)) + } + expect_type(args$jacobian, "logical") + expect_type(args$iter, "integer") +}) + +test_that("get_cmdstan_args() works for variational", { + args <- mod$get_cmdstan_args("variational") + expect_type(args, "list") + expect_named(args) + expected_names <- names(map_cmdstan_to_cmdstanr("variational")) + for (nm in expected_names) { + expect_true(nm %in% names(args), info = paste0("missing: ", nm)) + } +}) + +test_that("get_cmdstan_args() works for pathfinder", { + args <- mod$get_cmdstan_args("pathfinder") + expect_type(args, "list") + expect_named(args) + expected_names <- names(map_cmdstan_to_cmdstanr("pathfinder")) + for (nm in expected_names) { + expect_true(nm %in% names(args), info = paste0("missing: ", nm)) + } +}) + +test_that("get_cmdstan_args() works for laplace", { + args <- mod$get_cmdstan_args("laplace") + expect_type(args, "list") + expect_named(args) + expected_names <- names(map_cmdstan_to_cmdstanr("laplace")) + for (nm in expected_names) { + expect_true(nm %in% names(args), info = paste0("missing: ", nm)) + } + expect_type(args$jacobian, "logical") + expect_type(args$draws, "integer") +}) + +# internal helpers -------------------------------------------------------- + +test_that("parse_default_value() parses booleans", { + expect_identical(parse_default_value("Defaults to true"), TRUE) + expect_identical(parse_default_value("Defaults to false"), FALSE) +}) + +test_that("parse_default_value() parses integers", { + expect_identical(parse_default_value("Defaults to 1000"), 1000L) + expect_identical(parse_default_value("Defaults to -1"), -1L) + expect_identical(parse_default_value("Defaults to 0"), 0L) +}) + +test_that("parse_default_value() parses doubles", { + expect_identical(parse_default_value("Defaults to 0.8"), 0.8) + expect_identical(parse_default_value("Defaults to 1e-6"), 1e-6) + expect_identical(parse_default_value("Defaults to -0.5"), -0.5) +}) + +test_that("parse_default_value() returns strings for non-numeric values", { + expect_identical(parse_default_value("Defaults to lbfgs"), "lbfgs") + expect_identical(parse_default_value("Defaults to diagonal_e"), "diagonal_e") +}) + +test_that("map_cmdstan_to_cmdstanr() returns named character for valid methods", { + for (method in c("sample", "optimize", "variational", "pathfinder", "laplace")) { + mapping <- map_cmdstan_to_cmdstanr(method) + expect_type(mapping, "character") + expect_true(length(mapping) > 0, info = method) + expect_named(mapping) + } +}) + +test_that("map_cmdstan_to_cmdstanr() returns empty for unknown method", { + expect_length(map_cmdstan_to_cmdstanr("unknown"), 0) +}) From c7c976fcb3e73a79cd9fd07daaa6e9a361e9a22d Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Mon, 30 Mar 2026 22:38:23 +0300 Subject: [PATCH 02/17] windows fix --- R/model.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/R/model.R b/R/model.R index b38cbfe9..b1c70e6a 100644 --- a/R/model.R +++ b/R/model.R @@ -2275,7 +2275,9 @@ parse_cmdstan_args <- function(model_binary, method) { args = c(method, "help-all"), error_on_status = FALSE ) - output <- strsplit(ret$stdout, "\n")[[1]] + # CmdStan may write help text to stdout or stderr depending on the platform + raw <- paste0(ret$stdout, ret$stderr) + output <- strsplit(raw, "\r?\n")[[1]] arguments <- map_cmdstan_to_cmdstanr(method) target_args <- vapply(arguments, function(p) { From 966e785c16197316c76bee992438922d01ebb501 Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Tue, 31 Mar 2026 09:13:49 +0300 Subject: [PATCH 03/17] add tbb_path to parse_cmdstan_args (same as in run_info_cli) --- R/model.R | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/R/model.R b/R/model.R index b1c70e6a..d2b1ca51 100644 --- a/R/model.R +++ b/R/model.R @@ -2270,10 +2270,16 @@ CmdStanModel$set("public", name = "get_cmdstan_args", value = get_cmdstan_args) #' @return A named list with cmdstanr-style argument names and default #' values. parse_cmdstan_args <- function(model_binary, method) { - ret <- wsl_compatible_run( - command = wsl_safe_path(model_binary), - args = c(method, "help-all"), - error_on_status = FALSE + withr::with_path( + c( + toolchain_PATH_env_var(), + tbb_path() + ), + ret <- wsl_compatible_run( + command = wsl_safe_path(model_binary), + args = c(method, "help-all"), + error_on_status = FALSE + ) ) # CmdStan may write help text to stdout or stderr depending on the platform raw <- paste0(ret$stdout, ret$stderr) From 92d1b897a89b4e7e2ab4c0c88679d98d3dfbeef6 Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Tue, 31 Mar 2026 20:50:18 +0300 Subject: [PATCH 04/17] fix map_cmdstan_to_cmdstanr --- R/model.R | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/R/model.R b/R/model.R index d2b1ca51..e932d4d5 100644 --- a/R/model.R +++ b/R/model.R @@ -2360,14 +2360,12 @@ map_cmdstan_to_cmdstanr <- function(method) { max_treedepth = "sample.algorithm.hmc.engine.nuts.max_depth", metric = "sample.algorithm.hmc.metric", metric_file = "sample.algorithm.hmc.metric_file", - step_size = "sample.algorithm.hmc.stepsize", - num_chains = "sample.num_chains" + step_size = "sample.algorithm.hmc.stepsize" ), optimize = c( algorithm = "optimize.algorithm", jacobian = "optimize.jacobian", iter = "optimize.iter", - save_iterations = "optimize.save_iterations", init_alpha = "optimize.algorithm.lbfgs.init_alpha", tol_obj = "optimize.algorithm.lbfgs.tol_obj", tol_rel_obj = "optimize.algorithm.lbfgs.tol_rel_obj", @@ -2386,7 +2384,7 @@ map_cmdstan_to_cmdstanr <- function(method) { adapt_iter = "variational.adapt.iter", tol_rel_obj = "variational.tol_rel_obj", eval_elbo = "variational.eval_elbo", - output_samples = "variational.output_samples" + draws = "variational.output_samples" ), pathfinder = c( init_alpha = "pathfinder.init_alpha", From 2466d424081cf80b7cc1e138cf709e11277f5874 Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Tue, 31 Mar 2026 21:00:23 +0300 Subject: [PATCH 05/17] Change the method name from get_cmdstan_args to cmdstan_defaults --- R/model.R | 20 +++++------ man/CmdStanModel.Rd | 2 +- man/model-method-check_syntax.Rd | 2 +- ...gs.Rd => model-method-cmdstan_defaults.Rd} | 14 ++++---- man/model-method-compile.Rd | 2 +- man/model-method-diagnose.Rd | 2 +- man/model-method-expose_functions.Rd | 2 +- man/model-method-format.Rd | 2 +- man/model-method-generate-quantities.Rd | 2 +- man/model-method-laplace.Rd | 2 +- man/model-method-optimize.Rd | 2 +- man/model-method-pathfinder.Rd | 2 +- man/model-method-sample.Rd | 2 +- man/model-method-sample_mpi.Rd | 2 +- man/model-method-variables.Rd | 2 +- man/model-method-variational.Rd | 2 +- ...n-args.R => test-model-cmdstan-defaults.R} | 34 +++++++++---------- 17 files changed, 48 insertions(+), 48 deletions(-) rename man/{model-method-get_cmdstan_args.Rd => model-method-cmdstan_defaults.Rd} (88%) rename tests/testthat/{test-model-get-cmdstan-args.R => test-model-cmdstan-defaults.R} (79%) diff --git a/R/model.R b/R/model.R index e932d4d5..c7cb5665 100644 --- a/R/model.R +++ b/R/model.R @@ -196,7 +196,7 @@ cmdstan_model <- function(stan_file = NULL, exe_file = NULL, compile = TRUE, ... #' [`$hpp_file()`][model-method-compile] | Return the file path to the `.hpp` file containing the generated C++ code. | #' [`$save_hpp_file()`][model-method-compile] | Save the `.hpp` file containing the generated C++ code. | #' [`$expose_functions()`][model-method-expose_functions] | Expose Stan functions for use in R. | -#' [`$get_cmdstan_args()`][model-method-get_cmdstan_args] | Get CmdStan default argument values for a method. | +#' [`$cmdstan_defaults()`][model-method-cmdstan_defaults] | Get CmdStan default argument values for a method. | #' #' ## Diagnostics #' @@ -2212,11 +2212,11 @@ CmdStanModel$set("public", name = "expose_functions", value = expose_functions) #' Get CmdStan default argument values #' -#' @name model-method-get_cmdstan_args -#' @aliases get_cmdstan_args +#' @name model-method-cmdstan_defaults +#' @aliases cmdstan_defaults #' @family CmdStanModel methods #' -#' @description The `$get_cmdstan_args()` method of a [`CmdStanModel`] +#' @description The `$cmdstan_defaults()` method of a [`CmdStanModel`] #' object queries the compiled model binary for the default argument #' values used by a given inference method. The returned list uses #' cmdstanr-style argument names (e.g., `iter_sampling` instead of @@ -2236,26 +2236,26 @@ CmdStanModel$set("public", name = "expose_functions", value = expose_functions) #' \dontrun{ #' mod <- cmdstan_model(file.path(cmdstan_path(), #' "examples/bernoulli/bernoulli.stan")) -#' mod$get_cmdstan_args("sample") -#' mod$get_cmdstan_args("optimize") +#' mod$cmdstan_defaults("sample") +#' mod$cmdstan_defaults("optimize") #' } #' -get_cmdstan_args <- function(method = c("sample", "optimize", "variational", +cmdstan_defaults <- function(method = c("sample", "optimize", "variational", "pathfinder", "laplace")) { method <- match.arg(method) if (length(self$exe_file()) == 0 || !file.exists(self$exe_file())) { stop( - "'$get_cmdstan_args()' requires a compiled model. ", + "'$cmdstan_defaults()' requires a compiled model. ", "Please compile the model first with '$compile()'.", call. = FALSE ) } parse_cmdstan_args(self$exe_file(), method) } -CmdStanModel$set("public", name = "get_cmdstan_args", value = get_cmdstan_args) +CmdStanModel$set("public", name = "cmdstan_defaults", value = cmdstan_defaults) -# get_cmdstan_args helpers ------------------------------------------------ +# cmdstan_defaults helpers ------------------------------------------------ #' Parse CmdStan default argument values from model binary #' diff --git a/man/CmdStanModel.Rd b/man/CmdStanModel.Rd index cc64b2b0..7057014e 100644 --- a/man/CmdStanModel.Rd +++ b/man/CmdStanModel.Rd @@ -30,7 +30,7 @@ methods, many of which have their own (linked) documentation pages: \code{\link[=model-method-compile]{$hpp_file()}} \tab Return the file path to the \code{.hpp} file containing the generated C++ code. \cr \code{\link[=model-method-compile]{$save_hpp_file()}} \tab Save the \code{.hpp} file containing the generated C++ code. \cr \code{\link[=model-method-expose_functions]{$expose_functions()}} \tab Expose Stan functions for use in R. \cr - \code{\link[=model-method-get_cmdstan_args]{$get_cmdstan_args()}} \tab Get CmdStan default argument values for a method. \cr + \code{\link[=model-method-cmdstan_defaults]{$cmdstan_defaults()}} \tab Get CmdStan default argument values for a method. \cr } } diff --git a/man/model-method-check_syntax.Rd b/man/model-method-check_syntax.Rd index 1d1126e9..5725defd 100644 --- a/man/model-method-check_syntax.Rd +++ b/man/model-method-check_syntax.Rd @@ -78,12 +78,12 @@ The Stan and CmdStan documentation: } Other CmdStanModel methods: +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-get_cmdstan_args.Rd b/man/model-method-cmdstan_defaults.Rd similarity index 88% rename from man/model-method-get_cmdstan_args.Rd rename to man/model-method-cmdstan_defaults.Rd index d83115f3..fa45b200 100644 --- a/man/model-method-get_cmdstan_args.Rd +++ b/man/model-method-cmdstan_defaults.Rd @@ -1,11 +1,11 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/model.R -\name{model-method-get_cmdstan_args} -\alias{model-method-get_cmdstan_args} -\alias{get_cmdstan_args} +\name{model-method-cmdstan_defaults} +\alias{model-method-cmdstan_defaults} +\alias{cmdstan_defaults} \title{Get CmdStan default argument values} \usage{ -get_cmdstan_args( +cmdstan_defaults( method = c("sample", "optimize", "variational", "pathfinder", "laplace") ) } @@ -19,7 +19,7 @@ A named list of default argument values for the specified method, with cmdstanr-style argument names. } \description{ -The \verb{$get_cmdstan_args()} method of a \code{\link{CmdStanModel}} +The \verb{$cmdstan_defaults()} method of a \code{\link{CmdStanModel}} object queries the compiled model binary for the default argument values used by a given inference method. The returned list uses cmdstanr-style argument names (e.g., \code{iter_sampling} instead of @@ -31,8 +31,8 @@ The model must be compiled before calling this method. \dontrun{ mod <- cmdstan_model(file.path(cmdstan_path(), "examples/bernoulli/bernoulli.stan")) -mod$get_cmdstan_args("sample") -mod$get_cmdstan_args("optimize") +mod$cmdstan_defaults("sample") +mod$cmdstan_defaults("optimize") } } diff --git a/man/model-method-compile.Rd b/man/model-method-compile.Rd index 1e7249a0..1dd12463 100644 --- a/man/model-method-compile.Rd +++ b/man/model-method-compile.Rd @@ -146,11 +146,11 @@ The Stan and CmdStan documentation: Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-diagnose.Rd b/man/model-method-diagnose.Rd index 18335822..a9bb6a6a 100644 --- a/man/model-method-diagnose.Rd +++ b/man/model-method-diagnose.Rd @@ -146,11 +146,11 @@ The Stan and CmdStan documentation: Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-expose_functions.Rd b/man/model-method-expose_functions.Rd index c37cd0f4..92c105bb 100644 --- a/man/model-method-expose_functions.Rd +++ b/man/model-method-expose_functions.Rd @@ -70,11 +70,11 @@ The Stan and CmdStan documentation: Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-format.Rd b/man/model-method-format.Rd index 56a68b72..9d28973e 100644 --- a/man/model-method-format.Rd +++ b/man/model-method-format.Rd @@ -86,11 +86,11 @@ The Stan and CmdStan documentation: Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-generate-quantities.Rd b/man/model-method-generate-quantities.Rd index 6dbc48ec..e2131f63 100644 --- a/man/model-method-generate-quantities.Rd +++ b/man/model-method-generate-quantities.Rd @@ -184,11 +184,11 @@ The Stan and CmdStan documentation: Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-laplace.Rd b/man/model-method-laplace.Rd index aecc1e5c..6977e301 100644 --- a/man/model-method-laplace.Rd +++ b/man/model-method-laplace.Rd @@ -238,12 +238,12 @@ https://mc-stan.org/docs/cmdstan-guide/ \seealso{ Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, \code{\link{model-method-sample}}, diff --git a/man/model-method-optimize.Rd b/man/model-method-optimize.Rd index f73971ba..07f3c841 100644 --- a/man/model-method-optimize.Rd +++ b/man/model-method-optimize.Rd @@ -363,12 +363,12 @@ https://mc-stan.org/docs/cmdstan-guide/ \seealso{ Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-pathfinder}}, \code{\link{model-method-sample}}, diff --git a/man/model-method-pathfinder.Rd b/man/model-method-pathfinder.Rd index f726d0a1..58036c17 100644 --- a/man/model-method-pathfinder.Rd +++ b/man/model-method-pathfinder.Rd @@ -382,12 +382,12 @@ https://mc-stan.org/docs/cmdstan-guide/ \seealso{ Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-sample}}, diff --git a/man/model-method-sample.Rd b/man/model-method-sample.Rd index cc9d2894..f5285024 100644 --- a/man/model-method-sample.Rd +++ b/man/model-method-sample.Rd @@ -448,12 +448,12 @@ https://mc-stan.org/docs/cmdstan-guide/ \seealso{ Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-sample_mpi.Rd b/man/model-method-sample_mpi.Rd index e3327132..b0cc6bc9 100644 --- a/man/model-method-sample_mpi.Rd +++ b/man/model-method-sample_mpi.Rd @@ -343,12 +343,12 @@ details on MPI support in Stan. Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-variables.Rd b/man/model-method-variables.Rd index 9afb24f3..1415fdc8 100644 --- a/man/model-method-variables.Rd +++ b/man/model-method-variables.Rd @@ -38,12 +38,12 @@ mod$variables() \seealso{ Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/man/model-method-variational.Rd b/man/model-method-variational.Rd index 7c0358b6..684b152b 100644 --- a/man/model-method-variational.Rd +++ b/man/model-method-variational.Rd @@ -353,12 +353,12 @@ https://mc-stan.org/docs/cmdstan-guide/ \seealso{ Other CmdStanModel methods: \code{\link{model-method-check_syntax}}, +\code{\link{model-method-cmdstan_defaults}}, \code{\link{model-method-compile}}, \code{\link{model-method-diagnose}}, \code{\link{model-method-expose_functions}}, \code{\link{model-method-format}}, \code{\link{model-method-generate-quantities}}, -\code{\link{model-method-get_cmdstan_args}}, \code{\link{model-method-laplace}}, \code{\link{model-method-optimize}}, \code{\link{model-method-pathfinder}}, diff --git a/tests/testthat/test-model-get-cmdstan-args.R b/tests/testthat/test-model-cmdstan-defaults.R similarity index 79% rename from tests/testthat/test-model-get-cmdstan-args.R rename to tests/testthat/test-model-cmdstan-defaults.R index 29a3502c..9d42c8e4 100644 --- a/tests/testthat/test-model-get-cmdstan-args.R +++ b/tests/testthat/test-model-cmdstan-defaults.R @@ -1,28 +1,28 @@ set_cmdstan_path() mod <- testing_model("bernoulli") -test_that("get_cmdstan_args() errors for uncompiled model", { +test_that("cmdstan_defaults() errors for uncompiled model", { mod_uncompiled <- cmdstan_model( stan_file = testing_stan_file("bernoulli"), compile = FALSE ) expect_error( - mod_uncompiled$get_cmdstan_args("sample"), - "'$get_cmdstan_args()' requires a compiled model", + mod_uncompiled$cmdstan_defaults("sample"), + "'$cmdstan_defaults()' requires a compiled model", fixed = TRUE ) }) -test_that("get_cmdstan_args() errors for invalid method", { +test_that("cmdstan_defaults() errors for invalid method", { expect_error( - mod$get_cmdstan_args("bogus"), + mod$cmdstan_defaults("bogus"), "'arg' should be one of", fixed = TRUE ) }) -test_that("get_cmdstan_args() returns named list for sample", { - args <- mod$get_cmdstan_args("sample") +test_that("cmdstan_defaults() returns named list for sample", { + args <- mod$cmdstan_defaults("sample") expect_type(args, "list") expect_named(args) expected_names <- names(map_cmdstan_to_cmdstanr("sample")) @@ -31,8 +31,8 @@ test_that("get_cmdstan_args() returns named list for sample", { } }) -test_that("get_cmdstan_args() returns expected default types for sample", { - args <- mod$get_cmdstan_args("sample") +test_that("cmdstan_defaults() returns expected default types for sample", { + args <- mod$cmdstan_defaults("sample") expect_type(args$iter_sampling, "integer") expect_type(args$iter_warmup, "integer") expect_type(args$thin, "integer") @@ -41,8 +41,8 @@ test_that("get_cmdstan_args() returns expected default types for sample", { expect_type(args$max_treedepth, "integer") }) -test_that("get_cmdstan_args() works for optimize", { - args <- mod$get_cmdstan_args("optimize") +test_that("cmdstan_defaults() works for optimize", { + args <- mod$cmdstan_defaults("optimize") expect_type(args, "list") expect_named(args) expected_names <- names(map_cmdstan_to_cmdstanr("optimize")) @@ -53,8 +53,8 @@ test_that("get_cmdstan_args() works for optimize", { expect_type(args$iter, "integer") }) -test_that("get_cmdstan_args() works for variational", { - args <- mod$get_cmdstan_args("variational") +test_that("cmdstan_defaults() works for variational", { + args <- mod$cmdstan_defaults("variational") expect_type(args, "list") expect_named(args) expected_names <- names(map_cmdstan_to_cmdstanr("variational")) @@ -63,8 +63,8 @@ test_that("get_cmdstan_args() works for variational", { } }) -test_that("get_cmdstan_args() works for pathfinder", { - args <- mod$get_cmdstan_args("pathfinder") +test_that("cmdstan_defaults() works for pathfinder", { + args <- mod$cmdstan_defaults("pathfinder") expect_type(args, "list") expect_named(args) expected_names <- names(map_cmdstan_to_cmdstanr("pathfinder")) @@ -73,8 +73,8 @@ test_that("get_cmdstan_args() works for pathfinder", { } }) -test_that("get_cmdstan_args() works for laplace", { - args <- mod$get_cmdstan_args("laplace") +test_that("cmdstan_defaults() works for laplace", { + args <- mod$cmdstan_defaults("laplace") expect_type(args, "list") expect_named(args) expected_names <- names(map_cmdstan_to_cmdstanr("laplace")) From baf13bc2e5fa9c4dbbac2be732d3f6c88d7398f4 Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Tue, 31 Mar 2026 21:09:48 +0300 Subject: [PATCH 06/17] more elaborate default reading --- R/model.R | 70 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/R/model.R b/R/model.R index c7cb5665..3b546898 100644 --- a/R/model.R +++ b/R/model.R @@ -2286,26 +2286,57 @@ parse_cmdstan_args <- function(model_binary, method) { output <- strsplit(raw, "\r?\n")[[1]] arguments <- map_cmdstan_to_cmdstanr(method) - target_args <- vapply(arguments, function(p) { - parts <- strsplit(p, "\\.")[[1]] - parts[length(parts)] - }, FUN.VALUE = character(1), USE.NAMES = TRUE) result <- list() n <- length(output) + # Track the current hierarchical path using indentation. + # Each entry in path_stack is list(indent, name) representing a section + # at a given indentation level (e.g., "adapt" at indent 4). + path_stack <- list() for (i in seq_len(n)) { line <- output[i] content <- trimws(line) + # Skip blank lines so they don't reset the path stack + if (!nzchar(content)) next + + indent <- nchar(sub("^(\\s*).*", "\\1", line)) + + # Pop path_stack entries at deeper or equal indentation + while (length(path_stack) > 0 && + path_stack[[length(path_stack)]]$indent >= indent) { + path_stack[[length(path_stack)]] <- NULL + } + + # Match section headers like "adapt" or "algorithm" (bare names, no =) + section_match <- regmatches( + content, + regexec("^([a-z_][a-z0-9_]*)$", content) + )[[1]] + if (length(section_match) >= 2) { + path_stack[[length(path_stack) + 1]] <- list( + indent = indent, + name = section_match[2] + ) + next + } + # Match argument lines like "num_samples=" or "t0=" - arg_match <- regmatches(content, regexec("^([a-z_][a-z0-9_]*)=", content))[[1]] + arg_match <- regmatches( + content, + regexec("^([a-z_][a-z0-9_]*)=", content) + )[[1]] if (length(arg_match) >= 2) { arg_name <- arg_match[2] - # Check if this is one of our target arguments - matches <- which(target_args == arg_name) + # Build the full dotted path: method.section1.section2...arg_name + sections <- vapply(path_stack, `[[`, "name", FUN.VALUE = character(1)) + full_path <- paste(c(sections, arg_name), collapse = ".") + + # Check if this full path matches any of our target arguments + matches <- which(arguments == full_path) if (length(matches) > 0) { # Look ahead for "Defaults to" line @@ -2322,7 +2353,7 @@ parse_cmdstan_args <- function(model_binary, method) { # Add to result for each matching cmdstanr argument name for (m in matches) { - cmdstanr_name <- names(target_args)[m] + cmdstanr_name <- names(arguments)[m] result[[cmdstanr_name]] <- default_value } } @@ -2357,22 +2388,23 @@ map_cmdstan_to_cmdstanr <- function(method) { term_buffer = "sample.adapt.term_buffer", window = "sample.adapt.window", save_metric = "sample.adapt.save_metric", - max_treedepth = "sample.algorithm.hmc.engine.nuts.max_depth", - metric = "sample.algorithm.hmc.metric", - metric_file = "sample.algorithm.hmc.metric_file", - step_size = "sample.algorithm.hmc.stepsize" + max_treedepth = "sample.hmc.nuts.max_depth", + metric = "sample.hmc.metric", + metric_file = "sample.hmc.metric_file", + step_size = "sample.hmc.stepsize", + chains = "sample.num_chains" ), optimize = c( algorithm = "optimize.algorithm", jacobian = "optimize.jacobian", iter = "optimize.iter", - init_alpha = "optimize.algorithm.lbfgs.init_alpha", - tol_obj = "optimize.algorithm.lbfgs.tol_obj", - tol_rel_obj = "optimize.algorithm.lbfgs.tol_rel_obj", - tol_grad = "optimize.algorithm.lbfgs.tol_grad", - tol_rel_grad = "optimize.algorithm.lbfgs.tol_rel_grad", - tol_param = "optimize.algorithm.lbfgs.tol_param", - history_size = "optimize.algorithm.lbfgs.history_size" + init_alpha = "optimize.lbfgs.init_alpha", + tol_obj = "optimize.lbfgs.tol_obj", + tol_rel_obj = "optimize.lbfgs.tol_rel_obj", + tol_grad = "optimize.lbfgs.tol_grad", + tol_rel_grad = "optimize.lbfgs.tol_rel_grad", + tol_param = "optimize.lbfgs.tol_param", + history_size = "optimize.lbfgs.history_size" ), variational = c( algorithm = "variational.algorithm", From e64c6569168b40910d9465e3c53f1bfd12cb2cbc Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Tue, 31 Mar 2026 21:46:38 +0300 Subject: [PATCH 07/17] test that cmdstan defaults match the current known values --- tests/testthat/test-model-cmdstan-defaults.R | 73 ++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/testthat/test-model-cmdstan-defaults.R b/tests/testthat/test-model-cmdstan-defaults.R index 9d42c8e4..98ef7ae8 100644 --- a/tests/testthat/test-model-cmdstan-defaults.R +++ b/tests/testthat/test-model-cmdstan-defaults.R @@ -85,6 +85,79 @@ test_that("cmdstan_defaults() works for laplace", { expect_type(args$draws, "integer") }) +# default values ---------------------------------------------------------- + +test_that("cmdstan_defaults() returns expected default values for sample", { + args <- mod$cmdstan_defaults("sample") + expect_identical(args$iter_sampling, 1000L) + expect_identical(args$iter_warmup, 1000L) + expect_identical(args$save_warmup, FALSE) + expect_identical(args$thin, 1L) + expect_identical(args$adapt_engaged, TRUE) + expect_identical(args$adapt_delta, 0.8) + expect_identical(args$init_buffer, 75L) + expect_identical(args$term_buffer, 50L) + expect_identical(args$window, 25L) + expect_identical(args$save_metric, FALSE) + expect_identical(args$max_treedepth, 10L) + expect_identical(args$metric, "diag_e") + expect_identical(args$step_size, 1L) + expect_identical(args$chains, 1L) +}) + +test_that("cmdstan_defaults() returns expected default values for optimize", { + args <- mod$cmdstan_defaults("optimize") + expect_identical(args$algorithm, "lbfgs") + expect_identical(args$jacobian, FALSE) + expect_identical(args$iter, 2000L) + expect_identical(args$init_alpha, 0.001) + expect_identical(args$tol_obj, 1e-12) + expect_identical(args$tol_rel_obj, 10000L) + expect_identical(args$tol_grad, 1e-08) + expect_identical(args$tol_rel_grad, 1e+07) + expect_identical(args$tol_param, 1e-08) + expect_identical(args$history_size, 5L) +}) + +test_that("cmdstan_defaults() returns expected default values for variational", { + args <- mod$cmdstan_defaults("variational") + expect_identical(args$algorithm, "meanfield") + expect_identical(args$iter, 10000L) + expect_identical(args$grad_samples, 1L) + expect_identical(args$elbo_samples, 100L) + expect_identical(args$eta, 1L) + expect_identical(args$adapt_engaged, TRUE) + expect_identical(args$adapt_iter, 50L) + expect_identical(args$tol_rel_obj, 0.01) + expect_identical(args$eval_elbo, 100L) + expect_identical(args$draws, 1000L) +}) + +test_that("cmdstan_defaults() returns expected default values for pathfinder", { + args <- mod$cmdstan_defaults("pathfinder") + expect_identical(args$init_alpha, 0.001) + expect_identical(args$tol_obj, 1e-12) + expect_identical(args$tol_rel_obj, 10000L) + expect_identical(args$tol_grad, 1e-08) + expect_identical(args$tol_rel_grad, 1e+07) + expect_identical(args$tol_param, 1e-08) + expect_identical(args$history_size, 5L) + expect_identical(args$draws, 1000L) + expect_identical(args$num_paths, 4L) + expect_identical(args$save_single_paths, FALSE) + expect_identical(args$psis_resample, TRUE) + expect_identical(args$calculate_lp, TRUE) + expect_identical(args$max_lbfgs_iters, 1000L) + expect_identical(args$single_path_draws, 1000L) + expect_identical(args$num_elbo_draws, 25L) +}) + +test_that("cmdstan_defaults() returns expected default values for laplace", { + args <- mod$cmdstan_defaults("laplace") + expect_identical(args$jacobian, TRUE) + expect_identical(args$draws, 1000L) +}) + # internal helpers -------------------------------------------------------- test_that("parse_default_value() parses booleans", { From 5be13958b8272c937a6c602e678456b2415409d9 Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 31 Mar 2026 13:53:00 -0600 Subject: [PATCH 08/17] Update test-model-cmdstan-defaults.R * remove redundant tests for type (we already use `identical` to test values so this covers types too) * combine default values into a nested list so I could simplify the code to test them with a single `expect_cmdstan_defaults()` helper --- tests/testthat/test-model-cmdstan-defaults.R | 213 +++++++------------ 1 file changed, 79 insertions(+), 134 deletions(-) diff --git a/tests/testthat/test-model-cmdstan-defaults.R b/tests/testthat/test-model-cmdstan-defaults.R index 98ef7ae8..0db9f9d7 100644 --- a/tests/testthat/test-model-cmdstan-defaults.R +++ b/tests/testthat/test-model-cmdstan-defaults.R @@ -1,6 +1,82 @@ set_cmdstan_path() mod <- testing_model("bernoulli") +expected_cmdstan_defaults <- list( + sample = list( + iter_sampling = 1000L, + iter_warmup = 1000L, + save_warmup = FALSE, + thin = 1L, + adapt_engaged = TRUE, + adapt_delta = 0.8, + init_buffer = 75L, + term_buffer = 50L, + window = 25L, + save_metric = FALSE, + max_treedepth = 10L, + metric = "diag_e", + metric_file = "", + step_size = 1L, + chains = 1L + ), + optimize = list( + algorithm = "lbfgs", + init_alpha = 0.001, + tol_obj = 1e-12, + tol_rel_obj = 10000L, + tol_grad = 1e-08, + tol_rel_grad = 1e+07, + tol_param = 1e-08, + history_size = 5L, + jacobian = FALSE, + iter = 2000L + ), + variational = list( + algorithm = "meanfield", + iter = 10000L, + grad_samples = 1L, + elbo_samples = 100L, + eta = 1L, + adapt_engaged = TRUE, + adapt_iter = 50L, + tol_rel_obj = 0.01, + eval_elbo = 100L, + draws = 1000L + ), + pathfinder = list( + init_alpha = 0.001, + tol_obj = 1e-12, + tol_rel_obj = 10000L, + tol_grad = 1e-08, + tol_rel_grad = 1e+07, + tol_param = 1e-08, + history_size = 5L, + draws = 1000L, + num_paths = 4L, + save_single_paths = FALSE, + psis_resample = TRUE, + calculate_lp = TRUE, + max_lbfgs_iters = 1000L, + single_path_draws = 1000L, + num_elbo_draws = 25L + ), + laplace = list( + jacobian = TRUE, + draws = 1000L + ) +) + +expect_cmdstan_defaults <- function(method, expected) { + args <- mod$cmdstan_defaults(method) + expect_type(args, "list") + expect_named(args) + expect_setequal(names(args), names(expected)) + + for (name in names(expected)) { + expect_identical(args[[name]], expected[[name]], info = paste0(method, "$", name)) + } +} + test_that("cmdstan_defaults() errors for uncompiled model", { mod_uncompiled <- cmdstan_model( stan_file = testing_stan_file("bernoulli"), @@ -21,143 +97,12 @@ test_that("cmdstan_defaults() errors for invalid method", { ) }) -test_that("cmdstan_defaults() returns named list for sample", { - args <- mod$cmdstan_defaults("sample") - expect_type(args, "list") - expect_named(args) - expected_names <- names(map_cmdstan_to_cmdstanr("sample")) - for (nm in expected_names) { - expect_true(nm %in% names(args), info = paste0("missing: ", nm)) - } -}) - -test_that("cmdstan_defaults() returns expected default types for sample", { - args <- mod$cmdstan_defaults("sample") - expect_type(args$iter_sampling, "integer") - expect_type(args$iter_warmup, "integer") - expect_type(args$thin, "integer") - expect_type(args$adapt_delta, "double") - expect_type(args$save_warmup, "logical") - expect_type(args$max_treedepth, "integer") -}) - -test_that("cmdstan_defaults() works for optimize", { - args <- mod$cmdstan_defaults("optimize") - expect_type(args, "list") - expect_named(args) - expected_names <- names(map_cmdstan_to_cmdstanr("optimize")) - for (nm in expected_names) { - expect_true(nm %in% names(args), info = paste0("missing: ", nm)) - } - expect_type(args$jacobian, "logical") - expect_type(args$iter, "integer") -}) - -test_that("cmdstan_defaults() works for variational", { - args <- mod$cmdstan_defaults("variational") - expect_type(args, "list") - expect_named(args) - expected_names <- names(map_cmdstan_to_cmdstanr("variational")) - for (nm in expected_names) { - expect_true(nm %in% names(args), info = paste0("missing: ", nm)) - } -}) - -test_that("cmdstan_defaults() works for pathfinder", { - args <- mod$cmdstan_defaults("pathfinder") - expect_type(args, "list") - expect_named(args) - expected_names <- names(map_cmdstan_to_cmdstanr("pathfinder")) - for (nm in expected_names) { - expect_true(nm %in% names(args), info = paste0("missing: ", nm)) +test_that("cmdstan_defaults() returns expected names and values", { + for (method in names(expected_cmdstan_defaults)) { + expect_cmdstan_defaults(method, expected_cmdstan_defaults[[method]]) } }) -test_that("cmdstan_defaults() works for laplace", { - args <- mod$cmdstan_defaults("laplace") - expect_type(args, "list") - expect_named(args) - expected_names <- names(map_cmdstan_to_cmdstanr("laplace")) - for (nm in expected_names) { - expect_true(nm %in% names(args), info = paste0("missing: ", nm)) - } - expect_type(args$jacobian, "logical") - expect_type(args$draws, "integer") -}) - -# default values ---------------------------------------------------------- - -test_that("cmdstan_defaults() returns expected default values for sample", { - args <- mod$cmdstan_defaults("sample") - expect_identical(args$iter_sampling, 1000L) - expect_identical(args$iter_warmup, 1000L) - expect_identical(args$save_warmup, FALSE) - expect_identical(args$thin, 1L) - expect_identical(args$adapt_engaged, TRUE) - expect_identical(args$adapt_delta, 0.8) - expect_identical(args$init_buffer, 75L) - expect_identical(args$term_buffer, 50L) - expect_identical(args$window, 25L) - expect_identical(args$save_metric, FALSE) - expect_identical(args$max_treedepth, 10L) - expect_identical(args$metric, "diag_e") - expect_identical(args$step_size, 1L) - expect_identical(args$chains, 1L) -}) - -test_that("cmdstan_defaults() returns expected default values for optimize", { - args <- mod$cmdstan_defaults("optimize") - expect_identical(args$algorithm, "lbfgs") - expect_identical(args$jacobian, FALSE) - expect_identical(args$iter, 2000L) - expect_identical(args$init_alpha, 0.001) - expect_identical(args$tol_obj, 1e-12) - expect_identical(args$tol_rel_obj, 10000L) - expect_identical(args$tol_grad, 1e-08) - expect_identical(args$tol_rel_grad, 1e+07) - expect_identical(args$tol_param, 1e-08) - expect_identical(args$history_size, 5L) -}) - -test_that("cmdstan_defaults() returns expected default values for variational", { - args <- mod$cmdstan_defaults("variational") - expect_identical(args$algorithm, "meanfield") - expect_identical(args$iter, 10000L) - expect_identical(args$grad_samples, 1L) - expect_identical(args$elbo_samples, 100L) - expect_identical(args$eta, 1L) - expect_identical(args$adapt_engaged, TRUE) - expect_identical(args$adapt_iter, 50L) - expect_identical(args$tol_rel_obj, 0.01) - expect_identical(args$eval_elbo, 100L) - expect_identical(args$draws, 1000L) -}) - -test_that("cmdstan_defaults() returns expected default values for pathfinder", { - args <- mod$cmdstan_defaults("pathfinder") - expect_identical(args$init_alpha, 0.001) - expect_identical(args$tol_obj, 1e-12) - expect_identical(args$tol_rel_obj, 10000L) - expect_identical(args$tol_grad, 1e-08) - expect_identical(args$tol_rel_grad, 1e+07) - expect_identical(args$tol_param, 1e-08) - expect_identical(args$history_size, 5L) - expect_identical(args$draws, 1000L) - expect_identical(args$num_paths, 4L) - expect_identical(args$save_single_paths, FALSE) - expect_identical(args$psis_resample, TRUE) - expect_identical(args$calculate_lp, TRUE) - expect_identical(args$max_lbfgs_iters, 1000L) - expect_identical(args$single_path_draws, 1000L) - expect_identical(args$num_elbo_draws, 25L) -}) - -test_that("cmdstan_defaults() returns expected default values for laplace", { - args <- mod$cmdstan_defaults("laplace") - expect_identical(args$jacobian, TRUE) - expect_identical(args$draws, 1000L) -}) - # internal helpers -------------------------------------------------------- test_that("parse_default_value() parses booleans", { From 55329a5d65c6e53840d316f37fb442cd353231d2 Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 31 Mar 2026 14:01:39 -0600 Subject: [PATCH 09/17] Simplify cmdstan defaults path lookup --- R/model.R | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/R/model.R b/R/model.R index 3b546898..1a58c762 100644 --- a/R/model.R +++ b/R/model.R @@ -2286,6 +2286,8 @@ parse_cmdstan_args <- function(model_binary, method) { output <- strsplit(raw, "\r?\n")[[1]] arguments <- map_cmdstan_to_cmdstanr(method) + argument_paths <- unname(arguments) + cmdstanr_names <- names(arguments) result <- list() n <- length(output) @@ -2335,10 +2337,10 @@ parse_cmdstan_args <- function(model_binary, method) { sections <- vapply(path_stack, `[[`, "name", FUN.VALUE = character(1)) full_path <- paste(c(sections, arg_name), collapse = ".") - # Check if this full path matches any of our target arguments - matches <- which(arguments == full_path) + # Check if this full path matches one of our target arguments + match_idx <- match(full_path, argument_paths, nomatch = 0L) - if (length(matches) > 0) { + if (match_idx > 0L) { # Look ahead for "Defaults to" line default_value <- NULL for (j in (i + 1):min(i + 5, n)) { @@ -2351,11 +2353,7 @@ parse_cmdstan_args <- function(model_binary, method) { if (grepl("^[a-z_][a-z0-9_]*=", next_content)) break } - # Add to result for each matching cmdstanr argument name - for (m in matches) { - cmdstanr_name <- names(arguments)[m] - result[[cmdstanr_name]] <- default_value - } + result[[cmdstanr_names[[match_idx]]]] <- default_value } } } From 7189dc75b57aa7280ad995a6945790c9db0bfbd3 Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 31 Mar 2026 14:06:33 -0600 Subject: [PATCH 10/17] Use "key" instead of "path" to describe the hierarchical arguments --- R/model.R | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/R/model.R b/R/model.R index 1a58c762..c5141a02 100644 --- a/R/model.R +++ b/R/model.R @@ -2286,29 +2286,29 @@ parse_cmdstan_args <- function(model_binary, method) { output <- strsplit(raw, "\r?\n")[[1]] arguments <- map_cmdstan_to_cmdstanr(method) - argument_paths <- unname(arguments) + argument_keys <- unname(arguments) cmdstanr_names <- names(arguments) result <- list() n <- length(output) - # Track the current hierarchical path using indentation. - # Each entry in path_stack is list(indent, name) representing a section + # Track the current hierarchical argument key using indentation. + # Each entry in key_stack is list(indent, name) representing a section # at a given indentation level (e.g., "adapt" at indent 4). - path_stack <- list() + key_stack <- list() for (i in seq_len(n)) { line <- output[i] content <- trimws(line) - # Skip blank lines so they don't reset the path stack + # Skip blank lines so they don't reset the key stack if (!nzchar(content)) next indent <- nchar(sub("^(\\s*).*", "\\1", line)) - # Pop path_stack entries at deeper or equal indentation - while (length(path_stack) > 0 && - path_stack[[length(path_stack)]]$indent >= indent) { - path_stack[[length(path_stack)]] <- NULL + # Pop key_stack entries at deeper or equal indentation + while (length(key_stack) > 0 && + key_stack[[length(key_stack)]]$indent >= indent) { + key_stack[[length(key_stack)]] <- NULL } # Match section headers like "adapt" or "algorithm" (bare names, no =) @@ -2317,7 +2317,7 @@ parse_cmdstan_args <- function(model_binary, method) { regexec("^([a-z_][a-z0-9_]*)$", content) )[[1]] if (length(section_match) >= 2) { - path_stack[[length(path_stack) + 1]] <- list( + key_stack[[length(key_stack) + 1]] <- list( indent = indent, name = section_match[2] ) @@ -2333,12 +2333,12 @@ parse_cmdstan_args <- function(model_binary, method) { if (length(arg_match) >= 2) { arg_name <- arg_match[2] - # Build the full dotted path: method.section1.section2...arg_name - sections <- vapply(path_stack, `[[`, "name", FUN.VALUE = character(1)) - full_path <- paste(c(sections, arg_name), collapse = ".") + # Build the full dotted argument key: method.section1.section2...arg_name + sections <- vapply(key_stack, `[[`, "name", FUN.VALUE = character(1)) + full_key <- paste(c(sections, arg_name), collapse = ".") - # Check if this full path matches one of our target arguments - match_idx <- match(full_path, argument_paths, nomatch = 0L) + # Check if this full argument key matches one of our target arguments + match_idx <- match(full_key, argument_keys, nomatch = 0L) if (match_idx > 0L) { # Look ahead for "Defaults to" line From ebd3060bd04a9a570110e20f2b927a36791f4264 Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 31 Mar 2026 14:16:00 -0600 Subject: [PATCH 11/17] Simplify cmdstan defaults section stack --- R/model.R | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/R/model.R b/R/model.R index c5141a02..58f25552 100644 --- a/R/model.R +++ b/R/model.R @@ -2291,24 +2291,24 @@ parse_cmdstan_args <- function(model_binary, method) { result <- list() n <- length(output) - # Track the current hierarchical argument key using indentation. - # Each entry in key_stack is list(indent, name) representing a section - # at a given indentation level (e.g., "adapt" at indent 4). - key_stack <- list() + # Track the current hierarchical argument key using section indentation. + section_indents <- integer(0) + section_names <- character(0) for (i in seq_len(n)) { line <- output[i] content <- trimws(line) - # Skip blank lines so they don't reset the key stack + # Skip blank lines so they don't reset the section stack if (!nzchar(content)) next indent <- nchar(sub("^(\\s*).*", "\\1", line)) - # Pop key_stack entries at deeper or equal indentation - while (length(key_stack) > 0 && - key_stack[[length(key_stack)]]$indent >= indent) { - key_stack[[length(key_stack)]] <- NULL + # Drop sections at deeper or equal indentation + while (length(section_indents) > 0 && + section_indents[[length(section_indents)]] >= indent) { + section_indents <- section_indents[-length(section_indents)] + section_names <- section_names[-length(section_names)] } # Match section headers like "adapt" or "algorithm" (bare names, no =) @@ -2317,10 +2317,8 @@ parse_cmdstan_args <- function(model_binary, method) { regexec("^([a-z_][a-z0-9_]*)$", content) )[[1]] if (length(section_match) >= 2) { - key_stack[[length(key_stack) + 1]] <- list( - indent = indent, - name = section_match[2] - ) + section_indents <- c(section_indents, indent) + section_names <- c(section_names, section_match[2]) next } @@ -2334,8 +2332,7 @@ parse_cmdstan_args <- function(model_binary, method) { arg_name <- arg_match[2] # Build the full dotted argument key: method.section1.section2...arg_name - sections <- vapply(key_stack, `[[`, "name", FUN.VALUE = character(1)) - full_key <- paste(c(sections, arg_name), collapse = ".") + full_key <- paste(c(section_names, arg_name), collapse = ".") # Check if this full argument key matches one of our target arguments match_idx <- match(full_key, argument_keys, nomatch = 0L) From 90d5f3af223b9180fe36ad2b2fa16e1091eb0d2c Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 31 Mar 2026 14:30:07 -0600 Subject: [PATCH 12/17] Create some small helper functions to improve readability of main code --- R/model.R | 65 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/R/model.R b/R/model.R index 58f25552..ca6514e7 100644 --- a/R/model.R +++ b/R/model.R @@ -2311,25 +2311,15 @@ parse_cmdstan_args <- function(model_binary, method) { section_names <- section_names[-length(section_names)] } - # Match section headers like "adapt" or "algorithm" (bare names, no =) - section_match <- regmatches( - content, - regexec("^([a-z_][a-z0-9_]*)$", content) - )[[1]] - if (length(section_match) >= 2) { + section_name <- parse_cmdstan_section_name(content) + if (!is.null(section_name)) { section_indents <- c(section_indents, indent) - section_names <- c(section_names, section_match[2]) + section_names <- c(section_names, section_name) next } - # Match argument lines like "num_samples=" or "t0=" - arg_match <- regmatches( - content, - regexec("^([a-z_][a-z0-9_]*)=", content) - )[[1]] - - if (length(arg_match) >= 2) { - arg_name <- arg_match[2] + arg_name <- parse_cmdstan_arg_name(content) + if (!is.null(arg_name)) { # Build the full dotted argument key: method.section1.section2...arg_name full_key <- paste(c(section_names, arg_name), collapse = ".") @@ -2338,18 +2328,7 @@ parse_cmdstan_args <- function(model_binary, method) { match_idx <- match(full_key, argument_keys, nomatch = 0L) if (match_idx > 0L) { - # Look ahead for "Defaults to" line - default_value <- NULL - for (j in (i + 1):min(i + 5, n)) { - next_content <- trimws(output[j]) - if (grepl("^Defaults to", next_content)) { - default_value <- parse_default_value(next_content) - break - } - # Stop if we hit another argument - if (grepl("^[a-z_][a-z0-9_]*=", next_content)) break - } - + default_value <- find_cmdstan_default_value(output, i, n) result[[cmdstanr_names[[match_idx]]]] <- default_value } } @@ -2358,6 +2337,38 @@ parse_cmdstan_args <- function(model_binary, method) { result } +#' Parse CmdStan section name from a help-all line +#' @noRd +parse_cmdstan_section_name <- function(line) { + match <- regmatches(line, regexec("^([a-z_][a-z0-9_]*)$", line))[[1]] + if (length(match) >= 2) match[2] else NULL +} + +#' Parse CmdStan argument name from a help-all line +#' @noRd +parse_cmdstan_arg_name <- function(line) { + match <- regmatches(line, regexec("^([a-z_][a-z0-9_]*)=", line))[[1]] + if (length(match) >= 2) match[2] else NULL +} + +#' Find CmdStan default value following a help-all argument line +#' @noRd +find_cmdstan_default_value <- function(output, line_idx, n_lines) { + default_value <- NULL + + for (j in (line_idx + 1):min(line_idx + 5, n_lines)) { + next_content <- trimws(output[j]) + if (grepl("^Defaults to", next_content)) { + default_value <- parse_default_value(next_content) + break + } + # Stop if we hit another argument + if (grepl("^[a-z_][a-z0-9_]*=", next_content)) break + } + + default_value +} + #' Parse default value from "Defaults to ..." line #' @noRd parse_default_value <- function(line) { From d2cff542aeae60915e4b0a2ea47135bdc7666a46 Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 31 Mar 2026 14:32:19 -0600 Subject: [PATCH 13/17] Rename a few variables for clarity --- R/model.R | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/R/model.R b/R/model.R index ca6514e7..d70987fe 100644 --- a/R/model.R +++ b/R/model.R @@ -2285,11 +2285,11 @@ parse_cmdstan_args <- function(model_binary, method) { raw <- paste0(ret$stdout, ret$stderr) output <- strsplit(raw, "\r?\n")[[1]] - arguments <- map_cmdstan_to_cmdstanr(method) - argument_keys <- unname(arguments) - cmdstanr_names <- names(arguments) + argument_map <- map_cmdstan_to_cmdstanr(method) + cmdstan_keys <- unname(argument_map) + public_names <- names(argument_map) - result <- list() + defaults <- list() n <- length(output) # Track the current hierarchical argument key using section indentation. section_indents <- integer(0) @@ -2325,16 +2325,16 @@ parse_cmdstan_args <- function(model_binary, method) { full_key <- paste(c(section_names, arg_name), collapse = ".") # Check if this full argument key matches one of our target arguments - match_idx <- match(full_key, argument_keys, nomatch = 0L) + match_idx <- match(full_key, cmdstan_keys, nomatch = 0L) if (match_idx > 0L) { default_value <- find_cmdstan_default_value(output, i, n) - result[[cmdstanr_names[[match_idx]]]] <- default_value + defaults[[public_names[[match_idx]]]] <- default_value } } } - result + defaults } #' Parse CmdStan section name from a help-all line From ec68b51085fe29200f103fa2a71567ed1cea6d60 Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 31 Mar 2026 14:33:27 -0600 Subject: [PATCH 14/17] Add one more code comment --- R/model.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/R/model.R b/R/model.R index d70987fe..fe3a2dc9 100644 --- a/R/model.R +++ b/R/model.R @@ -2322,6 +2322,8 @@ parse_cmdstan_args <- function(model_binary, method) { if (!is.null(arg_name)) { # Build the full dotted argument key: method.section1.section2...arg_name + # The top-level method heading (e.g. "sample") is tracked as a section, + # so it becomes the first segment of the key. full_key <- paste(c(section_names, arg_name), collapse = ".") # Check if this full argument key matches one of our target arguments From c9f04f87ce25d70d413bca0529a1d7588c63514b Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 31 Mar 2026 14:37:16 -0600 Subject: [PATCH 15/17] move helper functions into the "internal" code section of model.R --- R/model.R | 369 +++++++++++++++++++++++++++--------------------------- 1 file changed, 185 insertions(+), 184 deletions(-) diff --git a/R/model.R b/R/model.R index fe3a2dc9..ce7cef13 100644 --- a/R/model.R +++ b/R/model.R @@ -2255,7 +2255,128 @@ cmdstan_defaults <- function(method = c("sample", "optimize", "variational", CmdStanModel$set("public", name = "cmdstan_defaults", value = cmdstan_defaults) -# cmdstan_defaults helpers ------------------------------------------------ + +# internal ---------------------------------------------------------------- +assert_valid_stanc_options <- function(stanc_options) { + i <- 1 + names <- names(stanc_options) + for (s in stanc_options) { + if (!is.null(names[i]) && nzchar(names[i])) { + name <- names[i] + } else { + name <- s + } + if (startsWith(name, "--")) { + stop("No leading hyphens allowed in stanc options (", name, "). ", + "Use options without leading hyphens, for example ", + "`stanc_options = list('allow-undefined')`", + call. = FALSE) + } + i <- i + 1 + } + invisible(stanc_options) +} + +assert_stan_file_exists <- function(stan_file) { + if (!file.exists(stan_file)) { + stop("The Stan file used to create the `CmdStanModel` object does not exist.", call. = FALSE) + } +} + +include_paths_stanc3_args <- function(include_paths = NULL, standalone_call = FALSE) { + stancflags <- NULL + if (!is.null(include_paths)) { + assert_dir_exists(include_paths, access = "r") + include_paths <- sapply(absolute_path(include_paths), wsl_safe_path) + # Calling stanc3 directly through processx::run does not need quoting + if (!isTRUE(standalone_call)) { + paths_w_space <- grep(" ", include_paths) + include_paths[paths_w_space] <- paste0("'", include_paths[paths_w_space], "'") + } + include_paths <- paste0(include_paths, collapse = ",") + include_paths_flag <- "--include-paths=" + if (isTRUE(standalone_call)) { + stancflags <- c(stancflags, "--include-paths", include_paths) + } else { + stancflags <- paste0(stancflags, include_paths_flag, include_paths) + } + } + stancflags +} + +model_variables <- function(stan_file, include_paths = NULL, allow_undefined = FALSE) { + if (allow_undefined) { + allow_undefined_arg <- "--allow-undefined" + } else { + allow_undefined_arg <- NULL + } + out_file <- tempfile(fileext = ".json") + run_log <- wsl_compatible_run( + command = stanc_cmd(), + args = c(wsl_safe_path(stan_file), + "--info", + include_paths_stanc3_args(include_paths), + allow_undefined_arg), + wd = cmdstan_path(), + echo = FALSE, + echo_cmd = FALSE, + stdout = out_file, + error_on_status = TRUE + ) + variables <- jsonlite::read_json(out_file, na = "null") + variables$data <- variables$inputs + variables$inputs <- NULL + variables$transformed_parameters <- variables[["transformed parameters"]] + variables[["transformed parameters"]] <- NULL + variables$generated_quantities <- variables[["generated quantities"]] + variables[["generated quantities"]] <- NULL + variables$functions <- NULL + variables$distributions <- NULL + variables +} + +is_variables_method_supported <- function(mod) { + mod$has_stan_file() && file.exists(mod$stan_file()) +} + +resolve_exe_path <- function(dir = NULL, + private_dir = NULL, + self_exe_file = NULL, + self_stan_file = NULL) { + if (is.null(dir) && !is.null(private_dir)) { + dir <- absolute_path(private_dir) + } else if (!is.null(dir)) { + dir <- absolute_path(dir) + } + if (!is.null(dir)) { + dir <- repair_path(dir) + assert_dir_exists(dir, access = "rw") + if (length(self_exe_file) != 0) { + self_exe_file <- file.path(dir, basename(self_exe_file)) + } + } + if (length(self_exe_file) == 0) { + if (is.null(dir)) { + exe_base <- self_stan_file + } else { + exe_base <- file.path(dir, basename(self_stan_file)) + } + exe <- cmdstan_ext(strip_ext(exe_base)) + if (dir.exists(exe)) { + stop( + "There is a subfolder matching the model name ", + "in the same folder as the model! ", + "Please remove or rename the subfolder and try again.", + call. = FALSE + ) + } + } else { + exe <- self_exe_file + } + exe +} + +# cmdstan_defaults() helpers #' Parse CmdStan default argument values from model binary #' @@ -2385,189 +2506,69 @@ parse_default_value <- function(line) { #' @noRd map_cmdstan_to_cmdstanr <- function(method) { switch(method, - sample = c( - iter_sampling = "sample.num_samples", - iter_warmup = "sample.num_warmup", - save_warmup = "sample.save_warmup", - thin = "sample.thin", - adapt_engaged = "sample.adapt.engaged", - adapt_delta = "sample.adapt.delta", - init_buffer = "sample.adapt.init_buffer", - term_buffer = "sample.adapt.term_buffer", - window = "sample.adapt.window", - save_metric = "sample.adapt.save_metric", - max_treedepth = "sample.hmc.nuts.max_depth", - metric = "sample.hmc.metric", - metric_file = "sample.hmc.metric_file", - step_size = "sample.hmc.stepsize", - chains = "sample.num_chains" - ), - optimize = c( - algorithm = "optimize.algorithm", - jacobian = "optimize.jacobian", - iter = "optimize.iter", - init_alpha = "optimize.lbfgs.init_alpha", - tol_obj = "optimize.lbfgs.tol_obj", - tol_rel_obj = "optimize.lbfgs.tol_rel_obj", - tol_grad = "optimize.lbfgs.tol_grad", - tol_rel_grad = "optimize.lbfgs.tol_rel_grad", - tol_param = "optimize.lbfgs.tol_param", - history_size = "optimize.lbfgs.history_size" - ), - variational = c( - algorithm = "variational.algorithm", - iter = "variational.iter", - grad_samples = "variational.grad_samples", - elbo_samples = "variational.elbo_samples", - eta = "variational.eta", - adapt_engaged = "variational.adapt.engaged", - adapt_iter = "variational.adapt.iter", - tol_rel_obj = "variational.tol_rel_obj", - eval_elbo = "variational.eval_elbo", - draws = "variational.output_samples" - ), - pathfinder = c( - init_alpha = "pathfinder.init_alpha", - tol_obj = "pathfinder.tol_obj", - tol_rel_obj = "pathfinder.tol_rel_obj", - tol_grad = "pathfinder.tol_grad", - tol_rel_grad = "pathfinder.tol_rel_grad", - tol_param = "pathfinder.tol_param", - history_size = "pathfinder.history_size", - draws = "pathfinder.num_psis_draws", - num_paths = "pathfinder.num_paths", - save_single_paths = "pathfinder.save_single_paths", - psis_resample = "pathfinder.psis_resample", - calculate_lp = "pathfinder.calculate_lp", - max_lbfgs_iters = "pathfinder.max_lbfgs_iters", - single_path_draws = "pathfinder.num_draws", - num_elbo_draws = "pathfinder.num_elbo_draws" - ), - laplace = c( - jacobian = "laplace.jacobian", - draws = "laplace.draws" - ), - character(0) + sample = c( + iter_sampling = "sample.num_samples", + iter_warmup = "sample.num_warmup", + save_warmup = "sample.save_warmup", + thin = "sample.thin", + adapt_engaged = "sample.adapt.engaged", + adapt_delta = "sample.adapt.delta", + init_buffer = "sample.adapt.init_buffer", + term_buffer = "sample.adapt.term_buffer", + window = "sample.adapt.window", + save_metric = "sample.adapt.save_metric", + max_treedepth = "sample.hmc.nuts.max_depth", + metric = "sample.hmc.metric", + metric_file = "sample.hmc.metric_file", + step_size = "sample.hmc.stepsize", + chains = "sample.num_chains" + ), + optimize = c( + algorithm = "optimize.algorithm", + jacobian = "optimize.jacobian", + iter = "optimize.iter", + init_alpha = "optimize.lbfgs.init_alpha", + tol_obj = "optimize.lbfgs.tol_obj", + tol_rel_obj = "optimize.lbfgs.tol_rel_obj", + tol_grad = "optimize.lbfgs.tol_grad", + tol_rel_grad = "optimize.lbfgs.tol_rel_grad", + tol_param = "optimize.lbfgs.tol_param", + history_size = "optimize.lbfgs.history_size" + ), + variational = c( + algorithm = "variational.algorithm", + iter = "variational.iter", + grad_samples = "variational.grad_samples", + elbo_samples = "variational.elbo_samples", + eta = "variational.eta", + adapt_engaged = "variational.adapt.engaged", + adapt_iter = "variational.adapt.iter", + tol_rel_obj = "variational.tol_rel_obj", + eval_elbo = "variational.eval_elbo", + draws = "variational.output_samples" + ), + pathfinder = c( + init_alpha = "pathfinder.init_alpha", + tol_obj = "pathfinder.tol_obj", + tol_rel_obj = "pathfinder.tol_rel_obj", + tol_grad = "pathfinder.tol_grad", + tol_rel_grad = "pathfinder.tol_rel_grad", + tol_param = "pathfinder.tol_param", + history_size = "pathfinder.history_size", + draws = "pathfinder.num_psis_draws", + num_paths = "pathfinder.num_paths", + save_single_paths = "pathfinder.save_single_paths", + psis_resample = "pathfinder.psis_resample", + calculate_lp = "pathfinder.calculate_lp", + max_lbfgs_iters = "pathfinder.max_lbfgs_iters", + single_path_draws = "pathfinder.num_draws", + num_elbo_draws = "pathfinder.num_elbo_draws" + ), + laplace = c( + jacobian = "laplace.jacobian", + draws = "laplace.draws" + ), + character(0) ) } - -# internal ---------------------------------------------------------------- -assert_valid_stanc_options <- function(stanc_options) { - i <- 1 - names <- names(stanc_options) - for (s in stanc_options) { - if (!is.null(names[i]) && nzchar(names[i])) { - name <- names[i] - } else { - name <- s - } - if (startsWith(name, "--")) { - stop("No leading hyphens allowed in stanc options (", name, "). ", - "Use options without leading hyphens, for example ", - "`stanc_options = list('allow-undefined')`", - call. = FALSE) - } - i <- i + 1 - } - invisible(stanc_options) -} - -assert_stan_file_exists <- function(stan_file) { - if (!file.exists(stan_file)) { - stop("The Stan file used to create the `CmdStanModel` object does not exist.", call. = FALSE) - } -} - -include_paths_stanc3_args <- function(include_paths = NULL, standalone_call = FALSE) { - stancflags <- NULL - if (!is.null(include_paths)) { - assert_dir_exists(include_paths, access = "r") - include_paths <- sapply(absolute_path(include_paths), wsl_safe_path) - # Calling stanc3 directly through processx::run does not need quoting - if (!isTRUE(standalone_call)) { - paths_w_space <- grep(" ", include_paths) - include_paths[paths_w_space] <- paste0("'", include_paths[paths_w_space], "'") - } - include_paths <- paste0(include_paths, collapse = ",") - include_paths_flag <- "--include-paths=" - if (isTRUE(standalone_call)) { - stancflags <- c(stancflags, "--include-paths", include_paths) - } else { - stancflags <- paste0(stancflags, include_paths_flag, include_paths) - } - } - stancflags -} - -model_variables <- function(stan_file, include_paths = NULL, allow_undefined = FALSE) { - if (allow_undefined) { - allow_undefined_arg <- "--allow-undefined" - } else { - allow_undefined_arg <- NULL - } - out_file <- tempfile(fileext = ".json") - run_log <- wsl_compatible_run( - command = stanc_cmd(), - args = c(wsl_safe_path(stan_file), - "--info", - include_paths_stanc3_args(include_paths), - allow_undefined_arg), - wd = cmdstan_path(), - echo = FALSE, - echo_cmd = FALSE, - stdout = out_file, - error_on_status = TRUE - ) - variables <- jsonlite::read_json(out_file, na = "null") - variables$data <- variables$inputs - variables$inputs <- NULL - variables$transformed_parameters <- variables[["transformed parameters"]] - variables[["transformed parameters"]] <- NULL - variables$generated_quantities <- variables[["generated quantities"]] - variables[["generated quantities"]] <- NULL - variables$functions <- NULL - variables$distributions <- NULL - variables -} - - -is_variables_method_supported <- function(mod) { - mod$has_stan_file() && file.exists(mod$stan_file()) -} -resolve_exe_path <- function(dir = NULL, - private_dir = NULL, - self_exe_file = NULL, - self_stan_file = NULL) { - if (is.null(dir) && !is.null(private_dir)) { - dir <- absolute_path(private_dir) - } else if (!is.null(dir)) { - dir <- absolute_path(dir) - } - if (!is.null(dir)) { - dir <- repair_path(dir) - assert_dir_exists(dir, access = "rw") - if (length(self_exe_file) != 0) { - self_exe_file <- file.path(dir, basename(self_exe_file)) - } - } - if (length(self_exe_file) == 0) { - if (is.null(dir)) { - exe_base <- self_stan_file - } else { - exe_base <- file.path(dir, basename(self_stan_file)) - } - exe <- cmdstan_ext(strip_ext(exe_base)) - if (dir.exists(exe)) { - stop( - "There is a subfolder matching the model name ", - "in the same folder as the model! ", - "Please remove or rename the subfolder and try again.", - call. = FALSE - ) - } - } else { - exe <- self_exe_file - } - exe -} From ed257c794df5af3e61c8f864aa1453cf37a49c5e Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Wed, 1 Apr 2026 09:39:01 +0300 Subject: [PATCH 16/17] drop chains = "sample.num_chains" from the default map --- R/model.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/R/model.R b/R/model.R index ce7cef13..0359678a 100644 --- a/R/model.R +++ b/R/model.R @@ -2520,8 +2520,7 @@ map_cmdstan_to_cmdstanr <- function(method) { max_treedepth = "sample.hmc.nuts.max_depth", metric = "sample.hmc.metric", metric_file = "sample.hmc.metric_file", - step_size = "sample.hmc.stepsize", - chains = "sample.num_chains" + step_size = "sample.hmc.stepsize" ), optimize = c( algorithm = "optimize.algorithm", From b807f1600bd3fc1ef46c5ec8a18fb596dea69651 Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Wed, 1 Apr 2026 09:41:21 +0300 Subject: [PATCH 17/17] update defaults test --- tests/testthat/test-model-cmdstan-defaults.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/testthat/test-model-cmdstan-defaults.R b/tests/testthat/test-model-cmdstan-defaults.R index 0db9f9d7..608bd3e3 100644 --- a/tests/testthat/test-model-cmdstan-defaults.R +++ b/tests/testthat/test-model-cmdstan-defaults.R @@ -16,8 +16,7 @@ expected_cmdstan_defaults <- list( max_treedepth = 10L, metric = "diag_e", metric_file = "", - step_size = 1L, - chains = 1L + step_size = 1L ), optimize = list( algorithm = "lbfgs",