Skip to content

Commit a446038

Browse files
authored
Merge pull request #33 from cellgeni/feat/recast-and-convert
Rename convert to recast; add format conversion as convert (h5ad ↔ zarr)
2 parents dc44cb2 + 265fb85 commit a446038

23 files changed

Lines changed: 2160 additions & 847 deletions

‎CHANGELOG.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,42 @@ Notable changes to `adata-cli`. Versions are `MAJOR.MINOR.PATCH`; tags carry no
55

66
## Unreleased
77

8+
### Added
9+
10+
- **`adata convert` changes a store's format**
11+
([#20](https://github.com/cellgeni/adata-cli/issues/20),
12+
[#22](https://github.com/cellgeni/adata-cli/issues/22)). This release
13+
converts between h5ad and Zarr v2/v3 in any direction; the output's
14+
extension picks the format. Every element is copied as it is, streamed,
15+
keeping chunking and, where the target can express it, compression;
16+
`--compression` chooses instead. A Zarr output keeps the source's version
17+
unless `--zarr-format` says otherwise.
18+
19+
Everything is checked before anything is written, and the output is
20+
written beside its destination and moved into place only once complete,
21+
so a failed conversion leaves nothing behind. An existing output is
22+
refused unless `--overwrite` is given. A flag that does not apply to the
23+
conversion asked for is refused rather than ignored.
24+
25+
Formats are registered in `adata.conversion`, each with the converters
26+
that read and write it; 10x HDF5, MTX folders, loom and others follow as
27+
separate issues under #20.
28+
29+
### Changed
30+
31+
- **The matrix command `convert` is now `recast`**
32+
([#21](https://github.com/cellgeni/adata-cli/issues/21)). It changes a
33+
matrix's dtype, layout or density exactly as `convert` did in 0.6; the
34+
name `convert` now means format conversion. The hint `concat` prints for
35+
mismatched encodings names `recast`.
36+
37+
### Deprecated
38+
39+
- **`convert` with `recast`'s flags.** Given any of `--dtype`,
40+
`--indices-dtype`, `--layout`, `--all` or `--inplace`, `convert` still
41+
runs `recast` as before, with a warning. **This is removed in 1.0.0**,
42+
with the `h5ad` command name and the `info` alias.
43+
844
### Fixed
945

1046
- **Writing an h5ad with a `None` in it to Zarr failed.** anndata stores

‎README.md‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A command-line tool for exploring huge AnnData stores (`.h5ad` and `.zarr`) with
88
- Auto-detects `.h5ad` files vs `.zarr` directories
99
- Chunked processing for dense and sparse matrices (CSR/CSC)
1010
- Reads every AnnData on-disk layout, from 0.7.x through the current spec, and always writes the current one
11-
- Converts between HDF5 and Zarr (v2 and v3) in either direction
11+
- Converts between HDF5 and Zarr (v2 and v3) in either direction with `adata convert`
1212
- Rich terminal output with progress indicators, kept on stderr so results pipe cleanly
1313

1414
**Documentation: [cellgeni.github.io/adata-cli](https://cellgeni.github.io/adata-cli/)**
@@ -56,7 +56,8 @@ Run help at any level (e.g. `adata --help`, `adata export --help`).
5656
- `subset` – stream and write a filtered copy, selected by obs/var name lists (`--obs`/`--var`) or by expression (`--obs-query`/`--var-query`).
5757
- `split` – write one store per distinct value of an annotation column, with a CSV manifest.
5858
- `concat` – concatenate stores along the obs axis, with `--join inner|outer` and merge strategies for var and uns.
59-
- `convert` – change a matrix's dtype, layout (CSR/CSC/dense) or density, streaming; refuses a lossy cast or a large size increase unless forced.
59+
- `convert` – change a store's format: h5ad ↔ Zarr v2/v3, streaming, with the output written beside its destination and moved into place only once complete.
60+
- `recast` – change a matrix's dtype, layout (CSR/CSC/dense) or density, streaming; refuses a lossy cast or a large size increase unless forced. Called `convert` until 0.7.0.
6061
- `export` – extract data from a store; subcommands: `dataframe` (any dataframe group to CSV), `array` (dense to `.npy`), `sparse` (CSR/CSC to `.mtx`), `dict` (JSON), `image` (PNG). Results go to stdout when no `--output` is given.
6162
- `import` – write new data into a store at any path; subcommands: `dataframe` (CSV), `array` (`.npy`), `sparse` (`.mtx`), `dict` (JSON), `image` (PNG/JPEG/TIFF).
6263

@@ -82,16 +83,23 @@ adata concat per_sample/*.h5ad -o merged.h5ad --join outer --label sample
8283
### Shrinking a store, and making encodings agree
8384

8485
```bash
85-
adata convert data.h5ad X --inplace --dtype float32
86-
adata convert data.h5ad --all -o small.h5ad --dtype float32 --indices-dtype int32
87-
adata convert data.h5ad X -o csc.h5ad --layout csc
86+
adata recast data.h5ad X --inplace --dtype float32
87+
adata recast data.h5ad --all -o small.h5ad --dtype float32 --indices-dtype int32
88+
adata recast data.h5ad X -o csc.h5ad --layout csc
8889
```
8990

90-
Counts written as float64 halve with no loss — and `convert` proves that
91+
Counts written as float64 halve with no loss — and `recast` proves that
9192
before it writes, by casting every value and casting it back. `concat`
9293
refuses inputs whose matrices disagree about CSR versus CSC; `--layout` is
9394
how you make them agree.
9495

96+
### Changing format
97+
98+
```bash
99+
adata convert data.h5ad -o data.zarr
100+
adata convert data.zarr -o data.h5ad --compression gzip
101+
```
102+
95103
## Documentation
96104

97105
- [Get started](docs/GET_STARTED.md) — a short tutorial

‎benchmarks/cases.py‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,7 @@ def _py(body: str) -> str:
232232
contenders=[
233233
Contender(
234234
"adata-cli",
235-
argv=[
236-
"adata", "subset", "{input0}", "-o", "{output}",
237-
"-q", "quality >= 0",
238-
],
235+
argv=["adata", "convert", "{input0}", "-o", "{output}"],
239236
),
240237
Contender(
241238
"anndata (in memory)",
@@ -485,15 +482,15 @@ def _py(body: str) -> str:
485482
),
486483
],
487484
),
488-
# -- convert ------------------------------------------------------------
485+
# -- recast (named convert until 0.7.0; the case names keep the history) --
489486
Case(
490487
name="convert-dtype",
491488
question="Rewrite X as float32.",
492489
contenders=[
493490
Contender(
494491
"adata-cli",
495492
argv=[
496-
"adata", "convert", "{input0}", "X", "-o", "{output}",
493+
"adata", "recast", "{input0}", "X", "-o", "{output}",
497494
"--dtype", "float32", "--force",
498495
],
499496
),
@@ -518,14 +515,14 @@ def _py(body: str) -> str:
518515
Contender(
519516
"adata-cli (streaming)",
520517
argv=[
521-
"adata", "convert", "{input0}", "X", "-o", "{output}",
518+
"adata", "recast", "{input0}", "X", "-o", "{output}",
522519
"--layout", "csc",
523520
],
524521
),
525522
Contender(
526523
"adata-cli (--in-memory)",
527524
argv=[
528-
"adata", "convert", "{input0}", "X", "-o", "{output}",
525+
"adata", "recast", "{input0}", "X", "-o", "{output}",
529526
"--layout", "csc", "--in-memory",
530527
],
531528
),

‎benchmarks/page_template.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ for time, and a table that hid the cost would not be worth publishing.
1212

1313
## What is measured
1414

15-
Fifteen cases on a GitHub-hosted runner: concat (inner, outer, `--merge same`),
16-
subset by query, h5ad→zarr, `view`, `ls`, `create`, `export` dataframe / array /
17-
sparse, `import dataframe`, `split`, and a peak-RSS-against-input-size sweep. Inputs
15+
Seventeen cases on a GitHub-hosted runner: concat (inner, outer, `--merge same`),
16+
subset by query, h5ad→zarr (`convert`), `view`, `ls`, `create`, `export` dataframe /
17+
array / sparse, `import dataframe`, `split`, `recast` dtype and layout, and a
18+
peak-RSS-against-input-size sweep. Inputs
1819
are 50,000 × 20,000 at 5% density, plus the 2,000 × 36,601 shape that hung in 0.5.1.
1920

2021
Every run also reports a startup floor (`--version` for each contender), because the

‎docs/COMMANDS.md‎

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Run `adata <command> --help` for the authoritative flag list.
1919
- [split](#split)
2020
- [concat](#concat)
2121
- [convert](#convert)
22+
- [recast](#recast)
2223
- [create](#create)
2324
- [export](#export)
2425
- [import](#import)
@@ -84,27 +85,72 @@ holds more genes than the main object.
8485

8586
## `convert`
8687

88+
Change a store's format. The output's extension picks the format; `--to`
89+
names it when the extension does not say, and `--from` names the input's
90+
when it cannot be detected from its content.
91+
92+
```bash
93+
adata convert data.h5ad -o data.zarr
94+
adata convert data.zarr -o data.h5ad --compression gzip
95+
adata convert old.zarr -o new.zarr --zarr-format 3
96+
adata convert data.h5ad -o data.zarr --overwrite
97+
```
98+
99+
| Flag | Meaning |
100+
|---|---|
101+
| `--output`, `-o` | Output path (required) |
102+
| `--from` | Input format, if it cannot be detected |
103+
| `--to` | Output format, if the extension does not say |
104+
| `--zarr-format` | Zarr version to write; defaults to the source's, else 3 |
105+
| `--compression` | `none`, `gzip` or `lzf` for h5ad; `none`, `gzip`, `zstd` or `blosc` for Zarr |
106+
| `--overwrite` | Replace an existing output |
107+
| `--force` | Convert despite a lossy or inflating result |
108+
109+
| Format | Extension | Read | Write |
110+
|---|---|---|---|
111+
| `h5ad` | `.h5ad` | yes | yes |
112+
| `zarr` | `.zarr` (v2 or v3) | yes | yes |
113+
114+
Between h5ad and Zarr every element is copied as it is, streamed, so the
115+
store's size is not a limit. Chunking carries over, and so does compression
116+
where the target can express it: Zarr codecs travel between stores of the same
117+
Zarr version, a compressed Zarr array becomes gzip in an h5ad, and an h5ad
118+
written to Zarr gets zarr-python's default codec. `--compression` replaces all
119+
of that with one choice. Elements are not re-encoded, so a store written by
120+
anndata 0.7 keeps its legacy layout.
121+
122+
Every check runs before anything is written. The output is written beside
123+
its destination (`out.adata-tmp.zarr`) and moved into place only once
124+
complete, so a failed conversion leaves nothing behind. An existing output is
125+
refused unless `--overwrite` is given, and is replaced only once its
126+
successor is finished.
127+
128+
To change a matrix's dtype, layout or density instead, use
129+
[`recast`](#recast).
130+
131+
## `recast`
132+
87133
Change a matrix's dtype, layout or density. Counts held as float64 cost twice
88134
the disk and twice the read for no information; a tool that wants CSC cannot
89135
use a CSR store; and `concat` refuses inputs whose encodings disagree.
90136

91137
```bash
92-
adata convert data.h5ad X -o out.h5ad --dtype float32
93-
adata convert data.h5ad X -o out.h5ad --layout csc
94-
adata convert data.h5ad X --inplace --dtype float32 --indices-dtype int32
95-
adata convert data.h5ad --all -o out.h5ad --dtype float32
96-
adata convert data.h5ad layers/counts -o out.h5ad --layout dense --force
138+
adata recast data.h5ad X -o out.h5ad --dtype float32
139+
adata recast data.h5ad X -o out.h5ad --layout csc
140+
adata recast data.h5ad X --inplace --dtype float32 --indices-dtype int32
141+
adata recast data.h5ad --all -o out.h5ad --dtype float32
142+
adata recast data.h5ad layers/counts -o out.h5ad --layout dense --force
97143
```
98144

99145
| Flag | Meaning |
100146
|---|---|
101147
| `--output`, `-o` | Output path. Required unless `--inplace` |
102148
| `--inplace` | Replace the source (written to a temporary path first) |
103-
| `--all` | Convert `X`, every layer and `raw/X` |
149+
| `--all` | Recast `X`, every layer and `raw/X` |
104150
| `--dtype` | New dtype for the values, e.g. `float32` |
105151
| `--indices-dtype` | New dtype for sparse indices: `int32` or `int64` |
106152
| `--layout` | `csr`, `csc`, `dense` or `sparse` |
107-
| `--force` | Convert despite a lossy cast or a large size increase |
153+
| `--force` | Recast despite a lossy cast or a large size increase |
108154
| `--in-memory` | Transpose in memory rather than streaming |
109155
| `--chunk`, `-C` | Row chunk size for dense matrices |
110156
| `--zarr-format` | Zarr version to write; defaults to the source's |
@@ -128,6 +174,10 @@ Transposing streams by default and works on matrices too large to load, at
128174
the cost of two extra passes over the nonzeros. `--in-memory` is faster when
129175
the matrix fits.
130176

177+
Until 0.7.0 this command was called `convert`. Until 1.0.0, `convert` given
178+
any of `--dtype`, `--indices-dtype`, `--layout`, `--all` or `--inplace` still
179+
runs `recast`, with a warning.
180+
131181
## `split`
132182

133183
One store per distinct value of a column.
@@ -277,13 +327,14 @@ adata subset data.h5ad -o cortex.h5ad --obs barcodes.txt
277327
## Zarr format selection
278328

279329
New Zarr stores follow the source store's version, so a v2 input is not
280-
silently upgraded. `--zarr-format 2|3` on `create`, `subset`, `split` and
281-
`concat` overrides that. Writing a Zarr store from an `.h5ad` source defaults
282-
to v3.
330+
silently upgraded. `--zarr-format 2|3` on `create`, `subset`, `split`,
331+
`concat`, `convert` and `recast` overrides that. Writing a Zarr store from
332+
an `.h5ad` source defaults to v3.
283333

284334
```bash
285335
adata create out.zarr --n-obs 100 --n-var 50 --zarr-format 2
286336
adata subset data.zarr -o out.zarr --obs keep.txt --zarr-format 3
287337
adata split data.zarr --by sample -o parts/ --suffix .zarr --zarr-format 3
288338
adata concat a.zarr b.zarr -o merged.zarr --zarr-format 3
339+
adata convert data.zarr -o v3.zarr --zarr-format 3
289340
```

‎docs/TESTING.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ and the compatibility suite as a separate job.
2323
| `test_anndata_roundtrip.py` | anndata writes the fixtures, reads back our output |
2424
| `test_anndata_versions.py` | Compatibility with six real anndata releases (see below) |
2525
| `test_commands_phase2.py`, `test_commands_coverage.py`, `test_cli.py` | Command surfaces and error paths |
26+
| `test_convert.py` | `convert` between every backend pair, read back by anndata; its refusals, staging, and the forwarding of recast's old flags |
27+
| `test_recast.py` | `recast` against scipy: casts, transposes, density changes and every refusal |
2628
| `test_performance.py` | Complexity guards -- what an operation costs, not how long it takes (see below) |
2729
| `test_benchmark_harness.py` | That the benchmark's measurement is trustworthy |
2830
| `test_subset.py`, `test_export.py`, `test_import.py`, `test_info_read.py`, `test_zarr.py`, `test_query.py` | Per-feature unit tests |
@@ -232,7 +234,8 @@ should scale with, scale only that, and hold everything else fixed.
232234
| `export dataframe` | n_obs, n_obs_columns | linear |
233235
| `export array`, `sparse` (both paths), `dict`, `image` | elements, nnz, keys, pixels | linear |
234236
| `import dataframe`, `array`, `sparse`, `dict`, `image` | rows, elements, nnz, keys, pixels | linear in writes |
235-
| h5ad to zarr | n_obs | linear |
237+
| h5ad to zarr (`subset`), `convert` both directions | n_obs | linear |
238+
| `recast --dtype`, `--layout csc`, `--layout dense` | nnz, n_obs | linear; the streaming transpose's **peak memory** is bounded |
236239

237240
The `slow` marker is on the three guards that build a 65,536-row store; they still gate
238241
merges, and `-m "not slow"` skips them locally. `perf` is on the tracing guards.

‎docs/index.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ adata subset data.h5ad -o cortex.h5ad --obs-query "cluster == Cortex_2"
5151
adata split data.h5ad --by sample -o per_sample/
5252
adata concat per_sample/*.h5ad -o merged.h5ad --join outer --label sample
5353

54-
adata convert data.h5ad X -o small.h5ad --dtype float32
54+
adata convert data.h5ad -o data.zarr
55+
adata recast data.h5ad X -o small.h5ad --dtype float32
5556

5657
adata create new.h5ad --n-obs 5000 --n-var 2000
5758
adata import sparse new.h5ad X counts.mtx --inplace
@@ -62,9 +63,9 @@ adata import sparse new.h5ad X counts.mtx --inplace
6263
Every AnnData on-disk layout is readable, from anndata 0.7.x through the
6364
current spec; everything written is in the current spec. This is verified in
6465
CI against stores written by anndata 0.8, 0.9, 0.10, 0.11, 0.12 and 0.13, in
65-
both formats. Both backends are
66-
supported in both directions, so `adata subset in.h5ad -o out.zarr` converts as
67-
it filters.
66+
both formats. Both backends are supported in both directions: `adata convert
67+
in.h5ad -o out.zarr` converts a whole store, and `subset`, `split` and
68+
`concat` convert as they write when the output's backend differs.
6869

6970
| Element | Read | Write |
7071
|---|---|---|

0 commit comments

Comments
 (0)