Introduction

srtdisk provides two ways to load h5ad files into R as Seurat objects:

  1. Convert() + LoadH5Seurat() — the traditional two-step approach via an intermediate h5Seurat file
  2. LoadH5AD() — direct loading without intermediate files (new in v0.3.0)

LoadH5AD() is simpler and faster for common use cases: load a scanpy-processed h5ad file directly into a Seurat object, preserving expression data, metadata, dimensional reductions, and neighbor graphs.

library(Seurat)
library(srtdisk)

Loading a CellxGene h5ad file

The package bundles a colorectal cancer sample from CellxGene (935 cells x 25,344 genes). Let’s load it directly with LoadH5AD():

h5ad_path <- system.file("testdata", "crc_sample.h5ad", package = "srtdisk")
crc <- LoadH5AD(h5ad_path, verbose = TRUE)
crc
#> An object of class Seurat 
#> 25344 features across 935 samples within 1 assay 
#> Active assay: RNA (25344 features, 3466 variable features)
#>  2 layers present: counts, data
#>  1 dimensional reduction calculated: umap

The h5ad file is loaded in a single call. Let’s inspect the result:

# Assays and layers
cat("Assays:", paste(Assays(crc), collapse = ", "), "\n")
#> Assays: RNA
cat("Layers:", paste(Layers(crc), collapse = ", "), "\n")
#> Layers: counts, data

# Metadata columns
cat("Metadata columns:", ncol(crc[[]]), "\n")
#> Metadata columns: 50
head(crc[[]][, 1:min(5, ncol(crc[[]]))], 3)
#>                         orig.ident nCount_RNA nFeature_RNA         DC 1
#> 120703423724317_KG182M2       H5AD      47096         7255 -0.028095834
#> 120703436319453_KG182M2       H5AD       6742         2719  0.005544095
#> 120703436877172_KG182M2       H5AD      13992         4172  0.045204181
#>                                 DC 2
#> 120703423724317_KG182M2 -0.006741798
#> 120703436319453_KG182M2  0.020778758
#> 120703436877172_KG182M2 -0.032072473
# Dimensional reductions
cat("Reductions:", paste(names(crc@reductions), collapse = ", "), "\n")
#> Reductions: umap

# Neighbor graphs
if (length(crc@graphs) > 0) {
  cat("Graphs:", paste(names(crc@graphs), collapse = ", "), "\n")
}

Visualize

If the h5ad file contains UMAP coordinates (stored in obsm/X_umap), they are automatically restored:

if ("umap" %in% names(crc@reductions)) {
  DimPlot(crc, reduction = "umap", group.by = "tissue", pt.size = 0.5)
}

Comparison: LoadH5AD vs Convert + LoadH5Seurat

The traditional approach requires two steps and creates an intermediate h5Seurat file:

# Traditional approach (two steps)
local_h5ad <- file.path(tempdir(), basename(h5ad_path))
file.copy(h5ad_path, local_h5ad, overwrite = TRUE)
#> [1] TRUE
Convert(local_h5ad, dest = "h5seurat", overwrite = TRUE)
h5seurat_path <- sub("\\.h5ad$", ".h5seurat", local_h5ad)
crc_traditional <- LoadH5Seurat(h5seurat_path)

cat("LoadH5AD cells:", ncol(crc), "\n")
#> LoadH5AD cells: 935
cat("Traditional cells:", ncol(crc_traditional), "\n")
#> Traditional cells: 935
cat("LoadH5AD features:", nrow(crc), "\n")
#> LoadH5AD features: 25344
cat("Traditional features:", nrow(crc_traditional), "\n")
#> Traditional features: 25344

Both approaches produce equivalent Seurat objects. LoadH5AD() is simpler when you don’t need the intermediate h5Seurat file.

Loading a Seurat-originated h5ad

You can also use LoadH5AD() to round-trip data that was originally created in Seurat, exported to h5ad via Convert(), and optionally processed in Python.

library(SeuratData)
if ("pbmc3k.final" %in% rownames(InstalledData())) {
  data("pbmc3k.final", package = "pbmc3k.SeuratData")
  pbmc <- UpdateSeuratObject(pbmc3k.final)

  # Export to h5ad
  SaveH5Seurat(pbmc, filename = "pbmc3k_direct.h5Seurat", overwrite = TRUE)
  Convert("pbmc3k_direct.h5Seurat", dest = "h5ad", overwrite = TRUE)

  # Load back with LoadH5AD
  pbmc_loaded <- LoadH5AD("pbmc3k_direct.h5ad")
  pbmc_loaded
} else {
  message("pbmc3k.final dataset not installed; skipping roundtrip example.")
}
if (exists("pbmc", inherits = FALSE) && exists("pbmc_loaded", inherits = FALSE)) {
  cat("Original cells:", ncol(pbmc), "\n")
  cat("Loaded cells:", ncol(pbmc_loaded), "\n")

  # Check reductions
  cat("Original reductions:", paste(names(pbmc@reductions), collapse = ", "), "\n")
  cat("Loaded reductions:", paste(names(pbmc_loaded@reductions), collapse = ", "), "\n")
}
if (exists("pbmc_loaded", inherits = FALSE) && "umap" %in% names(pbmc_loaded@reductions)) {
  DimPlot(pbmc_loaded, reduction = "umap", group.by = "seurat_annotations",
          label = TRUE, pt.size = 0.5) + NoLegend()
}

What LoadH5AD preserves

LoadH5AD() reads the following from h5ad files:

