--- title: "Large-scale single-cell RNA-seq data analysis using GDS files and Seurat" author: "Dr. Xiuwen Zheng (Genomics Research Center, AbbVie)" date: "Feb 2023" output: BiocStyle::html_document: toc_float: true BiocStyle::pdf_document: default vignette: > %\VignetteIndexEntry{scRNA-seq data analysis with GDS files and Seurat} %\VignetteKeywords{scRNAseq, GDS, Seurat} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r style, echo=FALSE, results='asis'} BiocStyle::markdown() knitr::opts_chunk$set(warning=FALSE, error=FALSE, message=FALSE) ``` # Introduction Single-cell RNA sequencing (scRNA-seq) has revolutionized our understanding of gene expression heterogeneity within complex biological systems. As scRNA-seq technology becomes increasingly accessible and cost-effective, experiments are generating data from larger and larger numbers of cells. However, the analysis of large scRNA-seq data remains a challenge, particularly in terms of scalability. While numerous analysis tools have been developed to tackle the complexities of scRNA-seq data, their scalability is often limited, posing a major bottleneck in the analysis of large-scale experiments. In particular, the R package [Seurat](https://cran.r-project.org/package=Seurat) is one of the most widely used tools for exploring and analyzing scRNA-seq data, but its scalability is often limited by available memory. To address this issue, we introduce a new R package called “SCArray.sat” that extends the Seurat classes and functions to support Genomic Data Structure ([GDS](http://www.bioconductor.org/packages/gdsfmt)) files as a [DelayedArray](http://www.bioconductor.org/packages/DelayedArray) backend for data representation. GDS files store multiple dense and sparse array-based data sets in a hierarchical structure. This package defines a new class, called “SCArrayAssay” (derived from the Seurat class “Assay”), which wraps raw counts, normalized expressions, and scaled data matrices based on GDS-specific DelayedMatrix. It is designed to integrate seamlessly with the Seurat package to provide common data analysis in a workflow, with optimized algorithms for GDS data files. ~ ```{r workflow, echo=FALSE, fig.cap="Overview of the SCArray framework.", fig.wide=TRUE} knitr::include_graphics("scarray_sat.svg") ``` ~ The SeuratObject package defines the `Assay` class with three members/slots `counts`, `data` and `scale.data` storing raw counts, normalized expressions and scaled data matrix respectively. However, `counts` and `data` should be either a dense matrix or a sparse matrix defined in the [Matrix](https://cran.r-project.org/package=Matrix) package. The scalability of the sparse matrix is limited by the number of non-zero values (should be < 2^31), since the Matrix package uses 32-bit indices internally. `scale.data` in the `Assay` class is defined as a dense matrix, so it is also limited by the available memory. The new class `SCArrayAssay` is derived from `Assay`, with three additional slots `counts2`, `data2` and `scale.data2` replacing the old ones. These new slots can be DelayedMatrix wrapping an on-disk data matrix, without loading the data in memory. The SCArray.sat package takes advantage of the S3 object-oriented methods defined in the SeuratObject and Seurat packages to reduce code redundancy, by implementing the functions specific to the classes `SCArrayAssay` and `SC_GDSMatrix` (GDS-specific DelayedMatrix). Table 1 shows a list of key S3 methods for data analysis. For example, the function `NormalizeData.SC_GDSMatrix()` will be called when a `SC_GDSMatrix` object is passed to the S3 generic `NormalizeData()`, while `NormalizeData.Assay()` and `NormalizeData.Seurat()` are unchanged. In addition, the SCArray and SCArray.sat packages implement the optimized algorithms for the calculations, by reducing the on-disk data access and taking the GDS data structure into account. ~ **Table 1: Key S3 methods implemented in the SCArray.sat package.** | **Methods** | **Description** | **Note** | |:--------------------------|:--------------------------|:---------------------------| |GetAssayData.SCArrayAssay() |Accessor function for ‘SCArrayAssay’ objects | | |SetAssayData.SCArrayAssay |Setter functions for ‘Assay’ objects | | |NormalizeData.SC_GDSMatrix() |Normalize raw count data | Store a DelayedMatrix | |ScaleData.SC_GDSMatrix() |Scale and center the normalized data | | |FindVariableFeatures.SC_GDSMatrix() |Identifies features | | |RunPCA.SC_GDSMatrix() |Run a PCA dimensionality reduction | | *SC_GDSMatrix: GDS- and single-cell- specific DelayedMatrix.* ~ ~ # Installation * Requires [SCArray](http://www.bioconductor.org/packages/SCArray/) (≥ v1.7.13), [SeuratObject](https://cran.r-project.org/package=SeuratObject) (≥ v4.0), [Seurat](https://cran.r-project.org/package=Seurat) (≥ v4.0) * Bioconductor repository To install this package, start R and enter: ```{r, eval=FALSE} if (!requireNamespace("BiocManager", quietly=TRUE)) install.packages("BiocManager") BiocManager::install("SCArray.sat") ``` # Examples ## Small Datasets ```{r} # Load the packages suppressPackageStartupMessages({ library(Seurat) library(SCArray) library(SCArray.sat) }) # Input GDS file with raw counts fn <- system.file("extdata", "example.gds", package="SCArray") # show the file contents (f <- scOpen(fn)) scClose(f) # close the file # Create a Seurat object from the GDS file d <- scNewSeuratGDS(fn) class(GetAssay(d)) # SCArrayAssay, derived from Assay d <- NormalizeData(d) d <- FindVariableFeatures(d, nfeatures=500) d <- ScaleData(d) ``` **Let's check the internal data matrices,** ```{r} # get the file name for the on-disk object scGetFiles(d) # raw counts m <- GetAssayData(d, "counts") scGetFiles(m) # the file name storing raw count data m # normalized expression # the normalized data does not save in neither the file nor the memory GetAssayData(d, "data") # scaled and centered data matrix # in this example, the scaled data does not save in neither the file nor the memory GetAssayData(d, "scale.data") ``` **Perform PCA and UMAP:** ```{r fig.wide=TRUE} d <- RunPCA(d, ndims.print=1:2) DimPlot(d, reduction="pca") d <- RunUMAP(d, dims=1:50) # use all PCs calculated by RunPCA() DimPlot(d, reduction="umap") ``` ~ ## Large Datasets Let's download a large single-cell RNA-seq dataset from Bioconductor, and convert it to a GDS file. This step will take a while. If the `TENxBrainData` package is not installed, run: ```R # install a Bioconductor package BiocManager::install("TENxBrainData") ``` Then, ```R library(TENxBrainData) library(SCArray) # scRNA-seq data for 1.3 million brain cells from E18 mice (10X Genomics) # the data will be downloaded automatically at the first time. # raw count data is stored in HDF5 format tenx <- TENxBrainData() rownames(tenx) <- rowData(tenx)$Ensembl # since rownames(tenx)=NULL # save it to a GDS file SCArray::scConvGDS(tenx, "1M_sc_neurons.gds") ``` After the file conversion, users can use this GDS file with Seurat to analyze the data. ~ ~ # Benchmarks ## Test Datasets The datasets used in the benchmark (Table 2) were generated from the 1.3 million brain cells, with the following R codes: ```R library(SCArray) library(SCArray.sat) sce <- scExperiment("1M_sc_neurons.gds") # load the full 1.3M cells # D100 dataset scConvGDS(sce[, 1:1e5], "1M_sc_neurons_d100.gds") # save to a GDS # in-memory Seurat object obj <- scMemory(scNewSeuratGDS("1M_sc_neurons_d100.gds")) saveRDS(obj, "1M_sc_neurons_d100_seuratobj.rds") # save to a RDS # D250 dataset scConvGDS(sce[, 1:2.5e5], "1M_sc_neurons_d250.gds") obj <- scMemory(scNewSeuratGDS("1M_sc_neurons_d250.gds")) saveRDS(obj, "1M_sc_neurons_d250_seuratobj.rds") # D500 dataset scConvGDS(sce[, 1:5e5], "1M_sc_neurons_d500.gds") obj <- scMemory(scNewSeuratGDS("1M_sc_neurons_d500.gds")) saveRDS(obj, "1M_sc_neurons_d500_seuratobj.rds") # Dfull dataset scConvGDS(sce, "1M_sc_neurons_dfull.gds") ``` **Table 2: Datasets in the benchmarks.** | **Dataset** | **# of features** | **# of cells** | **GDS file** | **RDS (Seurat Object)** | |:-------------|:-------------------|:----------------|:--------------|:-------------------------| |D100 |27,998 | 100K | 209MB | 419MB | |D250 |27,998 | 250K | 529MB | 1.1GB | |D500 |27,998 | 500K | 1.1GB | 2.2GB | |Dfull |27,998 | 1.3 million | 2.8GB | Out of the limit of sparse matrix | *the number of non-zeros should be < 2^31 in a sparse matrix.* ~ ## R Codes in the Benchmark The following R script is used in the benchmark for testing GDS files, and the R codes for testing the Seurat Object are similar except the input file. ```R suppressPackageStartupMessages({ library(Seurat) library(SCArray.sat) }) # the input GDS file can be for d250, d500, dfull fn <- "1M_sc_neurons_d100.gds" d <- scNewSeuratGDS(fn) d <- NormalizeData(d) d <- FindVariableFeatures(d, nfeatures=2000) # using the default d <- ScaleData(d) d <- RunPCA(d) d <- RunUMAP(d, dims=1:50) saveRDS(d, "d100.rds") # or d250.rds, d500.rds, dfull.rds q('no') ``` ~ ## Memory Usage and Elapsed Time With large test datasets, the SCArray.sat package significantly reduces the memory usages compared with the Seurat package, while the in-memory implementation in Seurat is only 2 times faster than SCArray.sat. When the full dataset "Dfull" was tested, Seurat failed to load the data since the number of non-zeros is out of the limit of sparse matrix. ```{r benchmark, echo=FALSE, fig.cap="The benchmark on PCA & UMAP with large datasets (CPU: Intel Xeon Gold 6248 @2.50GHz, RAM: 176GB).", fig.wide=TRUE} knitr::include_graphics("benchmark.svg") ``` ~ ~ # Miscellaneous ## Save SCArrayAssay The Seurat object with `SCArrayAssay` can be directly saved to a RDS (R object) file, in which the raw counts in the GDS file is not stored in the RDS file. This can avoid data duplication, and is helpful for faster meta data loading. Please keep the GDS and RDS files in the same directory or the same relative paths. The R object can be reloaded later in another R session, and GDS files are reopened internally when accessing the count data. ```{r} d # the example for the small dataset save_fn <- tempfile(fileext=".rds") # or specify an appropriate location save_fn saveRDS(d, save_fn) # save to a RDS file remove(d) # delete the variable d gc() # trigger a garbage collection d <- readRDS(save_fn) # load from a RDS file d GetAssayData(d, "counts") # reopens the GDS file automatically ``` ~ ## Multicore or Multi-process Implementation The multicore and multi-process features are supported by SCArray and SCArray.sat via the Bioconductor package "BiocParallel". To enable the parallel feature, users can use the function `setAutoBPPARAM()` in the DelayedArray package to setup multi-process workers. For examples, ```R library(BiocParallel) DelayedArray::setAutoBPPARAM(MulticoreParam(4)) # 4 child processes ``` ### RunUMAP() The Seurat package utilizes the "future" package to perform parallel calculations. To enable multi-threading in `RunUMAP()`, users should also set a "plan" for parallel processing, since Seurat uses `nbrOfWorkers()` to determine the number of threads internally. E.g., ```R library(future) plan(multicore, workers=4) # 4 cores message("Number of parallel workers: ", nbrOfWorkers()) ``` ### FindMarkers() In the function `FindMarkers()`, users should use `DelayedArray::setAutoBPPARAM()` to set a parallel environment and reset the `future` package via `plan(sequential)`. ~ ## Downgrade SCArrayAssay The `SCArrayAssay` object can be downgraded to the regular `Assay`. It is useful when the downstream functions or packages do not support DelayedArray. ```{r} is(GetAssay(d)) new_d <- scMemory(d) # downgrade the active assay is(GetAssay(new_d)) ``` If users only want to 'downgrade' the scaled data matrix, then ```{r} is(GetAssayData(d, "scale.data")) # it is a DelayedMatrix new_d <- scMemory(d, slot="scale.data") # downgrade "scale.data" in the active assay is(GetAssay(new_d)) # it is still SCArrayAssay is(GetAssayData(new_d, "scale.data")) # in-memory matrix ``` ~ ## Conversion from Seurat to SingleCellExperiment A Seurat object with `SCArrayAssay` can be converted to a Bioconductor `SingleCellExperiment` object using `as.SingleCellExperiment()` in the Seurat package. The DelayedMatrix in `SCArrayAssay will be saved in the new SingleCellExperiment object. For example, ```{r} is(d) sce <- as.SingleCellExperiment(d) is(sce) sce counts(sce) # raw counts ``` ~ ## List of supported functions Not all of the functions in the Seurat package can be applied to the `SCArrayAssay` object. Here is the list of currently supported and unsupported functions we have tested. The unsupported methods maybe available on request in the future release of SCArray.sat. Note that the supported states may depend on the package versions of Seurat and SeuratObject, and SeuratObject_4.1.3 and Seurat_4.3.0 were tested here. **Table 3: The states of functions and methods with the support of SCArrayAssay.** |State| Functions | Notes | |:---:|:----------------------|:-------------| | ✓ |CreateSeuratObject() | | | ✓ |FindVariableFeatures() | | | ✓ |NormalizeData() | | | ✓ |RunPCA() | | | ✓ |ScaleData() | | | ☑ |FindMarkers() | data read via blocking | | ☑ |FoldChange() | | | ☑ |RunICA() | | | ☑ |RunSPCA() | | | ☑ |RunLDA() | | | ☑ |RunSLSI() | | | ⦿ |FindNeighbors | | | ⦿ |HVFInfo() | | | ⦿ |RunUMAP() | | | ⦿ |RunTSNE() | | | ⦿ |ProjectDim() | | | ⦿ |ProjectUMAP() | | | ⦿ |SVFInfo() | | | ⦿ |VariableFeatures() | | | ✗ |CreateAssayObject() | use CreateAssayObject2() instead | | ✗ |as.Seurat() | | | ✗ |RunCCA() | | | ✗ |SCTransform() | | * ✓ Supported (implemented in SCArray.sat, optimized for memory uses) * ☑ Supported (mainly relying on the implementation in Seurat) * ⦿ Supported (implemented in Seurat, not in SCArray.sat) * ✗ Unsupported (raising an error) ~ ## Debugging `options(SCArray.verbose=TRUE)` is used to enable displaying debug information when calling the functions in the SCArray and SCArray.sat packages. For example, ```{r} options(SCArray.verbose=TRUE) d <- ScaleData(d) ``` ~ ~ # Session Information ```{r} # print version information about R, the OS and attached or loaded packages sessionInfo() ``` ```{r echo=FALSE} unlink("test.rds", force=TRUE) ```