diff --git a/DESCRIPTION b/DESCRIPTION index 8be8cab..bd468c7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -42,4 +42,6 @@ Collate: writeZarrArray.R ZarrSparseMatrixSeed-class.R ZarrSparseMatrix-class.R + ZarrADMatrixSeed-class.R + ZarrADMatrix-class.R zzz.R diff --git a/NAMESPACE b/NAMESPACE index 7154728..568b661 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -21,7 +21,10 @@ exportClasses( ZarrArray, ZarrMatrix, ZarrRealizationSink, ZarrSparseMatrixSeed, CSC_ZarrSparseMatrixSeed, CSR_ZarrSparseMatrixSeed, - ZarrSparseMatrix + ZarrSparseMatrix, + ZarrADMatrixSeed, + Dense_ZarrADMatrixSeed, CSC_ZarrADMatrixSeed, CSR_ZarrADMatrixSeed, + ZarrADMatrix ) @@ -31,6 +34,8 @@ exportClasses( S3method(t, CSC_ZarrSparseMatrixSeed) S3method(t, CSR_ZarrSparseMatrixSeed) +S3method(t, CSC_ZarrADMatrixSeed) +S3method(t, CSR_ZarrADMatrixSeed) ### We also export them thru the export() directive so that (a) they can be ### called directly, (b) tab-completion on the name of the generic shows them, @@ -38,7 +43,9 @@ S3method(t, CSR_ZarrSparseMatrixSeed) export( t.CSC_ZarrSparseMatrixSeed, - t.CSR_ZarrSparseMatrixSeed + t.CSR_ZarrSparseMatrixSeed, + t.CSC_ZarrADMatrixSeed, + t.CSR_ZarrADMatrixSeed ) @@ -93,6 +100,11 @@ export( ZarrSparseMatrixSeed, ## ZarrSparseMatrix-class.R: - ZarrSparseMatrix -) - + ZarrSparseMatrix, + + ## ZarrADMatrixSeed-class.R + ZarrADMatrixSeed, + + ## ZarrADMatrix-class.R + ZarrADMatrix +) \ No newline at end of file diff --git a/R/ZarrADMatrix-class.R b/R/ZarrADMatrix-class.R new file mode 100644 index 0000000..6bb9f56 --- /dev/null +++ b/R/ZarrADMatrix-class.R @@ -0,0 +1,33 @@ +### ========================================================================= +### ZarrADMatrix objects +### ------------------------------------------------------------------------- +### + +setClass("ZarrADMatrix", + contains="DelayedMatrix", + representation(seed="ZarrADMatrixSeed") +) + + +### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +### Constructor +### + +setMethod("DelayedArray", "ZarrADMatrixSeed", + function(seed) new_DelayedArray(seed, Class="ZarrADMatrix") +) + +### Works directly on an ZarrADMatrixSeed derivative, in which case it must +### be called with a single argument. +ZarrADMatrix <- function(zarr_store, layer=NULL) +{ + if (is(zarr_store, "ZarrADMatrixSeed")) { + if (!is.null(layer)) + stop(wmsg("ZarrADMatrix() must be called with a single argument ", + "when passed an ZarrADMatrixSeed derivative")) + seed <- zarr_store + } else { + seed <- ZarrADMatrixSeed(zarr_store, layer=layer) + } + DelayedArray(seed) +} \ No newline at end of file diff --git a/R/ZarrADMatrixSeed-class.R b/R/ZarrADMatrixSeed-class.R new file mode 100644 index 0000000..706cc4a --- /dev/null +++ b/R/ZarrADMatrixSeed-class.R @@ -0,0 +1,125 @@ +### ========================================================================= +### ZarrADMatrixSeed objects +### ------------------------------------------------------------------------- + + +setClass("ZarrADMatrixSeed", + contains=c("Array", "OutOfMemoryObject"), + representation("VIRTUAL") +) + +setClass("Dense_ZarrADMatrixSeed", + contains=c("ZarrADMatrixSeed", "ZarrArraySeed"), + representation(dimnames="list"), + prototype(dimnames=list(NULL, NULL)) +) +setClass("CSC_ZarrADMatrixSeed", + contains=c("ZarrADMatrixSeed", "CSC_ZarrSparseMatrixSeed") +) +setClass("CSR_ZarrADMatrixSeed", + contains=c("ZarrADMatrixSeed", "CSR_ZarrSparseMatrixSeed") +) + + +### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +### dimnames() method for Dense_ZarrADMatrixSeed objects +### + +### We overwrite the method for ZarrArraySeed objects with a method that +### accesses the slot, not the store +setMethod("dimnames", "Dense_ZarrADMatrixSeed", + function(x) S4Arrays:::simplify_NULL_dimnames(x@dimnames) +) + + +### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +### Transposition +### + +### S3/S4 combo for t.CSC_ZarrADMatrixSeed +t.CSC_ZarrADMatrixSeed <- function(x) +{ + new2("CSR_ZarrADMatrixSeed", callNextMethod()) +} +setMethod("t", "CSC_ZarrADMatrixSeed", t.CSC_ZarrADMatrixSeed) + +### S3/S4 combo for t.CSR_ZarrADMatrixSeed +t.CSR_ZarrADMatrixSeed <- function(x) +{ + new2("CSC_ZarrADMatrixSeed", callNextMethod()) +} +setMethod("t", "CSR_ZarrADMatrixSeed", t.CSR_ZarrADMatrixSeed) + + +### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +### Constructor +### + +.load_zarr_ad_rownames <- function(zarr_store, name="var") +{ + ok <- try(zarr_node_is_group(zarr_store, name), silent=TRUE) + if (!isTRUE(ok)) + return(NULL) + ROWNAMES_DATASET <- file.path(name, "_index") + ok <- try(zarr_node_is_dataset(zarr_store, ROWNAMES_DATASET), silent=TRUE) + if (!isTRUE(ok)) + return(NULL) + read_zarr_array(file.path(zarr_store, ROWNAMES_DATASET)) +} + +### Must return a list of length 2. +.load_zarr_ad_dimnames <- function(zarr_store) +{ + ans_rownames <- .load_zarr_ad_rownames(zarr_store) + ans_colnames <- .load_zarr_ad_rownames(zarr_store, name="obs") + if (is.null(ans_rownames) && is.null(ans_colnames)) + warning(wmsg("could not find dimnames in this anndata-zarr store")) + list(ans_rownames, ans_colnames) +} + +### Returns an ZarrADMatrixSeed derivative (can be either a Dense_ZarrADMatrixSeed, +### or a CSC_ZarrSparseMatrixSeed, or a CSR_ZarrSparseMatrixSeed object). +ZarrADMatrixSeed <- function(zarr_store, layer=NULL) +{ + if (!isSingleString(zarr_store)) + stop(wmsg("'zarr_store' must be a single string specifying the ", + "path to the anndata-zarr store")) + zarr_store <- file_path_as_absolute(zarr_store) + if (is.null(layer)) { + name <- "X" + } else { + if (!isSingleString(layer) || !nzchar(layer)) + stop(wmsg("'layer' must be NULL or a single non-empty string")) + name <- file.path("layers", layer) + } + if (!zarr_exists(zarr_store, name)) { + msg <- c("Zarr object \"", name, "\" does not exist ", + "in this Zarr store") + if (is.null(layer)) + msg <- c(msg, " Is this a valid anndata-zarr store?") + stop(wmsg(msg)) + } + dimnames <- .load_zarr_ad_dimnames(zarr_store) + + if (zarr_node_is_dataset(zarr_store, name)) { + ans0 <- ZarrArraySeed(zarr_store, name) + if (length(dim(ans0)) != 2L) + stop(wmsg("Zarr dataset \"", name, "\" in store \"", zarr_store, "\" ", + "does not have exactly 2 dimensions. Please consider ", + "using the ZarrArray() constructor to access this ", + "dataset.")) + ans <- new2("Dense_ZarrADMatrixSeed", ans0, dimnames=dimnames) + } else if (zarr_node_is_group(zarr_store, name)) { + ans0 <- ZarrSparseMatrixSeed(zarr_store, name) + if (is(ans0, "CSC_ZarrSparseMatrixSeed")) + ans_class <- "CSC_ZarrADMatrixSeed" + else + ans_class <- "CSR_ZarrADMatrixSeed" + ans <- new2(ans_class, ans0, dimnames=dimnames) + } else { + stop(wmsg("Zarr object \"", name, "\" in store \"", zarr_store, "\" ", + "is neither a dataset or a group. Is this a valid ", + "anndata-zarr store?")) + } + ans +} \ No newline at end of file diff --git a/R/ZarrSparseMatrixSeed-class.R b/R/ZarrSparseMatrixSeed-class.R index b0542f5..0d15e52 100644 --- a/R/ZarrSparseMatrixSeed-class.R +++ b/R/ZarrSparseMatrixSeed-class.R @@ -324,7 +324,7 @@ ZarrSparseMatrixSeed <- function(zarr_store, group, subdata=NULL, dim <- .get_sparse_matrix_dim(zarr_store, group, dim=dim) ## Get sparse layout to use ("CSC" or "CSR"). - ## For consistency with H5SparseMatrixSeed, we flip the notions of rows + ## For consistency with ZarrSparseMatrixSeed, we flip the notions of rows ## and columns w.r.t. to the AnnData convention. So: ## - "compressed sparse row" in the AnnData-style Zarr store ## becomes "compressed sparse column" at the R level, diff --git a/TODO b/TODO index 934bac7..781e274 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,3 @@ -o Implement ZarrADMatrixSeed and ZarrADMatrix objects (analog to - H5ADMatrixSeed and H5ADMatrix objects from the HDF5Array package). - o Does Rarr provide an equivalent of h5ls() for quick inspection of a Zarr store? This would be very useful in the context of using things like ZarrSparseMatrixSeed() or ZarrSparseMatrix() where the diff --git a/man/ZarrADMatrix-class.Rd b/man/ZarrADMatrix-class.Rd new file mode 100644 index 0000000..da03103 --- /dev/null +++ b/man/ZarrADMatrix-class.Rd @@ -0,0 +1,118 @@ +\name{ZarrADMatrix-class} +\docType{class} + +\alias{class:ZarrADMatrix} +\alias{ZarrADMatrix-class} +\alias{ZarrADMatrix} + +\alias{DelayedArray,ZarrADMatrixSeed-method} + +\alias{nzcount,ZarrADMatrix-method} +\alias{extractNonzeroDataByCol,ZarrADMatrix-method} +\alias{extractNonzeroDataByRow,ZarrADMatrix-method} + +\title{anndata-zarr central matrices (or matrices in the /layers group) + as DelayedMatrix objects} + +\description{ + \code{anndata-zarr} stores are Zarr stores used for on-disk representation + of AnnData Python objects. At the very minimum, they contain a central + data matrix, named \code{X}, of shape #observations x #variables, and + possibly additional data matrices (stored in the Zarr group \code{/layers}) + that share the shape and dimnames of \code{X}. + See \url{https://anndata.readthedocs.io/} for more information. + + The ZarrADMatrix class is a \link[DelayedArray]{DelayedMatrix} subclass + for representing and operating on the central matrix of an \code{anndata-zarr} + store, or any matrix in its \code{/layers} group. + + All the operations available for \link[DelayedArray]{DelayedMatrix} + objects work on ZarrADMatrix objects. +} + +\usage{ +## Constructor function: +ZarrADMatrix(zarr_store, layer=NULL) +} + +\arguments{ + \item{zarr_store}{ + The path (as a single string) to the \code{anndata-zarr} store. + } + \item{layer}{ + \code{NULL} (the default) or the name of a matrix in the \code{/layers} + group. By default (i.e. when \code{layer} is not specified) + \code{ZarrADMatrix()} returns the central matrix (\code{X}). + } +} + +\value{ + \code{ZarrADMatrix()} returns an ZarrADMatrix object of shape #variables x + #observations. Note that in Python and Zarr the shape of this matrix is + considered to be #observations x #variables, but in R it is transposed. + This follows the widely adopted convention of transposing Zarr matrices + when they get loaded into R. +} + +\references{ + \url{https://anndata.readthedocs.io/} for AnnData Python objects + and the \code{anndata-zarr} format. +} + +\seealso{ + \itemize{ + \item \link{ZarrArray} objects for representing conventional (a.k.a. + dense) Zarr datasets as \link[DelayedArray]{DelayedArray} objects. + + \item \link{ZarrSparseMatrix} objects for representing Zarr sparse matrices + as \link[DelayedArray]{DelayedMatrix} objects. + + \item \link[DelayedArray]{DelayedMatrix} objects in the \pkg{DelayedArray} + package. + + \item The \link{ZarrADMatrixSeed} helper class. + + \item \code{\link[anndataR]{read_zarr}} and + \code{\link[anndataR]{write_zarr}} in + the \pkg{anndataR} package for + importing/exporting an \code{anndata-zarr} store as/from a + \link[SingleCellExperiment]{SingleCellExperiment} object. + + \item \link[SparseArray]{SparseArray} objects in the \pkg{SparseArray} + package. + } +} + +\examples{ +library(anndataR) +zarr_zip <- system.file("extdata", + paste0("example_v2.zarr.zip"), + package = "anndataR") +td <- tempdir(check = TRUE) +unzip(zarr_zip, exdir = td) +zarr_store <- file.path(td, paste0("example_v2.zarr")) + +X <- ZarrADMatrix(zarr_store) +X + +class(X) # ZarrADMatrix +is(X, "DelayedMatrix") # TRUE + +class(seed(X)) # Dense_ZarrADMatrixSeed + +dim(X) +path(X) +is_sparse(X) # FALSE + +## Use coercion to load the full dataset into memory: +as.matrix(X) # as ordinary array (usually not recommended) + +\dontrun{ + ## Works only if ZarrADMatrix object is sparse! + as(X, "dgCMatrix") # as dgCMatrix + as(X, "SparseArray") # as SparseArray object (most efficient) + SparseArray(X) # equivalent to 'as(X, "SparseArray")' +} +} +\keyword{classes} +\keyword{methods} diff --git a/man/ZarrADMatrixSeed-class.Rd b/man/ZarrADMatrixSeed-class.Rd new file mode 100644 index 0000000..995e36e --- /dev/null +++ b/man/ZarrADMatrixSeed-class.Rd @@ -0,0 +1,133 @@ +\name{ZarrADMatrixSeed-class} +\docType{class} + +\alias{class:ZarrADMatrixSeed} +\alias{ZarrADMatrixSeed-class} +\alias{ZarrADMatrixSeed} + +\alias{class:Dense_ZarrADMatrixSeed} +\alias{Dense_ZarrADMatrixSeed-class} +\alias{Dense_ZarrADMatrixSeed} + +\alias{class:CSC_ZarrADMatrixSeed} +\alias{CSC_ZarrADMatrixSeed-class} +\alias{CSC_ZarrADMatrixSeed} + +\alias{class:CSR_ZarrADMatrixSeed} +\alias{CSR_ZarrADMatrixSeed-class} +\alias{CSR_ZarrADMatrixSeed} + +\alias{dimnames,Dense_ZarrADMatrixSeed-method} + +\alias{t.CSC_ZarrADMatrixSeed} +\alias{t,CSC_ZarrADMatrixSeed-method} +\alias{t.CSR_ZarrADMatrixSeed} +\alias{t,CSR_ZarrADMatrixSeed-method} + +\title{ZarrADMatrixSeed objects} + +\description{ + ZarrADMatrixSeed is a low-level helper class used to represent a pointer + to the central matrix stored of an \code{anndata-zarr} store, or to one of the + matrices in the \code{/layers} group. + + It is a virtual class with three concrete subclasses: Dense_ZarrADMatrixSeed, + CSC_ZarrADMatrixSeed, and CSR_ZarrADMatrixSeed: + \itemize{ + \item The Dense_ZarrADMatrixSeed class is used when the matrix is stored + as a conventional Zarr dataset in the \code{anndata-zarr} store. It is + a direct entension of the \link{ZarrArraySeed} class. + + \item The CSC_ZarrADMatrixSeed or CSR_ZarrADMatrixSeed classes is used + when the matrix is stored in the \emph{Compressed Sparse Column} + or \emph{Compressed Sparse Row} format in the \code{anndata-zarr} + store. CSC_ZarrADMatrixSeed is a direct entension of + \link{CSC_ZarrSparseMatrixSeed}, and CSR_ZarrADMatrixSeed a + direct entension of \link{CSR_ZarrSparseMatrixSeed}. + } + + Note that an ZarrADMatrixSeed derivative is not intended to be used directly. + Most end users will typically create and manipulate a higher-level + \link{ZarrADMatrix} object instead. See \code{?\link{ZarrADMatrix}} for + more information. +} + +\usage{ +## Constructor function: +ZarrADMatrixSeed(zarr_store, layer=NULL) +} + +\arguments{ + \item{zarr_store, layer}{ + See \code{?\link{ZarrADMatrix}} for a description of these arguments. + } +} + +\details{ + Dense_ZarrADMatrixSeed objects support the same limited set of methods as + \link{ZarrArraySeed} objects, and CSC_ZarrADMatrixSeed and CSR_ZarrADMatrixSeed + objects support the same limited set of methods as \link{ZarrSparseMatrixSeed} + objects. + See \code{?\link{ZarrArraySeed}} and \code{?\link{ZarrSparseMatrixSeed}} + for the details. +} + +\value{ + \code{ZarrADMatrixSeed()} returns an ZarrADMatrixSeed derivative + (Dense_ZarrADMatrixSeed or CSC_ZarrADMatrixSeed or CSR_ZarrADMatrixSeed) + of shape #variables x #observations. +} + +\section{ZarrADMatrixSeed vs ZarrADMatrix objects}{ + In order to have access to the full set of operations that are available + for \link[DelayedArray]{DelayedMatrix} objects, an ZarrADMatrixSeed + derivative first needs to be wrapped in a \link[DelayedArray]{DelayedMatrix} + object, typically by calling the \code{\link[DelayedArray]{DelayedArray}()} + constructor on it. + + This is what the \code{\link{ZarrADMatrix}()} constructor function does. + + Note that the result of this wrapping is an \link{ZarrADMatrix} object, + which is just an ZarrADMatrixSeed derivative wrapped in a + \link[DelayedArray]{DelayedMatrix} object. +} + +\references{ + \url{https://anndata.readthedocs.io/} for AnnData Python objects + and the \code{ZarrAD} format. +} + +\seealso{ + \itemize{ + \item \link{ZarrADMatrix} objects. + + \item \link{ZarrArraySeed} and \link{ZarrSparseMatrixSeed} objects. + + \item \code{\link[zellkonverter]{readZarrAD}} and + \code{\link[zellkonverter]{writeZarrAD}} in + the \pkg{zellkonverter} package for + importing/exporting an \code{anndata-zarr} store as/from a + \link[SingleCellExperiment]{SingleCellExperiment} object. + } +} + +\examples{ +library(anndataR) +zarr_zip <- system.file("extdata", + paste0("example_v2.zarr.zip"), + package = "anndataR") +td <- tempdir(check = TRUE) +unzip(zarr_zip, exdir = td) +zarr_store <- file.path(td, paste0("example_v2.zarr")) + +seed <- ZarrADMatrix(zarr_store) +seed +path(seed) +dim(seed) +is_sparse(seed) + +DelayedArray(seed) +stopifnot(class(DelayedArray(seed)) == "ZarrADMatrix") +} +\keyword{classes} +\keyword{methods} diff --git a/man/ZarrSparseMatrix-class.Rd b/man/ZarrSparseMatrix-class.Rd index 22426c6..6706d45 100644 --- a/man/ZarrSparseMatrix-class.Rd +++ b/man/ZarrSparseMatrix-class.Rd @@ -97,7 +97,7 @@ class(ZSM) # ZarrSparseMatrix is(ZSM, "DelayedMatrix") # TRUE seed(ZSM) -class(seed(ZSM)) # CSC_H5SparseMatrixSeed +class(seed(ZSM)) # CSC_ZarrSparseMatrixSeed dim(ZSM) path(ZSM) diff --git a/tests/testthat/test-ZarrADMatrix-class.R b/tests/testthat/test-ZarrADMatrix-class.R new file mode 100644 index 0000000..3025c67 --- /dev/null +++ b/tests/testthat/test-ZarrADMatrix-class.R @@ -0,0 +1,28 @@ +skip_if_not_installed("anndataR") + +# test on both v2 and v3 +for(v in c("v2", "v3")){ + + # unpack zarr + zarr_dir <- system.file("extdata", + paste0("example_", v, ".zarr.zip"), + package = "anndataR") + td <- tempdir(check = TRUE) + unzip(zarr_dir, exdir = td) + zarr_path <- file.path(td, paste0("example_", v, ".zarr")) + + test_that("read sparse", { + + # read sparse matrix + A <- ZarrADMatrix(zarr_path) + expect_true(is(A, "ZarrADMatrix")) + expect_true(is(A, "DelayedArray")) + expect_true(is(seed(A), "ZarrADMatrixSeed")) + expect_identical(tools::file_path_as_absolute(path(A)), zarr_path) + expect_identical(dim(A), c(100L, 50L)) + expect_identical(type(A), "double") + expect_identical(chunkdim(A), c(100L, 1L)) + + expect_equal(1,1) + }) +}