pyadata-cli 0.6.0__tar.gz → 0.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/PKG-INFO +1 -1
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/pyproject.toml +1 -1
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/cli.py +1 -1
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/split.py +18 -15
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/concat.py +53 -8
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/convert.py +1 -35
- pyadata_cli-0.6.1/src/adata/core/subset.py +1083 -0
- pyadata_cli-0.6.0/src/adata/core/subset.py +0 -767
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/LICENSE +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/README.md +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/__init__.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/__init__.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/concat.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/convert.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/create.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/export.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/import_data.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/info.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/ls.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/subset.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/__init__.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/info.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/query.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/read.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/select.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/elements/__init__.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/elements/read.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/elements/spec.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/elements/strings.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/elements/write.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/__init__.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/array.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/common.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/dataframe.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/image.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/json_data.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/sparse.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/validate.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/info.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/py.typed +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/read.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/storage/__init__.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/util/__init__.py +0 -0
- {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/util/path.py +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# so the distribution is published as `pyadata-cli`. The import package
|
|
4
4
|
# and the command are both still `adata`.
|
|
5
5
|
name = "pyadata-cli"
|
|
6
|
-
version = "0.6.
|
|
6
|
+
version = "0.6.1"
|
|
7
7
|
description = "Streaming CLI for exploring and editing large AnnData .h5ad and .zarr stores"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.12"
|
|
@@ -26,7 +26,7 @@ from adata.commands import export_image as export_image_cmd
|
|
|
26
26
|
|
|
27
27
|
app = typer.Typer(
|
|
28
28
|
help="Streaming CLI for huge AnnData .h5ad and .zarr stores "
|
|
29
|
-
"(view, ls, subset, split, concat, export, import)."
|
|
29
|
+
"(view, ls, create, subset, split, concat, convert, export, import)."
|
|
30
30
|
)
|
|
31
31
|
# Use stderr for status/progress to keep stdout clean for data output
|
|
32
32
|
# force_terminal=True ensures Rich output is visible even in non-TTY environments
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Requested in issue #2, modelled on cellgeni/scraft's `split_h5ad` but
|
|
4
4
|
streaming: the source is never loaded into memory, only the column being split
|
|
5
5
|
on is read, and each output is produced by the same subset machinery used by
|
|
6
|
-
`adata subset
|
|
6
|
+
`adata subset`, fanned out so every matrix is read once for all groups.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -17,7 +17,7 @@ import numpy as np
|
|
|
17
17
|
from rich.console import Console
|
|
18
18
|
|
|
19
19
|
from adata.core.select import group_indices
|
|
20
|
-
from adata.core.subset import
|
|
20
|
+
from adata.core.subset import split_h5ad
|
|
21
21
|
from adata.storage import detect_backend, open_store
|
|
22
22
|
|
|
23
23
|
|
|
@@ -112,19 +112,22 @@ def split_store(
|
|
|
112
112
|
|
|
113
113
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
114
114
|
|
|
115
|
-
for
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
115
|
+
# One call for every group, so each matrix is read once rather than once
|
|
116
|
+
# per group.
|
|
117
|
+
split_h5ad(
|
|
118
|
+
file,
|
|
119
|
+
[
|
|
120
|
+
(
|
|
121
|
+
out_path,
|
|
122
|
+
np.sort(groups[label]) if axis == "obs" else None,
|
|
123
|
+
np.sort(groups[label]) if axis == "var" else None,
|
|
124
|
+
)
|
|
125
|
+
for label, out_path, _ in planned
|
|
126
|
+
],
|
|
127
|
+
chunk_rows=chunk_rows,
|
|
128
|
+
console=console,
|
|
129
|
+
zarr_format=zarr_format,
|
|
130
|
+
)
|
|
128
131
|
|
|
129
132
|
if manifest:
|
|
130
133
|
_write_manifest(file, output_dir, column, planned, console)
|
|
@@ -389,6 +389,45 @@ def _matrix_kind(obj: Any) -> str:
|
|
|
389
389
|
return "other"
|
|
390
390
|
|
|
391
391
|
|
|
392
|
+
def _widen_to_hold(dtype: np.dtype, largest: int) -> np.dtype:
|
|
393
|
+
"""`dtype`, or the narrowest of int32/int64 wider than it that holds `largest`."""
|
|
394
|
+
dtype = np.dtype(dtype)
|
|
395
|
+
if largest <= np.iinfo(dtype).max:
|
|
396
|
+
return dtype
|
|
397
|
+
for candidate in (np.int32, np.int64):
|
|
398
|
+
if largest <= np.iinfo(candidate).max:
|
|
399
|
+
return np.dtype(np.result_type(dtype, candidate))
|
|
400
|
+
return np.dtype(np.int64)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _concat_index_dtypes(
|
|
404
|
+
sources: List[Any], n_minor: int
|
|
405
|
+
) -> Tuple[np.dtype, np.dtype]:
|
|
406
|
+
"""The `indices` and `indptr` dtypes for a concatenation of `sources`.
|
|
407
|
+
|
|
408
|
+
The widest the inputs used, so int32 inputs give an int32 output rather
|
|
409
|
+
than the int64 every output used to get. That is widened only when the
|
|
410
|
+
result could not be addressed otherwise: `indices` must reach the
|
|
411
|
+
output's minor dimension, and `indptr` its total nonzero count, which is
|
|
412
|
+
known before writing as the sum of the inputs'. Both are checked against
|
|
413
|
+
the chosen dtype's own limit, not int32's, since the spec allows
|
|
414
|
+
narrower index arrays and an int16 `indptr` would otherwise wrap.
|
|
415
|
+
"""
|
|
416
|
+
indices = np.result_type(*[s["indices"].dtype for s in sources])
|
|
417
|
+
indptr = np.result_type(*[s["indptr"].dtype for s in sources])
|
|
418
|
+
nnz = sum(int(s["data"].shape[0]) for s in sources)
|
|
419
|
+
return _widen_to_hold(indices, n_minor), _widen_to_hold(indptr, nnz)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _write_indptr(
|
|
423
|
+
group: Any, indptr: List[int], dtype: np.dtype, template: Any
|
|
424
|
+
) -> None:
|
|
425
|
+
from adata.core.subset import _sparse_dataset
|
|
426
|
+
|
|
427
|
+
values = np.asarray(indptr, dtype=np.int64).astype(dtype, copy=False)
|
|
428
|
+
_sparse_dataset(group, "indptr", dtype, values.size, template)[:] = values
|
|
429
|
+
|
|
430
|
+
|
|
392
431
|
def _concat_sparse(
|
|
393
432
|
dst_parent: Any,
|
|
394
433
|
name: str,
|
|
@@ -403,7 +442,7 @@ def _concat_sparse(
|
|
|
403
442
|
Absent columns need no fill: a sparse matrix's zeros are implicit, so
|
|
404
443
|
dropped entries simply do not appear in the output.
|
|
405
444
|
"""
|
|
406
|
-
from adata.core.subset import _append,
|
|
445
|
+
from adata.core.subset import _append, _growable_like
|
|
407
446
|
|
|
408
447
|
group = dst_parent.create_group(name)
|
|
409
448
|
spec.set_encoding(group, spec.CSR_MATRIX)
|
|
@@ -412,8 +451,11 @@ def _concat_sparse(
|
|
|
412
451
|
set_shape_attr(group, (n_rows, n_cols))
|
|
413
452
|
|
|
414
453
|
dtype = np.result_type(*[s["data"].dtype for s in sources])
|
|
415
|
-
|
|
416
|
-
|
|
454
|
+
index_dtype, pointer_dtype = _concat_index_dtypes(sources, n_cols)
|
|
455
|
+
out_data = _growable_like(group, "data", dtype, sources[0]["data"])
|
|
456
|
+
out_indices = _growable_like(
|
|
457
|
+
group, "indices", index_dtype, sources[0]["indices"]
|
|
458
|
+
)
|
|
417
459
|
indptr = [0]
|
|
418
460
|
nnz = 0
|
|
419
461
|
|
|
@@ -449,7 +491,7 @@ def _concat_sparse(
|
|
|
449
491
|
_append(out_indices, np.concatenate(kept_idx))
|
|
450
492
|
_append(out_data, np.concatenate(kept_data).astype(dtype))
|
|
451
493
|
|
|
452
|
-
|
|
494
|
+
_write_indptr(group, indptr, pointer_dtype, sources[0]["indptr"])
|
|
453
495
|
|
|
454
496
|
|
|
455
497
|
def _concat_dense(
|
|
@@ -509,7 +551,7 @@ def _concat_csc(
|
|
|
509
551
|
input's row offset added. Because every input's column is already sorted
|
|
510
552
|
and the offsets increase, the result is sorted without a re-sort.
|
|
511
553
|
"""
|
|
512
|
-
from adata.core.subset import _append,
|
|
554
|
+
from adata.core.subset import _append, _growable_like
|
|
513
555
|
from adata.elements.write import set_shape_attr
|
|
514
556
|
|
|
515
557
|
group = dst_parent.create_group(name)
|
|
@@ -517,8 +559,11 @@ def _concat_csc(
|
|
|
517
559
|
set_shape_attr(group, (n_rows, n_cols))
|
|
518
560
|
|
|
519
561
|
dtype = np.result_type(*[s["data"].dtype for s in sources])
|
|
520
|
-
|
|
521
|
-
|
|
562
|
+
index_dtype, pointer_dtype = _concat_index_dtypes(sources, n_rows)
|
|
563
|
+
out_data = _growable_like(group, "data", dtype, sources[0]["data"])
|
|
564
|
+
out_indices = _growable_like(
|
|
565
|
+
group, "indices", index_dtype, sources[0]["indices"]
|
|
566
|
+
)
|
|
522
567
|
indptr = [0]
|
|
523
568
|
nnz = 0
|
|
524
569
|
|
|
@@ -547,7 +592,7 @@ def _concat_csc(
|
|
|
547
592
|
nnz += int(sum(len(r) for r in rows))
|
|
548
593
|
indptr.append(nnz)
|
|
549
594
|
|
|
550
|
-
|
|
595
|
+
_write_indptr(group, indptr, pointer_dtype, sources[0]["indptr"])
|
|
551
596
|
|
|
552
597
|
|
|
553
598
|
def check_matrix_encodings(roots: List[Any], console: Console) -> None:
|
|
@@ -31,6 +31,7 @@ from rich.console import Console
|
|
|
31
31
|
|
|
32
32
|
from adata.elements import spec
|
|
33
33
|
from adata.elements.write import set_shape_attr
|
|
34
|
+
from adata.core.subset import _growable_like, _sparse_dataset
|
|
34
35
|
from adata.storage import (
|
|
35
36
|
copy_attrs,
|
|
36
37
|
create_dataset,
|
|
@@ -298,41 +299,6 @@ def _new_sparse_group(
|
|
|
298
299
|
return group
|
|
299
300
|
|
|
300
301
|
|
|
301
|
-
def _sparse_dataset(
|
|
302
|
-
group: Any, name: str, dtype: np.dtype, n: int, template: Any
|
|
303
|
-
) -> Any:
|
|
304
|
-
"""A 1-D dataset of `n` elements laid out like `template`.
|
|
305
|
-
|
|
306
|
-
Forwarding compression and chunking matters more here than anywhere
|
|
307
|
-
else: the point of a dtype change is usually to make the file smaller,
|
|
308
|
-
and creating the output with a fixed 65,536-element chunk made a
|
|
309
|
-
2,400-nonzero matrix allocate 786 KB of mostly empty chunk -- eight
|
|
310
|
-
times the source, from a conversion asked for to halve it.
|
|
311
|
-
"""
|
|
312
|
-
from adata.core.subset import _clamp_chunks
|
|
313
|
-
|
|
314
|
-
backend = "zarr" if is_zarr_group(group) else "hdf5"
|
|
315
|
-
kw = dataset_create_kwargs(template, target_backend=backend, dst_parent=group)
|
|
316
|
-
kw = _clamp_chunks(kw, max(1, n))
|
|
317
|
-
if "chunks" not in kw and n:
|
|
318
|
-
kw["chunks"] = (min(n, 1 << 16),)
|
|
319
|
-
return create_dataset(group, name, shape=(n,), dtype=dtype, **kw)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def _growable_like(group: Any, name: str, dtype: np.dtype, template: Any) -> Any:
|
|
323
|
-
"""Like `_sparse_dataset`, but extensible for a size not yet known."""
|
|
324
|
-
backend = "zarr" if is_zarr_group(group) else "hdf5"
|
|
325
|
-
kw = dataset_create_kwargs(template, target_backend=backend, dst_parent=group)
|
|
326
|
-
kw.pop("shards", None)
|
|
327
|
-
chunks = kw.pop("chunks", None)
|
|
328
|
-
step = int(chunks[0]) if chunks else 1 << 16
|
|
329
|
-
if is_zarr_group(group):
|
|
330
|
-
return group.create_array(name, shape=(0,), dtype=dtype, chunks=(step,), **kw)
|
|
331
|
-
return group.create_dataset(
|
|
332
|
-
name, shape=(0,), maxshape=(None,), dtype=dtype, chunks=(step,), **kw
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
|
|
336
302
|
def _write_sparse_arrays(
|
|
337
303
|
group: Any,
|
|
338
304
|
data: np.ndarray,
|