diff --git a/DESCRIPTION b/DESCRIPTION index 5259457..9bc69df 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -27,6 +27,7 @@ Imports: graphics, lhs, patchwork, + rlang, stats, utils Suggests: diff --git a/R/SmallFuncs.R b/R/SmallFuncs.R index bb1cfa8..9e9bd04 100644 --- a/R/SmallFuncs.R +++ b/R/SmallFuncs.R @@ -34,9 +34,10 @@ randParams <- function(boundsDT, rPoints, FAIL = TRUE) { } if (attempt >= 100 && FAIL) { - stop( - "Latin Hypercube Sampling could not produce the required distinct parameter sets. \nTry decreasing gsPoints or initPoints." - ) + cli::cli_abort(c( + "Latin Hypercube Sampling could not produce the required distinct parameter sets.", + i = "Try decreasing {.arg gsPoints} or {.arg initPoints}." + )) } attempt <- attempt + 1 @@ -88,7 +89,7 @@ zeroOneScale <- function(vec) { # If the scoring function returned the same results # this results in the function a vector of 1s. if (r == 0) { - stop("Results from FUN have 0 variance, cannot build GP.") + cli::cli_abort("Results from {.fn FUN} have 0 variance, cannot build GP.") } vec <- (vec - min(vec)) / r @@ -122,17 +123,15 @@ saveSoFar <- function(optObj, verbose) { { suppressWarnings(saveRDS(optObj, file = optObj$saveFile)) if (verbose > 0) { - cat( - " 4) Saving Intermediary Results to: \n ", - optObj$saveFile, - "\n" - ) + cli::cli_inform(c( + " " = "4) Saved intermediary results to {.file {optObj$saveFile}}." + )) } }, error = function(e) { if (verbose > 0) { - cat(cli::col_red( - " 4) Failed to save intermediary results. Please check file path.\n" + cli::cli_inform(c( + x = "4) Failed to save intermediary results. Please check file path." )) } } @@ -176,63 +175,86 @@ checkParameters <- function( errorHandling, plotProgress, parallel, - verbose + verbose, + call = rlang::caller_env() ) { if (iters.n < iters.k) { - stop( - "iters.n cannot be less than iters.k. See ?bayesOpt for parameter definitions." + cli::cli_abort( + c( + "{.arg iters.n} cannot be less than {.arg iters.k}.", + i = "See {.help [?bayesOpt](ParBayesianOptimization::bayesOpt)} for parameter definitions." + ), + call = call ) } if (iters.n %% 1 != 0 || iters.k %% 1 != 0) { - stop("iters.n and iters.k must be integers.") + cli::cli_abort( + "{.arg iters.n} and {.arg iters.k} must be integers.", + call = call + ) } if ( !is.character(acq) || length(acq) != 1 || !acq %in% c("ucb", "ei", "eips", "poi") ) { - stop( - "Acquisition function not recognized. ", - "acq must be one of \"ucb\", \"ei\", \"eips\" or \"poi\"." + cli::cli_abort( + c( + "Acquisition function not recognized.", + i = "{.arg acq} must be one of {.or {.val {c('ucb', 'ei', 'eips', 'poi')}}}." + ), + call = call ) } if (parallel && (getDoParWorkers() == 1)) { - stop("parallel is set to TRUE but no back end is registered.\n") + cli::cli_abort( + "{.arg parallel} is set to TRUE but no back end is registered.", + call = call + ) } if (!parallel && getDoParWorkers() > 1 && verbose > 0) { - message( - "parallel back end is registered, but parallel is set to false. Process will not be run in parallel." + cli::cli_inform( + "A parallel back end is registered, but {.arg parallel} is FALSE. Process will not be run in parallel." ) } if (!all(names(otherHalting) %in% c("timeLimit", "minUtility"))) { - stop( - "otherHalting element not recognized. Must be one of timeLimit and minUtility." + cli::cli_abort( + c( + "{.arg otherHalting} element not recognized.", + i = "Must be one of {.val timeLimit} and {.val minUtility}." + ), + call = call ) } if (!inherits(x = bounds, what = "list")) { - stop( - "bounds must be a list of parameter bounds with the same arguments as FUN." + cli::cli_abort( + "{.arg bounds} must be a list of parameter bounds with the same arguments as {.fn FUN}.", + call = call ) } if (any(lengths(bounds) != 2)) { - stop("Not all elements in bounds are length 2.") + cli::cli_abort( + "Not all elements in {.arg bounds} are length 2.", + call = call + ) } if (acqThresh > 1 || acqThresh < 0) { - stop("acqThresh must be in [0,1]") + cli::cli_abort("{.arg acqThresh} must be in [0,1].", call = call) } if (!is.logical(plotProgress)) { - stop("plotProgress must be logical") + cli::cli_abort("{.arg plotProgress} must be logical.", call = call) } if (!errorHandling %in% c("stop", "continue") && !is.numeric(errorHandling)) { - stop( - "errorHandling is malformed: Must be one of 'stop', 'continue', or an integer." + cli::cli_abort( + "{.arg errorHandling} is malformed: Must be one of {.val stop}, {.val continue}, or an integer.", + call = call ) } } # Arguments in ... are passed to DiceKriging::km(). Catch misspelled or # unsupported names before FUN runs, instead of when the GP is fitted. -checkKmArgs <- function(...) { +checkKmArgs <- function(..., call = rlang::caller_env()) { argNames <- names(list(...)) if (is.null(argNames) && ...length() > 0) { argNames <- rep("", ...length()) @@ -249,19 +271,22 @@ checkKmArgs <- function(...) { !is.na(pmatch(argNames, kmArgs, duplicates.ok = TRUE)) & argNames != "" if (any(collides)) { - stop( - "Arguments in ... match a DiceKriging::km() argument that is already ", - "supplied or set by updateGP() (design, response, control): ", - paste(argNames[collides], collapse = ", ") + cli::cli_abort( + c( + "Arguments in {.arg ...} match a {.fn DiceKriging::km} argument that is already supplied or set by {.fn updateGP} (design, response, control): {argNames[collides]}." + ), + call = call ) } bad <- argNames[unmatched] if (length(bad) > 0) { bad[bad == ""] <- "" - stop( - "Arguments in ... are passed to DiceKriging::km(), which does not accept: ", - paste(bad, collapse = ", "), - ". Check for misspelled arguments." + cli::cli_abort( + c( + "Arguments in {.arg ...} are passed to {.fn DiceKriging::km}, which does not accept: {bad}.", + i = "Check for misspelled arguments." + ), + call = call ) } } @@ -282,10 +307,6 @@ formatOtherHalting <- function(otherHalting) { return(otherHalting) } -# When the process stops early it will print this color. -# Built on each call so it uses the colours of the current terminal. -returnEarly <- function(...) cli::make_ansi_style("#FF6200")(...) - # Constructor for stopEarlyMsg class. makeStopEarlyMessage <- function(msg) { class(msg) <- "stopEarlyMsg" @@ -294,9 +315,13 @@ makeStopEarlyMessage <- function(msg) { # Multiple places the process can stop early. This just prints the message. printStopStatus <- function(optObj, verbose) { - if (verbose > 0) cat(returnEarly("\n", optObj$stopStatus, "\n")) + if (verbose > 0) cli::cli_inform(c("!" = "{unclass(optObj$stopStatus)}")) } +# Printed lines of a table. Messages built from them keep the table layout, +# which cli formatting would collapse. +tableLines <- function(tbl, ...) capture.output(print(tbl, ...)) + # Combining function for foreach. Allows the return of message without scores. rbindFE <- function(...) rbind(..., fill = TRUE) diff --git a/R/addIterations.R b/R/addIterations.R index 6500782..5d984a9 100644 --- a/R/addIterations.R +++ b/R/addIterations.R @@ -70,7 +70,7 @@ addIterations <- function( ) { startT <- Sys.time() if (!inherits(x = optObj, what = "bayesOpt")) { - stop("optObj must be of class bayesOpt") + cli::cli_abort("{.arg optObj} must be of class {.cls bayesOpt}.") } # Check the parameters @@ -119,13 +119,8 @@ addIterations <- function( scoreSummary$inBounds <- as.logical(apply(inBounds, 1, prod)) if (!all(scoreSummary$inBounds)) { nOut <- sum(!scoreSummary$inBounds) - warning( - cli::format_inline( - "Bounds have been tightened. {nOut} parameter set{?s} in ", - "scoreSummary {?is/are} outside the new bounds and will be ignored ", - "this round." - ), - call. = FALSE + cli::cli_warn( + "Bounds have been tightened. {nOut} parameter set{?s} in scoreSummary {?is/are} outside the new bounds and will be ignored this round." ) } # updateGP() fits only on rows flagged inBounds, so store the new flags and @@ -135,10 +130,10 @@ addIterations <- function( optObj$GauProList$gpUpToDate <- FALSE } if (sum(scoreSummary$inBounds & is.na(scoreSummary$errorMessage)) <= 2) { - stop( - "Not enough samples in scoreSummary to perform optimizations. ", - "At least 3 error-free samples within bounds are needed." - ) + cli::cli_abort(c( + "Not enough samples in scoreSummary to perform optimizations.", + i = "At least 3 error-free samples within bounds are needed." + )) } # Output from FUN is sunk into a temporary file. @@ -159,7 +154,7 @@ addIterations <- function( Epoch <- Epoch + 1 if (verbose > 0) { - cat("\nStarting Epoch", Epoch, "\n") + cli::cli_inform("Starting Epoch {Epoch}") } # How many runs to make this session @@ -167,7 +162,7 @@ addIterations <- function( # Fit GP if (verbose > 0) { - cat(" 1) Fitting Gaussian Process...\n") + cli::cli_inform(c(" " = "1) Fitting Gaussian Process...")) } optObj <- updateGP(optObj, bounds = bounds, verbose = 0, ...) @@ -180,9 +175,6 @@ addIterations <- function( } # Find local optimums of the acquisition function - if (verbose > 0) { - cat(" 2) Running local optimum search...") - } tm <- system.time({ LocalOptims <- getLocalOptimums( optObj, @@ -192,7 +184,7 @@ addIterations <- function( ) })[[3]] if (verbose > 0) { - cat(" ", tm, "seconds\n") + cli::cli_inform(c(" " = "2) Ran local optimum search: {tm} seconds.")) } # Should we continue? @@ -240,15 +232,6 @@ addIterations <- function( # Try to run the scoring function. If not all (but at least 1) new runs fail, # then foreach cannot call rbind correctly, and an error is thrown. - if (verbose > 0) { - cat( - " 3) Running FUN", - nrow(nextPars), - "times in", - Workers, - "thread(s)..." - ) - } sink(file = sinkFile) tm <- system.time({ NewResults <- foreach( @@ -321,35 +304,43 @@ addIterations <- function( ) if (verbose > 0) { - cat(" ", tm, "seconds\n") + cli::cli_inform(c( + " " = "3) Ran {.fn FUN} {nrow(nextPars)} time{?s} in {Workers} thread{?s}: {tm} seconds." + )) } # Print updates on parameter-score search if (verbose > 1) { - cat("\nResults from most recent parameter scoring:\n") - print(NewResults, row.names = FALSE) + # Plain message() keeps the table layout. + message(paste( + c( + "Results from most recent parameter scoring:", + tableLines(NewResults, row.names = FALSE) + ), + collapse = "\n" + )) if (max(NewResults$Score) > max(scoreSummary$Score)) { - cat("\nNew best parameter set found:\n") - print( - NewResults[ - which.max(get("Score")), - c(boundsDT$N, "Score"), - with = FALSE - ], - row.names = FALSE - ) + bestHeading <- "New best parameter set found:" + bestFrom <- NewResults } else { - cat("\nMaximum score was not raised this round. Best score is still:\n") - print( - scoreSummary[ - which.max(get("Score")), - c(boundsDT$N, "Score"), - with = FALSE - ], - row.names = FALSE - ) + bestHeading <- "Maximum score was not raised this round. Best score is still:" + bestFrom <- scoreSummary } + message(paste( + c( + bestHeading, + tableLines( + bestFrom[ + which.max(get("Score")), + c(boundsDT$N, "Score"), + with = FALSE + ], + row.names = FALSE + ) + ), + collapse = "\n" + )) } # Keep track of performance. diff --git a/R/bayesOpt.R b/R/bayesOpt.R index 493d5f5..def323b 100644 --- a/R/bayesOpt.R +++ b/R/bayesOpt.R @@ -262,14 +262,16 @@ bayesOpt <- function( # Initialization Setup if (is.null(initGrid) == is.null(initPoints)) { - stop("Please provide 1 of initGrid or initPoints, but not both.") + cli::cli_abort( + "Please provide 1 of {.arg initGrid} or {.arg initPoints}, but not both." + ) } if (!is.null(initGrid)) { setDT(initGrid) inBounds <- checkBounds(initGrid, bounds) inBounds <- as.logical(apply(inBounds, 1, prod)) if (!all(inBounds)) { - stop("initGrid not within bounds.") + cli::cli_abort("{.arg initGrid} not within {.arg bounds}.") } optObj$initPars$initialSample <- "User Provided Grid" initPoints <- nrow(initGrid) @@ -279,11 +281,13 @@ bayesOpt <- function( } optObj$initPars$initGrid <- initGrid if (nrow(initGrid) <= 2) { - stop("Cannot initialize with less than 3 samples.") + cli::cli_abort("Cannot initialize with less than 3 samples.") } optObj$initPars$initPoints <- nrow(initGrid) if (initPoints <= length(bounds)) { - stop("initPoints must be greater than the number of FUN inputs.") + cli::cli_abort( + "{.arg initPoints} must be greater than the number of {.fn FUN} inputs." + ) } # Output from FUN is sunk into a temporary file. @@ -308,15 +312,6 @@ bayesOpt <- function( } # Run initialization - if (verbose > 0) { - cat( - "\nRunning initial scoring function", - nrow(initGrid), - "times in", - Workers, - "thread(s)..." - ) - } sink(file = sinkFile) tm <- system.time({ scoreSummary <- foreach( @@ -369,7 +364,9 @@ bayesOpt <- function( sink() } if (verbose > 0) { - cat(" ", tm, "seconds\n") + cli::cli_inform( + "Ran initial scoring function {nrow(initGrid)} time{?s} in {Workers} thread{?s}: {tm} seconds." + ) } # Scan our list for any simpleErrors. If any exist, stop the process and return the errors. @@ -381,9 +378,11 @@ bayesOpt <- function( initGrid[se, ], errorMessage = sapply(scoreSummary[se], function(x) x$message) ) - stop( - "Errors encountered in initialization:\n", - paste(capture.output(print(errTable)), collapse = "\n") + rlang::abort( + paste( + c("Errors encountered in initialization:", tableLines(errTable)), + collapse = "\n" + ) ) } else { scoreSummary <- rbindlist(scoreSummary) @@ -426,7 +425,9 @@ bayesOpt <- function( # System.time function is not terribly precise for very small elapsed times. if (any(scoreSummary$Elapsed < 1) && acq == "eips") { - warning("FUN elapsed time is too low to be precise. Switching acq to 'ei'.") + cli::cli_warn( + "{.fn FUN} elapsed time is too low to be precise. Switching {.arg acq} to {.val ei}." + ) acq <- 'ei' } diff --git a/R/changeSaveFile.R b/R/changeSaveFile.R index cbff0ab..ae2ce43 100644 --- a/R/changeSaveFile.R +++ b/R/changeSaveFile.R @@ -27,7 +27,7 @@ #' @export changeSaveFile <- function(optObj, saveFile = NULL) { if (!inherits(x = optObj, what = "bayesOpt")) { - stop("optObj should be of class bayesOpt.") + cli::cli_abort("{.arg optObj} should be of class {.cls bayesOpt}.") } # See if saveFile can be written to. @@ -36,9 +36,10 @@ changeSaveFile <- function(optObj, saveFile = NULL) { toupper(substr(saveFile, nchar(saveFile) - 4 + 1, nchar(saveFile))) != ".RDS" ) { - stop( - "saveFile is saved as an RDS using saveRDS() - please change file extension in saveFile parameter." - ) + cli::cli_abort(c( + "{.arg saveFile} is saved as an RDS using {.fn saveRDS}.", + i = "Please change the file extension in {.arg saveFile} to {.file .RDS}." + )) } } optObj$saveFile <- saveFile diff --git a/R/getBestPars.R b/R/getBestPars.R index f369de3..63fb68f 100644 --- a/R/getBestPars.R +++ b/R/getBestPars.R @@ -31,7 +31,9 @@ getBestPars <- function( N = 1 ) { if (N > nrow(optObj$scoreSummary)) { - stop("N is greater than the iterations that have been run.") + cli::cli_abort( + "{.arg N} is greater than the iterations that have been run." + ) } if (N == 1) { diff --git a/R/getLocalOptimums.R b/R/getLocalOptimums.R index 04f974c..2e7c095 100644 --- a/R/getLocalOptimums.R +++ b/R/getLocalOptimums.R @@ -122,18 +122,18 @@ getLocalOptimums <- function( # Checking for convergence if (tryN >= 4) { if (verbose > 0) { - cat( - "\n - Maximum convergence attempts exceeded - process is probably sampling random points." - ) + cli::cli_inform(c( + " " = "- Maximum convergence attempts exceeded - process is probably sampling random points." + )) } continue <- FALSE } else if ( max(LocalOptims$gpUtility) < acqN$base || !any(LocalOptims$gradCount > 2) ) { if (verbose > 0) { - cat( - "\n - Convergence Not Found. Trying again with tighter parameters..." - ) + cli::cli_inform(c( + " " = "- Convergence Not Found. Trying again with tighter parameters..." + )) } gsPoints <- gsPoints * (tryN + 1) convThresh <- reduceThresh(convThresh) diff --git a/R/updateGP.R b/R/updateGP.R index 6ed380b..e0bb7b1 100644 --- a/R/updateGP.R +++ b/R/updateGP.R @@ -37,7 +37,7 @@ updateGP <- function(optObj, bounds = optObj$bounds, verbose = 1, ...) { if (optObj$GauProList$gpUpToDate) { if (verbose > 0) { - message("Gaussian Processes are already up to date.") + cli::cli_inform("Gaussian Processes are already up to date.") } return(optObj) } else { @@ -90,7 +90,7 @@ updateGP <- function(optObj, bounds = optObj$bounds, verbose = 1, ...) { } if (tries >= 10) { - warning("Could not obtain meaningful lengthscales.") + cli::cli_warn("Could not obtain meaningful lengthscales.") break } diff --git a/tests/testthat/test-bayesOpt1D.R b/tests/testthat/test-bayesOpt1D.R index 97d67a0..aae0887 100644 --- a/tests/testthat/test-bayesOpt1D.R +++ b/tests/testthat/test-bayesOpt1D.R @@ -181,3 +181,23 @@ test_that("addIterations() needs 3 usable samples within bounds", { "within bounds are needed" ) }) + +test_that("verbose output is sent as messages", { + skip_on_cran() + set.seed(1991) + + run <- function() { + bayesOpt( + function(x) list(Score = -x^2), + list(x = c(-5, 5)), + initPoints = 4, + iters.n = 2, + verbose = 2 + ) + } + + msgs <- paste(capture_messages(run()), collapse = "") + expect_match(msgs, "Starting Epoch 1") + expect_match(msgs, "Results from most recent parameter scoring") + expect_silent(suppressMessages(run())) +}) diff --git a/tests/testthat/test-changeSaveFile.R b/tests/testthat/test-changeSaveFile.R index 5a6f00a..94389c9 100644 --- a/tests/testthat/test-changeSaveFile.R +++ b/tests/testthat/test-changeSaveFile.R @@ -1,7 +1,7 @@ optObj <- structure(list(saveFile = NULL), class = "bayesOpt") test_that("changeSaveFile() rejects objects that are not bayesOpt", { - expect_error(changeSaveFile(list(), "x.rds"), "class bayesOpt") + expect_error(changeSaveFile(list(), "x.rds"), "of class") }) test_that("changeSaveFile() rejects extensions other than .rds", { diff --git a/tests/testthat/test-checkParameters.R b/tests/testthat/test-checkParameters.R index f6f8983..bfff2e3 100644 --- a/tests/testthat/test-checkParameters.R +++ b/tests/testthat/test-checkParameters.R @@ -21,7 +21,7 @@ test_that("valid parameters pass", { }) test_that("invalid parameters are errors", { - expect_error(checkArgs(iters.n = 1), "iters.n cannot be less than iters.k") + expect_error(checkArgs(iters.n = 1), "cannot be less than") expect_error(checkArgs(iters.n = 4.5), "must be integers") expect_error(checkArgs(iters.k = 1.5), "must be integers") expect_error(checkArgs(acq = "foo"), "Acquisition function not recognized") @@ -35,14 +35,17 @@ test_that("invalid parameters are errors", { ) expect_error( checkArgs(otherHalting = list(foo = 1)), - "otherHalting element not recognized" + "element not recognized" + ) + expect_error( + checkArgs(bounds = c(x = 1)), + "must be a list of parameter bounds" ) - expect_error(checkArgs(bounds = c(x = 1)), "bounds must be a list") expect_error(checkArgs(bounds = list(x = c(0, 1, 2))), "length 2") - expect_error(checkArgs(acqThresh = 1.5), "acqThresh must be in") - expect_error(checkArgs(acqThresh = -0.1), "acqThresh must be in") - expect_error(checkArgs(plotProgress = "yes"), "plotProgress must be logical") - expect_error(checkArgs(errorHandling = "foo"), "errorHandling is malformed") + expect_error(checkArgs(acqThresh = 1.5), "must be in \\[0,1\\]") + expect_error(checkArgs(acqThresh = -0.1), "must be in \\[0,1\\]") + expect_error(checkArgs(plotProgress = "yes"), "must be logical") + expect_error(checkArgs(errorHandling = "foo"), "is malformed") }) test_that("parallel = TRUE needs a registered backend", { diff --git a/tests/testthat/test-getBestPars.R b/tests/testthat/test-getBestPars.R index f376c16..ef5308b 100644 --- a/tests/testthat/test-getBestPars.R +++ b/tests/testthat/test-getBestPars.R @@ -27,5 +27,8 @@ test_that("N > 1 returns a data.table ordered by descending Score", { }) test_that("N larger than the number of runs is an error", { - expect_error(getBestPars(fakeOptObj(), N = 4), "N is greater") + expect_error( + getBestPars(fakeOptObj(), N = 4), + "is greater than the iterations" + ) }) diff --git a/tests/testthat/test-inputValidation.R b/tests/testthat/test-inputValidation.R index b50f32a..2351927 100644 --- a/tests/testthat/test-inputValidation.R +++ b/tests/testthat/test-inputValidation.R @@ -17,7 +17,7 @@ test_that("bayesOpt() rejects bad initial grids", { expect_error( bayesOpt(FUN, bounds, initGrid = outside, verbose = 0), - "initGrid not within bounds" + "not within" ) expect_error( bayesOpt(FUN, bounds, initGrid = small, verbose = 0), @@ -34,7 +34,7 @@ test_that("bayesOpt() rejects bad initial grids", { initPoints = 3, verbose = 0 ), - "greater than the number of FUN inputs" + "greater than the number of" ) }) @@ -76,7 +76,7 @@ test_that("addIterations() rejects objects it cannot continue", { class = "bayesOpt" ) - expect_error(addIterations(unclass(optObj)), "must be of class bayesOpt") + expect_error(addIterations(unclass(optObj)), "must be of class") expect_error(addIterations(optObj, verbose = 0), "Not enough samples") })