From 4fec26597c7bd25af182c4e981f30cd7ab683653 Mon Sep 17 00:00:00 2001 From: Mengyuan Shen Date: Wed, 29 Jul 2026 11:38:52 +1000 Subject: [PATCH 1/6] seurat return multiple assays --- R/seurat.R | 22 +++++++++++++++++++--- man/get_seurat.Rd | 5 ++++- tests/testthat/test-query.R | 23 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/R/seurat.R b/R/seurat.R index 0aa20a9d..52d9fab8 100644 --- a/R/seurat.R +++ b/R/seurat.R @@ -14,7 +14,11 @@ as.sparse.DelayedMatrix <- function(x, ...) { #' @inheritDotParams get_single_cell_experiment #' @export #' @return A Seurat object containing the same data as a call to -#' [get_single_cell_experiment()] +#' [get_single_cell_experiment()]. When multiple assays are requested (e.g. +#' `assays = c("counts", "cpm")`), all assays are present in the returned +#' object: the first assay is stored as the `"originalexp"` Seurat assay, and +#' every subsequent assay is added under its own name (e.g. `"cpm"`). +#' @importFrom SummarizedExperiment assayNames assay #' @examples #' # Use the lightweight sample database URL (for fast checks during development only) #' meta <- get_metadata(cloud_metadata = cellNexus::SAMPLE_DATABASE_URL) |> head(2) @@ -29,6 +33,18 @@ as.sparse.DelayedMatrix <- function(x, ...) { get_seurat <- function(...) { rlang::check_installed(c("Seurat", "SeuratObject")) - get_single_cell_experiment(...) |> - SeuratObject::as.Seurat(data = NULL) + sce <- get_single_cell_experiment(...) + + sce_assays <- assayNames(sce) + first_assay <- sce_assays[1] + seurat_obj <- SeuratObject::as.Seurat(sce, counts = first_assay, data = NULL) + # Attach any additional assays that were not part of the initial conversion. + if (length(sce_assays) > 1) { + for (assay_name in sce_assays[-1]) { + seurat_obj[[assay_name]] <- SeuratObject::CreateAssayObject( + counts = assay(sce, assay_name) + ) + } + } + seurat_obj } diff --git a/man/get_seurat.Rd b/man/get_seurat.Rd index 1c6063da..4b54e6ce 100644 --- a/man/get_seurat.Rd +++ b/man/get_seurat.Rd @@ -38,7 +38,10 @@ the counts for. By default counts for all features will be returned.} } \value{ A Seurat object containing the same data as a call to -\code{\link[=get_single_cell_experiment]{get_single_cell_experiment()}} +\code{\link[=get_single_cell_experiment]{get_single_cell_experiment()}}. When multiple assays are requested (e.g. +\code{assays = c("counts", "cpm")}), all assays are present in the returned +object: the first assay is stored as the \code{"originalexp"} Seurat assay, and +every subsequent assay is added under its own name (e.g. \code{"cpm"}). } \description{ Given a data frame of HCA metadata, returns a Seurat object corresponding to diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index ca3096d6..24e1c247 100755 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -130,6 +130,29 @@ test_that("get_seurat() returns the appropriate data in Seurat format", { ) }) +test_that("get_seurat() with multiple assays returns all assays in the Seurat object", { + meta <- get_metadata(cloud_metadata = SAMPLE_DATABASE_URL) |> + head(2) + + seurat <- get_seurat(meta, assays = c("counts", "cpm"), features = "ENSG00000010610") + + expect_s4_class(seurat, "Seurat") + # "originalexp" is the Seurat assay name for the first SCE assay (counts) + expect_true("originalexp" %in% SeuratObject::Assays(seurat)) + # "cpm" should also be present as a named assay + expect_true("cpm" %in% SeuratObject::Assays(seurat)) +}) + +test_that("get_seurat() with only cpm assay succeeds and returns a Seurat object", { + meta <- get_metadata(cloud_metadata = SAMPLE_DATABASE_URL) |> + head(2) + + seurat <- get_seurat(meta, assays = "cpm", features = "ENSG00000010610") + + expect_s4_class(seurat, "Seurat") + expect_true("originalexp" %in% SeuratObject::Assays(seurat)) +}) + test_that("as.sparse() works on DelayedMatrix", { skip_if_not_installed("DelayedArray") skip_if_not_installed("Matrix") From 2171de67ba2b9a9bb97b42facd1c091f6613d792 Mon Sep 17 00:00:00 2001 From: Mengyuan Shen Date: Wed, 29 Jul 2026 13:33:27 +1000 Subject: [PATCH 2/6] skip missing sct files helper --- R/counts.R | 132 ++++++++++++++++++++++++++++++++++++++-- R/metadata.R | 4 +- R/utils.R | 25 ++++++-- man/get_metadata_url.Rd | 4 +- 4 files changed, 151 insertions(+), 14 deletions(-) diff --git a/R/counts.R b/R/counts.R index 5257d031..f81e0586 100644 --- a/R/counts.R +++ b/R/counts.R @@ -342,6 +342,83 @@ get_metacell <- function(data, ) } +#' Skip groups whose SCT h5ad file is absent from the local cache +#' +#' SCT normalisation can fail for entire samples (too few cells, extreme count +#' distributions, etc.), in which case no SCT h5ad file is written to the +#' store. \code{file_id_cellNexus_single_cell} is always pre-computed and +#' populated, so missing files are only detectable after sync via +#' \code{file.exists()}. This helper removes those groups \emph{before} the +#' per-assay read loop so that all assay experiment lists stay aligned (same +#' length, same order). +#' +#' Because the metadata carries no pre-computed flag for SCT failures, users +#' cannot identify the affected samples before calling +#' \code{get_single_cell_experiment()}. The warning therefore instructs them to +#' compare their input \code{sample_id}s with those in +#' \code{SummarizedExperiment::colData()} of the returned object. +#' +#' @param groups A list of per-file metadata tibbles from +#' \code{dplyr::group_split()}. +#' @param cache_directory Root local cache directory (scalar character). +#' @param cell_aggregation Aggregation level string (e.g. \code{""} for +#' single-cell). +#' @param grouping_column Name of the file-ID column used to build the path. +#' @return The filtered \code{groups} list (only entries whose SCT file exists +#' in the cache). Calls \code{cli_abort()} when no groups remain. +#' @importFrom purrr map_lgl map +#' @importFrom cli cli_alert_warning cli_abort +#' @keywords internal +#' @noRd +.skip_groups_missing_sct_file <- function(groups, cache_directory, cell_aggregation, grouping_column) { + sct_base_path <- if (nchar(cell_aggregation) > 0) { + file.path(cell_aggregation, assay_map[["sct"]]) + } else { + assay_map[["sct"]] + } + + has_file <- map_lgl(groups, function(gr) { + file.exists(file.path( + cache_directory, gr$atlas_id[1], sct_base_path, gr[[grouping_column]][1] + )) + }) + + if (all(has_file)) return(groups) + + skipped <- groups[!has_file] + n_cells_skipped <- sum(vapply(skipped, nrow, integer(1L))) + skipped_sample_ids <- if (all(map_lgl(skipped, ~ "sample_id" %in% names(.x)))) { + unique(unlist(map(skipped, ~ unique(.x$sample_id)))) + } else { + character(0) + } + n_samples_skipped <- length(skipped_sample_ids) + + example_str <- if (n_samples_skipped > 0L) { + paste0( + " Example affected sample_id(s): ", + paste(head(skipped_sample_ids, 3L), collapse = ", "), "." + ) + } else { + "" + } + + cli_alert_warning(paste( + "cellNexus says: {n_cells_skipped} cell(s) from {n_samples_skipped} sample(s)", + "are missing their SCT h5ad file and will be skipped.", + "SCT normalisation can fail for samples with too few cells or extreme count", + "distributions — in those cases no SCT file is written to the store.", + "After retrieval, identify skipped samples with:", + "`dplyr::setdiff(unique(your_metadata$sample_id),", + "unique(SummarizedExperiment::colData(result)$sample_id))`.", + "{example_str}" + )) + + # Return empty list when nothing remains; the caller decides whether to abort + # (SCT-only query) or fall back to other assays (mixed-assay query). + groups[has_file] +} + #' Sync, read, filter and compile experiments from cache #' #' Shared internal implementation used by [get_single_cell_experiment()], @@ -407,9 +484,24 @@ get_metacell <- function(data, ) }) - # Combine all file lists and download in one parallel batch + # Combine all file lists, then sync with assay-appropriate strictness. + # SCT files may legitimately be absent from the cloud (normalisation failed + # for that sample), so HTTP 404 is treated as "not available" rather than + # an error. All other assay files are synced strictly. all_files <- do.call(rbind, file_lists) - sync_all_assay_files(all_files) + sct_subdir <- assay_map[["sct"]] + is_sct_file <- grepl( + paste0("/", sct_subdir, "/"), all_files$full_url, fixed = TRUE + ) + if (any(!is_sct_file)) { + sync_all_assay_files(all_files[!is_sct_file, , drop = FALSE]) + } + if (any(is_sct_file)) { + sync_all_assay_files( + all_files[is_sct_file, , drop = FALSE], + ignore_not_found = TRUE + ) + } } cli_alert_info("Reading files.") @@ -418,6 +510,37 @@ get_metacell <- function(data, groups <- raw_data |> group_split(.data[[grouping_column]]) + # For SCT, some samples may have no h5ad file (normalisation failed). + # Drop those groups now so all per-assay experiment lists stay aligned. + if ("sct" %in% assays) { + groups_with_sct <- .skip_groups_missing_sct_file( + groups, cache_directory, cell_aggregation, grouping_column + ) + + if (length(groups_with_sct) == 0L) { + # No SCT files at all. When other assays were also requested, fall back to + # returning those for ALL groups. When SCT was the only assay, abort. + other_assays <- setdiff(assays, "sct") + if (length(other_assays) == 0L) { + cli_abort(paste( + "cellNexus says: No groups remain after removing samples with missing SCT files.", + "SCT normalisation appears to have failed for every sample in your query.", + "Try a different set of samples or use assays = \"counts\" instead." + )) + } + cli_alert_warning(paste( + "cellNexus says: No SCT files were found for any sample in your query.", + "Falling back to returning only: {paste(other_assays, collapse = ', ')}.", + "The 'sct' assay will not be present in the returned object." + )) + assays <- other_assays + subdirs <- assay_map[assays] + # Keep the full unfiltered `groups` so all cells are returned. + } else { + groups <- groups_with_sct + } + } + # Outer imap iterates over assays; each assay gets its own labelled progress bar. # This also fixes dir_prefix construction: current_subdir is properly scoped here. experiments <- imap(subdirs, function(current_subdir, current_assay) { @@ -903,7 +1026,7 @@ build_assay_file_list <- function( #' @param file_list A data frame with full_url and output_file columns #' @importFrom cli cli_alert_info #' @noRd -sync_all_assay_files <- function(file_list) { +sync_all_assay_files <- function(file_list, ignore_not_found = FALSE) { if (nrow(file_list) == 0) { return(invisible(character(0))) } @@ -916,7 +1039,8 @@ sync_all_assay_files <- function(file_list) { sync_remote_files( file_list$full_url[to_download], file_list$output_file[to_download], - progress = TRUE + progress = TRUE, + ignore_not_found = ignore_not_found ) } diff --git a/R/metadata.R b/R/metadata.R index 79e7de42..e2f24c14 100644 --- a/R/metadata.R +++ b/R/metadata.R @@ -21,7 +21,7 @@ metadata_aliases <- c( #' Returns the URLs for all metadata files #' #' @param databases A character vector of atlas aliases or raw parquet -#' filenames. Recognised aliases are `"hca_2024"` and `"hca_2025"`. +#' filenames. Recognised aliases are `"hca_2024"` #' Raw filenames (e.g. `"atlas_versions.parquet"`) are passed through #' unchanged and are intended for internal package use. #' @export @@ -35,7 +35,7 @@ metadata_aliases <- c( #' and analytical layers for the Human Cell Atlas data." bioRxiv (2026). #' doi:10.64898/2026.04.14.718336. #' @source [Shen et al.,2026](https://www.biorxiv.org/content/10.64898/2026.04.14.718336v3) -get_metadata_url <- function(databases = c("hca_2024", "hca_2025")) { +get_metadata_url <- function(databases = c("hca_2024")) { dbs <- ifelse( databases %in% names(metadata_aliases), metadata_aliases[databases], diff --git a/R/utils.R b/R/utils.R index d44e2f40..aa4bc10d 100644 --- a/R/utils.R +++ b/R/utils.R @@ -87,7 +87,8 @@ get_default_cache_dir <- function() { #' @return `NULL`, invisibly #' @keywords internal #' @noRd -sync_remote_file <- function(full_url, output_file, overwrite = FALSE, ...) { +sync_remote_file <- function(full_url, output_file, overwrite = FALSE, + ignore_not_found = FALSE, ...) { if (isTRUE(overwrite) && file.exists(output_file)) { file.remove(output_file) } @@ -104,8 +105,16 @@ sync_remote_file <- function(full_url, output_file, overwrite = FALSE, ...) { GET(full_url, write_disk(output_file), ...) |> stop_for_status(), error = function(e) { - # Clean up if we had an error - file.remove(output_file) + # Clean up partial download regardless. + if (file.exists(output_file)) file.remove(output_file) + # For SCT assays the file may legitimately not exist (normalisation + # failed for that sample). When the caller opts in, treat HTTP 404 as + # "file not available" rather than a hard error; the file is simply + # absent from the cache and downstream logic handles it. + if (ignore_not_found && + grepl("Not Found|404", conditionMessage(e), ignore.case = TRUE)) { + return(invisible(NULL)) + } cli_abort("File {full_url} could not be downloaded. {e}") } ) @@ -126,7 +135,8 @@ sync_remote_file <- function(full_url, output_file, overwrite = FALSE, ...) { #' @return The output_files vector, invisibly #' @keywords internal #' @noRd -sync_remote_files <- function(urls, output_files, progress = TRUE) { +sync_remote_files <- function(urls, output_files, progress = TRUE, + ignore_not_found = FALSE) { if (length(urls) == 0) { return(invisible(character(0))) } @@ -173,14 +183,17 @@ sync_remote_files <- function(urls, output_files, progress = TRUE) { if (file.exists(f)) file.remove(f) } cli_alert_warning("{sum(failed)} file{?s} failed to download") - if (sum(failed) == length(urls_to_download)) { + if (!ignore_not_found && sum(failed) == length(urls_to_download)) { cli_abort("All downloads failed. Check your network connection.") } } } else { # Sequential fallback for (i in seq_along(urls_to_download)) { - sync_remote_file(urls_to_download[i], files_to_download[i]) + sync_remote_file( + urls_to_download[i], files_to_download[i], + ignore_not_found = ignore_not_found + ) } } diff --git a/man/get_metadata_url.Rd b/man/get_metadata_url.Rd index 4a066734..0af431c4 100644 --- a/man/get_metadata_url.Rd +++ b/man/get_metadata_url.Rd @@ -7,11 +7,11 @@ \href{https://www.biorxiv.org/content/10.64898/2026.04.14.718336v3}{Shen et al.,2026} } \usage{ -get_metadata_url(databases = c("hca_2024", "hca_2025")) +get_metadata_url(databases = c("hca_2024")) } \arguments{ \item{databases}{A character vector of atlas aliases or raw parquet -filenames. Recognised aliases are \code{"hca_2024"} and \code{"hca_2025"}. +filenames. Recognised aliases are \code{"hca_2024"} Raw filenames (e.g. \code{"atlas_versions.parquet"}) are passed through unchanged and are intended for internal package use.} } From 5d7d4feefdb0871f165d16678317f6a3c4bd3f07 Mon Sep 17 00:00:00 2001 From: Mengyuan Shen Date: Wed, 29 Jul 2026 15:15:49 +1000 Subject: [PATCH 3/6] remove non-ASCII characters --- R/counts.R | 4 ++-- man/get_pseudobulk.Rd | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/R/counts.R b/R/counts.R index f81e0586..687c2e47 100644 --- a/R/counts.R +++ b/R/counts.R @@ -165,7 +165,7 @@ get_single_cell_experiment <- function(data, #' feature rownames; `get_pseudobulk()` restores them after coercion. #' @details #' Columns in `data` that are constant within each -#' `sample_id` × `cell_type_unified_ensemble` combination (including +#' `sample_id` x `cell_type_unified_ensemble` combination (including #' user-added annotations) are retained in `colData`. Cell-level columns are #' dropped via internal `keep_specific_annotation_columns()`. #' @return By default, a `SingleCellExperiment` object. If @@ -407,7 +407,7 @@ get_metacell <- function(data, "cellNexus says: {n_cells_skipped} cell(s) from {n_samples_skipped} sample(s)", "are missing their SCT h5ad file and will be skipped.", "SCT normalisation can fail for samples with too few cells or extreme count", - "distributions — in those cases no SCT file is written to the store.", + "distributions -- in those cases no SCT file is written to the store.", "After retrieval, identify skipped samples with:", "`dplyr::setdiff(unique(your_metadata$sample_id),", "unique(SummarizedExperiment::colData(result)$sample_id))`.", diff --git a/man/get_pseudobulk.Rd b/man/get_pseudobulk.Rd index cc1d8d6c..58110b80 100644 --- a/man/get_pseudobulk.Rd +++ b/man/get_pseudobulk.Rd @@ -62,7 +62,7 @@ corresponding to the samples in that data frame } \details{ Columns in \code{data} that are constant within each -\code{sample_id} × \code{cell_type_unified_ensemble} combination (including +\code{sample_id} x \code{cell_type_unified_ensemble} combination (including user-added annotations) are retained in \code{colData}. Cell-level columns are dropped via internal \code{keep_specific_annotation_columns()}. } From 35dbcdd0198a91e6e8d3570af5bd45442b69aeb8 Mon Sep 17 00:00:00 2001 From: Mengyuan Shen Date: Wed, 29 Jul 2026 15:47:24 +1000 Subject: [PATCH 4/6] version bump --- DESCRIPTION | 2 +- R/counts.R | 13 ++++++++----- R/utils.R | 2 +- inst/NEWS.Rd | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index ae6ef6ac..06178daf 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: cellNexus Title: Queries the Human Cell Atlas -Version: 0.99.29 +Version: 0.99.30 Authors@R: c( person( "Stefano", diff --git a/R/counts.R b/R/counts.R index 687c2e47..2cbcc9aa 100644 --- a/R/counts.R +++ b/R/counts.R @@ -383,10 +383,12 @@ get_metacell <- function(data, )) }) - if (all(has_file)) return(groups) + if (all(has_file)) { + return(groups) + } - skipped <- groups[!has_file] - n_cells_skipped <- sum(vapply(skipped, nrow, integer(1L))) + skipped <- groups[!has_file] + n_cells_skipped <- sum(vapply(skipped, nrow, integer(1L))) skipped_sample_ids <- if (all(map_lgl(skipped, ~ "sample_id" %in% names(.x)))) { unique(unlist(map(skipped, ~ unique(.x$sample_id)))) } else { @@ -489,9 +491,10 @@ get_metacell <- function(data, # for that sample), so HTTP 404 is treated as "not available" rather than # an error. All other assay files are synced strictly. all_files <- do.call(rbind, file_lists) - sct_subdir <- assay_map[["sct"]] + sct_subdir <- assay_map[["sct"]] is_sct_file <- grepl( - paste0("/", sct_subdir, "/"), all_files$full_url, fixed = TRUE + paste0("/", sct_subdir, "/"), all_files$full_url, + fixed = TRUE ) if (any(!is_sct_file)) { sync_all_assay_files(all_files[!is_sct_file, , drop = FALSE]) diff --git a/R/utils.R b/R/utils.R index aa4bc10d..cdcd5170 100644 --- a/R/utils.R +++ b/R/utils.R @@ -112,7 +112,7 @@ sync_remote_file <- function(full_url, output_file, overwrite = FALSE, # "file not available" rather than a hard error; the file is simply # absent from the cache and downstream logic handles it. if (ignore_not_found && - grepl("Not Found|404", conditionMessage(e), ignore.case = TRUE)) { + grepl("Not Found|404", conditionMessage(e), ignore.case = TRUE)) { return(invisible(NULL)) } cli_abort("File {full_url} could not be downloaded. {e}") diff --git a/inst/NEWS.Rd b/inst/NEWS.Rd index fdad3bae..09071b1f 100644 --- a/inst/NEWS.Rd +++ b/inst/NEWS.Rd @@ -1,6 +1,43 @@ \name{NEWS} \title{News for Package \pkg{cellNexus}} +\section{News in version 0.99.30}{ +\itemize{ + \item \code{get_seurat()} now correctly handles multiple assays and non-default + first assays. + \itemize{ + \item When \code{assays = c("counts", "cpm")} (or any multi-assay request), + all assays are transferred to the returned Seurat object: the first assay + occupies the \code{"originalexp"} slot and every subsequent assay is added + under its own name (e.g. \code{"cpm"}). + \item When the first requested assay is not \code{"counts"} (e.g. + \code{assays = "cpm"}), the conversion no longer errors with + \emph{"No data in provided assay - counts"}. The first assay in the SCE is + now passed explicitly to \code{SeuratObject::as.Seurat()}. + } + \item SCT assay: robust handling of samples for which SCT normalisation failed. + \itemize{ + \item HTTP 404 responses for SCT h5ad files are now treated as + \emph{"file not available"} rather than a fatal download error. The SCT + sync path uses \code{ignore_not_found = TRUE} while all other assay syncs + remain strict. + \item A new internal helper \code{.skip_groups_missing_sct_file()} detects + cached-file absence after sync, emits a \code{cellNexus says:} warning with + the affected cell/sample counts and up to three example \code{sample_id}s, + and instructs users to identify skipped samples post-retrieval via + \code{dplyr::setdiff(unique(your_metadata\$sample_id), + unique(SummarizedExperiment::colData(result)\$sample_id))}. + \item When \code{assays = "sct"} only and \emph{every} sample is missing its + SCT file, \code{get_single_cell_experiment()} aborts with a clear message. + \item When \code{assays = c("counts", "sct")} (or any mixed-assay request) + and \emph{every} sample is missing its SCT file, the function falls back + gracefully to returning the non-SCT assays for all cells with a warning, + rather than aborting. + } + \item Tests. Added coverage for \code{get_seurat()} multi-assay and + non-default-assay paths. +}} + \section{News in version 0.99.27}{ \itemize{ \item \code{get_pseudobulk()} now retains metadata columns that are constant within each From 1ffd52eaf9c9d0fd14433bef149e132102c9304d Mon Sep 17 00:00:00 2001 From: Mengyuan Shen Date: Wed, 29 Jul 2026 17:12:44 +1000 Subject: [PATCH 5/6] rename seurat assay name to match sce --- R/seurat.R | 11 +++++++---- man/get_seurat.Rd | 6 ++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/R/seurat.R b/R/seurat.R index 52d9fab8..7ea1883c 100644 --- a/R/seurat.R +++ b/R/seurat.R @@ -14,10 +14,8 @@ as.sparse.DelayedMatrix <- function(x, ...) { #' @inheritDotParams get_single_cell_experiment #' @export #' @return A Seurat object containing the same data as a call to -#' [get_single_cell_experiment()]. When multiple assays are requested (e.g. -#' `assays = c("counts", "cpm")`), all assays are present in the returned -#' object: the first assay is stored as the `"originalexp"` Seurat assay, and -#' every subsequent assay is added under its own name (e.g. `"cpm"`). +#' [get_single_cell_experiment()]. All requested assays are present in the +#' returned object under their original names (e.g. `"counts"`, `"cpm"`). #' @importFrom SummarizedExperiment assayNames assay #' @examples #' # Use the lightweight sample database URL (for fast checks during development only) @@ -38,6 +36,11 @@ get_seurat <- function(...) { sce_assays <- assayNames(sce) first_assay <- sce_assays[1] seurat_obj <- SeuratObject::as.Seurat(sce, counts = first_assay, data = NULL) + # as.Seurat() always names the first assay "originalexp"; rename it to match + # the actual SCE assay name so the Seurat object is not misleading. + if (first_assay != "originalexp") { + seurat_obj <- SeuratObject::RenameAssays(seurat_obj, originalexp = first_assay) + } # Attach any additional assays that were not part of the initial conversion. if (length(sce_assays) > 1) { for (assay_name in sce_assays[-1]) { diff --git a/man/get_seurat.Rd b/man/get_seurat.Rd index 4b54e6ce..6c2e0cc1 100644 --- a/man/get_seurat.Rd +++ b/man/get_seurat.Rd @@ -38,10 +38,8 @@ the counts for. By default counts for all features will be returned.} } \value{ A Seurat object containing the same data as a call to -\code{\link[=get_single_cell_experiment]{get_single_cell_experiment()}}. When multiple assays are requested (e.g. -\code{assays = c("counts", "cpm")}), all assays are present in the returned -object: the first assay is stored as the \code{"originalexp"} Seurat assay, and -every subsequent assay is added under its own name (e.g. \code{"cpm"}). +\code{\link[=get_single_cell_experiment]{get_single_cell_experiment()}}. All requested assays are present in the +returned object under their original names (e.g. \code{"counts"}, \code{"cpm"}). } \description{ Given a data frame of HCA metadata, returns a Seurat object corresponding to From 0bcc3a2cba7e469d72ac652ef9c8679685bab1f7 Mon Sep 17 00:00:00 2001 From: myushen Date: Thu, 30 Jul 2026 15:26:32 +1000 Subject: [PATCH 6/6] seurat return correct assay name --- R/seurat.R | 10 +++++++--- tests/testthat/test-query.R | 6 ++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/R/seurat.R b/R/seurat.R index 7ea1883c..9261f9e2 100644 --- a/R/seurat.R +++ b/R/seurat.R @@ -36,10 +36,14 @@ get_seurat <- function(...) { sce_assays <- assayNames(sce) first_assay <- sce_assays[1] seurat_obj <- SeuratObject::as.Seurat(sce, counts = first_assay, data = NULL) - # as.Seurat() always names the first assay "originalexp"; rename it to match - # the actual SCE assay name so the Seurat object is not misleading. + # as.Seurat() always names the first assay "originalexp"; replace it with a + # copy under the real name to avoid the misleading label. if (first_assay != "originalexp") { - seurat_obj <- SeuratObject::RenameAssays(seurat_obj, originalexp = first_assay) + assay_obj <- seurat_obj[["originalexp"]] + SeuratObject::Key(assay_obj) <- paste0(first_assay, "_") + seurat_obj[[first_assay]] <- assay_obj + SeuratObject::DefaultAssay(seurat_obj) <- first_assay + seurat_obj[["originalexp"]] <- NULL } # Attach any additional assays that were not part of the initial conversion. if (length(sce_assays) > 1) { diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index 24e1c247..c4035b6e 100755 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -137,9 +137,7 @@ test_that("get_seurat() with multiple assays returns all assays in the Seurat ob seurat <- get_seurat(meta, assays = c("counts", "cpm"), features = "ENSG00000010610") expect_s4_class(seurat, "Seurat") - # "originalexp" is the Seurat assay name for the first SCE assay (counts) - expect_true("originalexp" %in% SeuratObject::Assays(seurat)) - # "cpm" should also be present as a named assay + expect_true("counts" %in% SeuratObject::Assays(seurat)) expect_true("cpm" %in% SeuratObject::Assays(seurat)) }) @@ -150,7 +148,7 @@ test_that("get_seurat() with only cpm assay succeeds and returns a Seurat object seurat <- get_seurat(meta, assays = "cpm", features = "ENSG00000010610") expect_s4_class(seurat, "Seurat") - expect_true("originalexp" %in% SeuratObject::Assays(seurat)) + expect_true("cpm" %in% SeuratObject::Assays(seurat)) }) test_that("as.sparse() works on DelayedMatrix", {