Skip to content

Commit b1a767a

Browse files
authored
Merge pull request #818 from martinmodrak/813-user_header
Improve handling of user header
2 parents a9c898b + f4a7fb2 commit b1a767a

File tree

4 files changed

+142
-28
lines changed

4 files changed

+142
-28
lines changed

R/model.R

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,8 @@ CmdStanModel <- R6::R6Class(
397397
#' `functions` field in the compiled model object. This can also be done after
398398
#' compilation using the
399399
#' [`$expose_functions()`][model-method-expose_functions] method.
400+
#' @param dry_run (logical) If `TRUE`, the code will do all checks before compilation,
401+
#' but skip the actual C++ compilation. Used to speedup tests.
400402
#'
401403
#' @param threads Deprecated and will be removed in a future release. Please
402404
#' turn on threading via `cpp_options = list(stan_threads = TRUE)` instead.
@@ -450,8 +452,10 @@ compile <- function(quiet = TRUE,
450452
compile_model_methods = FALSE,
451453
compile_hessian_method = FALSE,
452454
compile_standalone = FALSE,
455+
dry_run = FALSE,
453456
#deprecated
454457
threads = FALSE) {
458+
455459
if (length(self$stan_file()) == 0) {
456460
stop("'$compile()' cannot be used because the 'CmdStanModel' was not created with a Stan file.", call. = FALSE)
457461
}
@@ -500,15 +504,63 @@ compile <- function(quiet = TRUE,
500504
exe <- self$exe_file()
501505
}
502506

507+
# Resolve stanc and cpp options
508+
if (pedantic) {
509+
stanc_options[["warn-pedantic"]] <- TRUE
510+
}
511+
512+
if (isTRUE(cpp_options$stan_opencl)) {
513+
stanc_options[["use-opencl"]] <- TRUE
514+
}
515+
516+
# Note that unlike cpp_options["USER_HEADER"], the user_header variable is deliberately
517+
# not transformed with wsl_safe_path() as that breaks the check below on WSLv1
518+
if (!is.null(user_header)) {
519+
if (!is.null(cpp_options[["USER_HEADER"]]) || !is.null(cpp_options[["user_header"]])) {
520+
warning("User header specified both via user_header argument and via cpp_options arguments")
521+
}
522+
523+
cpp_options[["USER_HEADER"]] <- wsl_safe_path(absolute_path(user_header))
524+
stanc_options[["allow-undefined"]] <- TRUE
525+
private$using_user_header_ <- TRUE
526+
}
527+
else if (!is.null(cpp_options[["USER_HEADER"]])) {
528+
if(!is.null(cpp_options[["user_header"]])) {
529+
warning('User header specified both via cpp_options[["USER_HEADER"]] and cpp_options[["user_header"]].', call. = FALSE)
530+
}
531+
532+
user_header <- cpp_options[["USER_HEADER"]]
533+
cpp_options[["USER_HEADER"]] <- wsl_safe_path(absolute_path(cpp_options[["USER_HEADER"]]))
534+
private$using_user_header_ <- TRUE
535+
}
536+
else if (!is.null(cpp_options[["user_header"]])) {
537+
user_header <- cpp_options[["user_header"]]
538+
cpp_options[["user_header"]] <- wsl_safe_path(absolute_path(cpp_options[["user_header"]]))
539+
private$using_user_header_ <- TRUE
540+
}
541+
542+
543+
if(!is.null(user_header)) {
544+
user_header <- absolute_path(user_header) # As mentioned above, just absolute, not wsl_safe_path()
545+
if(!file.exists(user_header)) {
546+
stop(paste0("User header file '", user_header, "' does not exist."), call. = FALSE)
547+
}
548+
}
549+
503550
# compile if:
504551
# - the user forced compilation,
505552
# - the executable does not exist
506553
# - the stan model was changed since last compilation
554+
# - a user header is used and the user header changed since last compilation (#813)
507555
if (!file.exists(exe)) {
508556
force_recompile <- TRUE
509557
} else if (file.exists(self$stan_file())
510558
&& file.mtime(exe) < file.mtime(self$stan_file())) {
511559
force_recompile <- TRUE
560+
} else if (!is.null(user_header)
561+
&& file.exists(user_header)
562+
&& file.mtime(exe) < file.mtime(user_header)) {
563+
force_recompile <- TRUE
512564
}
513565

514566
if (!force_recompile) {
@@ -530,7 +582,7 @@ compile <- function(quiet = TRUE,
530582

531583
if (os_is_wsl() && (compile_model_methods || compile_standalone)) {
532584
warning("Additional model methods and standalone functions are not ",
533-
"currently available with WSL CmdStan and will not be compiled",
585+
"currently available with WSLv1 CmdStan and will not be compiled.",
534586
call. = FALSE)
535587
compile_model_methods <- FALSE
536588
compile_standalone <- FALSE
@@ -548,23 +600,6 @@ compile <- function(quiet = TRUE,
548600

549601
stancflags_val <- include_paths_stanc3_args(include_paths)
550602

551-
if (pedantic) {
552-
stanc_options[["warn-pedantic"]] <- TRUE
553-
}
554-
555-
if (isTRUE(cpp_options$stan_opencl)) {
556-
stanc_options[["use-opencl"]] <- TRUE
557-
}
558-
if (!is.null(user_header)) {
559-
cpp_options[["USER_HEADER"]] <- wsl_safe_path(user_header)
560-
stanc_options[["allow-undefined"]] <- TRUE
561-
}
562-
if (!is.null(cpp_options[["USER_HEADER"]])) {
563-
cpp_options[["USER_HEADER"]] <- wsl_safe_path(absolute_path(cpp_options[["USER_HEADER"]]))
564-
}
565-
if (!is.null(cpp_options[["user_header"]])) {
566-
cpp_options[["user_header"]] <- wsl_safe_path(absolute_path(cpp_options[["user_header"]]))
567-
}
568603
if (is.null(stanc_options[["name"]])) {
569604
stanc_options[["name"]] <- paste0(self$model_name(), "_model")
570605
}
@@ -588,10 +623,17 @@ compile <- function(quiet = TRUE,
588623
self$functions$hpp_code <- get_standalone_hpp(temp_stan_file, stancflags_standalone)
589624
self$functions$external <- !is.null(user_header)
590625
self$functions$existing_exe <- FALSE
626+
627+
stancflags_val <- paste0("STANCFLAGS += ", stancflags_val, paste0(" ", stancflags_combined, collapse = " "))
628+
629+
if (dry_run) {
630+
return(invisible(self))
631+
}
632+
591633
if (compile_standalone) {
592634
expose_stan_functions(self$functions, !quiet)
593635
}
594-
stancflags_val <- paste0("STANCFLAGS += ", stancflags_val, paste0(" ", stancflags_combined, collapse = " "))
636+
595637
withr::with_path(
596638
c(
597639
toolchain_PATH_env_var(),

man/model-method-compile.Rd

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/helper-custom-expectations.R

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
1+
#' @param ... arguments passed to mod$compile()
2+
expect_compilation <- function(mod, ...) {
3+
if(length(mod$exe_file()) > 0 && file.exists(mod$exe_file())) {
4+
before_mtime <- file.mtime(mod$exe_file())
5+
} else {
6+
before_mtime <- NULL
7+
}
8+
expect_interactive_message(mod$compile(...), "Compiling Stan program...")
9+
if(length(mod$exe_file()) == 0 || !file.exists(mod$exe_file())) {
10+
fail(sprint("Model executable '%s' does not exist after compilation.", mod$exe_file()))
11+
}
12+
if(!is.null(before_mtime)) {
13+
after_mtime <- file.mtime(mod$exe_file())
14+
expect(before_mtime != after_mtime, sprintf("Exe file '%s' has NOT changed, despite expecting (re)compilation", mod$exe_file()))
15+
}
16+
invisible(mod)
17+
}
18+
19+
#' @param ... arguments passed to mod$compile()
20+
expect_no_recompilation <- function(mod, ...) {
21+
if(length(mod$exe_file()) == 0 || !file.exists(mod$exe_file())) {
22+
fail(sprint("Model executable '%s' does not exist, cannot test if recompilation is triggerred.", mod$exe_file()))
23+
}
24+
25+
before_mtime <- file.mtime(mod$exe_file())
26+
expect_interactive_message(mod$compile(...), "Model executable is up to date!")
27+
after_mtime <- file.mtime(mod$exe_file())
28+
expect(before_mtime == after_mtime, sprintf("Model executable '%s' has changed, despite expecting no recompilation", mod$exe_file()))
29+
invisible(mod)
30+
}
31+
132
expect_sample_output <- function(object, num_chains = NULL) {
233

334
output <- "Running MCMC with"

tests/testthat/test-model-compile.R

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ set_cmdstan_path()
44
stan_program <- cmdstan_example_file()
55
mod <- cmdstan_model(stan_file = stan_program, compile = FALSE)
66

7+
78
test_that("object initialized correctly", {
89
expect_equal(mod$stan_file(), stan_program)
910
expect_equal(mod$exe_file(), character(0))
@@ -28,8 +29,8 @@ test_that("compile() method works", {
2829
if (file.exists(exe)) {
2930
file.remove(exe)
3031
}
31-
expect_interactive_message(mod$compile(quiet = TRUE), "Compiling Stan program...")
32-
expect_interactive_message(mod$compile(quiet = TRUE), "Model executable is up to date!")
32+
expect_compilation(mod, quiet = TRUE)
33+
expect_no_recompilation(mod, quiet = TRUE)
3334
checkmate::expect_file_exists(mod$hpp_file())
3435
checkmate::expect_file_exists(exe)
3536
file.remove(exe)
@@ -39,10 +40,7 @@ test_that("compile() method works", {
3940

4041
test_that("compile() method forces recompilation force_recompile = TRUE", {
4142
mod$compile(quiet = TRUE)
42-
expect_interactive_message(
43-
mod$compile(quiet = TRUE, force_recompile = TRUE),
44-
"Compiling Stan program..."
45-
)
43+
expect_compilation(mod, quiet = TRUE, force_recompile = TRUE)
4644
})
4745

4846
test_that("compile() method forces recompilation if model modified", {
@@ -52,7 +50,7 @@ test_that("compile() method forces recompilation if model modified", {
5250
mod$compile(quiet = TRUE)
5351
}
5452
Sys.setFileTime(mod$stan_file(), Sys.time() + 1) #touch file to trigger recompile
55-
expect_interactive_message(mod$compile(quiet = TRUE), "Compiling Stan program...")
53+
expect_compilation(mod, quiet = TRUE)
5654
})
5755

5856
test_that("compile() method works with spaces in path", {
@@ -70,7 +68,7 @@ test_that("compile() method works with spaces in path", {
7068
if (file.exists(exe)) {
7169
file.remove(exe)
7270
}
73-
expect_interactive_message(mod_spaces$compile(), "Compiling Stan program...")
71+
expect_compilation(mod_spaces)
7472
file.remove(stan_model_with_spaces)
7573
file.remove(exe)
7674
unlink(dir_with_spaces, recursive = TRUE)
@@ -156,6 +154,8 @@ test_that("switching threads on and off works without rebuild", {
156154
mod$compile(force_recompile = TRUE)
157155
after_mtime <- file.mtime(main_path_o)
158156
expect_equal(before_mtime, after_mtime)
157+
158+
expect_warning(mod$compile(threads = TRUE, dry_run = TRUE), "deprecated")
159159
})
160160

161161
test_that("multiple cpp_options work", {
@@ -595,7 +595,7 @@ test_that("cmdstan_model errors with no args ", {
595595
})
596596

597597
test_that("cmdstan_model works with user_header", {
598-
skip_if(os_is_macos() | (os_is_windows() && !os_is_wsl()))
598+
skip_if(os_is_macos())
599599
tmpfile <- tempfile(fileext = ".hpp")
600600
hpp <-
601601
"
@@ -627,6 +627,43 @@ test_that("cmdstan_model works with user_header", {
627627
stanc_options = list("allow-undefined")
628628
)
629629
expect_true(file.exists(mod_2$exe_file()))
630+
631+
# Check recompilation upon changing header
632+
expect_no_recompilation(mod, quiet = TRUE, user_header = tmpfile)
633+
634+
Sys.setFileTime(tmpfile, Sys.time() + 1) #touch file to trigger recompile
635+
expect_compilation(mod, quiet = TRUE, user_header = tmpfile)
636+
637+
# Alternative spec of user header
638+
expect_no_recompilation(mod,
639+
quiet = TRUE,
640+
cpp_options = list(user_header = tmpfile),
641+
dry_run = TRUE
642+
)
643+
644+
# Error/warning messages
645+
expect_error(
646+
cmdstan_model(
647+
stan_file = testing_stan_file("bernoulli_external"),
648+
cpp_options = list(USER_HEADER = "non_existent.hpp"),
649+
stanc_options = list("allow-undefined")
650+
),
651+
"header file '[^']*' does not exist"
652+
)
653+
654+
expect_warning(cmdstan_model(
655+
stan_file = testing_stan_file("bernoulli_external"),
656+
cpp_options = list(USER_HEADER = tmpfile, user_header = tmpfile),
657+
dry_run = TRUE),
658+
"User header specified both"
659+
)
660+
expect_warning(cmdstan_model(
661+
stan_file = testing_stan_file("bernoulli_external"),
662+
user_header = tmpfile,
663+
cpp_options = list(USER_HEADER = tmpfile),
664+
dry_run = TRUE),
665+
"User header specified both"
666+
)
630667
})
631668

632669
test_that("cmdstan_model cpp_options dont captialize cxxflags ", {

0 commit comments

Comments
 (0)