From 2edc717627eedcf75079e0e55946856e7dbd08da Mon Sep 17 00:00:00 2001 From: Ze Date: Wed, 1 Jul 2026 12:52:09 -0700 Subject: [PATCH] Fix perturbation assay names with invalid characters --- R/ModulePerturbation.R | 6 +++ R/PerturbationTransitions.R | 3 +- R/Utilities.R | 36 ++++++++++++++- .../test-perturbation-name-sanitization.R | 45 +++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/testthat/test-perturbation-name-sanitization.R diff --git a/R/ModulePerturbation.R b/R/ModulePerturbation.R index 1788c61..d211e8f 100644 --- a/R/ModulePerturbation.R +++ b/R/ModulePerturbation.R @@ -75,6 +75,8 @@ TFPerturbation <- function( wgcna_name = NULL ){ + perturbation_name <- .sanitize_perturbation_name(perturbation_name) + # set as active assay if wgcna_name is not given if(is.null(wgcna_name)){ wgcna_name <- seurat_obj@misc$active_wgcna } @@ -362,6 +364,8 @@ ModulePerturbation <- function( wgcna_name = NULL ){ + perturbation_name <- .sanitize_perturbation_name(perturbation_name) + # set as active assay if wgcna_name is not given if(is.null(wgcna_name)){wgcna_name <- seurat_obj@misc$active_wgcna} @@ -710,6 +714,8 @@ CustomPerturbation <- function( wgcna_name = NULL ){ + perturbation_name <- .sanitize_perturbation_name(perturbation_name) + if(is.null(wgcna_name)){ wgcna_name <- seurat_obj@misc$active_wgcna } # check assay diff --git a/R/PerturbationTransitions.R b/R/PerturbationTransitions.R index d0eee93..b66b47a 100644 --- a/R/PerturbationTransitions.R +++ b/R/PerturbationTransitions.R @@ -43,6 +43,8 @@ PerturbationTransitions <- function( paste(names(seurat_obj@assays), collapse = ', '))) } + perturbation_name <- .resolve_perturbation_assay_name(seurat_obj, perturbation_name) + # check perturbation assay exists if(!(perturbation_name %in% names(seurat_obj@assays))){ stop(paste0("Perturbation assay '", perturbation_name, "' not found in seurat_obj@assays. ", @@ -145,4 +147,3 @@ PerturbationTransitions <- function( colDeltaCor_velocyto <- function(e, d, nthreads = 1L) { .Call('_velocyto_R_colDeltaCor', PACKAGE = 'velocyto.R', e, d, nthreads) } - diff --git a/R/Utilities.R b/R/Utilities.R index bea8d87..d640878 100644 --- a/R/Utilities.R +++ b/R/Utilities.R @@ -100,6 +100,40 @@ PerturbationLog2FC <- function( return(plot_df) } +#' @keywords internal +#' @noRd +.sanitize_perturbation_name <- function(perturbation_name) { + if(!is.character(perturbation_name) || length(perturbation_name) != 1 || + is.na(perturbation_name) || !nzchar(perturbation_name)){ + stop("perturbation_name must be a non-empty character string.") + } + + safe_name <- make.names(perturbation_name) + if(!identical(safe_name, perturbation_name)){ + warning(paste0( + "perturbation_name '", perturbation_name, "' is not a valid Seurat assay name. ", + "Using '", safe_name, "' instead." + ), call. = FALSE) + } + + safe_name +} + +#' @keywords internal +#' @noRd +.resolve_perturbation_assay_name <- function(seurat_obj, perturbation_name) { + if(perturbation_name %in% names(seurat_obj@assays)){ + return(perturbation_name) + } + + safe_name <- make.names(perturbation_name) + if(safe_name %in% names(seurat_obj@assays)){ + return(safe_name) + } + + perturbation_name +} + #' Check for Signal Saturation After Network Propagation #' #' After running \code{ApplyPropagation}, checks whether the propagated @@ -272,4 +306,4 @@ CheckSignalDecay <- function( } invisible(result) -} \ No newline at end of file +} diff --git a/tests/testthat/test-perturbation-name-sanitization.R b/tests/testthat/test-perturbation-name-sanitization.R new file mode 100644 index 0000000..c31b7fb --- /dev/null +++ b/tests/testthat/test-perturbation-name-sanitization.R @@ -0,0 +1,45 @@ +test_that("perturbation names are converted to valid Seurat assay names", { + examples <- data.frame( + input = c( + "HuMicA-M1_down", + "module 1_up", + "M-1/down", + "1module_down", + "red_up" + ), + expected = c( + "HuMicA.M1_down", + "module.1_up", + "M.1.down", + "X1module_down", + "red_up" + ), + stringsAsFactors = FALSE + ) + + for(i in seq_len(nrow(examples))){ + input <- examples$input[i] + expected <- examples$expected[i] + + if(identical(input, expected)){ + expect_silent( + safe_name <- .sanitize_perturbation_name(input) + ) + } else { + expect_warning( + safe_name <- .sanitize_perturbation_name(input), + paste0("Using '", expected, "' instead"), + fixed = TRUE + ) + } + + expect_identical(safe_name, expected) + } +}) + +test_that("perturbation names must be a single non-empty string", { + expect_error(.sanitize_perturbation_name(character()), "non-empty character string") + expect_error(.sanitize_perturbation_name(c("red_up", "blue_up")), "non-empty character string") + expect_error(.sanitize_perturbation_name(""), "non-empty character string") + expect_error(.sanitize_perturbation_name(NA_character_), "non-empty character string") +})