From a280d32446235d5291d7bc359235e29fe6a452a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:17:45 +0000 Subject: [PATCH 1/5] Initial plan From 321f397c6848932e3a2d8cf3f13297376969acfb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:26:04 +0000 Subject: [PATCH 2/5] Replace fastmap and stringi with base R, move packages to Suggests Co-authored-by: ms609 <1695515+ms609@users.noreply.github.com> --- DESCRIPTION | 16 +++++++--------- R/ClusterStrings.R | 22 ++++++++++++++++------ R/Consistency.R | 12 +++++------- R/MaximizeParsimony.R | 14 +++++--------- R/TaxonInfluence.R | 14 +++++++++++--- R/mpl_morphy_objects.R | 3 +-- tests/testthat/test-ClusterStrings.R | 3 +++ tests/testthat/test-TaxonInfluence.R | 2 ++ 8 files changed, 50 insertions(+), 36 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 911f634f8..641565691 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -36,29 +36,27 @@ Depends: R (>= 4.0) Imports: ape (>= 5.6), cli (>= 3.0), - cluster, - fastmap, fastmatch (>= 1.1.3), - fs, - future, PlotTools, - promises, - protoclust, Rcpp, Rdpack (>= 0.7), Rogue (> 2.0.0), - shiny (>= 1.6.0), - shinyjs, stats, - stringi, TreeDist (>= 2.6.3), TreeTools (>= 1.16), Suggests: + cluster, + fs, + future, knitr, phangorn (>= 2.2.1), + promises, + protoclust, Quartet, readxl, rmarkdown, + shiny (>= 1.6.0), + shinyjs, shinytest, spelling, testthat, diff --git a/R/ClusterStrings.R b/R/ClusterStrings.R index 1ee081e7d..caf8ae952 100644 --- a/R/ClusterStrings.R +++ b/R/ClusterStrings.R @@ -18,8 +18,6 @@ #' paste0("AnotherCluster_", letters[1:6]))) #' @template MRS #' @importFrom utils adist -#' @importFrom cluster pam silhouette -#' @importFrom protoclust protoclust #' @importFrom stats as.dist cutree #' @family utility functions #' @export @@ -28,6 +26,18 @@ ClusterStrings <- function (x, maxCluster = 12) { stop("`maxCluster` must be at least two.") } + if (!requireNamespace("cluster", quietly = TRUE)) { + stop("Package 'cluster' is required for ClusterStrings(). ", + "Install it with: install.packages('cluster')", + call. = FALSE) + } + + if (!requireNamespace("protoclust", quietly = TRUE)) { + stop("Package 'protoclust' is required for ClusterStrings(). ", + "Install it with: install.packages('protoclust')", + call. = FALSE) + } + if (length(unique(x)) < maxCluster) { nom <- unique(x) structure(match(x, nom), "med" = nom) @@ -42,19 +52,19 @@ ClusterStrings <- function (x, maxCluster = 12) { kInc <- 1 / (nMethodsChecked * nK) pamClusters <- lapply(possibleClusters, function (k) { - pam(dists, k = k) + cluster::pam(dists, k = k) }) pamSils <- vapply(pamClusters, function (pamCluster) { - mean(silhouette(pamCluster)[, 3]) + mean(cluster::silhouette(pamCluster)[, 3]) }, double(1)) bestPam <- which.max(pamSils) pamSil <- pamSils[bestPam] pamCluster <- pamClusters[[bestPam]][["clustering"]] - hTree <- protoclust(as.dist(dists)) + hTree <- protoclust::protoclust(as.dist(dists)) hClusters <- lapply(possibleClusters, function (k) cutree(hTree, k = k)) hSils <- vapply(hClusters, function (hCluster) { - mean(silhouette(hCluster, dists)[, 3]) + mean(cluster::silhouette(hCluster, dists)[, 3]) }, double(1)) bestH <- which.max(hSils) hSil <- hSils[bestH] diff --git a/R/Consistency.R b/R/Consistency.R index bd695f758..717412fcf 100644 --- a/R/Consistency.R +++ b/R/Consistency.R @@ -109,8 +109,7 @@ Consistency <- function (dataset, tree, nRelabel = 0, compress = FALSE) { } -#' @importFrom fastmap fastmap -.CharLengthCache <- fastmap() +.CharLengthCache <- new.env(parent = emptyenv()) #' Expected length #' @@ -127,7 +126,6 @@ Consistency <- function (dataset, tree, nRelabel = 0, compress = FALSE) { #' #' @export #' @importFrom stats median -#' @importFrom stringi stri_paste #' @family tree scoring #' @template MRS ExpectedLength <- function(dataset, tree, nRelabel = 1000, compress = FALSE) { @@ -151,9 +149,9 @@ ExpectedLength <- function(dataset, tree, nRelabel = 1000, compress = FALSE) { }, integer(nLevels))) .LengthForChar <- function(x) { - key <- stri_paste(c(nRelabel, x), collapse = ",") - if (.CharLengthCache$has(key)) { - .CharLengthCache$get(key) + key <- paste(c(nRelabel, x), collapse = ",") + if (exists(key, envir = .CharLengthCache, inherits = FALSE)) { + get(key, envir = .CharLengthCache) } else { patterns <- apply(unname(unique(t( as.data.frame(replicate(nRelabel, sample(rep(seq_along(x), x))))))), @@ -170,7 +168,7 @@ ExpectedLength <- function(dataset, tree, nRelabel = 1000, compress = FALSE) { contrast = rwContrast, class = "phyDat") ret <- median(FastCharacterLength(tree, phy)) - .CharLengthCache$set(key, ret) + assign(key, ret, envir = .CharLengthCache) ret } } diff --git a/R/MaximizeParsimony.R b/R/MaximizeParsimony.R index ef6d237cc..3519ecba6 100644 --- a/R/MaximizeParsimony.R +++ b/R/MaximizeParsimony.R @@ -999,17 +999,13 @@ Resample <- function(dataset, tree, method = "jack", proportion = 2 / 3, #' Launch tree search graphical user interface #' #' @rdname MaximizeParsimony -#' @importFrom cluster pam silhouette -#' @importFrom future future -#' @importFrom PlotTools SpectrumLegend -#' @importFrom promises future_promise -#' @importFrom protoclust protoclust -#' @importFrom Rogue ColByStability -#' @importFrom shiny runApp -#' @importFrom shinyjs useShinyjs -#' @importFrom TreeDist ClusteringInfoDistance #' @export EasyTrees <- function () {#nocov start + if (!requireNamespace("shiny", quietly = TRUE)) { + stop("Package 'shiny' is required for EasyTrees(). ", + "Install it with: install.packages('shiny')", + call. = FALSE) + } shiny::runApp(system.file("Parsimony", package = "TreeSearch")) } diff --git a/R/TaxonInfluence.R b/R/TaxonInfluence.R index 51397ddfc..67710a539 100644 --- a/R/TaxonInfluence.R +++ b/R/TaxonInfluence.R @@ -106,7 +106,6 @@ #' @family tree scoring #' @importFrom ape read.nexus write.nexus #' @importFrom cli cli_alert_info cli_h1 -#' @importFrom fs path_sanitize #' @importFrom stats weighted.mean #' @importFrom TreeDist ClusteringInfoDistance #' @encoding UTF-8 @@ -122,6 +121,11 @@ TaxonInfluence <- function( ... ) { if (!is.null(savePath)) { + if (!requireNamespace("fs", quietly = TRUE)) { + stop("Package 'fs' is required when using savePath. ", + "Install it with: install.packages('fs')", + call. = FALSE) + } saveDir <- dirname(paste0(savePath, "taxon_name")) if (!dir.exists(saveDir)) { dir.create(saveDir, recursive = TRUE) @@ -153,9 +157,13 @@ TaxonInfluence <- function( # Return: vapply(names(dataset), function(leaf) { - leafFile <- paste0(savePath, path_sanitize(leaf), ".nex") + leafFile <- if (!is.null(savePath)) { + paste0(savePath, fs::path_sanitize(leaf), ".nex") + } else { + NULL + } - result <- if (useCache && file.exists(leafFile)) { + result <- if (useCache && !is.null(leafFile) && file.exists(leafFile)) { if (verbosity > 1) { cli_alert_info(paste("Seeking cache: ", leafFile)) } diff --git a/R/mpl_morphy_objects.R b/R/mpl_morphy_objects.R index 66dbf1aa5..e4941d769 100644 --- a/R/mpl_morphy_objects.R +++ b/R/mpl_morphy_objects.R @@ -225,10 +225,9 @@ MorphyErrorCheck <- function(action) { #' Score a tree: [`MorphyTreeLength()`] #' #' @family Morphy API functions -#' @importFrom stringi stri_paste #' @export SingleCharMorphy <- function (char, gap = "inapp") { - char <- stri_paste(c(char, ";"), collapse = "") + char <- paste(c(char, ";"), collapse = "") entries <- gregexpr("\\{[^\\{]+\\}|\\([^\\()]+\\)|[^;]", char) nTip <- length(entries[[1]]) morphyObj <- mpl_new_Morphy() diff --git a/tests/testthat/test-ClusterStrings.R b/tests/testthat/test-ClusterStrings.R index 6efef66e8..cc356d7bb 100644 --- a/tests/testthat/test-ClusterStrings.R +++ b/tests/testthat/test-ClusterStrings.R @@ -1,4 +1,7 @@ test_that("ClusterStrings() works", { + skip_if_not_installed("cluster") + skip_if_not_installed("protoclust") + x <- rep(letters[1:6], 1:6) expect_equal(ClusterStrings(x), structure(rep(1:6, 1:6), "med" = letters[1:6])) diff --git a/tests/testthat/test-TaxonInfluence.R b/tests/testthat/test-TaxonInfluence.R index 6a605ead5..9ec779104 100644 --- a/tests/testthat/test-TaxonInfluence.R +++ b/tests/testthat/test-TaxonInfluence.R @@ -23,6 +23,8 @@ test_that("TaxonInfluence() works", { }) test_that("TaxonInfluence() saves intermediate trees", { + skip_if_not_installed("fs") + library("TreeTools") # for phyDat manipulation data("congreveLamsdellMatrices", package = "TreeSearch") set.seed(0) From 936acda0876ececbada6258100347e77870595c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:27:10 +0000 Subject: [PATCH 3/5] Update NAMESPACE to remove imports for suggested packages Co-authored-by: ms609 <1695515+ms609@users.noreply.github.com> --- NAMESPACE | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 79e40d690..8efc93b32 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -205,25 +205,15 @@ importFrom(cli,cli_h1) importFrom(cli,cli_progress_bar) importFrom(cli,cli_progress_done) importFrom(cli,cli_progress_update) -importFrom(cluster,pam) -importFrom(cluster,silhouette) -importFrom(fastmap,fastmap) importFrom(fastmatch,"%fin%") importFrom(fastmatch,fmatch) -importFrom(fs,path_sanitize) -importFrom(future,future) importFrom(graphics,par) -importFrom(promises,future_promise) -importFrom(protoclust,protoclust) -importFrom(shiny,runApp) -importFrom(shinyjs,useShinyjs) importFrom(stats,as.dist) importFrom(stats,cutree) importFrom(stats,median) importFrom(stats,runif) importFrom(stats,setNames) importFrom(stats,weighted.mean) -importFrom(stringi,stri_paste) importFrom(utils,adist) importFrom(utils,combn) useDynLib(TreeSearch, .registration = TRUE) From 3673dd31edef56f2bc4b9bd5f301da4aaa4a686e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:31:12 +0000 Subject: [PATCH 4/5] Add clarifying comment for cache implementation Co-authored-by: ms609 <1695515+ms609@users.noreply.github.com> --- R/Consistency.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/Consistency.R b/R/Consistency.R index 717412fcf..ea7ec4751 100644 --- a/R/Consistency.R +++ b/R/Consistency.R @@ -109,6 +109,7 @@ Consistency <- function (dataset, tree, nRelabel = 0, compress = FALSE) { } +# Cache for character length calculations (replaces previous fastmap implementation) .CharLengthCache <- new.env(parent = emptyenv()) #' Expected length From be2930e1de8a8fc0f13dd601f2d028b9e67d7420 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:28:46 +0000 Subject: [PATCH 5/5] Keep fastmap and stringi for performance; add user prompts for package installation Co-authored-by: ms609 <1695515+ms609@users.noreply.github.com> --- DESCRIPTION | 2 ++ NAMESPACE | 2 ++ R/ClusterStrings.R | 13 ++----------- R/Consistency.R | 13 +++++++------ R/MaximizeParsimony.R | 6 +----- R/TaxonInfluence.R | 6 +----- R/TreeSearch_utilities.R | 25 +++++++++++++++++++++++++ R/mpl_morphy_objects.R | 3 ++- 8 files changed, 42 insertions(+), 28 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 641565691..f7f37f8cc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -36,12 +36,14 @@ Depends: R (>= 4.0) Imports: ape (>= 5.6), cli (>= 3.0), + fastmap, fastmatch (>= 1.1.3), PlotTools, Rcpp, Rdpack (>= 0.7), Rogue (> 2.0.0), stats, + stringi, TreeDist (>= 2.6.3), TreeTools (>= 1.16), Suggests: diff --git a/NAMESPACE b/NAMESPACE index 8efc93b32..bcd152bec 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -205,6 +205,7 @@ importFrom(cli,cli_h1) importFrom(cli,cli_progress_bar) importFrom(cli,cli_progress_done) importFrom(cli,cli_progress_update) +importFrom(fastmap,fastmap) importFrom(fastmatch,"%fin%") importFrom(fastmatch,fmatch) importFrom(graphics,par) @@ -214,6 +215,7 @@ importFrom(stats,median) importFrom(stats,runif) importFrom(stats,setNames) importFrom(stats,weighted.mean) +importFrom(stringi,stri_paste) importFrom(utils,adist) importFrom(utils,combn) useDynLib(TreeSearch, .registration = TRUE) diff --git a/R/ClusterStrings.R b/R/ClusterStrings.R index caf8ae952..43953f206 100644 --- a/R/ClusterStrings.R +++ b/R/ClusterStrings.R @@ -26,17 +26,8 @@ ClusterStrings <- function (x, maxCluster = 12) { stop("`maxCluster` must be at least two.") } - if (!requireNamespace("cluster", quietly = TRUE)) { - stop("Package 'cluster' is required for ClusterStrings(). ", - "Install it with: install.packages('cluster')", - call. = FALSE) - } - - if (!requireNamespace("protoclust", quietly = TRUE)) { - stop("Package 'protoclust' is required for ClusterStrings(). ", - "Install it with: install.packages('protoclust')", - call. = FALSE) - } + .InstallSuggestedPackage("cluster") + .InstallSuggestedPackage("protoclust") if (length(unique(x)) < maxCluster) { nom <- unique(x) diff --git a/R/Consistency.R b/R/Consistency.R index ea7ec4751..bd695f758 100644 --- a/R/Consistency.R +++ b/R/Consistency.R @@ -109,8 +109,8 @@ Consistency <- function (dataset, tree, nRelabel = 0, compress = FALSE) { } -# Cache for character length calculations (replaces previous fastmap implementation) -.CharLengthCache <- new.env(parent = emptyenv()) +#' @importFrom fastmap fastmap +.CharLengthCache <- fastmap() #' Expected length #' @@ -127,6 +127,7 @@ Consistency <- function (dataset, tree, nRelabel = 0, compress = FALSE) { #' #' @export #' @importFrom stats median +#' @importFrom stringi stri_paste #' @family tree scoring #' @template MRS ExpectedLength <- function(dataset, tree, nRelabel = 1000, compress = FALSE) { @@ -150,9 +151,9 @@ ExpectedLength <- function(dataset, tree, nRelabel = 1000, compress = FALSE) { }, integer(nLevels))) .LengthForChar <- function(x) { - key <- paste(c(nRelabel, x), collapse = ",") - if (exists(key, envir = .CharLengthCache, inherits = FALSE)) { - get(key, envir = .CharLengthCache) + key <- stri_paste(c(nRelabel, x), collapse = ",") + if (.CharLengthCache$has(key)) { + .CharLengthCache$get(key) } else { patterns <- apply(unname(unique(t( as.data.frame(replicate(nRelabel, sample(rep(seq_along(x), x))))))), @@ -169,7 +170,7 @@ ExpectedLength <- function(dataset, tree, nRelabel = 1000, compress = FALSE) { contrast = rwContrast, class = "phyDat") ret <- median(FastCharacterLength(tree, phy)) - assign(key, ret, envir = .CharLengthCache) + .CharLengthCache$set(key, ret) ret } } diff --git a/R/MaximizeParsimony.R b/R/MaximizeParsimony.R index 3519ecba6..218cf9165 100644 --- a/R/MaximizeParsimony.R +++ b/R/MaximizeParsimony.R @@ -1001,11 +1001,7 @@ Resample <- function(dataset, tree, method = "jack", proportion = 2 / 3, #' @rdname MaximizeParsimony #' @export EasyTrees <- function () {#nocov start - if (!requireNamespace("shiny", quietly = TRUE)) { - stop("Package 'shiny' is required for EasyTrees(). ", - "Install it with: install.packages('shiny')", - call. = FALSE) - } + .InstallSuggestedPackage("shinyjs") shiny::runApp(system.file("Parsimony", package = "TreeSearch")) } diff --git a/R/TaxonInfluence.R b/R/TaxonInfluence.R index 67710a539..f27956775 100644 --- a/R/TaxonInfluence.R +++ b/R/TaxonInfluence.R @@ -121,11 +121,7 @@ TaxonInfluence <- function( ... ) { if (!is.null(savePath)) { - if (!requireNamespace("fs", quietly = TRUE)) { - stop("Package 'fs' is required when using savePath. ", - "Install it with: install.packages('fs')", - call. = FALSE) - } + .InstallSuggestedPackage("fs") saveDir <- dirname(paste0(savePath, "taxon_name")) if (!dir.exists(saveDir)) { dir.create(saveDir, recursive = TRUE) diff --git a/R/TreeSearch_utilities.R b/R/TreeSearch_utilities.R index 0945cfe59..63abfe6f2 100644 --- a/R/TreeSearch_utilities.R +++ b/R/TreeSearch_utilities.R @@ -11,3 +11,28 @@ EmptyPhyDat <- function(tree) { #' @rdname TreeSearch #' @export DoNothing <- function(x = NULL) {x} + +# Helper function to install suggested packages with user prompt +.InstallSuggestedPackage <- function(package) { + if (!requireNamespace(package, quietly = TRUE)) { + if (interactive()) { + message("Package '", package, "' is required but not installed.") + response <- readline(prompt = paste0("Install ", package, "? (y/n): ")) + if (tolower(trimws(response)) == "y") { + install.packages(package) + if (!requireNamespace(package, quietly = TRUE)) { + stop("Failed to install package '", package, "'", call. = FALSE) + } + } else { + stop("Package '", package, "' is required. ", + "Install it with: install.packages('", package, "')", + call. = FALSE) + } + } else { + stop("Package '", package, "' is required. ", + "Install it with: install.packages('", package, "')", + call. = FALSE) + } + } +} + diff --git a/R/mpl_morphy_objects.R b/R/mpl_morphy_objects.R index e4941d769..66dbf1aa5 100644 --- a/R/mpl_morphy_objects.R +++ b/R/mpl_morphy_objects.R @@ -225,9 +225,10 @@ MorphyErrorCheck <- function(action) { #' Score a tree: [`MorphyTreeLength()`] #' #' @family Morphy API functions +#' @importFrom stringi stri_paste #' @export SingleCharMorphy <- function (char, gap = "inapp") { - char <- paste(c(char, ";"), collapse = "") + char <- stri_paste(c(char, ";"), collapse = "") entries <- gregexpr("\\{[^\\{]+\\}|\\([^\\()]+\\)|[^;]", char) nTip <- length(entries[[1]]) morphyObj <- mpl_new_Morphy()