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 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/config.vsh.yaml b/src/convert/from_h5mu_to_seurat/config.vsh.yaml new file mode 100644 index 0000000..c8e23c2 --- /dev/null +++ b/src/convert/from_h5mu_to_seurat/config.vsh.yaml @@ -0,0 +1,88 @@ +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 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 ] +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/aviti/aviti_teton_tiny.h5mu + - path: /resources_test/cosmx/Lung5_Rep2_tiny.h5mu + - path: /resources_test/xenium/xenium_tiny.h5mu +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 + test_setup: + - type: r + cran: [ testthat ] +runners: + - type: executable + - type: nextflow + directives: + 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 new file mode 100644 index 0000000..0b7b0f5 --- /dev/null +++ b/src/convert/from_h5mu_to_seurat/script.R @@ -0,0 +1,93 @@ +library(anndataR) +library(hdf5r) +library(Seurat) + +### VIASH START +par <- list( + 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 + 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 +} + +# Read in H5AD +h5ad_path <- h5mu_to_h5ad(par$input, par$modality) + +# Convert to Seurat +seurat_obj <- read_h5ad( + h5ad_path, + mode = "r", + as = "Seurat", + assay_name = par$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 (is.null(par$centroid_nsides)) { + par$centroid_nsides <- Inf + } + + if (is.null(par$centroid_theta)) { + par$centroid_theta <- 0L + } + + centroids <- CreateCentroids( + coords = spatial_coords, + nsides = par$centroid_nsides, + radius = par$centroid_radius, + theta = par$centroid_theta + ) + + # 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 new file mode 100644 index 0000000..9cae0ea --- /dev/null +++ b/src/convert/from_h5mu_to_seurat/test.R @@ -0,0 +1,203 @@ +library(testthat, warn.conflicts = FALSE) +library(hdf5r) +library(Seurat) + +## VIASH START +meta <- list( + executable = "target/executable/convert/from_h5ad_to_spatial_seurat", + resources_dir = "resources_test", + name = "from_h5ad_to_spatial_seurat" +) +## VIASH END + + +# ---- No FOV ---------------------------------------------------------- +cat("> Test conversion without adding FOV\n") + +in_h5mu <- paste0( + meta[["resources_dir"]], + "/xenium_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", "Xenium" + ) +) + +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), "Xenium") +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]) + +expect_false("fov" %in% names(obj)) + +# # ---- Xenium ---------------------------------------------------------- +cat("> Test conversion Xenium\n") + +in_h5mu <- paste0( + meta[["resources_dir"]], + "/xenium_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", "Xenium", + "--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), "Xenium") +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, "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.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" + ) +) + +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)))