h5ad Location Seurat Destination Description
X Default assay data layer Expression matrix (sparse or dense)
raw/X counts layer Raw counts if present
layers/* Additional layers Named layers mapped to Seurat slots
obs meta.data Cell metadata (categorical preserved as factors)
var Feature metadata Gene-level annotations
var['highly_variable'] VariableFeatures() Variable feature selection
obsm/X_umap reductions$umap UMAP coordinates
obsm/X_pca reductions$pca PCA embeddings
obsm/X_tsne reductions$tsne tSNE coordinates
obsm/spatial Spatial coordinates Via ConvertH5ADSpatialToSeurat()
obsp/connectivities graphs$RNA_snn SNN graph
obsp/distances graphs$RNA_nn Distance graph
uns/* misc Unstructured annotations

When to use LoadH5AD vs Convert

Scenario Recommended
Quick exploration of an h5ad file LoadH5AD()
Round-trip editing (load, modify, re-export) Convert() + LoadH5Seurat()
Need h5Seurat for other tools Convert()
Loading scanpy-processed data for Seurat analysis LoadH5AD()
Working with spatial h5ad from CellxGene LoadH5AD()

Session Info

sessionInfo()
#> R version 4.5.2 (2025-10-31)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS Tahoe 26.3
#> 
#> Matrix products: default
#> BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib 
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
#> 
#> locale:
#> [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#> 
#> time zone: America/Indiana/Indianapolis
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#>  [1] stxKidney.SeuratData_0.1.0    stxBrain.SeuratData_0.1.2    
#>  [3] ssHippo.SeuratData_3.1.4      pbmcref.SeuratData_1.0.0     
#>  [5] pbmcMultiome.SeuratData_0.1.4 pbmc3k.SeuratData_3.1.4      
#>  [7] panc8.SeuratData_3.0.2        cbmc.SeuratData_3.1.4        
#>  [9] SeuratData_0.2.2.9002         srtdisk_0.3.1                
#> [11] Seurat_5.4.0                  SeuratObject_5.3.0           
#> [13] sp_2.2-1                      reticulate_1.45.0            
#> 
#> loaded via a namespace (and not attached):
#>   [1] RColorBrewer_1.1-3     jsonlite_2.0.0         magrittr_2.0.4        
#>   [4] spatstat.utils_3.2-2   farver_2.1.2           rmarkdown_2.30        
#>   [7] vctrs_0.7.1            ROCR_1.0-12            spatstat.explore_3.7-0
#>  [10] htmltools_0.5.9        sass_0.4.10            sctransform_0.4.3     
#>  [13] parallelly_1.46.1      KernSmooth_2.23-26     bslib_0.10.0          
#>  [16] htmlwidgets_1.6.4      ica_1.0-3              plyr_1.8.9            
#>  [19] plotly_4.12.0          zoo_1.8-15             cachem_1.1.0          
#>  [22] igraph_2.2.2           mime_0.13              lifecycle_1.0.5       
#>  [25] pkgconfig_2.0.3        Matrix_1.7-4           R6_2.6.1              
#>  [28] fastmap_1.2.0          fitdistrplus_1.2-6     future_1.69.0         
#>  [31] shiny_1.13.0           digest_0.6.39          patchwork_1.3.2       
#>  [34] tensor_1.5.1           RSpectra_0.16-2        irlba_2.3.7           
#>  [37] labeling_0.4.3         progressr_0.18.0       spatstat.sparse_3.1-0 
#>  [40] httr_1.4.8             polyclip_1.10-7        abind_1.4-8           
#>  [43] compiler_4.5.2         bit64_4.6.0-1          withr_3.0.2           
#>  [46] S7_0.2.1               fastDummies_1.7.5      MASS_7.3-65           
#>  [49] rappdirs_0.3.4         tools_4.5.2            lmtest_0.9-40         
#>  [52] otel_0.2.0             httpuv_1.6.16          future.apply_1.20.2   
#>  [55] goftest_1.2-3          glue_1.8.0             nlme_3.1-168          
#>  [58] promises_1.5.0         grid_4.5.2             Rtsne_0.17            
#>  [61] cluster_2.1.8.2        reshape2_1.4.5         generics_0.1.4        
#>  [64] hdf5r_1.3.12           gtable_0.3.6           spatstat.data_3.1-9   
#>  [67] tidyr_1.3.2            data.table_1.18.2.1    spatstat.geom_3.7-0   
#>  [70] RcppAnnoy_0.0.23       ggrepel_0.9.7          RANN_2.6.2            
#>  [73] pillar_1.11.1          stringr_1.6.0          spam_2.11-3           
#>  [76] RcppHNSW_0.6.0         later_1.4.8            splines_4.5.2         
#>  [79] dplyr_1.2.0            lattice_0.22-9         survival_3.8-6        
#>  [82] bit_4.6.0              deldir_2.0-4           tidyselect_1.2.1      
#>  [85] miniUI_0.1.2           pbapply_1.7-4          knitr_1.51            
#>  [88] gridExtra_2.3          scattermore_1.2        xfun_0.56             
#>  [91] matrixStats_1.5.0      stringi_1.8.7          lazyeval_0.2.2        
#>  [94] yaml_2.3.12            evaluate_1.0.5         codetools_0.2-20      
#>  [97] tibble_3.3.1           cli_3.6.5              uwot_0.2.4            
#> [100] xtable_1.8-8           jquerylib_0.1.4        dichromat_2.0-0.1     
#> [103] Rcpp_1.1.1             globals_0.19.1         spatstat.random_3.4-4 
#> [106] png_0.1-8              spatstat.univar_3.1-6  parallel_4.5.2        
#> [109] ggplot2_4.0.2          dotCall64_1.2          listenv_0.10.1        
#> [112] viridisLite_0.4.3      scales_1.4.0           ggridges_0.5.7        
#> [115] purrr_1.2.1            crayon_1.5.3           rlang_1.1.7           
#> [118] cowplot_1.2.0