Skip to content

Ultra-large DelayedMatrix backend and portable single-file Seurat objects (#9798)#10405

Open
BenjaminDEMAILLE wants to merge 17 commits into
satijalab:mainfrom
BenjaminDEMAILLE:feature/large-matrix-portable
Open

Ultra-large DelayedMatrix backend and portable single-file Seurat objects (#9798)#10405
BenjaminDEMAILLE wants to merge 17 commits into
satijalab:mainfrom
BenjaminDEMAILLE:feature/large-matrix-portable

Conversation

@BenjaminDEMAILLE

@BenjaminDEMAILLE BenjaminDEMAILLE commented Jun 8, 2026

Copy link
Copy Markdown

Motivation

Closes the actionable part of #9798: enabling Seurat to work with matrices larger than the dgCMatrix 2³¹ limit, and making such objects portable as a single file — "a Seurat object in one file, the way you don't fuss over an AnnData .h5ad."

Companion PR: satijalab/seurat-object#295 adds StitchMatrix.DelayedMatrix to SeuratObject (alongside StitchMatrix.IterableMatrix). This PR requires SeuratObject (>= 5.4.0.9002) and will go red on CI until that ships.

What's in this PR

1. DelayedMatrix (DelayedArray/HDF5Array) layer backend mirroring the BPCells IterableMatrix backend (as.DelayedMatrix, as.sparse, .CalcN, LogNormalize, VST). DelayedMatrix carries dimnames and 64-bit indexing, so it works as a Seurat layer past 2³¹.

  • In RAM, 64-bit: for sparse input, as.DelayedMatrix(x) uses a SparseArray::SVT_SparseMatrix seed (per-column tree, no flat 2³¹ index vector), so a layer can hold >2³¹ non-zeros in memory and a plain saveRDS writes one self-contained file — an in-memory AnnData equivalent.
  • On disk, 64-bit: as.DelayedMatrix(x, ondisk = TRUE) realizes to a 10x-style HDF5 TENxMatrix (64-bit indptr).

2. "Total" pipeline coverage on DelayedMatrix-backed objects, verified numerically equal to dgCMatrix:
NormalizeData (LogNormalize native; CLR/RC via materialize-when-fits), FindVariableFeatures (vst native; mvp/dispersion materialize-when-fits), ScaleData, RunPCA, neighbors/clusters/UMAP, SCTransform, FindMarkers/FindAllMarkers/FoldChange, AverageExpression/AggregateExpression/PseudobulkExpression (block-wise), and multi-layer splitJoinLayersScaleDataRunPCAIntegrateLayers (CCA, RPCA, Harmony). Non-block-native steps materialize-when-fits with a warning (never silent subsampling), matching BPCells.

3. Ultra-large interoperability + portability. CheckMatrixSize() (actionable 2³¹ guidance), as.sparse.spam (spam is coercion-only — no dimnames), AsInMemory() (on-disk → in-memory, single-rds), and single-file objects: SaveSeurat/LoadSeurat (bundle, survives a file move) and SaveSeuratH5/LoadSeuratH5 (one .h5, a .h5ad analogue).

Tests

New tests/testthat/test_large_matrix.R (guarded with skip_if_not_installed) — backend equivalence to dgCMatrix, in-RAM SVT + single-rds round-trip, 64-bit on-disk, multi-layer integration, CLR/RC + mvp + pseudobulk, bundle/HDF5 round-trips across a file move. Locally: new tests pass; test_utilities (48) and test_preprocessing (97) unchanged. Adds spam, HDF5Array, DelayedMatrixStats, rhdf5, SparseArray to Suggests.

Kept intentionally separate from #10017.

🤖 Generated with Claude Code

BenjaminDEMAILLE and others added 17 commits June 6, 2026 17:44
Declare DelayedMatrixStats, HDF5Array, rhdf5 and spam in Suggests and
register R/spam.R, R/delayedarray.R, R/save-portable.R and R/save-h5.R
in Collate for the ultra-large matrix and portable-object work (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CheckMatrixSize reports a matrix's (non-zero) element count against R's
2^31-1 indexing limit and points users to on-disk backends when the limit
is approached. RowSumSparse/RowMeanSparse/RowVarSparse now compute natively
on DelayedMatrix via DelayedMatrixStats instead of force-converting to
dgCMatrix, which would fail past 2^31 entries (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the BPCells IterableMatrix backend for DelayedArray/HDF5Array-backed
layers: as.DelayedMatrix() coercion, as.sparse/.CalcN methods, and block-wise
LogNormalize and VST that never materialize the full matrix. DelayedMatrix
carries dimnames and uses 64-bit indexing, so it works as a Seurat layer past
the dgCMatrix 2^31 limit. NormalizeData -> FindVariableFeatures now run natively
on DelayedMatrix with results identical to dgCMatrix (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
as.sparse.spam converts a 64-bit-indexed spam matrix into a dgCMatrix for use
in Seurat. spam objects cannot be Seurat layers directly because they do not
carry dimnames; for out-of-memory ultra-large layers use the DelayedMatrix or
BPCells backends. This method covers the conversion path (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AsInMemory converts BPCells/DelayedMatrix layers of a Seurat object into
in-memory dgCMatrix, yielding a self-contained object that plain saveRDS()
writes to a single portable file -- the AnnData .h5ad experience for data
that fits in RAM. Errors with guidance toward SaveSeurat()/SaveSeuratH5()
when a layer exceeds the 2^31 limit (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add StitchMatrix.DelayedMatrix so multi-layer (split-by-batch) DelayedMatrix
objects can be joined and scaled, using DelayedArray::arbind/acbind to avoid
base/S4Vectors cbind mis-dispatch. Extend CCAIntegration's on-disk handling
to convert DelayedMatrix (like BPCells) to dgCMatrix for CCA. With these,
JoinLayers, ScaleData, RunPCA and CCA/RPCA/Harmony IntegrateLayers all run on
DelayedMatrix-backed objects (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SaveSeurat() writes a Seurat object plus all of its out-of-memory layers
(BPCells, DelayedMatrix/HDF5) into one gzip tar archive, copying each on-disk
store into the bundle and recording it in a manifest. LoadSeurat() extracts the
archive and reconnects the layers to the extracted stores, so a bundle can be
moved or shared like an AnnData .h5ad with no absolute-path breakage. Fully
in-memory objects are simply serialized inside the archive (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SaveSeuratH5() writes a Seurat object to one HDF5 file -- the closest analogue
to an AnnData .h5ad: each assay layer is stored as a 10x-style sparse HDF5
dataset (via writeTENxMatrix) and reopened lazily as a DelayedMatrix by
LoadSeuratH5(), so the file is portable, cross-tool readable and not bound by
the dgCMatrix 2^31 limit. meta.data (numeric/character/factor/logical) and
dimensional reductions are stored alongside; split layers are joined per assay.
v1 omits graphs/images/commands/misc (use SaveSeurat for a full bundle) (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add man pages for AsInMemory, SaveSeurat, LoadSeurat, SaveSeuratH5 and
LoadSeuratH5 (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover CheckMatrixSize, the DelayedMatrix backend (coercion, .CalcN,
LogNormalize/VST equivalence to dgCMatrix, PCA), multi-layer JoinLayers and
RPCA integration, spam coercion, AsInMemory + single-file saveRDS, the
SaveSeurat/LoadSeurat bundle across a file move, and the SaveSeuratH5/
LoadSeuratH5 HDF5 container. All guarded with skip_if_not_installed (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a section covering SaveSeurat/LoadSeurat, SaveSeuratH5/LoadSeuratH5 and
AsInMemory for writing portable, self-contained objects with on-disk layers
(satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
as.DelayedMatrix(x, ondisk = TRUE) realizes x to a 10x-style HDF5 TENxMatrix
store whose non-zero-count pointer (indptr) is 64-bit, so the resulting
DelayedMatrix can represent more than 2^31 non-zero entries and is processed
block-wise. The default in-memory wrapping is bounded by its seed (a dgCMatrix
seed stays under 2^31). Documents the 64-bit path and tests a >2^31-element
on-disk matrix (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
as.DelayedMatrix now wraps sparse input in a SparseArray SVT_SparseMatrix seed
(when available) rather than a dgCMatrix. Because SVT stores non-zeros in a
per-column tree instead of one flat index vector, the layer can hold more than
2^31 non-zeros entirely in RAM while carrying dimnames, so a plain saveRDS
writes the whole object to one self-contained file -- an in-memory AnnData
equivalent. ondisk = TRUE remains the out-of-memory HDF5 path. Adds SparseArray
to Suggests and a round-trip test (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a .AsSparseIfFits helper that materializes an on-disk layer (DelayedMatrix,
BPCells) to dgCMatrix for operations that are not block-native, erroring with
guidance when the layer exceeds the 2^31 limit. Apply it to CLR/RC NormalizeData
and mean.var.plot/dispersion feature selection, and compute PseudobulkExpression
(AverageExpression/AggregateExpression) on DelayedMatrix via a block-wise
multiply instead of rejecting it. All produce results identical to dgCMatrix
(satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop Seurat's StitchMatrix.DelayedMatrix in favor of the canonical method now
provided by SeuratObject (>= 5.4.0.9000), alongside StitchMatrix.IterableMatrix.
Bumps the SeuratObject dependency accordingly. Multi-layer DelayedMatrix
JoinLayers/ScaleData/integration continue to work via the SeuratObject method.

Companion to satijalab/seurat-object#295 (satijalab#9798).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
seurat-object main advanced to 5.4.0.9001 (satijalab#294) after this branch was cut,
so the StitchMatrix.DelayedMatrix dev snapshot is now 5.4.0.9002. Pin the
dependency to that version; a user on 5.4.0.9001 lacks the method and would
otherwise satisfy the old >= 5.4.0.9000 constraint while hitting a runtime
error in JoinLayers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant