Skip to content

Adding sparse array support #245

Description

@elyall

Zarr would benefit from formal support of storing sparse arrays, though I'm not sure what this would look like and the process for getting there (e.g. placing the functionality in the core spec, creating a numcodec, and/or creating a ZEP to get there). But from some of the linked comments below it does seem very achievable.

If I'm wrong anywhere, please correct me. I'm not an expert on these packages.

Example use case: parallel processing of single cell RNA sequencing (scRNA-seq) data across a cluster.

Single cell sequencing data produces large arrays, often sparse, and often too large to store in memory. They would benefit from chunked, sparse, on-disk storage to empower chunked parallel processing on a Dask or Ray cluster in an efficient manner.

The current scRNA-seq analysis stack utilizes annData to wrap the data and scanpy to analyze it. annData was built upon HDF5 files but there has been an effort to utilize zarr and dask (with the current caveat seemingly that the central matrix is a dense np.ndarray due to zarr's limitation). scanpy doesn't fully support dask arrays at the moment (e.g. scverse/scanpy#2491) but since dask arrays follow the numpy API that shouldn't be hard to fix. Dask supports sparse arrays and Ray can be used as a plug-in scheduler for dask. In practice I've found anndata is able to load in a scipy sparse csr matrix from an hdf5 file.

Some previous efforts and discussions I've found

Proposed test code to set goalpost

We can try to implement all five of the array formats I've come across (included below), or stick to sparse.COO which follows the numpy API most completely, is more powerful by being n-dimensional, and is easy to chunk as the coordinates are exact (though less space efficient than csr or csc). Also sparse.COO is easily converted in memory to other sparse array types.

import zarr
import sparse
import numpy as np

# generate data
sparse_array = sparse.random((100,100), density=0.1, random_state=42)
sparse_arrays = [sparse_array, sparse.DOK.from_coo(sparse_array), sparse.GCXS.from_coo(sparse_array), sparse_array.tocsr(), sparse_array.tocsc()]

for sparse_array in sparse_arrays:
    zarr.save("test.zarr", sparse_array, chunk=(20,20))  # write
    read_array = zarr.load("test.zarr")  # read

    # test
    assert type(read_array)==type(sparse_array)
    assert read_array.shape==sparse_array.shape
    if isinstance(read_array, np.ndarray):
        assert (sparse_array==read_array).all()  # np.ndarray doesn't implement .nnz() method
    else:
        assert (sparse_array!=read_array).nnz==0  # not all sparse formats implement .all() method

My ultimate use case

I'd like to analyze this dataset on a (local/remote) ray cluster using scanpy's default workflow at the start.

Some relevant stakeholders

@ivirshup, @alimanfoo, @rabernat, @jakirkham, @daletovar, @MSanKeys963

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions