Cube files#
A cube file is a Zarr v3 group that any Zarr v3 reader can open.
write_cube() saves a cube and read_cube() opens
one, leaving its layers on disk until they are used; a read then opens only the chunks
that hold the selected groups and features. cytepool.io.zarr is the minimal Zarr v3
reader and writer behind them.
Reading and writing cube files; cytepool.io.anndata converts cubes to and from
AnnData and MuData.
- cytepool.io.read_cube(path)#
Open a cube file.
The metadata and the sample, group and feature tables are read now. The layers stay on disk: when a method needs a group of a layer, the cube reads the chunks that hold the samples and features it needs, and checks those values then (see “Lazy layers” in
Cube). Opening a cube therefore takes little memory, whatever its size, and a cube derived withselect()reads only the chunks that hold the selected features of the selected groups.The digests stored with the file give the cube’s fingerprint without reading the layers. The tables, cell numbers and normalization are checked against theirs now, and each group of a layer whenever a read covers all of it.
- Parameters:
path – The cube file’s directory.
- Returns:
A cube whose layers are
ZarrArrayobjects.- Raises:
FileNotFoundError – If there is no store at
path.ValueError – If the store is not a cube file, has another format version, is corrupt, uses a Zarr feature this reader does not support, or holds tables, cell numbers or a normalization that do not match its stored digests.
TypeError – If a stored value has a type the cube does not accept.
RuntimeError – If the store was replaced while it was being read.
Used in: Export a cube from another tool, Differential expression per cell type, Bulk RNA-seq
Explained in: The cube and its additive layers, Cube files and provenance records
- cytepool.io.write_cube(cube, path, *, compression='gzip', level=None, overwrite=False)#
Save a cube as a cube file, a Zarr v3 group (see the module documentation).
The store appears at
pathonly once it is complete, so a write that fails leaves no store there; withoverwrite=Trueit leaves the old store as it was. Lazy layers are read, and their values checked, one group at a time. Each group is hashed as it is written, and the digests are stored with the file, so the fingerprint of the saved cube needs no further pass over its values.- Parameters:
cube – The cube to save.
path – The directory to create, by convention ending in
.cube.zarr. Its parent must exist.compression –
"gzip"(the default),"zstd"(Python 3.14 or newer) or None for no compression.level – The compression level: 0 to 9 for gzip (default 4), -131072 to 22 for zstd (default 3); None without compression.
overwrite – Replace what is at
path, the whole directory, if it is a cube file (a Zarr group whoseformatis"cytepool.cube") or an empty directory. Nothing else is ever replaced, nor the working directory or one that holds it.
- Raises:
TypeError – If
cubeis not a Cube, or an argument has the wrong type.ValueError – If
compressionorlevelis invalid,pathis a URL, or a lazy layer gives a value that breaks a constraint.OverflowError – If a sum of a derived lazy layer does not fit (see “Sums” in
Cube).FileExistsError – If
pathexists andoverwriteis False, or it is not somethingoverwritereplaces.FileNotFoundError – If the parent directory of
pathdoes not exist.
Used in: Export a cube from another tool, Differential expression per cell type
Explained in: The cube and its additive layers, Cube files and provenance records
A minimal reader and writer for Zarr v3 arrays and groups on a local file system.
This module implements, with numpy and the standard library only, the part of the
Zarr core specification, version 3.1 (Miles et al. 2025), that cube files need, with
the bytes and gzip codecs of the Zarr specifications (Zarr developers 2023)
and the zstd codec of its registered extensions (Maitin-Shepard 2025).
Reading supports:
group and array metadata documents (
zarr.json);the core data types bool, int8 to int64, uint8 to uint64, float16, float32, float64, complex64 and complex128, the floating-point types in the binary formats of IEEE 754 (IEEE 2019);
the regular chunk grid, and the default and v2 chunk key encodings with either separator;
fill values in every form the specification permits; a chunk that is not stored reads as the fill value;
the
bytescodec in either byte order, followed by any sequence ofgzipcodecs, whose chunks are gzip streams (RFC 1952) of DEFLATE data (RFC 1951; both Deutsch 1996), andzstdcodecs, whose chunks are Zstandard frames (RFC 8878; Collet and Kucherawy 2021);zstdneeds thecompression.zstdmodule of Python 3.14 or newer.
Anything else, such as another data type, the blosc, crc32c, transpose or
sharding_indexed codec, another chunk grid or a storage transformer, raises a
ValueError that names it; zarr-python reads such stores.
A read opens only the chunks that hold a selected element and decodes each of them once,
one at a time, so it holds the result and about one chunk: compressed chunks are read
from disk and decoded in pieces of at most 256 KiB. A zstd decoder also keeps its
frame’s window, outside Python’s allocations: up to the size of the chunk, and 2 MiB at
zstd’s default level of 3. A chunk compressed twice holds the output of both steps.
Selections take integers and slices, and, through ZarrArray.oindex, integer arrays
and boolean masks along any axes.
Reading is strict. A metadata document must be a JSON object (Bray 2017) without
repeated keys or NaN and Infinity tokens, and every field must be valid; a field the
specification does not define is an error unless its value is an object with
"must_understand": false. A chunk must decode to exactly the bytes its shape
needs, compressed streams must be complete and pass their checksums, and bool chunks
may hold only the bytes 0 and 1. Memory and time stay bounded on malformed files:
arrays whose chunks would hold more than 2 GiB (MAX_CHUNK_BYTES) are refused, at
most the bytes a valid encoding of a chunk can take are read from disk, every
decoding step stops just past the most bytes a valid chunk can produce, and a chunk
may hold at most 1,024 gzip members or zstd frames (writers use one).
Writing produces arrays with a regular chunk grid, the default chunk key encoding with
/, a fill value of zero (false for bool) and the bytes codec, little
endian, followed by gzip, zstd (with a checksum) or no compression. A chunk
whose bytes are all zero equals the fill value and is not stored.
References
Miles A, Striebel J, Rzepka N, Maitin-Shepard J, Moore J, eds (2025). Zarr core specification, version 3.1. https://zarr-specs.readthedocs.io/en/latest/v3/core/
Zarr developers (2023). Bytes codec, version 1.0, and gzip codec, version 1.0. Zarr specifications, https://zarr-specs.readthedocs.io/en/latest/v3/codecs/
Maitin-Shepard J (2025). Zstd codec. Zarr extensions, zarr-developers/zarr-extensions
Deutsch P (1996). GZIP file format specification version 4.3. RFC 1952, RFC Editor. doi:10.17487/RFC1952
Deutsch P (1996). DEFLATE Compressed Data Format Specification version 1.3. RFC 1951, RFC Editor. doi:10.17487/RFC1951
Collet Y, Kucherawy M (2021). Zstandard Compression and the ‘application/zstd’ Media Type. RFC 8878, RFC Editor. doi:10.17487/RFC8878
Bray T (2017). The JavaScript Object Notation (JSON) Data Interchange Format. RFC 8259, RFC Editor. doi:10.17487/RFC8259
IEEE (2019). IEEE Standard for Floating-Point Arithmetic. IEEE Std 754-2019. doi:10.1109/IEEESTD.2019.8766229
- cytepool.io.zarr.METADATA_FILE = 'zarr.json'#
The name of the metadata document of every Zarr v3 group and array.
- cytepool.io.zarr.DATA_TYPES = mappingproxy({'bool': dtype('bool'), 'int8': dtype('int8'), 'int16': dtype('int16'), 'int32': dtype('int32'), 'int64': dtype('int64'), 'uint8': dtype('uint8'), 'uint16': dtype('uint16'), 'uint32': dtype('uint32'), 'uint64': dtype('uint64'), 'float16': dtype('float16'), 'float32': dtype('float32'), 'float64': dtype('float64'), 'complex64': dtype('complex64'), 'complex128': dtype('complex128')})#
The Zarr v3 core data types this module reads and writes, by name.
- cytepool.io.zarr.DEFAULT_LEVELS = mappingproxy({'gzip': 4, 'zstd': 3})#
The compression level
write_arrayuses when none is given.
- cytepool.io.zarr.MAX_CHUNK_BYTES = 2147483648#
The largest chunk, in bytes once decoded, that this module reads or writes.
- class cytepool.io.zarr.ZarrArray(path, metadata, stamp)#
A Zarr v3 array in a local directory, read chunk by chunk.
Open one with
open_array. Indexing with integers and slices, as inarray[2],array[1, :, 100:200]orarray[..., ::2], reads only the chunks the selection touches and returns a new numpy array in native byte order, which owns its memory (a numpy scalar when every axis gets an integer, as in numpy).array.oindex[...]also takes, along any axis, a 1-D integer array or a boolean mask (as a list or a numpy array), and selects every combination of the positions each axis selects (orthogonal indexing, as in zarr-python):array.oindex[0, [3, 1], [5, 9]]isnumpy.asarray(array)[0][numpy.ix_([3, 1], [5, 9])]. Integer arrays may be in any order, repeat positions and count negative positions from the end.numpy.asarray(array)reads the whole array. A chunk that is not stored reads as the fill value.Every read first checks that the array’s
zarr.jsonis still the file it was when the array was opened, and raises otherwise, so an array replaced on disk is never read through out-of-date metadata: on the same device the inode, size and modification time must match; through another mount, where inode numbers can differ, the size and modification time. A store replaced by an identical copy therefore also counts as changed on the same device.A
ZarrArrayholds no open files, so it can be pickled and read from another process that sees the same path.- property path#
The array’s directory.
- property shape#
The array’s shape.
- property dtype#
The data type of the values a read returns, in native byte order.
- property ndim#
The number of dimensions.
- property chunks#
The chunk shape of the regular chunk grid.
- property fill_value#
The value of elements in chunks that are not stored, as a numpy scalar.
- property attributes#
A copy of the array’s attributes.
- property dimension_names#
The name of each dimension (None for an unnamed one), or None.
- property oindex#
Orthogonal indexing, which selects along each axis independently.
array.oindex[key]reads whatkeyselects.keygives each axis an integer, a slice, a 1-D integer array or a boolean mask with one value per position (arrays as lists or numpy arrays), and may hold one.... The result has an axis for every axis that gets no integer, holding the selected positions in the order given, as in zarr-python’soindex. It raises the errors of__getitem__, and IndexError for an array that is not 1-D or a mask of the wrong length.
- cytepool.io.zarr.open_array(path)#
Open a Zarr v3 array for reading.
- Parameters:
path – The array’s directory, which holds its
zarr.json.- Returns:
A
ZarrArray; nothing but the metadata is read.- Raises:
FileNotFoundError – If there is no array metadata at
path.ValueError – If the metadata is invalid or uses a feature this module does not support (the message names it).
- cytepool.io.zarr.read_group_attributes(path)#
Read and check a Zarr v3 group’s metadata and return its attributes.
Consolidated metadata, if present, is ignored: each node is read from its own
zarr.json.- Parameters:
path – The group’s directory, which holds its
zarr.json.- Raises:
FileNotFoundError – If there is no group metadata at
path.ValueError – If the metadata is invalid, describes an array, or belongs to a Zarr v2 store.
- cytepool.io.zarr.write_group(path, attributes=None)#
Write a Zarr v3 group’s metadata, creating its directory if needed.
- Parameters:
path – The group’s directory; its parent must exist.
attributes – JSON-serialisable attributes.
- Raises:
FileExistsError – If the directory already has a
zarr.json.ValueError, TypeError – If an attribute cannot be written as JSON.
- cytepool.io.zarr.write_array(path, data, *, chunks, compression='gzip', level=None, attributes=None, dimension_names=None)#
Write an array as a new Zarr v3 array directory.
Chunks are written first and
zarr.jsonlast, so a write that fails leaves a directory that does not open as an array. A chunk whose bytes are all zero is not stored, since zero is the fill value.- Parameters:
path – The directory to create; its parent must exist and it must not.
data – A numpy array, or any object with
shapeanddtypewhosedata[start:stop]returns those rows along the first axis, such as aZarrArray. It is read one row of chunks at a time.chunks – The chunk shape: a positive length for each dimension.
compression –
"gzip","zstd"or None (seecheck_compression).level – The compression level, or None for the default.
attributes – JSON-serialisable attributes.
dimension_names – A name or None for each dimension.
- Raises:
TypeError – If
datahas no shape and dtype, its data type is not a core data type, it is or gives rows as a numpy masked array, or another argument has the wrong type.ValueError – If an argument is invalid, a chunk would exceed
MAX_CHUNK_BYTES, ordatagives rows of the wrong shape or dtype.FileExistsError – If
pathexists.
- cytepool.io.zarr.check_compression(compression, level)#
Check a compression and level for
write_arrayand return the level used.- Parameters:
compression –
"gzip","zstd"(Python 3.14 or newer) or None for no compression.level – 0 to 9 for gzip, -131072 to 22 for zstd (0 asks zstd for its default), or None for
DEFAULT_LEVELS(4 for gzip, 3 for zstd). It must be None without compression.
- Returns:
The compression level, or None without compression.
- Raises:
TypeError – If
compressionis not a string or None, orlevelis not an integer.ValueError – If
compressionis unknown or unavailable, orlevelis out of range or given without compression.