Changelog
Notable changes to adata-cli. Versions are MAJOR.MINOR.PATCH; tags carry no
v prefix.
0.17.2
Space Ranger 4’s segmented cells join Visium HD’s bins: convert --cells
reads the cell matrix with each cell’s centroid, its area and its nucleus’s,
and Space Ranger’s clusters, UMAP and PCA, reading the cells’ GeoJSON
outlines a feature at a time.
Added
- Visium HD segmented cells (#83).
adata convert outs -o cells.h5ad --cells, or thesegmented_outputs/folder given itself, readsfiltered_feature_cell_matrix.h5(raw_with--unfiltered) with:obsm["spatial"]: each outline’s area-weighted centroid in full-resolution pixels, as shapely computes it, so scanpy and squidpy plot the cells over the images;obs["cell_area"]andobs["nucleus_area"], in µm², as Xenium names them;obs["graphclust"],obsm["X_umap"]andobsm["X_pca"]from the cells’analysis/;- the images, scale factors and metadata in
uns["spatial"].
Without
--cellsan HD output is still read as 8 µm bins, and the summary says the cells are there. Checked in CI against 10x’s scanpy and geopandas recipe, and on 10x’s SR 4.0.1 tiny sample against shapely.- The GeoJSON, one line of a few hundred MB on a full slide, is read a feature at a time; about 50 bytes a cell are kept, so that a cell with no outline is refused before anything is written. At 50,000 cells the import peaks at 190 MB, against 771 MB for the recipe.
- An
import-visium-cellsbenchmark case, against that recipe.
Changed
- Xenium’s
analysis/reading is shared with the segmented cells, which lay theirs out the same way; Xenium’s output is unchanged.
0.17.1
Visium HD joins standard Visium and Xenium: convert reads a Space Ranger
3 or 4 HD output at one bin size, with its bins’ positions, images and
scale factors, through a new optional spatial extra (pyarrow) for the
Parquet positions.
Added
- Visium HD (#81).
adata convert outs -o bins.h5adreads the 8 µm bins, as 10x recommends for analysis;--bin-size 2or16picks another size, and a bin folder given directly reads as it is. The layout is standard Visium’s: obsin_tissue,array_row,array_col;obsm["spatial"]in full-resolution pixels; the bin folder’s images and scale factors inuns["spatial"]. Checked in CI against scanpy with pandas reading the positions, and on 10x’s SR 4.0.1 tiny sample at every bin size.- The positions are read as a buffered stream, the barcodes and the numbers in separate passes. On that sample the 2 µm bins (11.2 million positions, 330,035 in the matrix) convert in 3.5 s with a 470 MB peak without images, where pandas alone takes about 1 GB to read them.
- The
spatialextra:pip install 'pyadata-cli[spatial]'adds pyarrow, which only Visium HD needs; the container image has it.
Changed
- Images are written a strip of rows at a time, as they are read, rather than loaded before the write starts: Visium HD’s hires image is 6,000 pixels, some 100 MB.
- Barcodes are matched as bytes, a quarter of the memory of numpy’s unicode, for Visium, Visium HD and Xenium alike.
0.17.0
convert reads 10x’s spatial outputs with their coordinates: a Space
Ranger folder for standard Visium, with its spot positions, tissue images
and scale factors where squidpy puts them, and a Xenium bundle, with its
cell table, centroids and Xenium’s own clusters, UMAP and PCA. No new
dependency; Visium HD, whose positions are Parquet only, comes later.
Added
- Visium (Space Ranger 1–4)
(#79).
adata convert outs -o section.h5adgives whatsquidpy.read.visiumgives, checked against it in CI: obsin_tissue,array_row,array_col;obsm["spatial"]in full-resolution pixels;uns["spatial"][library]with the images (uint8, as the PNGs hold them), the scale factors and the metadata.- New flags
--library-idand--no-images;--unfilteredreadsraw_feature_bc_matrix.h5, as it reads Parse’s unfiltered matrix. - Space Ranger 1’s headerless
tissue_positions_list.csv, decimal positions, and 10x’s downloads named with their sample are read. - Refused before anything is written: a barcode with no position, aggr output, Visium HD, and a folder whose matrix is Matrix Market only.
- New flags
- Xenium (XOA 1–4)
(#79).
adata convert bundle -o cells.h5adputscells.csv.gz’s columns in obs and the centroids inobsm["spatial"](µm), and Xenium’s graph-based clusters, UMAP and PCA inobs["graphclust"],obsm["X_umap"]andobsm["X_pca"], read fromanalysis.tar.gzas Xenium ships it. Checked against 10x’s scanpy recipe in CI.- The cell table is matched to the matrix while it is scanned and
written straight through, as Xenium writes both in one order; a table
in another order goes through scratch buckets (
core/scatter.py), so memory stays at a block either way.
- The cell table is matched to the matrix while it is scanned and
written straight through, as Xenium writes both in one order; a table
in another order goes through scratch buckets (
- An
import-xeniumbenchmark case, against 10x’s scanpy recipe.
Fixed
- A Space Ranger
outs/folder was refused (#79) as a Cell Ranger 2 MTX layout (“pick one with –genome”): its two MEX folders were taken for genomes. A Xenium bundle likewise. - A CSV saved with a byte order mark, as Excel saves one, kept the mark in its first column’s name, so a table looked up by that name was refused as lacking it, or a Visium positions file taken for one with no header.
0.16.2
export image writes to stdout without --output, as every other export
does, and the documentation is a site with a sidebar and search, guides
and a troubleshooting page.
Changed
- The documentation is a Just the Docs site
(#76), laid out as
cellgeni’s pipeline docs: Home, Getting started, six guides (inspect and
export, subset and split, concatenate, recast, convert formats,
pipelines and containers), Reference, Troubleshooting, Developers and
this changelog, at https://cellgeni.github.io/adata-cli/.
- The command reference keeps one section per command; each format
convertreads or writes has a page of its own. - Troubleshooting quotes the tool’s own error messages, each with its fix.
- Old URLs (
/COMMANDS.htmland five more) redirect,#anchorincluded. - The changelog moved to
docs/changelog.md, and the README is half its length.
- The command reference keeps one section per command; each format
Fixed
export imagewithout--outputwrites PNG to stdout (#77). It crashed with'NoneType' object has no attribute 'parent', where every other export writes to stdout. With-o, the format still follows the extension.
0.16.1
subset, split and the MuData export now narrow categorical columns as
anndata’s own subsetting does, so their obs and var equal what anndata
gives, and outputs that kept every label of a large categorical are smaller.
Fixed
- A subset keeps only the categories its rows use
(#74). anndata drops a
category no kept row uses, on obs and var (the axis kept whole too), and
narrows
uns["<col>_colors"]with it;subset,splitand the MuData export kept every category. A peaks subset of a 10x Multiome matrix held all 134,905 interval labels for 98,319 peaks, and its.h5muwas 84.7 MB where it is now 74.2 MB.- Colours follow anndata’s rule: those of the categories kept, along the first axis (RGBA rows too); a list of the wrong length is dropped, a single colour becomes a list of one; obs before var.
- Order and
orderedare kept. A whole AnnData written to.h5muwith--modkeeps its categories, as nothing is subset.
0.16.0
Reads and writes MuData, the container muon, mudata and SCENIC+ keep
multimodal data in: an .h5mu (or a MuData Zarr store) converts to one
AnnData per modality, with the joint results on its cells, and an AnnData
converts back to an .h5mu, split by feature type. Copies of anndata 0.7’s
dataframes, which every copy broke, now come out readable.
Added
adata convert x.h5mu -o x.h5adwrites each modality as an AnnData (#31), tox.<mod>.h5ad, or the one--modnames:- the modality as it is stored;
- the global obs columns, obsm and obsp (a joint clustering, a WNN
embedding, the joint graph) taken onto its cells through
obsmap, in its order, a block at a time; graphs keep CSR or CSC; - a global name the modality already has keeps the modality’s own, with
a warning; a column pulled from a modality (
<mod>:col) goes back only to that modality.
Without maps, cells are matched by name; a map that does not place each cell once is refused before anything is written. A global sparse element keeps its index dtypes, compression and duplicate entries. An
.h5muis no longer taken for an h5ad, nor a MuData Zarr store for a Zarr AnnData. On muon’sminipbcite.h5mu, both modalities equal what mudata reads.adata convert x.h5ad -o x.h5muwrites a MuData (#31). var is split by feature type as muon does (Gene Expression →rna, Peaks →atac, Antibody Capture →prot), or the whole AnnData is written under--mod:- each modality keeps X, layers, var, varm, varp and raw for its
features, every matrix read once for all of them; raw’s features are
shared out by raw/var’s own feature types, so genes and peaks var was
filtered of still land in their modality (with
--mod, raw is kept whole); - obs, obsm, obsp and uns are written once, globally;
- the maps, masks and userblock mudata expects are written too.
mudata reads the result without a warning; a 10x Multiome matrix goes to
.h5muin two steps.- each modality keeps X, layers, var, varm, varp and raw for its
features, every matrix read once for all of them; raw’s features are
shared out by raw/var’s own feature types, so genes and peaks var was
filtered of still land in their modality (with
Fixed
- Copies of anndata 0.7’s dataframes are readable
(#72). Their categoricals
point into
__categoriesby an HDF5 object reference, which cannot leave the file:subsetwrote an h5ad that anndata could not read, andsubsetorconvertto Zarr stopped with “Object of type Reference is not JSON serializable”. Every copy now writes them as currentcategoricalgroups, in a dataframe of the current version.
0.15.0
Writes SCope looms, so results from pySCENIC, SCENIC+ or pycistopic can go
back to the SCope viewer after editing as AnnData. A SCope loom now
converts to h5ad and back with its regulons, embeddings and clusterings.
concat keeps what those imports leave in obsm and varm.
Added
adata convert ... -o x.loom --scopewrites a SCope loom (#61):- obsm and varm dataframes (
RegulonsAUC,Regulons,ClusterMarkers_<id>) become compound attributes; - two-column embeddings become
Embeddings_X/_Y, the default also asEmbedding; - the clusterings a SCope import recorded, plus those
--clusteringsnames, becomeClusterings; MetaDatadescribes the embeddings, clusterings, regulon thresholds, metrics and annotations. Ids and names recorded on import are kept.
The rules SCope’s server reads by are kept:
- clustering and cluster ids are 0..n-1 in order;
- regulon names have no spaces, in both the AUC and the Regulons attributes;
- cluster markers are written only with their metrics, since SCope crashes on them otherwise.
A loom round trip keeps what was edited in between:
ClusterIDcomes from the first clustering, the thresholds from theregulon_thresholdstable, the marker metrics from the marker tables written. A non-categorical clustering, or embeddings, clusterings or a table too many for one HDF5 compound attribute, are refused before writing. On the public pySCENIC loom (Zenodo 10944066), loom → h5ad → SCope loom gives back the same AUC, regulons, embeddings, clusterings and thresholds through the pySCENIC tutorial’s recipe.- obsm and varm dataframes (
Fixed
concatstacks dataframes in obsm and merges varm (#61). It skipped obsm dataframes (a SCope import’sRegulonsAUC) as “not a dense array”, and never wrote varm, though--mergewas meant to cover it, as in anndata.- obsm dataframes are now stacked as obs is, their columns following
--join. - varm is merged as var’s columns are, on the output’s genes, compared
and copied a
--chunkof genes at a time, its chunking and compression kept.
Both match
anndata.concatfor each join and merge strategy.- obsm dataframes are now stacked as obs is, their columns following
- A loom export no longer stops half written when two elements ask for
one attribute name. An obs column and an obsm entry of one name (or a
var column named like an attribute loom writes itself) were both written
to the same
col_attrs/row_attrsname, and the second write failed. The first now keeps the name, and the others are left out with a warning, decided before anything is written.
0.14.0
Reads pycisTopic’s pickles, the only way pycisTopic saves its objects,
without pycisTopic and without running anything from the file. pandas and
scipy, which rebuild the pickle’s tables and matrices, come with a new
optional extra: pip install 'pyadata-cli[pickle]'. The Docker image
includes the extra.
Added
adata convertreads pycisTopic pickles (#28), for pycisTopic’sCistopicObjectand itsCistopicImputedFeatures.- CistopicObject:
fragment_matrixbecomes X, CSR cells x regions with its dtype kept, andbinary_matrixbecomes a layer;cell_dataandregion_databecome obs and var, column dtypes kept (categoricals, nullable integers and booleans);- the selected model’s topics become
obsm["X_topic"]andvarm["topic_region"]; - projections become
obsm["X_<name>"]; - the project, fragment files and the model’s tables go into
uns["pycistopic"].
- CistopicImputedFeatures (imputed accessibility, gene activity)
becomes its own AnnData, CSR or
--dense.
The file is read by an unpickler that resolves only the names such an object holds: numpy, scipy.sparse, pandas and a few builtins. Any other name is refused, so a pickle that names
os.systemis not run. pycisTopic pins pandas 1.5, and its pickles read with pandas 2 and 3, as CI checks.A public GEO pickle (GSE292438, 5,027 cells x 124,083 regions, 15 topics; 76 MB gzipped, 328 MB unpacked) converted in 8.8 s at a 842 MB peak, the pickle being held whole. Every matrix, column and topic table equalled what
pickle.loadgives under pycisTopic’s pandas 1.5.- CistopicObject:
Fixed
- Error messages keep their square brackets
(#28). Errors and
warnings were printed as Rich markup, so a bracketed word that read as a
tag vanished: the install hint
'pyadata-cli[pickle]'printed as'pyadata-cli'.
0.13.0
Adds the last of scanpy’s core readers to adata convert: dense matrices
in delimited text, as read_csv and read_text read them, and one dataset
of a plain HDF5 file, as read_hdf does. Both are streamed, so memory is
set by a block of the file rather than by the matrix, and X is CSR by
default where anndata gives a dense float32 array.
Added
adata convertreads text matrices (#25):.csv,.tsv,.tab,.txtand.data, gzipped or not.- The layout is found as anndata finds it: a header with a corner cell
(pandas) or one field short (R’s
write.table), column names in the last#comment line, or no header; row names or none.--delimiterand--first-column-names/--no-first-column-namesoverride the detection. --transposereads genes as rows, as count tables often store them.- Integers stay integers (int32 or int64, exact past 2**53); other values are float64.
- A ragged line, or a value that is not a number, is refused by line and column before any output exists.
A 400 MB CSV of 100,000 cells x 2,000 genes took 14 s at a 159 MB peak.
anndata.read_csvpluswrite_h5adtook 12.6 s at 1.3 GB. A public GEO table (GSE75748, 1,018 cells x 19,097 genes, gzipped, genes as rows) matchedread_csv(...).T. It took 7.5 s at 222 MB, against 2.9 s at 315 MB: small tables are slower read twice.- The layout is found as anndata finds it: a header with a corner cell
(pandas) or one field short (R’s
adata convertreads one dataset of a plain HDF5 file (#25):--keynames it. Without it, the refusal lists the 2-D numeric datasets and the 1-D ones that could name their rows and columns.- Names come from
read_hdf’srow_namesandcol_names, or from--obs-names-keyand--var-names-key. --transposereads genes as rows.- The dataset is read a slab of whole chunks at a time, and keeps its dtype, complex included.
.hdf5,.he5and.hdffiles are recognised by name. A plain.h5that is not a 10x matrix needs--from hdf5, andconvertnow says so.
0.12.0
Reads SCope looms: what pySCENIC, SCENIC+ and pycistopic write for the
SCope viewer. Their regulons, embeddings, clusterings and metadata, which
anndata.read_loom refuses and 0.11 left out, now convert with the rest
of the loom.
Added
adata convertreads SCope looms (#27):- each regulon’s AUC per cell and its target genes (or regions) as
dataframes with the regulons’ names as columns,
obsm["RegulonsAUC"]andvarm["Regulons"]; any other compound attribute, such as VSN’s cluster markers, becomes a dataframe the same way; - the embeddings as
obsm["X_<name>"]and the clusterings as categorical obs columns, named and labelled from the loom’sMetaData; MetaData, plain or compressed, decoded intouns["scope"], with the regulon thresholds as a dataframe and each embedding’s and clustering’s SCope id and name.
Region-based looms (SCENIC+, pycistopic) are named by region. On a public pySCENIC loom (Zenodo 10944066: 500 cells, 298 regulons, 5 embeddings) this took 0.5 s at a 143 MB peak and matched the pySCENIC tutorial’s loompy recipe value for value, as CI checks on a fixture. The tables are read a block of cells at a time: a 2.8 GB loom of 600,000 cells and 600 regulons took 6.4 s at a 294 MB peak. Writing a loom still writes a plain one; SCope export is #61.
- each regulon’s AUC per cell and its target genes (or regions) as
dataframes with the regulons’ names as columns,
Fixed
- R’s factors and logicals in looms read as R meant them
(#27). R’s writers
store a factor as an HDF5 enum, which came out as its codes: the
CellTypeof SCENIC’s example loom as 1 to 5. A factor is now a categorical of its levels, in R’s order, and a logical (FALSE, TRUE, NA) a nullable boolean.
0.11.1
Reads the looms other tools write, as found converting 29 real looms from
Baysor, R’s LoomExperiment and anndata/loompy: all now convert, and match
anndata.read_loom where it can read them.
Fixed
-
A
.loomholding the matrix alone is read (#59). Baysor writes some looms as/matrixwith no attribute groups, which were not recognised. In a file named.loomthat is now a loom, its cells and genes numbered; a 7,979,604 x 8,729 one converted in 10.5 minutes at a 211 MB peak. -
Baysor’s gene names are used (#59). Baysor names genes in a
Nameattribute; its looms came out with numbered genes. -
R’s LoomExperiment looms read cleanly (#59). The spec version, stored as a one-element array, showed as
['2.0.1'], and graph vertices stored as floats were truncated without a check; they must now be whole numbers. loompy, and soanndata.read_loom, refuses these files.
0.11.0
Adds loom to adata convert, both ways. A velocyto, loompy or
kallisto|bustools loom converts a slab of cells at a time, with the
matrices sparse, their dtypes kept, and the graphs and global attributes
that anndata.read_loom drops; an AnnData exports to a loom that loompy
validates.
Added
adata convertreads looms (#26), spec 2.0.1 and 3.0.0, asanndata.read_loomreads them:- X is the
splicedlayer when there is one, with the main matrix kept aslayers["matrix"];--layerpicks another; - attributes become obs and var columns, or obsm and varm when they have
several columns; graphs become obsp and varp, and global attributes
uns["loom"]; - genes are named by id (
Accession, or kallisto|bustools’gene_id), the symbols kept as a column, unless--var-names gene_symbols;--obs-names-attrand--var-names-attrname any other attribute; - X and the layers are CSR, or dense with
--dense, and keep their dtype: velocyto’s integer counts stay integers.
On a public 10k-cell velocyto loom this took 26 s at a 159 MB peak, where
anndata.read_loomtook 29 s and 9.9 GB, and gave the same matrices, names and columns. SCope’s compound attributes are left out with a warning, for #27.- X is the
adata convertwrites looms (#26): spec 3.0.0 as loompy 3 writes it, which loompy’s validator accepts. X (or--layer) is the main matrix, with every layer, obs and var as attributes (CellID,Gene,Accession), 2-D obsm and varm, obsp and varp as graphs, and uns scalars as global attributes. A loom’s matrices are dense: the summary gives their size before compression and warns above 10 GB.
0.10.0
Adds Parse Biosciences split-pipe output to adata convert, read in two
streaming passes like the MTX folders of 0.8.0. Features are now named by
gene id by default, for every format: ids are unique where symbols repeat,
so a gene-level matrix no longer needs --make-unique (a probe-level Flex
matrix still does).
Added
adata convertreads Parse Biosciences split-pipe output (#30). A run folder, a sample folder or aDGE_filtered/DGE_unfilteredfolder is recognised whatever its name:count_matrix.mtx(cells x genes) becomes X through the streaming MTX reader; integer counts stay integers;cell_metadata.csvbecomes obs, indexed bybc_wells, each column typed as pandas reads it and stored as anndata writes it;all_genes.csvbecomes var, withgene_symbolsandgenome;--sample NAMEreads one sample,--all-sampleswrites one output per sample, and--unfilteredreads every barcode;--genome Gkeeps G’s genes and the cells split-pipe called as G.
Checked against Parse’s own scanpy recipe in CI, and on a real split-pipe v1.6.1 run: X, obs and var all match, for 29,588 cells and for the 431,061 barcodes of the unfiltered folder, at a peak of about 200 MB.
- A streaming CSV table reader (#55),
formats/table.py: two passes over the file, columns typed aspd.read_csvtypes them, theobsdataframe written a block of rows at a time. It reads Parse’s cell metadata, and is there for #25’s delimited tables.
Changed
- Features are named by gene id by default
(#54). This changes what
adata convertwrites from 10x HDF5, MTX folders, CellBender and Parse: var names are the gene ids, and the symbols are invar["gene_symbols"], the layoutscanpy.read_10x_mtx(var_names="gene_ids")gives. Symbols repeat (Cell Ranger’s GRCh38-2020-A reference repeats 10), so naming by them needed--make-unique.--var-names gene_symbolsgives the old names, as scanpy and CellBender’s loaders give them. A probe-level (Flex) matrix still repeats gene ids, and still warns.
Fixed
- Categories are ordered as anndata orders them
(#55). anndata sorts the
categories it makes from strings naturally (
A2beforeA10);convertsorted them plainly, so a categorical var column holding numbers in its values came out in a different order from scanpy’s.
0.9.0
Adds CellBender’s remove-background output to adata convert. The
denoised counts become X, and CellBender’s latent variables are kept under
the names its own loaders use, so notebooks written against those keep
working. --raw adds the Cell Ranger counts CellBender started from.
Added
adata convertreads CellBenderremove-backgroundoutput (#29). Anoutput.h5oroutput_filtered.h5from CellBender 0.2, 0.3 or 0.4 is recognised by its latents, whatever it is called:- per-droplet latents (
cell_probability,cell_size,background_fraction,droplet_efficiency;latent_*in 0.2) become obs columns, NaN for droplets CellBender did not analyse, withobs["cellbender_analyzed"]marking the ones it did; - the embedding becomes
obsm["gene_expression_encoding"](latent_gene_encodingin 0.2), the ambient profilevar["ambient_expression"], and the analysed featuresvar["cellbender_analyzed"]; - everything else (fitted constants, learning curve, settings) goes to
uns["cellbender"].
The 10x HDF5 flags apply.
--from 10x-h5still reads such a file as a plain 10x matrix. Checked against real CellBender 0.2.2 and 0.3.2 output, full and filtered, and CellBender’s ownload_anndata_from_input_and_output: X, both layers, every latent at its barcode, var andunsall match (#52).- per-droplet latents (
-
--min-cell-probability P(#29) keeps the droplets CellBender gives a cell probability of at leastP, in file order. At 0.5 it picks the same cells as CellBender’s ownoutput_filtered.h5. --raw raw_feature_bc_matrix.h5(#29) adds the Cell Ranger counts of the same barcodes aslayers["cellranger"], and the denoised counts aslayers["cellbender"], as CellBender’s loader does. A filtered file lists cells by count rather than in the raw file’s order, so its raw counts go through the on-disk bucket sort the MTX import uses, now shared incore/csr_build.py(#51).
Changed
- An output extension picks a format that can be written
(#51).
.h5stays a 10x HDF5 matrix, and--to cellbenderis refused, since CellBender files can be read but not written.
0.8.0
Adds 10x Genomics and STARsolo Matrix Market folders to adata convert, in
both directions, velocyto’s included. They are read in two streaming passes,
so a folder’s size is not a limit. import sparse and export sparse move
onto the same reader and writer, which changes what import sparse writes
and fixes a symmetric file being read as half its matrix.
Added
adata convertreads 10x and STARsolo MTX folders (#24). A folder holdingmatrix.mtx,barcodes.tsvandfeatures.tsv(or Cell Ranger 2’sgenes.tsv), each plain or gzipped, is recognised whatever its name:- Cell Ranger 3 and later;
- Cell Ranger 2, with one subfolder per genome, picked with
--genome; - STARsolo
Gene/,GeneFull/and the like; - STARsolo
Velocyto/raw/, whosespliced,unsplicedandambiguousmatrices become layers of those names, with X a copy ofspliced; - any of these with every file under one GEO-style prefix
(
GSM123_matrix.mtx.gz).
The result matches
scanpy.read_10x_mtx(gex_only=False)column for column, checked against scanpy in CI, except that integer counts stay integers. The 10x HDF5 flags apply (--genome,--var-names,--make-unique,--feature-type,--split-feature-types), as does--compression. The first pass checks the whole folder before any output exists; the second writes X, straight through when entries are in barcode order, as Cell Ranger and STARsolo write them, and through on-disk buckets when they are not. On 50 million entries this took the time scanpy did, at under a fifth of its peak memory, and the peak did not grow with the input.-
adata convert --to 10x-mtxwrites a Cell Ranger 3 folder (#24): gzipped (or not, with--compression none), three feature columns, entries in barcode order, from X or the matrix--layernames. Feature ids and types come from var as for the 10x HDF5 export. A folder imported byconvertexports with Cell Ranger’s%metadata_jsonintact. - A benchmark case for MTX import (#49), against scanpy’s
read_10x_mtx.
Changed
-
import sparsekeeps integer values as integers (#24). AnintegerMatrix Market file becomes int32, or int64 when a value needs it; arealone becomes float64; apatternone becomes int32 ones. All three were float32, which cannot hold counts above 2^24 exactly.adata recast --dtype float32narrows on request. Duplicate entries are now summed, as scipy does, and reported. The data is gzipped in an h5ad, and a parent group made on the way (layers) is a proper dict. -
import sparseandexport sparsestream (#48), and read or write gzipped files (.mtx.gz). Import makes two passes and checks the whole file before the store is touched; it used to build a Python object per entry. Export readsindicesanddataonce per block, where it read both once per row, and writes floats with the digits to read back exactly and whole numbers as integers.
Fixed
-
A
symmetricMatrix Market file was read as half its matrix (#48). It stores one triangle and means both, andimport sparseread it asgeneral.symmetric,skew-symmetric,hermitian,complexand densearrayfiles are now refused by name. -
import sparseoverflowedindptrpast 2^31 entries (#48); it widens to int64 when needed. -
recast --layoutspent time in proportion to the matrix on every block of a streaming transpose (#47): pass 1 counted over every output row per block, and pass 2 compared each block against every bucket. Both now cost what the block costs.
0.7.1
Fixes ways that subset, recast, split and concat could lose or
corrupt data while reporting success. The worst was that subset emptied a
Zarr store given as both its input and its output. All four commands now
write beside their output and rename it into place only once it is complete,
as convert already did. Also: concat no longer upgrades Zarr v2 inputs to
v3, and two help examples that did not run now do.
Fixed
-
subset x.zarr -o x.zarremptied the store (#35). The output was opened for writing, which clears a Zarr store, before the selection was read from the source. An output that names the input is now refused; use--inplace. The selection is now resolved before the output exists, so a query that fails no longer leaves an empty store behind, or truncates an existing one. -
--inplacedeleted the source before moving the new store in (#35). This affectedsubsetandrecast: an interrupt between the two steps lost the data. The source is now renamed aside and deleted only once its replacement is in place. -
A failure part way through could leave a partial store that read as a finished one (#35). This affected
subset,recast,splitandconcat. Each now writes to a temporary path beside its output (out.adata-tmp.zarr) and moves it into place only on success. A failure leaves an existing output as it was.splitmoves all its outputs together, writes its manifest last, and refuses an output that would land on its input.The helper that does this, shared with
convert, has three fixes of its own:- It no longer deletes a leftover
*.adata-old.*backup, which after an interrupted run may be the only copy of the data; it now refuses to start. - It puts back the outputs already replaced if a rename fails part way.
- It keeps a Zarr store named without the
.zarrsuffix as Zarr.
- It no longer deletes a leftover
-
concatwrote Zarr v3 whatever its inputs’ version (#36), contrary to the documentation. It now follows the version its Zarr inputs share, and writes v3 when they differ. h5ad inputs do not count.--zarr-formatstill overrides it. -
subsetandconcatnow check--zarr-formatthemselves (#37), as every other command does, and say that it must be 2 or 3. Contributed by @iamcipherdev. -
Six
--helpexamples still saidh5ad, and two of them did not run (#38). A new test checks every help example against the real commands and options. Contributed by @iamcipherdev.
0.7.0
Adds format conversion. adata convert now changes a store’s format –
between h5ad and Zarr v2/v3, and to and from 10x Genomics HDF5 matrices –
and the matrix command that used to be convert is recast; its old
spelling keeps working, with a warning, until 1.0.0. Also fixes four ways a
store could fail or bloat on its way between backends, one of which left
recast --layout outputs three times the size they should be.
Added
-
adata convertchanges a store’s format (#20, #22). This release converts between h5ad and Zarr v2/v3 in any direction; the output’s extension picks the format. Every element is copied as it is, streamed, keeping chunking and, where the target can express it, compression;--compressionchooses instead. A Zarr output keeps the source’s version unless--zarr-formatsays otherwise.Everything is checked before anything is written, and the output is written beside its destination and moved into place only once complete, so a failed conversion leaves nothing behind. An existing output is refused unless
--overwriteis given. A flag that does not apply to the conversion asked for is refused rather than ignored.Formats are registered in
adata.conversion, each with the converters that read and write it; MTX folders, loom and others follow as separate issues under #20. -
convertreads and writes 10x Genomics HDF5 matrices (#23): Cell Ranger’s*_feature_bc_matrix.h5, v2 and v3+, and the same layout from Space Ranger, Xenium and CellBender. Reading gives whatscanpy.read_10x_h5(gex_only=False)gives, column for column – checked against scanpy itself in CI – except thatXkeeps the integer counts and the run’s metadata is kept inuns["cellranger"].--feature-typekeeps one type,--split-feature-typeswrites one output per type from a single pass over the matrix,--genomepicks a genome, and--var-namesand--make-uniquedo what scanpy’s options of the same name do.Writing produces the Cell Ranger 3+ layout from X,
raw/Xor any layer (--layer), for CellBender, SoupX, DropletUtils and Seurat: gzip and fixed-width strings, which PyTables – and so CellBender – needs. A file imported byconvertexports with its metadata intact, and the round trip reproduces Cell Ranger’s file dataset for dataset.
Changed
- The matrix command
convertis nowrecast(#21). It changes a matrix’s dtype, layout or density exactly asconvertdid in 0.6; the nameconvertnow means format conversion. The hintconcatprints for mismatched encodings namesrecast.
Deprecated
convertwithrecast’s flags. Given any of--dtype,--indices-dtype,--layout,--allor--inplace,convertstill runsrecastas before, with a warning. This is removed in 1.0.0, with theh5adcommand name and theinfoalias.
Fixed
-
recast --layoutleft its h5ad output up to three times too large. The streaming transpose kept its intermediate buckets in the output and deleted them when done, but HDF5 never returns a deleted dataset’s space to the file. On a 20,000 x 2,000 matrix at 5% the output was 55 MB where the in-memory transpose wrote 17 MB. The buckets now go in a separate file beside the output, removed afterwards; Zarr outputs were unaffected. -
Writing an h5ad with a
Nonein it to Zarr failed. anndata storesNoneas a null, which on HDF5 is an empty dataspace with no value to read, and copying one raisedTypeError: float() argument must be a string or a real number, not 'Empty'. AnyNoneinunswas enough, sosubset in.h5ad -o out.zarrfailed on such stores, and so didsplit,importandrecastwriting Zarr from them. Nulls are now written in the target’s own spelling: an empty dataspace on HDF5, a 0-d bool on Zarr, as anndata does. The 0-d bool that a Zarr store uses for a missingrawnow becomes an empty dataspace in an h5ad too, rather than a strayFalse. -
Zarr written to h5ad came out uncompressed. Zarr codecs have no HDF5 equivalent, so they were dropped and nothing replaced them; a compressed Zarr store became an h5ad several times its size. A compressed array now becomes a gzip-compressed dataset (level 4, h5py’s default). Strings and arrays that were not compressed are left as they were.
-
An empty array chunked larger than itself could not be written to h5ad. Zarr allows a chunk bigger than its array and h5py does not, so a zero-length array copied from Zarr failed, and so did
recast --dtypeor--layouton an all-zero sparse matrix from a compressed h5ad. A zero-length dataset now lets HDF5 choose its own chunks.
0.6.1
Makes the outputs of subset, split and concat the size anndata writes
(they were about twice that), and makes split read each matrix once rather
than once per group. In 0.6.0 a hand-written anndata loop split the ci-tier
benchmark 7x faster than split did; split is now the faster of the two.
Fixed
-
subset,splitandconcatwrote sparse matrices about twice as large as they needed to be. Every sparse output widenedindicesandindptrto int64, whatever the source used, and was written without the source’s compression, so with an lzf input thedataandindicescame out uncompressed. Outputs now keep the source’s index dtypes and storage settings.concatwidens only when the combined matrix could not be addressed otherwise.convertalready did this; the code next to it did not. -
splitread the whole matrix once per group. It ran a separate subset for each group. When groups were interleaved through the file, as samples usually are, each subset read nearly all of X, so a split into k groups read and decompressed X k times: 56.7 s against 7.8 s for a hand-written anndata loop in the 0.6.0 benchmark.splitnow reads each matrix once and shares every block out among all the outputs. It holds up to a quarter of the file-descriptor limit open at once, and makes one pass per batch beyond that.subsetshares the same writer, which now gathers rows in one vectorised step instead of a Python loop over rows, and skips blocks that hold none of the selected rows. A new guard intests/test_performance.pychecks that split’s reads of X do not grow with the number of groups.Dataframe columns are no longer read through an h5py fancy index, which cost 0.38 s per column per group on 50,000 cells. They are now read as contiguous blocks and selected in memory. With both changes, the ci-tier benchmark on a laptop splits in 1.6 s against anndata’s 2.3 s.
0.6.0
Adds adata convert, and fixes four quadratic paths that made concat and
split appear to hang on real data. Three of the four were found by a new
suite of complexity guards, which now run on every pull request; the first
was reported from a pipeline that had to kill twelve tasks after 98 minutes.
Added
-
adata convertchanges a matrix’s dtype, layout or density on disk (#13). Counts stored as float64 halve with--dtype float32;--indices-dtype int32halves the index arrays of a matrix small enough to address that way;--layout csr|csctransposes between the sparse encodings;--layout dense|sparsechanges the density. Works onX, any layer,raw/Xor any 2-D array by path, and on all of them at once with--all.A cast that would not round-trip is refused before anything is written – every value is cast and cast back, because whether float64 counts survive float32 depends on the counts, not on the dtypes. So is a densification that would inflate the store beyond four times its size.
--forceoverrides either, and both messages say what they measured.Transposing streams by default, holding one bucket of nonzeros rather than the matrix, so it works on files too large to load;
--in-memoryis faster when the matrix fits. Buckets are balanced by nonzero count rather than by coordinate range, because a single-cell matrix is skewed – a few genes carry most of the counts – and equal-width bounds put most of one in a single bucket.--indices-dtypeis checked against both whatindicesmust address and whatindptrmust reach, which differ: a narrow matrix with more than 2^31 nonzeros needs int64 offsets over int32 coordinates. Both are preserved from the source when not specified. -
concatnow names the command to run when inputs disagree about a matrix encoding. The check itself is not new, but nothing tested it and it could not suggest a fix, because there was none. -
Complexity guards in the test suite (
tests/test_performance.py). Cost regressions now fail at merge time. They count operations rather than seconds – h5py and zarr reads, Zarr store traffic, Python allocation and executed lines – and assert that successive increments grow no faster than linearly, so nothing here can fail because a CI runner was busy. See docs/TESTING.md. Every subcommand is covered:ls,view,create, all fiveexportand all fiveimportvariants,spliton both axes, and theconcatoptions nothing else reached. Two claims are now enforced rather than described –viewandlsread zero data elements at any store size, and streaming stays far below the input curve at a fixed--chunk. -
A comparative benchmark (
benchmarks/), run on every tag against anndata and against scanpy where scanpy has a real equivalent. Reports peak RSS, wall time and output size; publishes to docs/BENCHMARKS.md and the release notes. Report-only – it never fails a build. Fifteen cases, covering every command with a real baseline, includingh5ls -rforlsand the rows where adata-cli is the slower of the two. -
--merge dropand--uns-merge dropare accepted.dropwas already the documented default behaviour but was rejected as a value, so a config could not state it explicitly.
Fixed
-
concat --mergenever finished on a real store. Aligning a var column onto the target index re-read the whole column from disk once per target variable, so the cost was quadratic: at 36,601 variables a merge that should take a fraction of a second ran for hours at 100% CPU with the output file never growing past its header. Reported against 0.5.1 (REQ-71798), where 12 of 13 pipeline tasks had to be killed after 98 minutes. The column is now read once per input, and--merge first/--merge only, which decide on presence alone, read no column values at all. -
concatwas quadratic in the number of categories in an obs column. Category merging probed a list rather than a dict: 2,096,128 string comparisons to union 1,024 categories, and around 5e9 for a 100k-category column. Found by the new guards. -
split --bywas quadratic, O(n_rows x n_groups).group_indicesgrouped rows withnp.nonzero(values == label)inside a loop over distinct labels, rescanning each chunk once per label: at 4,096 rows, 16,384 elements scanned for 4 groups and 1,048,576 for 256. A million cells split by a thousand samples is ~10^9 comparisons. Onenp.uniquepass per chunk makes it flat in the group count. Found by the new guards; order of first appearance, which names the output files, is unchanged. -
concatbuilt a Python object per row for nullable and string obs columns, then walked the list twice more. Filling a typed buffer by slice removes three full passes over every such column. -
Copying variable-length strings ignored its own read budget. The width of a vlen element was assumed to be 64 bytes, because h5py reports the itemsize of a pointer, so the step was the same 524,288 elements whatever the data held: 2 GiB per read at 4 KiB elements against a stated 32 MiB budget, and for any array shorter than that step, the whole array in one go. Copying 200,000 strings of 4 KiB peaked at 827 MB. The width is now sampled from the first 256 elements. Reported by an automated review on PR #14 and confirmed by measurement;
unscan hold arbitrary text, so this was not a width the layer could assume. -
Peak RSS in the benchmark was floored by the runner’s own memory on Linux. A forked child inherits its parent’s resident pages and
execvefolds that into themaxrssthe kernel reports, so every contender would have measured at least whatbenchmarks/run.pyused to build the fixtures — around 200 MB — and the tables would have read “everything costs about the same”. Commands are now forked from a small shim: with a 330 MB parent, a no-op child goes from 326 MB to 8 MB. Caught bytest_benchmark_harness.py, which exists for exactly this. The published figures were measured on macOS, which resets the high-water mark at exec, and are unchanged.
0.5.1
Makes the container image usable from Nextflow, and stops copy_dataset
reading one row at a time from row-chunked stores.
Fixed
- Copying a row-chunked store was dominated by read latency. The read step
was the source’s chunk height verbatim, so a store chunked
(1, n_cols)was copied one row per read. On a network filesystem (Lustre, NFS) each read is a round-trip, so a million-row copy spent nearly all of its time waiting. Reads are now sized to a 32 MiB budget, rounded down to a whole number of source chunks. A(1_000_000, 30_000)float32 store chunked(1, 30_000)goes from 1 row per read to 279. - Read sizing no longer trusts
itemsizefor variable-length strings. h5py reports 8 there because the value is a pointer, which overestimated the row count by an order of magnitude and broke the memory bound.
Container
- The image could not be used from a Nextflow process. Nextflow requires
/bin/bashto be the container entrypoint, soENTRYPOINT ["adata"]made every Docker- and Podman-backed task fail withNo such command '/bin/bash'. Apptainer was unaffected, assingularity execignores the entrypoint. - Task metrics were silently lost.
procpsis absent from the base image, so Nextflow could not runpsto collect them. The required tool set (bash,ps,awk,date,grep,sed,tail,tee) is now installed and asserted at build time. PYTHONNOUSERSITEis set, so a bind-mounted$HOMEunder Apptainer can no longer shadow the image’s virtualenv with the user’s~/.localpackages.XDG_CACHE_HOMEpoints at/tmp, so the image tolerates being run under an arbitrary UID with no writable$HOME.- Added a
.dockerignore. Local builds were copying the host’s.venv,.gitand.pytest_cacheinto the image.
Changed
- The image no longer sets an entrypoint, so the command must be named
explicitly:
docker run IMAGE adata view file.h5ad, wheredocker run IMAGE view file.h5adpreviously worked.
0.5.0
Renamed from h5ad to adata-cli, restored compatibility with current
AnnData files, and added four commands.
Renamed
- Distribution
pyadata-clion PyPI (the short name was already taken by an unrelated project), import packageadata, commandadata. infois nowview.- The
h5adcommand and theinfosubcommand remain as aliases that warn and then run normally. Both are removed in 1.0.0.
Fixed
- The CLI could not read any file written by anndata >= 0.11. Since pandas’
future.infer_stringbecame the default, anndata writesobs/_indexas anullable-string-arraygroup rather than a dataset, andaxis_lenrequired a dataset — soinfo,export dataframeandsubsetall failed outright. - HDF5 -> Zarr conversion failed on any store with string columns, i.e. all
of them: an h5py variable-length string dataset reports
dtype == object, which Zarr rejects. All four backend pairings now convert. subsetsilently droppedraw/. It is now carried over and matched against its own var axis. Unrecognised top-level keys are copied with a warning rather than dropped.subsetcorrupted group-valued columns, copying them whole while narrowing everything else. This was invisible before, because files containing such columns could not be read at all.- Categoricals no longer degrade to plain strings on a CSV round-trip.
Noneround-trips via anndata’snullencoding instead of an invented_is_nonemarker attribute.column-orderis honoured on export; previously columns came out in whatever order the backend enumerated (alphabetical on HDF5).- Type detection dispatches on
encoding-typebefore falling back to structure. A group merely containing a member namedobs_namesis no longer misreported as a dataframe. subsetprefers the declared_indexover theobs_names/var_namesconvention, which had been backwards.- A Zarr v2 store is no longer silently upgraded to v3.
- Chunk shapes are clamped on the axis-column path, which could raise from h5py when subsetting below a column’s chunk size.
Added
adata ls— tree listing for any HDF5 or Zarr store, with no AnnData assumptions, so.loomand plain.h5work.--long,--depth, and-1for bare paths that pipe into other tools.adata create— write a new, empty store forimportto fill in.adata split --by <column>— one store per distinct value, with a CSV manifest. (#2)adata concat— concatenate along obs with--join inner|outer,--label/--keys/--index-uniqueand merge strategies for var and uns. Verified to agree withanndata.concat.adata subset --obs-query/--var-query— a small predicate language (==,!=,<,<=,>,>=,in,not in,and,or,not, parentheses) evaluated while streaming. No new dependency.adata import image, andimportat any path rather than onlyobs/var.--categorical/--no-auto-categoricalonimport dataframe.export dataframefrom any dataframe path, not onlyobs/var. (#4)export arraycan write to stdout. (#4)adata --version.--zarr-formaton the commands that create stores.
Changed
- Results go to stdout and status to stderr throughout.
view --treepreviously wrote its tree to stderr and only the header to stdout. (#4) - Everything written is tagged with its
encoding-typeandencoding-version; text is always variable-length UTF-8, as the spec requires, rather than fixed-width bytes. - Sparse subsetting streams in blocks instead of loading
data/indices/indptrwhole. - The Docker image ships
duckdbin place ofcsvkit. - CI runs the whole suite on Python 3.12 and 3.13 rather than naming test
files individually — which is why
test_storage_root_attrs.pyhad never run.
0.3.2 and earlier
See the git history. These releases were published to Quay only.