From 904a5b0acef8275196ab3467a80de5695c54569c Mon Sep 17 00:00:00 2001 From: "Morgan-Wall, Tyler T" Date: Tue, 20 Feb 2024 12:29:20 -0500 Subject: [PATCH] skpr v1.6.3: Fix augmented designs not matching factor and character columns from candidate set -Add `plot` argument to `plot_correlations()` -Ensure errors are removed from parallel output --- DESCRIPTION | 4 ++-- R/eval_design_custom_mc.R | 1 + R/eval_design_mc.R | 1 + R/eval_design_survival_mc.R | 1 + R/gen_design.R | 29 ++++++++++++++++++--------- R/plot_correlations.R | 6 +++++- src/genOptimalDesign.cpp | 4 ++-- src/genSplitPlotOptimalDesign.cpp | 4 ++-- src/getBlockedOptimalDesign.cpp | 4 ++-- tests/testthat/Rplots.pdf | Bin 179638 -> 179638 bytes tests/testthat/testthat-problems.rds | Bin 17755 -> 0 bytes 11 files changed, 36 insertions(+), 18 deletions(-) delete mode 100644 tests/testthat/testthat-problems.rds diff --git a/DESCRIPTION b/DESCRIPTION index 88f141f..a94fb39 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: skpr Title: Design of Experiments Suite: Generate and Evaluate Optimal Designs -Date: 2024-02-06 -Version: 1.6.2 +Date: 2024-02-20 +Version: 1.6.3 Authors@R: c(person("Tyler", "Morgan-Wall", email = "tylermw@gmail.com", role = c("aut", "cre")), person("George", "Khoury", email = "george.m.khoury@gmail.com", role = c("aut"))) Description: Generates and evaluates D, I, A, Alias, E, T, and G optimal designs. Supports generation and evaluation of blocked and split/split-split/.../N-split plot designs. Includes parametric and Monte Carlo power evaluation functions, and supports calculating power for censored responses. Provides a framework to evaluate power using functions provided in other packages or written by the user. Includes a Shiny graphical user interface that displays the underlying code used to create and evaluate the design to improve ease-of-use and make analyses more reproducible. For details, see Morgan-Wall et al. (2021) . diff --git a/R/eval_design_custom_mc.R b/R/eval_design_custom_mc.R index 03f3662..429e0e5 100644 --- a/R/eval_design_custom_mc.R +++ b/R/eval_design_custom_mc.R @@ -284,6 +284,7 @@ eval_design_custom_mc = function(design, model = NULL, alpha = 0.05, run_search = function(iterations) { prog = progressr::progressor(steps = nsim) foreach::foreach(i = iterations, + .errorhandling = "remove", .options.future = list(packages = parallel_pkgs, globals = c("extractPvalues", "pvalfunction", "parameter_names", "progress", "progressbarupdates", diff --git a/R/eval_design_mc.R b/R/eval_design_mc.R index d6497aa..cbedd2f 100644 --- a/R/eval_design_mc.R +++ b/R/eval_design_mc.R @@ -753,6 +753,7 @@ eval_design_mc = function(design, model = NULL, alpha = 0.05, run_search = function(iterations, is_shiny) { prog = progressr::progressor(steps = nsim) foreach::foreach (j = seq_along(iterations), .combine = "rbind", + .errorhandling = "remove", .options.future = list(packages = packagelist, globals = c("extractPvalues", "effectpowermc", "RunMatrixReduced", "is_shiny", "blocking", "responses", "contrastslist", "model_formula", "glmfamily", "glmfamilyname", "calceffect", diff --git a/R/eval_design_survival_mc.R b/R/eval_design_survival_mc.R index dd9f914..32d7c71 100644 --- a/R/eval_design_survival_mc.R +++ b/R/eval_design_survival_mc.R @@ -328,6 +328,7 @@ eval_design_survival_mc = function(design, model = NULL, alpha = 0.05, run_search = function(iterations, is_shiny, surv_args) { prog = progressr::progressor(steps = nsim) foreach::foreach(i = iterations, + .errorhandling = "remove", .options.future = list(packages = "survival", globals = c("extractPvalues", "rfunctionsurv", "parameter_names", "progress", "progressbarupdates", "model", "distribution", "RunMatrixReduced", "ModelMatrix", "anticoef" ,"nc", "prog", diff --git a/R/gen_design.R b/R/gen_design.R index 3546185..7315f8f 100644 --- a/R/gen_design.R +++ b/R/gen_design.R @@ -574,7 +574,15 @@ gen_design = function(candidateset, model, trials, "' don't match candidate set '",paste0(cand_coltype[aug_coltype != cand_coltype],collapse=", "), "'--attempting to covert columns to that of the candidate set") for(i in seq_len(ncol(augmentdesign))) { - augmentdesign[,i] = methods::as(augmentdesign[,i],cand_coltype[i]) + if(cand_coltype[i] == "factor") { + candsetlevels = levels(candidateset[,i]) + augmentlevels = unique(augmentdesign[,i, drop = TRUE]) + augmentlevels_unique = augmentlevels[!augmentlevels %in% candsetlevels] + candsetlevels_all = c(candsetlevels,augmentlevels_unique) + augmentdesign[,i] = factor(augmentdesign[,i, drop = TRUE],levels=candsetlevels_all) + } else { + augmentdesign[,i] = methods::as(augmentdesign[,i, drop = TRUE],cand_coltype[i]) + } } } if (nrow(augmentdesign) >= trials) { @@ -944,6 +952,7 @@ gen_design = function(candidateset, model, trials, run_search = function(iterations, is_shiny) { prog = progressr::progressor(steps = repeats) foreach::foreach(i = iterations, + .errorhandling = "remove", .options.future = list(globals = c("genOptimalDesign","genBlockedOptimalDesign", "candidatesetmm", "trials", "initialreplace", "augmentdesign", "augmentedrows", "augmentdesignmm", "progress", "is_shiny", "progressbarupdates", "repeats", "num_updates", @@ -1059,13 +1068,15 @@ gen_design = function(candidateset, model, trials, nc = future::nbrOfWorkers() run_search = function(iterations, is_shiny) { prog = progressr::progressor(steps = repeats) - foreach::foreach(i = iterations, .options.future = list(globals = c("genSplitPlotOptimalDesign", "candidatesetmm", "trials", "candidateset", - "initialreplace", "blockedmm", "interactionlist", "disallowedcomb", - "anydisallowed", - "progress", "is_shiny", "progressbarupdates", "repeats", "num_updates", - "initialdesign", "optimality", "mm", "aliasmm", "blockedmodelmatrix", - "minDopt", "tolerance", "kexchange" ,"V" ,"prog" ,"nc"), - seed = TRUE)) %dofuture% { + foreach::foreach(i = iterations, + .errorhandling = "remove", + .options.future = list(globals = c("genSplitPlotOptimalDesign", "candidatesetmm", "trials", "candidateset", + "initialreplace", "blockedmm", "interactionlist", "disallowedcomb", + "anydisallowed", + "progress", "is_shiny", "progressbarupdates", "repeats", "num_updates", + "initialdesign", "optimality", "mm", "aliasmm", "blockedmodelmatrix", + "minDopt", "tolerance", "kexchange" ,"V" ,"prog" ,"nc"), + seed = TRUE)) %dofuture% { if(progress || is_shiny) { if(is_shiny && i %in% progressbarupdates) { prog(sprintf(" (%i workers) ", nc), amount = repeats/num_updates) @@ -1092,7 +1103,7 @@ gen_design = function(candidateset, model, trials, criteria = list() designcounter = 1 - for (i in seq_len(repeats)) { + for (i in seq_len(length(genOutput))) { if (!is.na(genOutput[[i]]["criterion"])) { designs[designcounter] = genOutput[[i]]["modelmatrix"] rowindicies[designcounter] = genOutput[[i]]["indices"] diff --git a/R/plot_correlations.R b/R/plot_correlations.R index 8466381..c8fd763 100644 --- a/R/plot_correlations.R +++ b/R/plot_correlations.R @@ -11,6 +11,7 @@ #'@param custompar Default NULL. Custom parameters to pass to the `par` function for base R plotting. #'@param standardize Default `TRUE`. Whether to standardize (scale to -1 and 1 and center) the continuous numeric columns. Not #'standardizing the numeric columns can increase multi-collinearity (predictors that are correlated with other predictors in the model). +#'@param plot Default `TRUE`. If `FALSE`, this will return the correlation matrix. #'@return Silently returns the correlation matrix with the proper row and column names. #'@import graphics grDevices #'@export @@ -34,7 +35,7 @@ #'plot_correlations(cardesign, customcolors = c("blue", "grey", "red")) #'plot_correlations(cardesign, customcolors = c("blue", "green", "yellow", "orange", "red")) plot_correlations = function(genoutput, model = NULL, customcolors = NULL, pow = 2, custompar = NULL, - standardize = TRUE) { + standardize = TRUE, plot = TRUE) { #Remove skpr-generated REML blocking indicators if present if (!is.null(attr(genoutput, "splitanalyzable"))) { if (attr(genoutput, "splitanalyzable")) { @@ -107,6 +108,9 @@ plot_correlations = function(genoutput, model = NULL, customcolors = NULL, pow = mm = model.matrix(model, genoutput, contrasts.arg = contrastlist) #Generate pseudo inverse as it's likely the model matrix will be singular with extra terms cormat = abs(cov2cor(getPseudoInverse(t(mm) %*% solve(V) %*% mm))[-1, -1]) + if(!plot) { + return(cormat) + } if (is.null(customcolors)) { imagecolors = viridis::viridis(101) diff --git a/src/genOptimalDesign.cpp b/src/genOptimalDesign.cpp index b0e5836..943e718 100644 --- a/src/genOptimalDesign.cpp +++ b/src/genOptimalDesign.cpp @@ -38,12 +38,12 @@ List genOptimalDesign(Eigen::MatrixXd initialdesign, const Eigen::MatrixXd& cand Eigen::MatrixXd test(initialdesign.cols(), initialdesign.cols()); test.setZero(); if(nTrials < candidatelist.cols()) { - throw std::runtime_error("skpr: Too few runs to generate initial non-singular matrix: increase the number of runs or decrease the number of parameters in the matrix"); + Rcpp::stop("skpr: Too few runs to generate initial non-singular matrix: increase the number of runs or decrease the number of parameters in the matrix"); } //Check for singularity from a column perfectly correlating with the intercept. for(int j = 1; j < candidatelist.cols(); j++) { if(candidatelist.col(0).cwiseEqual(candidatelist.col(j)).all()) { - throw std::runtime_error("skpr: Singular model matrix from factor aliased into intercept, revise model"); + Rcpp::stop("skpr: Singular model matrix from factor aliased into intercept, revise model"); } } Eigen::VectorXi shuffledindices; diff --git a/src/genSplitPlotOptimalDesign.cpp b/src/genSplitPlotOptimalDesign.cpp index df0ee19..758b797 100644 --- a/src/genSplitPlotOptimalDesign.cpp +++ b/src/genSplitPlotOptimalDesign.cpp @@ -44,7 +44,7 @@ List genSplitPlotOptimalDesign(Eigen::MatrixXd initialdesign, Eigen::MatrixXd ca //Checks if the initial matrix is singular. If so, randomly generates a new design nTrials times. for(int j = 1; j < candidatelist.cols(); j++) { if(ones.col(0).cwiseEqual(candidatelist.col(j)).all()) { - throw std::runtime_error("skpr: Singular model matrix from factor aliased into intercept, revise model"); + Rcpp::stop("skpr: Singular model matrix from factor aliased into intercept, revise model"); } } int nTrials = initialdesign.rows(); @@ -82,7 +82,7 @@ List genSplitPlotOptimalDesign(Eigen::MatrixXd initialdesign, Eigen::MatrixXd ca Eigen::VectorXi candidateRow = initialRows; Eigen::VectorXi shuffledindices; if(nTrials < candidatelist.cols() + blockedCols + numberinteractions) { - throw std::runtime_error("skpr: Too few runs to generate initial non-singular matrix: increase the number of runs or decrease the number of parameters in the matrix"); + Rcpp::stop("skpr: Too few runs to generate initial non-singular matrix: increase the number of runs or decrease the number of parameters in the matrix"); } //Checks if the initial matrix is singular. If so, randomly generates a new design maxSingularityChecks times. for (int check = 0; check < maxSingularityChecks; check++) { diff --git a/src/getBlockedOptimalDesign.cpp b/src/getBlockedOptimalDesign.cpp index 6cb4604..632523d 100644 --- a/src/getBlockedOptimalDesign.cpp +++ b/src/getBlockedOptimalDesign.cpp @@ -37,12 +37,12 @@ List genBlockedOptimalDesign(Eigen::MatrixXd initialdesign, const Eigen::MatrixX const Eigen::MatrixXd vInv = V.colPivHouseholderQr().inverse(); if(nTrials < candidatelist.cols()) { - throw std::runtime_error("skpr: Too few runs to generate initial non-singular matrix: increase the number of runs or decrease the number of parameters in the matrix"); + Rcpp::stop("skpr: Too few runs to generate initial non-singular matrix: increase the number of runs or decrease the number of parameters in the matrix"); } //Check for singularity from a column perfectly correlating with the intercept. for(int j = 1; j < candidatelist.cols(); j++) { if(candidatelist.col(0).cwiseEqual(candidatelist.col(j)).all()) { - throw std::runtime_error("skpr: Singular model matrix from factor aliased into intercept, revise model"); + Rcpp::stop("skpr: Singular model matrix from factor aliased into intercept, revise model"); } } Eigen::VectorXi shuffledindices; diff --git a/tests/testthat/Rplots.pdf b/tests/testthat/Rplots.pdf index a00f046cefcd527cb02df650dbd3ff9f19f582d3..1006051a515289eed85d6b488a4ad4dc59ed37a0 100644 GIT binary patch delta 37 mcmdn?n`_%|t_c?GmWCFl=EepS?L}eC#^}~)#;wsz(R%^%N(=h{ delta 37 lcmdn?n`_%|t_c?GW`;(_KseD}6vk|fZjEN#8qE~F7Xa`G3*P_$ diff --git a/tests/testthat/testthat-problems.rds b/tests/testthat/testthat-problems.rds deleted file mode 100644 index 0b1c29038ddfdadc5df2ff28fc47e0d1cf834f83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17755 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