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.
Files changed (44) hide show
  1. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/PKG-INFO +1 -1
  2. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/pyproject.toml +1 -1
  3. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/cli.py +1 -1
  4. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/split.py +18 -15
  5. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/concat.py +53 -8
  6. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/convert.py +1 -35
  7. pyadata_cli-0.6.1/src/adata/core/subset.py +1083 -0
  8. pyadata_cli-0.6.0/src/adata/core/subset.py +0 -767
  9. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/LICENSE +0 -0
  10. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/README.md +0 -0
  11. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/__init__.py +0 -0
  12. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/__init__.py +0 -0
  13. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/concat.py +0 -0
  14. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/convert.py +0 -0
  15. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/create.py +0 -0
  16. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/export.py +0 -0
  17. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/import_data.py +0 -0
  18. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/info.py +0 -0
  19. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/ls.py +0 -0
  20. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/commands/subset.py +0 -0
  21. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/__init__.py +0 -0
  22. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/info.py +0 -0
  23. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/query.py +0 -0
  24. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/read.py +0 -0
  25. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/core/select.py +0 -0
  26. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/elements/__init__.py +0 -0
  27. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/elements/read.py +0 -0
  28. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/elements/spec.py +0 -0
  29. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/elements/strings.py +0 -0
  30. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/elements/write.py +0 -0
  31. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/__init__.py +0 -0
  32. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/array.py +0 -0
  33. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/common.py +0 -0
  34. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/dataframe.py +0 -0
  35. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/image.py +0 -0
  36. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/json_data.py +0 -0
  37. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/sparse.py +0 -0
  38. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/formats/validate.py +0 -0
  39. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/info.py +0 -0
  40. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/py.typed +0 -0
  41. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/read.py +0 -0
  42. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/storage/__init__.py +0 -0
  43. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/util/__init__.py +0 -0
  44. {pyadata_cli-0.6.0 → pyadata_cli-0.6.1}/src/adata/util/path.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyadata-cli
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Streaming CLI for exploring and editing large AnnData .h5ad and .zarr stores
5
5
  Keywords: anndata,h5ad,zarr,bioinformatics,single-cell,cli
6
6
  Author: Aljes Binkevich
@@ -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.0"
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 subset_h5ad
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 label, out_path, _ in planned:
116
- indices = np.sort(groups[label])
117
- subset_h5ad(
118
- file=file,
119
- output=out_path,
120
- obs_file=None,
121
- var_file=None,
122
- chunk_rows=chunk_rows,
123
- console=console,
124
- obs_indices=indices if axis == "obs" else None,
125
- var_indices=indices if axis == "var" else None,
126
- zarr_format=zarr_format,
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, _growable
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
- out_data = _growable(group, "data", dtype)
416
- out_indices = _growable(group, "indices", np.int64)
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
- create_dataset(group, "indptr", data=np.asarray(indptr, dtype=np.int64))
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, _growable
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
- out_data = _growable(group, "data", dtype)
521
- out_indices = _growable(group, "indices", np.int64)
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
- create_dataset(group, "indptr", data=np.asarray(indptr, dtype=np.int64))
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,