From 51e076df8de28915fd25aa2ff32a7746d0e2a717 Mon Sep 17 00:00:00 2001 From: dorien-er Date: Wed, 15 Oct 2025 17:31:50 +0200 Subject: [PATCH 01/12] update component for multimodal RDS --- _viash.yaml | 3 + .../from_h5mu_to_seurat/config.vsh.yaml | 65 ++++++++ src/convert/from_h5mu_to_seurat/script.R | 140 ++++++++++++++++++ src/convert/from_h5mu_to_seurat/test.R | 46 ++++++ .../from_xenium_to_spatialexperiment/script.R | 6 +- 5 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 src/convert/from_h5mu_to_seurat/config.vsh.yaml create mode 100644 src/convert/from_h5mu_to_seurat/script.R create mode 100644 src/convert/from_h5mu_to_seurat/test.R diff --git a/_viash.yaml b/_viash.yaml index 504fcce..f64a065 100644 --- a/_viash.yaml +++ b/_viash.yaml @@ -21,6 +21,9 @@ info: - type: s3 path: s3://openpipelines-bio/openpipeline_spatial/resources_test dest: resources_test + - type: s3 + path: s3://openpipelines-data + dest: resources_test_sc config_mods: | .resources += {path: '/src/workflows/utils/labels.config', dest: 'nextflow_labels.config'} diff --git a/src/convert/from_h5mu_to_seurat/config.vsh.yaml b/src/convert/from_h5mu_to_seurat/config.vsh.yaml new file mode 100644 index 0000000..c8029aa --- /dev/null +++ b/src/convert/from_h5mu_to_seurat/config.vsh.yaml @@ -0,0 +1,65 @@ +name: "from_h5mu_to_seurat" +namespace: "convert" +scope: "public" +description: | + Converts an h5ad file or a single modality of an h5mu file into a Seurat file. +authors: + - __merge__: /src/authors/dorien_roosen.yaml + roles: [ author, maintainer ] +arguments: + - name: "--input" + alternatives: ["-i"] + type: file + description: Input h5ad or h5mu file + direction: input + required: true + example: input.h5ad + - name: "--modality" + type: string + default: ["rna"] + multiple: true + example: ["rna", "prot", "vdj"] + required: false + description: Modality to be converted if the input file is an h5mu file. + - name: "--assay" + type: string + default: ["RNA"] + multiple: true + example: ["RNA", "ADT", "VDJ"] + description: | + Name of the assay to be created. + Order should match the order of the --modalities. + If layer dimensions of a modality are 0, the modality will be moved to the metadata instead of an assay. + - name: "--output" + alternatives: ["-o"] + type: file + description: Output Seurat file + direction: output + required: true + example: output.rds +resources: + - type: r_script + path: script.R +test_resources: + - type: r_script + path: test.R + - path: /resources_test_sc/pbmc_1k_protein_v3/ + - path: /resources_test_sc/10x_5k_anticmv/ +engines: + - type: docker + image: rocker/r2u:22.04 + setup: + - type: apt + packages: [ libhdf5-dev, libgeos-dev, hdf5-tools ] + - type: r + cran: [ anndata, hdf5r, Seurat, SeuratObject ] + github: scverse/anndataR@36f3caad9a7f360165c1510bbe0c62657580415a + bioc: [ rhdf5] + test_setup: + - type: r + cran: [ testthat ] +runners: + - type: executable + - type: nextflow + directives: + label: [lowmem, singlecpu] diff --git a/src/convert/from_h5mu_to_seurat/script.R b/src/convert/from_h5mu_to_seurat/script.R new file mode 100644 index 0000000..2b4dd67 --- /dev/null +++ b/src/convert/from_h5mu_to_seurat/script.R @@ -0,0 +1,140 @@ +library(anndataR) +library(hdf5r) +# library(Seurat) + +### VIASH START +par <- list( + input = "resources_test_sc/10x_5k_anticmv/5k_human_antiCMV_T_TBNK_connect.h5mu", + output = "resources_test_sc/10x_5k_anticmv/5k_human_antiCMV_T_TBNK_connect.rds", + assay = c("RNA", "ADT", "TCR"), + modality = c("rna", "prot", "vdj_t") +) +### VIASH END + +h5mu_to_h5ad <- function(h5mu_path, modality_name) { + tmp_path <- tempfile(fileext = ".h5ad") + mod_location <- paste("mod", modality_name, sep = "/") + h5src <- hdf5r::H5File$new(h5mu_path, "r") + h5dest <- hdf5r::H5File$new(tmp_path, "w") + # Copy over the child objects and the child attributes from root + # Root cannot be copied directly because it always exists and + # copying does not allow overwriting. + children <- hdf5r::list.objects(h5src, + path = mod_location, + full.names = FALSE, recursive = FALSE + ) + for (child in children) { + h5dest$obj_copy_from( + h5src, paste(mod_location, child, sep = "/"), + paste0("/", child) + ) + } + # Also copy the root attributes + root_attrs <- hdf5r::h5attr_names(x = h5src) + for (attr in root_attrs) { + h5a <- h5src$attr_open(attr_name = attr) + robj <- h5a$read() + h5dest$create_attr_by_name( + attr_name = attr, + obj_name = ".", + robj = robj, + space = h5a$get_space(), + dtype = h5a$get_type() + ) + } + h5src$close() + h5dest$close() + + tmp_path +} + +has_zero_dimension <- function(adata) { + adata_dims <- adata$shape() + any(adata_dims == 0) +} + +map_obs_to_metadata <- function(adata, seurat_obj, mod, assay) { + cells <- colnames(seurat_obj) + mod_meta <- adata$obs + obs_names <- rownames(mod_meta) + colnames(mod_meta) <- paste0(assay, "_", make.names(colnames(mod_meta))) + + if (!all(obs_names %in% cells)) { + stop(paste0( + "Not all cells in the adata modality", mod, + "are present in the Seurat object." + ) + ) + } + mod_meta <- mod_meta[match(cells, obs_names), , drop = FALSE] + rownames(mod_meta) <- cells + + seurat_obj <- AddMetaData(seurat_obj, mod_meta) +} + +map_modality_to_assay <- function(adata, seurat_obj, mod, assay) { + + +} + +# Initialize seurat object +seurat_obj <- NULL +modalities_to_metadata <- list() + +# Check that modalities and assays have the same length +if (length(par$modality) != length(par$assay)) { + stop("The number of modalities should match the number of assays.") +} + +# Loop through modalities and assays +for (i in seq_along(par$modality)) { + cat("Processing modality:", par$modality[i], "as assay:", par$assay[i], "\n") + + # Read the specific modality from h5mu file + adata_path <- h5mu_to_h5ad(par$input, par$modality[i]) + adata <- read_h5ad(adata_path, mode = "r+") + + # Check dimensions + # Modalities with dimension 0 will be added later + if (has_zero_dimension(adata)) { + if (adata$shape()[1] == 0) { + cat("Skipping modality", par$modality[i], "- has zero observations\n") + } else { + cat( + "Modality", + par$modality[i], + "has zero features - moving to metadata\n" + ) + modalities_to_metadata[[par$modality[i]]] <- par$assay[i] + } + next + } + + # Convert to Seurat assay + temp_seurat <- read_h5ad( + adata_path, + mode = "r", + as = "Seurat", + assay_name = par$assay[i] + ) + + # If this is the first modality, initialize the main Seurat object + if (is.null(seurat_obj)) { + seurat_obj <- temp_seurat + } else { + # Add as additional assay to existing Seurat object + seurat_obj[[par$assay[i]]] <- temp_seurat[[par$assay[i]]] + } +} + +if (is.null(seurat_obj)) { + stop("No valid modalities found to create a Seurat object. At least one modality must have non-zero dimensions.") +} + +for (mod in names(modalities_to_metadata)) { + assay <- modalities_to_metadata[[mod]] + cat("Moving modality", mod, "to metadata\n") + seurat_obj <- map_obs_to_metadata(adata, seurat_obj, mod, assay) +} + +saveRDS(seurat_obj, file = par$output) diff --git a/src/convert/from_h5mu_to_seurat/test.R b/src/convert/from_h5mu_to_seurat/test.R new file mode 100644 index 0000000..95aa302 --- /dev/null +++ b/src/convert/from_h5mu_to_seurat/test.R @@ -0,0 +1,46 @@ +library(testthat, warn.conflicts = FALSE) +library(hdf5r) + + +## VIASH START +meta <- list( + executable = "target/executable/convert/from_h5ad_to_seurat", + resources_dir = "resources_test", + name = "from_h5ad_to_seurat" +) +## VIASH END + +cat("> Checking conversion of h5mu file\n") + +in_h5mu <- paste0( + meta[["resources_dir"]], + "/pbmc_1k_protein_v3/pbmc_1k_protein_v3_filtered_feature_bc_matrix.h5mu" +) +out_rds <- "output.rds" + +cat("> Running ", meta[["name"]], "\n", sep = "") +out <- processx::run( + meta[["executable"]], + c( + "--input", in_h5mu, + "--output", out_rds + ) +) + +cat("> Checking whether output file exists\n") +expect_equal(out$status, 0) +expect_true(file.exists(out_rds)) + +cat("> Reading output file\n") +obj <- readRDS(file = out_rds) + +cat("> Checking whether Seurat object is in the right format\n") +expect_is(obj, "Seurat") +expect_equal(names(slot(obj, "assays")), "RNA") + +dim_rds <- dim(obj) +mu_in <- H5File$new(in_h5mu, mode = "r") +dim_ad <- mu_in[["/mod/rna/X"]]$attr_open("shape")$read() + +expect_equal(dim_rds[1], dim_ad[2]) +expect_equal(dim_rds[2], dim_ad[1]) diff --git a/src/convert/from_xenium_to_spatialexperiment/script.R b/src/convert/from_xenium_to_spatialexperiment/script.R index 16cf22f..5c46b1f 100644 --- a/src/convert/from_xenium_to_spatialexperiment/script.R +++ b/src/convert/from_xenium_to_spatialexperiment/script.R @@ -2,7 +2,7 @@ library(SpatialExperimentIO) ### VIASH START par <- list( - input = "resources_test/xenium/temp_dir.zip", + input = "output-XETG00150__0031015__slidearray0085__20241023__195946", add_experiment_xenium = TRUE, add_parquet_paths = TRUE, alternative_experiment_features = c( @@ -46,7 +46,9 @@ spe <- readXeniumSXE( coordNames = c("x_centroid", "y_centroid"), addExperimentXenium = par$add_experiment_xenium, addParquetPaths = par$add_parquet_paths, - altExps = par$alternative_experiment_features + altExps = par$alternative_experiment_features, + addCellBound = TRUE, + addNucBound = TRUE ) cat("Saving output...") From fceb33fa3324ba08b3fc61f301eb4cfd5734da23 Mon Sep 17 00:00:00 2001 From: dorien-er Date: Wed, 15 Oct 2025 17:32:31 +0200 Subject: [PATCH 02/12] update gitingnore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 97dc208..606d4b8 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ out/ output* output_log/ resources_test +resources_test_sc /viash_tools/ /test/ From 8eefdf9c526b11195a14bb052485b3d33bb2f7a2 Mon Sep 17 00:00:00 2001 From: dorien-er Date: Wed, 15 Oct 2025 18:00:59 +0200 Subject: [PATCH 03/12] expand tests --- .../from_h5mu_to_seurat/config.vsh.yaml | 2 +- src/convert/from_h5mu_to_seurat/script.R | 51 +++++++------- src/convert/from_h5mu_to_seurat/test.R | 70 ++++++++++++++++++- 3 files changed, 95 insertions(+), 28 deletions(-) diff --git a/src/convert/from_h5mu_to_seurat/config.vsh.yaml b/src/convert/from_h5mu_to_seurat/config.vsh.yaml index c8029aa..0b9f59b 100644 --- a/src/convert/from_h5mu_to_seurat/config.vsh.yaml +++ b/src/convert/from_h5mu_to_seurat/config.vsh.yaml @@ -52,7 +52,7 @@ engines: - type: apt packages: [ libhdf5-dev, libgeos-dev, hdf5-tools ] - type: r - cran: [ anndata, hdf5r, Seurat, SeuratObject ] + cran: [ anndata, hdf5r, Matrix, SeuratObject, Seurat ] github: scverse/anndataR@36f3caad9a7f360165c1510bbe0c62657580415a bioc: [ rhdf5] test_setup: diff --git a/src/convert/from_h5mu_to_seurat/script.R b/src/convert/from_h5mu_to_seurat/script.R index 2b4dd67..093b530 100644 --- a/src/convert/from_h5mu_to_seurat/script.R +++ b/src/convert/from_h5mu_to_seurat/script.R @@ -1,6 +1,6 @@ library(anndataR) library(hdf5r) -# library(Seurat) +library(Seurat) ### VIASH START par <- list( @@ -72,9 +72,22 @@ map_obs_to_metadata <- function(adata, seurat_obj, mod, assay) { seurat_obj <- AddMetaData(seurat_obj, mod_meta) } -map_modality_to_assay <- function(adata, seurat_obj, mod, assay) { - +map_modality_to_assay <- function(adata, seurat_obj, assay) { + temp_seurat <- read_h5ad( + adata, + mode = "r", + as = "Seurat", + assay_name = assay + ) + # If this is the first modality, initialize the main Seurat object + if (is.null(seurat_obj)) { + seurat_obj <- temp_seurat + } else { + # Add as additional assay to existing Seurat object + seurat_obj[[assay]] <- temp_seurat[[assay]] + } + seurat_obj } # Initialize seurat object @@ -88,43 +101,31 @@ if (length(par$modality) != length(par$assay)) { # Loop through modalities and assays for (i in seq_along(par$modality)) { - cat("Processing modality:", par$modality[i], "as assay:", par$assay[i], "\n") + mod <- par$modality[i] + assay <- par$assay[i] + cat("Processing modality:", mod, "as assay:", assay, "\n") # Read the specific modality from h5mu file - adata_path <- h5mu_to_h5ad(par$input, par$modality[i]) - adata <- read_h5ad(adata_path, mode = "r+") + adata_path <- h5mu_to_h5ad(par$input, mod) + adata <- read_h5ad(adata_path, mode = "r") - # Check dimensions - # Modalities with dimension 0 will be added later + # Check dimensions: modalities with dimension 0 will be added later if (has_zero_dimension(adata)) { if (adata$shape()[1] == 0) { - cat("Skipping modality", par$modality[i], "- has zero observations\n") + cat("Skipping modality", mod, "- has zero observations\n") } else { cat( "Modality", - par$modality[i], + mod, "has zero features - moving to metadata\n" ) - modalities_to_metadata[[par$modality[i]]] <- par$assay[i] + modalities_to_metadata[[mod]] <- assay } next } # Convert to Seurat assay - temp_seurat <- read_h5ad( - adata_path, - mode = "r", - as = "Seurat", - assay_name = par$assay[i] - ) - - # If this is the first modality, initialize the main Seurat object - if (is.null(seurat_obj)) { - seurat_obj <- temp_seurat - } else { - # Add as additional assay to existing Seurat object - seurat_obj[[par$assay[i]]] <- temp_seurat[[par$assay[i]]] - } + seurat_obj <- map_modality_to_assay(adata_path, seurat_obj, assay) } if (is.null(seurat_obj)) { diff --git a/src/convert/from_h5mu_to_seurat/test.R b/src/convert/from_h5mu_to_seurat/test.R index 95aa302..4f38dc1 100644 --- a/src/convert/from_h5mu_to_seurat/test.R +++ b/src/convert/from_h5mu_to_seurat/test.R @@ -5,12 +5,13 @@ library(hdf5r) ## VIASH START meta <- list( executable = "target/executable/convert/from_h5ad_to_seurat", - resources_dir = "resources_test", + resources_dir = "resources_test_sc", name = "from_h5ad_to_seurat" ) ## VIASH END -cat("> Checking conversion of h5mu file\n") +## Simple conversion +cat("> Checking conversion of single-modality of h5mu file\n") in_h5mu <- paste0( meta[["resources_dir"]], @@ -44,3 +45,68 @@ dim_ad <- mu_in[["/mod/rna/X"]]$attr_open("shape")$read() expect_equal(dim_rds[1], dim_ad[2]) expect_equal(dim_rds[2], dim_ad[1]) + +## Multi-modal conversion +cat("> Checking conversion of multi-modal h5mu file\n") + +in_h5mu <- paste0( + meta[["resources_dir"]], + "/10x_5k_anticmv/5k_human_antiCMV_T_TBNK_connect.h5mu" +) +out_rds <- "output.rds" + +cat("> Running ", meta[["name"]], "\n", sep = "") +out <- processx::run( + meta[["executable"]], + c( + "--input", in_h5mu, + "--modality", "rna", + "--modality", "prot", + "--modality", "vdj_t", + "--assay", "RNA", + "--assay", "ADT", + "--assay", "TCR", + "--output", out_rds + ) +) + +cat("> Checking whether output file exists\n") +expect_equal(out$status, 0) +expect_true(file.exists(out_rds)) + +cat("> Reading output file\n") +obj <- readRDS(file = out_rds) + +cat("> Checking whether Seurat object is in the right format\n") +expect_is(obj, "Seurat") +expect_true(all(names(slot(obj, "assays")) %in% c("RNA", "ADT"))) + +dim_rds <- dim(obj) +mu_in <- H5File$new(in_h5mu, mode = "r") +dim_ad <- mu_in[["/mod/rna/X"]]$attr_open("shape")$read() + +expect_equal(dim_rds[1], dim_ad[2]) +expect_equal(dim_rds[2], dim_ad[1]) + +vdj_t_cols <- c( + "TCR_is_cell", "TCR_high_confidence", "TCR_multi_chain", "TCR_extra_chains", + "TCR_IR_VJ_1_c_call", "TCR_IR_VJ_2_c_call", "TCR_IR_VDJ_1_c_call", + "TCR_IR_VDJ_2_c_call", "TCR_IR_VJ_1_consensus_count", + "TCR_IR_VJ_2_consensus_count", "TCR_IR_VDJ_1_consensus_count", + "TCR_IR_VDJ_2_consensus_count", "TCR_IR_VJ_1_d_call", + "TCR_IR_VJ_2_d_call", "TCR_IR_VDJ_1_d_call", "TCR_IR_VDJ_2_d_call", + "TCR_IR_VJ_1_duplicate_count", "TCR_IR_VJ_2_duplicate_count", + "TCR_IR_VDJ_1_duplicate_count", "TCR_IR_VDJ_2_duplicate_count", + "TCR_IR_VJ_1_j_call", "TCR_IR_VJ_2_j_call", "TCR_IR_VDJ_1_j_call", + "TCR_IR_VDJ_2_j_call", "TCR_IR_VJ_1_junction", "TCR_IR_VJ_2_junction", + "TCR_IR_VDJ_1_junction", "TCR_IR_VDJ_2_junction", "TCR_IR_VJ_1_junction_aa", + "TCR_IR_VJ_2_junction_aa", "TCR_IR_VDJ_1_junction_aa", "TCR_IR_VDJ_2_junction_aa", + "TCR_IR_VJ_1_locus", "TCR_IR_VJ_2_locus", "TCR_IR_VDJ_1_locus", + "TCR_IR_VDJ_2_locus", "TCR_IR_VJ_1_productive", "TCR_IR_VJ_2_productive", + "TCR_IR_VDJ_1_productive", "TCR_IR_VDJ_2_productive", "TCR_IR_VJ_1_v_call", + "TCR_IR_VJ_2_v_call", "TCR_IR_VDJ_1_v_call", "TCR_IR_VDJ_2_v_call", "TCR_has_ir" +) +obs_cols <- c("orig.ident", "nCount_RNA", "nFeature_RNA") + +expect_true(all(vdj_t_cols %in% colnames(obj@meta.data))) +expect_true(all(obs_cols %in% colnames(obj@meta.data))) \ No newline at end of file From 7c6e0b3932a615d8aa4bf0c7c67b934aa6ffd1c2 Mon Sep 17 00:00:00 2001 From: dorien-er Date: Wed, 15 Oct 2025 18:09:16 +0200 Subject: [PATCH 04/12] lint --- src/convert/from_cosmx_to_spatialexperiment/test.R | 5 +++-- src/convert/from_h5mu_to_seurat/script.R | 3 +-- src/convert/from_h5mu_to_seurat/test.R | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/convert/from_cosmx_to_spatialexperiment/test.R b/src/convert/from_cosmx_to_spatialexperiment/test.R index 3424487..771035b 100644 --- a/src/convert/from_cosmx_to_spatialexperiment/test.R +++ b/src/convert/from_cosmx_to_spatialexperiment/test.R @@ -75,8 +75,9 @@ spe <- paste0(meta[["resources_dir"]], "/Lung5_Rep2_tiny") out_rds <- "output.rds" create_folder_archive <- function( - folder_path, - archive = "Lung5_Rep2_tiny.zip") { + folder_path, + archive = "Lung5_Rep2_tiny.zip" +) { old_wd <- getwd() on.exit(setwd(old_wd)) setwd(meta$resources_dir) diff --git a/src/convert/from_h5mu_to_seurat/script.R b/src/convert/from_h5mu_to_seurat/script.R index 093b530..0c780e3 100644 --- a/src/convert/from_h5mu_to_seurat/script.R +++ b/src/convert/from_h5mu_to_seurat/script.R @@ -63,8 +63,7 @@ map_obs_to_metadata <- function(adata, seurat_obj, mod, assay) { stop(paste0( "Not all cells in the adata modality", mod, "are present in the Seurat object." - ) - ) + )) } mod_meta <- mod_meta[match(cells, obs_names), , drop = FALSE] rownames(mod_meta) <- cells diff --git a/src/convert/from_h5mu_to_seurat/test.R b/src/convert/from_h5mu_to_seurat/test.R index 4f38dc1..0a1b874 100644 --- a/src/convert/from_h5mu_to_seurat/test.R +++ b/src/convert/from_h5mu_to_seurat/test.R @@ -109,4 +109,4 @@ vdj_t_cols <- c( obs_cols <- c("orig.ident", "nCount_RNA", "nFeature_RNA") expect_true(all(vdj_t_cols %in% colnames(obj@meta.data))) -expect_true(all(obs_cols %in% colnames(obj@meta.data))) \ No newline at end of file +expect_true(all(obs_cols %in% colnames(obj@meta.data))) From a339b29910c7b18b27a874ed7caf91f4d398b707 Mon Sep 17 00:00:00 2001 From: dorien-er Date: Thu, 16 Oct 2025 08:13:09 +0200 Subject: [PATCH 05/12] lint --- .gitignore | 1 - _viash.yaml | 2 +- src/convert/from_h5mu_to_seurat/config.vsh.yaml | 4 ++-- src/convert/from_h5mu_to_seurat/script.R | 13 ++++++++----- src/convert/from_h5mu_to_seurat/test.R | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 606d4b8..97dc208 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ out/ output* output_log/ resources_test -resources_test_sc /viash_tools/ /test/ diff --git a/_viash.yaml b/_viash.yaml index f64a065..d56fdcf 100644 --- a/_viash.yaml +++ b/_viash.yaml @@ -23,7 +23,7 @@ info: dest: resources_test - type: s3 path: s3://openpipelines-data - dest: resources_test_sc + dest: resources_test config_mods: | .resources += {path: '/src/workflows/utils/labels.config', dest: 'nextflow_labels.config'} diff --git a/src/convert/from_h5mu_to_seurat/config.vsh.yaml b/src/convert/from_h5mu_to_seurat/config.vsh.yaml index 0b9f59b..26f2d0d 100644 --- a/src/convert/from_h5mu_to_seurat/config.vsh.yaml +++ b/src/convert/from_h5mu_to_seurat/config.vsh.yaml @@ -43,8 +43,8 @@ resources: test_resources: - type: r_script path: test.R - - path: /resources_test_sc/pbmc_1k_protein_v3/ - - path: /resources_test_sc/10x_5k_anticmv/ + - path: /resources_test/pbmc_1k_protein_v3/ + - path: /resources_test/10x_5k_anticmv/ engines: - type: docker image: rocker/r2u:22.04 diff --git a/src/convert/from_h5mu_to_seurat/script.R b/src/convert/from_h5mu_to_seurat/script.R index 0c780e3..326968c 100644 --- a/src/convert/from_h5mu_to_seurat/script.R +++ b/src/convert/from_h5mu_to_seurat/script.R @@ -4,8 +4,8 @@ library(Seurat) ### VIASH START par <- list( - input = "resources_test_sc/10x_5k_anticmv/5k_human_antiCMV_T_TBNK_connect.h5mu", - output = "resources_test_sc/10x_5k_anticmv/5k_human_antiCMV_T_TBNK_connect.rds", + input = "./5k_human_antiCMV_T_TBNK_connect.h5mu", + output = "./5k_human_antiCMV_T_TBNK_connect.rds", assay = c("RNA", "ADT", "TCR"), modality = c("rna", "prot", "vdj_t") ) @@ -68,11 +68,11 @@ map_obs_to_metadata <- function(adata, seurat_obj, mod, assay) { mod_meta <- mod_meta[match(cells, obs_names), , drop = FALSE] rownames(mod_meta) <- cells - seurat_obj <- AddMetaData(seurat_obj, mod_meta) + seurat_obj <- Seurat::AddMetaData(seurat_obj, mod_meta) } map_modality_to_assay <- function(adata, seurat_obj, assay) { - temp_seurat <- read_h5ad( + temp_seurat <- anndatar::read_h5ad( adata, mode = "r", as = "Seurat", @@ -128,7 +128,10 @@ for (i in seq_along(par$modality)) { } if (is.null(seurat_obj)) { - stop("No valid modalities found to create a Seurat object. At least one modality must have non-zero dimensions.") + stop( + "No valid modalities found to create a Seurat object.", + "At least one modality must have non-zero dimensions." + ) } for (mod in names(modalities_to_metadata)) { diff --git a/src/convert/from_h5mu_to_seurat/test.R b/src/convert/from_h5mu_to_seurat/test.R index 0a1b874..f33740a 100644 --- a/src/convert/from_h5mu_to_seurat/test.R +++ b/src/convert/from_h5mu_to_seurat/test.R @@ -5,7 +5,7 @@ library(hdf5r) ## VIASH START meta <- list( executable = "target/executable/convert/from_h5ad_to_seurat", - resources_dir = "resources_test_sc", + resources_dir = "resources_test", name = "from_h5ad_to_seurat" ) ## VIASH END From 6761d44f6f9f814ad0ebab37fd061d118a39b268 Mon Sep 17 00:00:00 2001 From: dorien-er Date: Thu, 16 Oct 2025 09:05:54 +0200 Subject: [PATCH 06/12] lint --- src/convert/from_h5mu_to_seurat/script.R | 2 +- src/convert/from_h5mu_to_seurat/test.R | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/convert/from_h5mu_to_seurat/script.R b/src/convert/from_h5mu_to_seurat/script.R index 326968c..90d483b 100644 --- a/src/convert/from_h5mu_to_seurat/script.R +++ b/src/convert/from_h5mu_to_seurat/script.R @@ -72,7 +72,7 @@ map_obs_to_metadata <- function(adata, seurat_obj, mod, assay) { } map_modality_to_assay <- function(adata, seurat_obj, assay) { - temp_seurat <- anndatar::read_h5ad( + temp_seurat <- anndataR::read_h5ad( adata, mode = "r", as = "Seurat", diff --git a/src/convert/from_h5mu_to_seurat/test.R b/src/convert/from_h5mu_to_seurat/test.R index f33740a..b7795ff 100644 --- a/src/convert/from_h5mu_to_seurat/test.R +++ b/src/convert/from_h5mu_to_seurat/test.R @@ -100,11 +100,12 @@ vdj_t_cols <- c( "TCR_IR_VJ_1_j_call", "TCR_IR_VJ_2_j_call", "TCR_IR_VDJ_1_j_call", "TCR_IR_VDJ_2_j_call", "TCR_IR_VJ_1_junction", "TCR_IR_VJ_2_junction", "TCR_IR_VDJ_1_junction", "TCR_IR_VDJ_2_junction", "TCR_IR_VJ_1_junction_aa", - "TCR_IR_VJ_2_junction_aa", "TCR_IR_VDJ_1_junction_aa", "TCR_IR_VDJ_2_junction_aa", - "TCR_IR_VJ_1_locus", "TCR_IR_VJ_2_locus", "TCR_IR_VDJ_1_locus", - "TCR_IR_VDJ_2_locus", "TCR_IR_VJ_1_productive", "TCR_IR_VJ_2_productive", - "TCR_IR_VDJ_1_productive", "TCR_IR_VDJ_2_productive", "TCR_IR_VJ_1_v_call", - "TCR_IR_VJ_2_v_call", "TCR_IR_VDJ_1_v_call", "TCR_IR_VDJ_2_v_call", "TCR_has_ir" + "TCR_IR_VJ_2_junction_aa", "TCR_IR_VDJ_1_junction_aa", + "TCR_IR_VDJ_2_junction_aa", "TCR_IR_VJ_1_locus", "TCR_IR_VJ_2_locus", + "TCR_IR_VDJ_1_locus", "TCR_IR_VDJ_2_locus", "TCR_IR_VJ_1_productive", + "TCR_IR_VJ_2_productive", "TCR_IR_VDJ_1_productive", + "TCR_IR_VDJ_2_productive", "TCR_IR_VJ_1_v_call", "TCR_IR_VJ_2_v_call", + "TCR_IR_VDJ_1_v_call", "TCR_IR_VDJ_2_v_call", "TCR_has_ir" ) obs_cols <- c("orig.ident", "nCount_RNA", "nFeature_RNA") From 801b374cf3752009ccf3f6c50cd7a5632ca84663 Mon Sep 17 00:00:00 2001 From: dorien-er Date: Thu, 16 Oct 2025 09:34:07 +0200 Subject: [PATCH 07/12] rename test resources --- _viash.yaml | 2 +- src/convert/from_h5mu_to_seurat/config.vsh.yaml | 4 ++-- src/convert/from_h5mu_to_seurat/test.R | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/_viash.yaml b/_viash.yaml index d56fdcf..5eb8ad9 100644 --- a/_viash.yaml +++ b/_viash.yaml @@ -23,7 +23,7 @@ info: dest: resources_test - type: s3 path: s3://openpipelines-data - dest: resources_test + dest: op_resources_test config_mods: | .resources += {path: '/src/workflows/utils/labels.config', dest: 'nextflow_labels.config'} diff --git a/src/convert/from_h5mu_to_seurat/config.vsh.yaml b/src/convert/from_h5mu_to_seurat/config.vsh.yaml index 26f2d0d..d536073 100644 --- a/src/convert/from_h5mu_to_seurat/config.vsh.yaml +++ b/src/convert/from_h5mu_to_seurat/config.vsh.yaml @@ -43,8 +43,8 @@ resources: test_resources: - type: r_script path: test.R - - path: /resources_test/pbmc_1k_protein_v3/ - - path: /resources_test/10x_5k_anticmv/ + - path: /resources_test_op/pbmc_1k_protein_v3/ + - path: /resources_test_op/10x_5k_anticmv/ engines: - type: docker image: rocker/r2u:22.04 diff --git a/src/convert/from_h5mu_to_seurat/test.R b/src/convert/from_h5mu_to_seurat/test.R index b7795ff..89fc149 100644 --- a/src/convert/from_h5mu_to_seurat/test.R +++ b/src/convert/from_h5mu_to_seurat/test.R @@ -5,7 +5,7 @@ library(hdf5r) ## VIASH START meta <- list( executable = "target/executable/convert/from_h5ad_to_seurat", - resources_dir = "resources_test", + resources_dir = "op_resources_test", name = "from_h5ad_to_seurat" ) ## VIASH END From 37ca9b9d8504d051ba669ea27276988c0a88ba6f Mon Sep 17 00:00:00 2001 From: dorien-er Date: Thu, 16 Oct 2025 09:45:31 +0200 Subject: [PATCH 08/12] rename test resources --- _viash.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_viash.yaml b/_viash.yaml index 5eb8ad9..3cf7f6e 100644 --- a/_viash.yaml +++ b/_viash.yaml @@ -23,7 +23,7 @@ info: dest: resources_test - type: s3 path: s3://openpipelines-data - dest: op_resources_test + dest: resources_test_op config_mods: | .resources += {path: '/src/workflows/utils/labels.config', dest: 'nextflow_labels.config'} From 025e6e4a0c717e071e9d29d8d96010afcfb94d7d Mon Sep 17 00:00:00 2001 From: dorien-er Date: Fri, 17 Oct 2025 12:50:17 +0200 Subject: [PATCH 09/12] add spatial h5mu conversion to seurat --- _viash.yaml | 3 - .../from_h5mu_to_seurat/config.vsh.yaml | 97 ++++++---- src/convert/from_h5mu_to_seurat/script.R | 124 ++++-------- src/convert/from_h5mu_to_seurat/test.R | 182 +++++++++++++----- 4 files changed, 233 insertions(+), 173 deletions(-) diff --git a/_viash.yaml b/_viash.yaml index 3cf7f6e..504fcce 100644 --- a/_viash.yaml +++ b/_viash.yaml @@ -21,9 +21,6 @@ info: - type: s3 path: s3://openpipelines-bio/openpipeline_spatial/resources_test dest: resources_test - - type: s3 - path: s3://openpipelines-data - dest: resources_test_op config_mods: | .resources += {path: '/src/workflows/utils/labels.config', dest: 'nextflow_labels.config'} diff --git a/src/convert/from_h5mu_to_seurat/config.vsh.yaml b/src/convert/from_h5mu_to_seurat/config.vsh.yaml index d536073..694a2c6 100644 --- a/src/convert/from_h5mu_to_seurat/config.vsh.yaml +++ b/src/convert/from_h5mu_to_seurat/config.vsh.yaml @@ -2,49 +2,73 @@ name: "from_h5mu_to_seurat" namespace: "convert" scope: "public" description: | - Converts an h5ad file or a single modality of an h5mu file into a Seurat file. + Converts an h5ad file or a single modality of an h5mu file into a Seurat file with a Field of View (FOV) field, + storing coordinates of spatially-resolved single cells based on centroid coordinates. authors: - __merge__: /src/authors/dorien_roosen.yaml roles: [ author, maintainer ] -arguments: - - name: "--input" - alternatives: ["-i"] - type: file - description: Input h5ad or h5mu file - direction: input - required: true - example: input.h5ad - - name: "--modality" - type: string - default: ["rna"] - multiple: true - example: ["rna", "prot", "vdj"] - required: false - description: Modality to be converted if the input file is an h5mu file. - - name: "--assay" - type: string - default: ["RNA"] - multiple: true - example: ["RNA", "ADT", "VDJ"] - description: | - Name of the assay to be created. - Order should match the order of the --modalities. - If layer dimensions of a modality are 0, the modality will be moved to the metadata instead of an assay. - - name: "--output" - alternatives: ["-o"] - type: file - description: Output Seurat file - direction: output - required: true - example: output.rds +argument_groups: + - name: Inputs + arguments: + - name: "--input" + alternatives: ["-i"] + type: file + description: Input h5ad or h5mu file + direction: input + required: true + example: input.h5ad + - name: "--modality" + type: string + default: "rna" + required: true + description: Modality to be converted if the input file is an h5mu file. + - name: "--obsm_centroid_coordinates" + type: string + description: | + Key name of the .obsm slot in the input file that contains the spatial (centroid) coordinates. + If not provided, no Field of View (FOV) will be created in the Seurat object. + - name: Centroid arguments + arguments: + - name: "--centroid_nsides" + type: integer + required: false + description: | + Number of sides of the polygon to be created around each centroid coordinate to represent the cell shape. + If not provided, circles will be created. + - name: "--centroid_radius" + type: double + required: false + description: | + Radius of the shape around each centroid coordinate to represent the cell shape. + If not provided, a default radius will be calculated based on the provided centroid coordinates. + - name: "--centroid_theta" + type: double + required: false + description: | + Angle to adjust the shapes around each centroid when plotting. + If not provided, no adjustment will be made (theta = 0) + - name: Outputs + arguments: + - name: "--output" + alternatives: ["-o"] + type: file + description: Output Seurat file + direction: output + required: true + example: output.rds + - name: "--assay" + type: string + default: "RNA" + description: Name of the assay to be created. resources: - type: r_script path: script.R test_resources: - type: r_script path: test.R - - path: /resources_test_op/pbmc_1k_protein_v3/ - - path: /resources_test_op/10x_5k_anticmv/ + - path: /resources_test/aviti/aviti_teton_tiny.h5mu + - path: /resources_test/cosmx/Lung5_Rep2_tiny.h5mu + - path: /resources_test/xenium/xenium_tiny_processed.h5mu engines: - type: docker image: rocker/r2u:22.04 @@ -52,9 +76,8 @@ engines: - type: apt packages: [ libhdf5-dev, libgeos-dev, hdf5-tools ] - type: r - cran: [ anndata, hdf5r, Matrix, SeuratObject, Seurat ] + cran: [ anndata, hdf5r, Seurat, SeuratObject ] github: scverse/anndataR@36f3caad9a7f360165c1510bbe0c62657580415a - bioc: [ rhdf5] test_setup: - type: r cran: [ testthat ] @@ -62,4 +85,4 @@ runners: - type: executable - type: nextflow directives: - label: [lowmem, singlecpu] + label: [lowmem, singlecpu] \ No newline at end of file diff --git a/src/convert/from_h5mu_to_seurat/script.R b/src/convert/from_h5mu_to_seurat/script.R index 90d483b..0b7b0f5 100644 --- a/src/convert/from_h5mu_to_seurat/script.R +++ b/src/convert/from_h5mu_to_seurat/script.R @@ -4,21 +4,24 @@ library(Seurat) ### VIASH START par <- list( - input = "./5k_human_antiCMV_T_TBNK_connect.h5mu", - output = "./5k_human_antiCMV_T_TBNK_connect.rds", - assay = c("RNA", "ADT", "TCR"), - modality = c("rna", "prot", "vdj_t") + input = "resources_test/xenium/xenium_tiny_processed.h5mu", + output = "test.rds", + obsm_centroid_coordinates = "spatial", + assay = "RNA", + centroid_nsides = 8, + centroid_radius = 3, + centroid_theta = 0.1, + modality = "rna" ) ### VIASH END + h5mu_to_h5ad <- function(h5mu_path, modality_name) { tmp_path <- tempfile(fileext = ".h5ad") mod_location <- paste("mod", modality_name, sep = "/") h5src <- hdf5r::H5File$new(h5mu_path, "r") h5dest <- hdf5r::H5File$new(tmp_path, "w") # Copy over the child objects and the child attributes from root - # Root cannot be copied directly because it always exists and - # copying does not allow overwriting. children <- hdf5r::list.objects(h5src, path = mod_location, full.names = FALSE, recursive = FALSE @@ -48,96 +51,43 @@ h5mu_to_h5ad <- function(h5mu_path, modality_name) { tmp_path } -has_zero_dimension <- function(adata) { - adata_dims <- adata$shape() - any(adata_dims == 0) -} - -map_obs_to_metadata <- function(adata, seurat_obj, mod, assay) { - cells <- colnames(seurat_obj) - mod_meta <- adata$obs - obs_names <- rownames(mod_meta) - colnames(mod_meta) <- paste0(assay, "_", make.names(colnames(mod_meta))) - - if (!all(obs_names %in% cells)) { - stop(paste0( - "Not all cells in the adata modality", mod, - "are present in the Seurat object." - )) - } - mod_meta <- mod_meta[match(cells, obs_names), , drop = FALSE] - rownames(mod_meta) <- cells +# Read in H5AD +h5ad_path <- h5mu_to_h5ad(par$input, par$modality) - seurat_obj <- Seurat::AddMetaData(seurat_obj, mod_meta) -} +# Convert to Seurat +seurat_obj <- read_h5ad( + h5ad_path, + mode = "r", + as = "Seurat", + assay_name = par$assay +) -map_modality_to_assay <- function(adata, seurat_obj, assay) { - temp_seurat <- anndataR::read_h5ad( - adata, - mode = "r", - as = "Seurat", - assay_name = assay - ) +# Create Centroids object +if (!is.null(par$obsm_centroid_coordinates)) { + reductions <- seurat_obj@reductions[[par$obsm_centroid_coordinates]] + spatial_coords <- as.data.frame(reductions@cell.embeddings) + colnames(spatial_coords) <- c("x_coord", "y_coord") - # If this is the first modality, initialize the main Seurat object - if (is.null(seurat_obj)) { - seurat_obj <- temp_seurat - } else { - # Add as additional assay to existing Seurat object - seurat_obj[[assay]] <- temp_seurat[[assay]] + if (is.null(par$centroid_nsides)) { + par$centroid_nsides <- Inf } - seurat_obj -} -# Initialize seurat object -seurat_obj <- NULL -modalities_to_metadata <- list() - -# Check that modalities and assays have the same length -if (length(par$modality) != length(par$assay)) { - stop("The number of modalities should match the number of assays.") -} - -# Loop through modalities and assays -for (i in seq_along(par$modality)) { - mod <- par$modality[i] - assay <- par$assay[i] - cat("Processing modality:", mod, "as assay:", assay, "\n") - - # Read the specific modality from h5mu file - adata_path <- h5mu_to_h5ad(par$input, mod) - adata <- read_h5ad(adata_path, mode = "r") - - # Check dimensions: modalities with dimension 0 will be added later - if (has_zero_dimension(adata)) { - if (adata$shape()[1] == 0) { - cat("Skipping modality", mod, "- has zero observations\n") - } else { - cat( - "Modality", - mod, - "has zero features - moving to metadata\n" - ) - modalities_to_metadata[[mod]] <- assay - } - next + if (is.null(par$centroid_theta)) { + par$centroid_theta <- 0L } - # Convert to Seurat assay - seurat_obj <- map_modality_to_assay(adata_path, seurat_obj, assay) -} - -if (is.null(seurat_obj)) { - stop( - "No valid modalities found to create a Seurat object.", - "At least one modality must have non-zero dimensions." + centroids <- CreateCentroids( + coords = spatial_coords, + nsides = par$centroid_nsides, + radius = par$centroid_radius, + theta = par$centroid_theta ) -} -for (mod in names(modalities_to_metadata)) { - assay <- modalities_to_metadata[[mod]] - cat("Moving modality", mod, "to metadata\n") - seurat_obj <- map_obs_to_metadata(adata, seurat_obj, mod, assay) + # Create FOV object + fov <- CreateFOV(coords = centroids, assay = par$assay) + seurat_obj[["fov"]] <- fov + seurat_obj@reductions[[par$obsm_centroid_coordinates]] <- NULL } + saveRDS(seurat_obj, file = par$output) diff --git a/src/convert/from_h5mu_to_seurat/test.R b/src/convert/from_h5mu_to_seurat/test.R index 89fc149..c70e360 100644 --- a/src/convert/from_h5mu_to_seurat/test.R +++ b/src/convert/from_h5mu_to_seurat/test.R @@ -1,21 +1,22 @@ library(testthat, warn.conflicts = FALSE) library(hdf5r) - +library(Seurat) ## VIASH START meta <- list( - executable = "target/executable/convert/from_h5ad_to_seurat", - resources_dir = "op_resources_test", - name = "from_h5ad_to_seurat" + executable = "target/executable/convert/from_h5ad_to_spatial_seurat", + resources_dir = "resources_test", + name = "from_h5ad_to_spatial_seurat" ) ## VIASH END -## Simple conversion -cat("> Checking conversion of single-modality of h5mu file\n") + +# ---- No FOV ---------------------------------------------------------- +cat("> Test conversion without adding FOV\n") in_h5mu <- paste0( meta[["resources_dir"]], - "/pbmc_1k_protein_v3/pbmc_1k_protein_v3_filtered_feature_bc_matrix.h5mu" + "/xenium_tiny_processed.h5mu" ) out_rds <- "output.rds" @@ -24,7 +25,9 @@ out <- processx::run( meta[["executable"]], c( "--input", in_h5mu, - "--output", out_rds + "--output", out_rds, + "--modality", "rna", + "--assay", "Xenium" ) ) @@ -34,24 +37,26 @@ expect_true(file.exists(out_rds)) cat("> Reading output file\n") obj <- readRDS(file = out_rds) +adata <- H5File$new(in_h5mu, mode = "r")[["/mod/rna/X"]] cat("> Checking whether Seurat object is in the right format\n") -expect_is(obj, "Seurat") -expect_equal(names(slot(obj, "assays")), "RNA") +expect_equal(Assays(obj), "Xenium") +expect_true(all(Layers(obj) == c("counts", "log_normalized"))) dim_rds <- dim(obj) -mu_in <- H5File$new(in_h5mu, mode = "r") -dim_ad <- mu_in[["/mod/rna/X"]]$attr_open("shape")$read() +dim_ad <- adata$attr_open("shape")$read() expect_equal(dim_rds[1], dim_ad[2]) expect_equal(dim_rds[2], dim_ad[1]) -## Multi-modal conversion -cat("> Checking conversion of multi-modal h5mu file\n") +expect_false("fov" %in% names(obj)) + +# # ---- Xenium ---------------------------------------------------------- +cat("> Test conversion Xenium\n") in_h5mu <- paste0( meta[["resources_dir"]], - "/10x_5k_anticmv/5k_human_antiCMV_T_TBNK_connect.h5mu" + "/xenium_tiny_processed.h5mu" ) out_rds <- "output.rds" @@ -60,13 +65,10 @@ out <- processx::run( meta[["executable"]], c( "--input", in_h5mu, + "--output", out_rds, "--modality", "rna", - "--modality", "prot", - "--modality", "vdj_t", - "--assay", "RNA", - "--assay", "ADT", - "--assay", "TCR", - "--output", out_rds + "--assay", "Xenium", + "--obsm_centroid_coordinates", "spatial" ) ) @@ -76,38 +78,126 @@ expect_true(file.exists(out_rds)) cat("> Reading output file\n") obj <- readRDS(file = out_rds) +adata <- H5File$new(in_h5mu, mode = "r")[["/mod/rna/X"]] cat("> Checking whether Seurat object is in the right format\n") -expect_is(obj, "Seurat") -expect_true(all(names(slot(obj, "assays")) %in% c("RNA", "ADT"))) +expect_equal(Assays(obj), "Xenium") +expect_true(all(Layers(obj) == c("counts", "log_normalized"))) dim_rds <- dim(obj) -mu_in <- H5File$new(in_h5mu, mode = "r") -dim_ad <- mu_in[["/mod/rna/X"]]$attr_open("shape")$read() +dim_ad <- adata$attr_open("shape")$read() expect_equal(dim_rds[1], dim_ad[2]) expect_equal(dim_rds[2], dim_ad[1]) -vdj_t_cols <- c( - "TCR_is_cell", "TCR_high_confidence", "TCR_multi_chain", "TCR_extra_chains", - "TCR_IR_VJ_1_c_call", "TCR_IR_VJ_2_c_call", "TCR_IR_VDJ_1_c_call", - "TCR_IR_VDJ_2_c_call", "TCR_IR_VJ_1_consensus_count", - "TCR_IR_VJ_2_consensus_count", "TCR_IR_VDJ_1_consensus_count", - "TCR_IR_VDJ_2_consensus_count", "TCR_IR_VJ_1_d_call", - "TCR_IR_VJ_2_d_call", "TCR_IR_VDJ_1_d_call", "TCR_IR_VDJ_2_d_call", - "TCR_IR_VJ_1_duplicate_count", "TCR_IR_VJ_2_duplicate_count", - "TCR_IR_VDJ_1_duplicate_count", "TCR_IR_VDJ_2_duplicate_count", - "TCR_IR_VJ_1_j_call", "TCR_IR_VJ_2_j_call", "TCR_IR_VDJ_1_j_call", - "TCR_IR_VDJ_2_j_call", "TCR_IR_VJ_1_junction", "TCR_IR_VJ_2_junction", - "TCR_IR_VDJ_1_junction", "TCR_IR_VDJ_2_junction", "TCR_IR_VJ_1_junction_aa", - "TCR_IR_VJ_2_junction_aa", "TCR_IR_VDJ_1_junction_aa", - "TCR_IR_VDJ_2_junction_aa", "TCR_IR_VJ_1_locus", "TCR_IR_VJ_2_locus", - "TCR_IR_VDJ_1_locus", "TCR_IR_VDJ_2_locus", "TCR_IR_VJ_1_productive", - "TCR_IR_VJ_2_productive", "TCR_IR_VDJ_1_productive", - "TCR_IR_VDJ_2_productive", "TCR_IR_VJ_1_v_call", "TCR_IR_VJ_2_v_call", - "TCR_IR_VDJ_1_v_call", "TCR_IR_VDJ_2_v_call", "TCR_has_ir" +cat("> Checking FOV object\n") +expect_true("fov" %in% names(obj)) +expect_true("fov" %in% Images(obj)) + +fov <- obj[["fov"]] +expect_equal(fov@assay, "Xenium") +expect_equal(fov@key, "Xenium_") + +centroids <- fov@boundaries$centroids +expect_equal(nrow(centroids@coords), dim_rds[2]) + +centroid_coords <- centroids@coords +expect_true(is.numeric(centroid_coords[, 1])) +expect_true(is.numeric(centroid_coords[, 2])) +expect_false(any(is.na(centroid_coords))) + +# # ---- Xenium with args------------------------------------------------- +cat("> Test conversion Xenium with centroid arguments\n") + +in_h5mu <- paste0( + meta[["resources_dir"]], + "/xenium_tiny_processed.h5mu" +) +out_rds <- "output.rds" + +cat("> Running ", meta[["name"]], "\n", sep = "") +out <- processx::run( + meta[["executable"]], + c( + "--input", in_h5mu, + "--output", out_rds, + "--modality", "rna", + "--assay", "Xenium", + "--obsm_centroid_coordinates", "spatial", + "--centroid_nsides", "8", + "--centroid_radius", "3", + "--centroid_theta", "0.1" + ) ) -obs_cols <- c("orig.ident", "nCount_RNA", "nFeature_RNA") -expect_true(all(vdj_t_cols %in% colnames(obj@meta.data))) -expect_true(all(obs_cols %in% colnames(obj@meta.data))) +cat("> Checking whether output file exists\n") +expect_equal(out$status, 0) +expect_true(file.exists(out_rds)) + +cat("> Reading output file\n") +obj <- readRDS(file = out_rds) +adata <- H5File$new(in_h5mu, mode = "r")[["/mod/rna/X"]] + +cat("> Checking FOV object\n") +fov <- obj[["fov"]] +centroids <- fov@boundaries$centroids +expect_equal(centroids@nsides, 8) +expect_equal(centroids@radius, 3) +expect_equal(centroids@theta, 0.1) + + +# ---- CosMx ---------------------------------------------------------- + +cat("> Test conversion CosMx\n") + +in_h5mu <- paste0( + meta[["resources_dir"]], + "/Lung5_Rep2_tiny.h5mu" +) +out_rds <- "output.rds" + +cat("> Running ", meta[["name"]], "\n", sep = "") +out <- processx::run( + meta[["executable"]], + c( + "--input", in_h5mu, + "--output", out_rds, + "--modality", "rna", + "--assay", "CosMx", + "--obsm_centroid_coordinates", "spatial" + ) +) + +cat("> Checking whether output file exists\n") +expect_equal(out$status, 0) +expect_true(file.exists(out_rds)) + +cat("> Reading output file\n") +obj <- readRDS(file = out_rds) +adata <- H5File$new(in_h5mu, mode = "r")[["/mod/rna/X"]] + +cat("> Checking whether Seurat object is in the right format\n") +expect_equal(Assays(obj), "CosMx") +expect_true(all(Layers(obj) == c("counts"))) + +dim_rds <- dim(obj) +dim_ad <- adata$attr_open("shape")$read() + +expect_equal(dim_rds[1], dim_ad[2]) +expect_equal(dim_rds[2], dim_ad[1]) + +cat("> Checking FOV object\n") +expect_true("fov" %in% names(obj)) +expect_true("fov" %in% Images(obj)) + +fov <- obj[["fov"]] +expect_equal(fov@assay, "CosMx") +expect_equal(fov@key, "CosMx_") + +centroids <- fov@boundaries$centroids +expect_equal(nrow(centroids@coords), dim_rds[2]) + +centroid_coords <- centroids@coords +expect_true(is.numeric(centroid_coords[, 1])) +expect_true(is.numeric(centroid_coords[, 2])) +expect_false(any(is.na(centroid_coords))) From 076d035daf1125be46d73418e4c119990caaeb94 Mon Sep 17 00:00:00 2001 From: dorien-er Date: Fri, 17 Oct 2025 13:19:04 +0200 Subject: [PATCH 10/12] update test resources --- src/convert/from_h5mu_to_seurat/config.vsh.yaml | 2 +- src/convert/from_h5mu_to_seurat/test.R | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/convert/from_h5mu_to_seurat/config.vsh.yaml b/src/convert/from_h5mu_to_seurat/config.vsh.yaml index 694a2c6..c8e23c2 100644 --- a/src/convert/from_h5mu_to_seurat/config.vsh.yaml +++ b/src/convert/from_h5mu_to_seurat/config.vsh.yaml @@ -68,7 +68,7 @@ test_resources: path: test.R - path: /resources_test/aviti/aviti_teton_tiny.h5mu - path: /resources_test/cosmx/Lung5_Rep2_tiny.h5mu - - path: /resources_test/xenium/xenium_tiny_processed.h5mu + - path: /resources_test/xenium/xenium_tiny.h5mu engines: - type: docker image: rocker/r2u:22.04 diff --git a/src/convert/from_h5mu_to_seurat/test.R b/src/convert/from_h5mu_to_seurat/test.R index c70e360..9cae0ea 100644 --- a/src/convert/from_h5mu_to_seurat/test.R +++ b/src/convert/from_h5mu_to_seurat/test.R @@ -16,7 +16,7 @@ cat("> Test conversion without adding FOV\n") in_h5mu <- paste0( meta[["resources_dir"]], - "/xenium_tiny_processed.h5mu" + "/xenium_tiny.h5mu" ) out_rds <- "output.rds" @@ -41,7 +41,7 @@ adata <- H5File$new(in_h5mu, mode = "r")[["/mod/rna/X"]] cat("> Checking whether Seurat object is in the right format\n") expect_equal(Assays(obj), "Xenium") -expect_true(all(Layers(obj) == c("counts", "log_normalized"))) +expect_true(all(Layers(obj) == c("counts"))) dim_rds <- dim(obj) dim_ad <- adata$attr_open("shape")$read() @@ -56,7 +56,7 @@ cat("> Test conversion Xenium\n") in_h5mu <- paste0( meta[["resources_dir"]], - "/xenium_tiny_processed.h5mu" + "/xenium_tiny.h5mu" ) out_rds <- "output.rds" @@ -82,7 +82,7 @@ adata <- H5File$new(in_h5mu, mode = "r")[["/mod/rna/X"]] cat("> Checking whether Seurat object is in the right format\n") expect_equal(Assays(obj), "Xenium") -expect_true(all(Layers(obj) == c("counts", "log_normalized"))) +expect_true(all(Layers(obj) == c("counts"))) dim_rds <- dim(obj) dim_ad <- adata$attr_open("shape")$read() @@ -111,7 +111,7 @@ cat("> Test conversion Xenium with centroid arguments\n") in_h5mu <- paste0( meta[["resources_dir"]], - "/xenium_tiny_processed.h5mu" + "/xenium_tiny.h5mu" ) out_rds <- "output.rds" From d6ec5d9ad1f980c9ecd76bdb19e02ff1ba3e1a94 Mon Sep 17 00:00:00 2001 From: dorien-er Date: Fri, 17 Oct 2025 13:23:09 +0200 Subject: [PATCH 11/12] undo irrelevant changes --- src/convert/from_xenium_to_spatialexperiment/script.R | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/convert/from_xenium_to_spatialexperiment/script.R b/src/convert/from_xenium_to_spatialexperiment/script.R index 5c46b1f..16cf22f 100644 --- a/src/convert/from_xenium_to_spatialexperiment/script.R +++ b/src/convert/from_xenium_to_spatialexperiment/script.R @@ -2,7 +2,7 @@ library(SpatialExperimentIO) ### VIASH START par <- list( - input = "output-XETG00150__0031015__slidearray0085__20241023__195946", + input = "resources_test/xenium/temp_dir.zip", add_experiment_xenium = TRUE, add_parquet_paths = TRUE, alternative_experiment_features = c( @@ -46,9 +46,7 @@ spe <- readXeniumSXE( coordNames = c("x_centroid", "y_centroid"), addExperimentXenium = par$add_experiment_xenium, addParquetPaths = par$add_parquet_paths, - altExps = par$alternative_experiment_features, - addCellBound = TRUE, - addNucBound = TRUE + altExps = par$alternative_experiment_features ) cat("Saving output...") From 3b5314b60d9b8492125d5a7ef51675a62385af6c Mon Sep 17 00:00:00 2001 From: dorien-er Date: Fri, 17 Oct 2025 13:24:38 +0200 Subject: [PATCH 12/12] update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0caf808..c597df6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# openpipeline_spatial x.x.x + +## NEW FUNCTIONALITY + +* `convert/from_h5mu_to_seurat`: Added converter component for H5MU data to Seurat objects with spatial FOV (PR #23). + # openpipeline_spatial 0.1.1 ## MINOR CHANGES