spatialdata-js-util 0.1.0__py3-none-any.whl
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.
- spatialdata_js_util/__init__.py +107 -0
- spatialdata_js_util/cli.py +803 -0
- spatialdata_js_util/codecs/__init__.py +67 -0
- spatialdata_js_util/codecs/backends.py +250 -0
- spatialdata_js_util/codecs/chunks.py +40 -0
- spatialdata_js_util/codecs/encoding.py +154 -0
- spatialdata_js_util/codecs/htj2k_wasm.py +266 -0
- spatialdata_js_util/codecs/names.py +26 -0
- spatialdata_js_util/codecs/probe/multicomponent.j2c +0 -0
- spatialdata_js_util/codecs/probe/multicomponent.json +3 -0
- spatialdata_js_util/codecs/probe/multicomponent.npy +0 -0
- spatialdata_js_util/codecs/vendor/__init__.py +1 -0
- spatialdata_js_util/codecs/vendor/encode-plane.mjs +164 -0
- spatialdata_js_util/codecs/vendor/openjph/index.mjs +131 -0
- spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph.mjs +2 -0
- spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph.wasm +0 -0
- spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph_simd.mjs +2 -0
- spatialdata_js_util/codecs/vendor/openjph/wasm/libopenjph_simd.wasm +0 -0
- spatialdata_js_util/codecs/zarr_codec.py +149 -0
- spatialdata_js_util/errors.py +5 -0
- spatialdata_js_util/images.py +849 -0
- spatialdata_js_util/index_permutations.py +311 -0
- spatialdata_js_util/points.py +445 -0
- spatialdata_js_util/points_zarr.py +799 -0
- spatialdata_js_util/provenance.py +17 -0
- spatialdata_js_util/pyramids.py +318 -0
- spatialdata_js_util/runners.py +458 -0
- spatialdata_js_util/store.py +305 -0
- spatialdata_js_util/tables.py +310 -0
- spatialdata_js_util/tui/__init__.py +0 -0
- spatialdata_js_util/tui/app.py +112 -0
- spatialdata_js_util/tui/models.py +45 -0
- spatialdata_js_util/tui/screens.py +1255 -0
- spatialdata_js_util/tui/widgets.py +50 -0
- spatialdata_js_util/verify.py +338 -0
- spatialdata_js_util-0.1.0.dist-info/METADATA +412 -0
- spatialdata_js_util-0.1.0.dist-info/RECORD +40 -0
- spatialdata_js_util-0.1.0.dist-info/WHEEL +5 -0
- spatialdata_js_util-0.1.0.dist-info/entry_points.txt +7 -0
- spatialdata_js_util-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
"""Unified `spatialdata-js-util` command line.
|
|
2
|
+
|
|
3
|
+
Subcommands are grouped by what they act on:
|
|
4
|
+
|
|
5
|
+
images recompress rasters with browser-readable codecs
|
|
6
|
+
points Morton-index Points elements
|
|
7
|
+
tables convert table matrices to CSC
|
|
8
|
+
codecs inspect codec backend availability
|
|
9
|
+
tui interactive workflow UI
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .errors import WriterCommandError
|
|
22
|
+
|
|
23
|
+
_EPILOG = """\
|
|
24
|
+
examples:
|
|
25
|
+
# Recompress every image in a store as lossy HTJ2K siblings
|
|
26
|
+
spatialdata-js-util images recompress in.zarr out.zarr \\
|
|
27
|
+
--codec experimental.openjph_htj2k --quality 0.0005 --sibling --overwrite
|
|
28
|
+
|
|
29
|
+
# Morton-sort transcripts in place
|
|
30
|
+
spatialdata-js-util points morton-from-zarr ~/data/xenium.zarr --points-key transcripts
|
|
31
|
+
|
|
32
|
+
# Convert table matrices to CSC for fast per-gene reads in the browser
|
|
33
|
+
spatialdata-js-util tables to-csc ~/data/xenium.zarr
|
|
34
|
+
|
|
35
|
+
# Show which HTJ2K backend is in use
|
|
36
|
+
spatialdata-js-util codecs info
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _positive_int(value: str) -> int:
|
|
41
|
+
parsed = int(value)
|
|
42
|
+
if parsed <= 0:
|
|
43
|
+
raise argparse.ArgumentTypeError("value must be positive")
|
|
44
|
+
return parsed
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _print_json(payload: dict[str, Any]) -> None:
|
|
48
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _run_command(command: Callable[[], dict[str, Any]]) -> None:
|
|
52
|
+
try:
|
|
53
|
+
_print_json(command())
|
|
54
|
+
except WriterCommandError as exc:
|
|
55
|
+
raise SystemExit(str(exc)) from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --------------------------------------------------------------------------- images
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _recompress_chunks(value: list[str] | None):
|
|
62
|
+
if value is None:
|
|
63
|
+
return None
|
|
64
|
+
if len(value) == 1 and value[0] == "auto":
|
|
65
|
+
return "auto"
|
|
66
|
+
try:
|
|
67
|
+
return tuple(int(part) for part in value)
|
|
68
|
+
except ValueError as exc:
|
|
69
|
+
# Called from the command body rather than as an argparse `type=`, so an
|
|
70
|
+
# ArgumentTypeError here would surface as a traceback rather than a usage
|
|
71
|
+
# error. Match how the rest of the command reports bad arguments.
|
|
72
|
+
raise SystemExit("error: chunks must be 'auto' or integer axis sizes") from exc
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _images_recompress(args: argparse.Namespace) -> None:
|
|
76
|
+
from .images import recompress_spatialdata
|
|
77
|
+
|
|
78
|
+
if args.quality is not None and args.reversible:
|
|
79
|
+
raise SystemExit("error: --quality cannot be used with --reversible")
|
|
80
|
+
if args.quality is not None and args.codec != "experimental.openjph_htj2k":
|
|
81
|
+
raise SystemExit(
|
|
82
|
+
"error: --quality is for HTJ2K only; use --codec experimental.openjph_htj2k"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
result = recompress_spatialdata(
|
|
86
|
+
args.source,
|
|
87
|
+
args.dest,
|
|
88
|
+
config=args.config,
|
|
89
|
+
overwrite=args.overwrite,
|
|
90
|
+
image_key=args.image_key,
|
|
91
|
+
codec=args.codec,
|
|
92
|
+
preset=args.preset,
|
|
93
|
+
chunks=_recompress_chunks(args.chunks),
|
|
94
|
+
quality=args.quality,
|
|
95
|
+
reversible=True if args.reversible else None,
|
|
96
|
+
sibling=args.sibling,
|
|
97
|
+
workers=args.workers,
|
|
98
|
+
# Any explicit --pyramid-* option means the user wants pyramids, so the
|
|
99
|
+
# bare --pyramid flag is a convenience rather than a requirement.
|
|
100
|
+
pyramid=args.pyramid
|
|
101
|
+
or args.pyramid_levels != "auto"
|
|
102
|
+
or args.pyramid_downscale != 2
|
|
103
|
+
or args.pyramid_min_size != 1024
|
|
104
|
+
or args.pyramid_force,
|
|
105
|
+
pyramid_levels=args.pyramid_levels,
|
|
106
|
+
pyramid_downscale=args.pyramid_downscale,
|
|
107
|
+
pyramid_min_size=args.pyramid_min_size,
|
|
108
|
+
pyramid_force=args.pyramid_force,
|
|
109
|
+
)
|
|
110
|
+
_print_json(result.manifest)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _pyramid_levels(value: str) -> int | str:
|
|
114
|
+
if value == "auto":
|
|
115
|
+
return "auto"
|
|
116
|
+
try:
|
|
117
|
+
parsed = int(value)
|
|
118
|
+
except ValueError:
|
|
119
|
+
raise argparse.ArgumentTypeError("--pyramid-levels must be 'auto' or an integer") from None
|
|
120
|
+
if parsed < 1:
|
|
121
|
+
raise argparse.ArgumentTypeError("--pyramid-levels must be at least 1")
|
|
122
|
+
return parsed
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _add_pyramid_args(parser: argparse.ArgumentParser, *, with_flag: bool) -> None:
|
|
126
|
+
if with_flag:
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--pyramid",
|
|
129
|
+
action="store_true",
|
|
130
|
+
help=(
|
|
131
|
+
"Build a multiscale pyramid for any image that has only one resolution "
|
|
132
|
+
"level, before recompressing. Implied by the other --pyramid-* options."
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--pyramid-levels",
|
|
137
|
+
type=_pyramid_levels,
|
|
138
|
+
default="auto",
|
|
139
|
+
metavar="N",
|
|
140
|
+
help=(
|
|
141
|
+
"Total resolution levels including full res, or 'auto' to halve until the "
|
|
142
|
+
"largest spatial axis fits --pyramid-min-size (default: auto)"
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--pyramid-downscale",
|
|
147
|
+
type=_positive_int,
|
|
148
|
+
default=2,
|
|
149
|
+
metavar="K",
|
|
150
|
+
help="Downscale factor between levels (default: 2)",
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--pyramid-min-size",
|
|
154
|
+
type=_positive_int,
|
|
155
|
+
default=1024,
|
|
156
|
+
metavar="PX",
|
|
157
|
+
help="Stop 'auto' once the largest spatial axis is this size or smaller (default: 1024)",
|
|
158
|
+
)
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--pyramid-force",
|
|
161
|
+
action="store_true",
|
|
162
|
+
help="Rebuild pyramids for images that already have more than one level",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _images_add_pyramid(args: argparse.Namespace) -> None:
|
|
167
|
+
from .pyramids import add_pyramids
|
|
168
|
+
|
|
169
|
+
def command() -> dict[str, Any]:
|
|
170
|
+
return add_pyramids(
|
|
171
|
+
args.source,
|
|
172
|
+
args.dest,
|
|
173
|
+
image_keys=args.image_key.split(",") if args.image_key else None,
|
|
174
|
+
include_labels=args.labels,
|
|
175
|
+
levels=args.pyramid_levels,
|
|
176
|
+
downscale=args.pyramid_downscale,
|
|
177
|
+
min_size=args.pyramid_min_size,
|
|
178
|
+
force=args.pyramid_force,
|
|
179
|
+
overwrite=args.overwrite,
|
|
180
|
+
).manifest
|
|
181
|
+
|
|
182
|
+
_run_command(command)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _images_inspect(args: argparse.Namespace) -> None:
|
|
186
|
+
manifest_path = Path(args.path)
|
|
187
|
+
if manifest_path.is_dir():
|
|
188
|
+
manifest_path = manifest_path.with_suffix(".manifest.json")
|
|
189
|
+
print(manifest_path.read_text())
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _add_images_commands(subparsers: argparse._SubParsersAction) -> None:
|
|
193
|
+
images = subparsers.add_parser(
|
|
194
|
+
"images",
|
|
195
|
+
help="recompress rasters with browser-readable codecs",
|
|
196
|
+
description="Recompress SpatialData/OME-Zarr image stores.",
|
|
197
|
+
)
|
|
198
|
+
image_commands = images.add_subparsers(dest="images_command", required=True, metavar="command")
|
|
199
|
+
|
|
200
|
+
recompress = image_commands.add_parser(
|
|
201
|
+
"recompress",
|
|
202
|
+
help="rewrite image rasters with JPEG 2000 or HTJ2K",
|
|
203
|
+
description=(
|
|
204
|
+
"Copy a SpatialData store and recompress configured image rasters. "
|
|
205
|
+
"Tables, shapes, points, and unconfigured rasters are preserved."
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
recompress.add_argument("source", help="Existing SpatialData Zarr store")
|
|
209
|
+
recompress.add_argument("dest", help="Output SpatialData Zarr store")
|
|
210
|
+
recompress.add_argument("--config", help="JSON recompression config")
|
|
211
|
+
recompress.add_argument(
|
|
212
|
+
"--image-key",
|
|
213
|
+
help="Apply convenience flags to one image only (default: all images)",
|
|
214
|
+
)
|
|
215
|
+
recompress.add_argument(
|
|
216
|
+
"--codec",
|
|
217
|
+
choices=["imagecodecs_jpeg2k", "experimental.openjph_htj2k"],
|
|
218
|
+
help="Image codec (all images unless --image-key is set)",
|
|
219
|
+
)
|
|
220
|
+
recompress.add_argument(
|
|
221
|
+
"--preset",
|
|
222
|
+
choices=["lossless", "balanced", "small"],
|
|
223
|
+
help="Named image preset (ignored when --quality is set)",
|
|
224
|
+
)
|
|
225
|
+
recompress.add_argument(
|
|
226
|
+
"--quality",
|
|
227
|
+
type=float,
|
|
228
|
+
metavar="Q",
|
|
229
|
+
help=(
|
|
230
|
+
"HTJ2K quantization step, relative to the dtype's full range "
|
|
231
|
+
"(lower = better fidelity, larger output). Implies lossy encoding; use "
|
|
232
|
+
"with --codec experimental.openjph_htj2k. Overrides preset quality. "
|
|
233
|
+
"Below one input LSB (1/256 for 8-bit, 1/65536 for 16-bit) the output "
|
|
234
|
+
"is bit-identical but LARGER than --preset lossless; that warns."
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
recompress.add_argument(
|
|
238
|
+
"--reversible",
|
|
239
|
+
action="store_true",
|
|
240
|
+
help="Force lossless HTJ2K (cannot be combined with --quality)",
|
|
241
|
+
)
|
|
242
|
+
recompress.add_argument(
|
|
243
|
+
"--chunks",
|
|
244
|
+
nargs="+",
|
|
245
|
+
metavar="CHUNK",
|
|
246
|
+
help="Use 'auto' or pass one integer per raster axis",
|
|
247
|
+
)
|
|
248
|
+
recompress.add_argument("--overwrite", action="store_true")
|
|
249
|
+
recompress.add_argument(
|
|
250
|
+
"--sibling",
|
|
251
|
+
action="store_true",
|
|
252
|
+
help=(
|
|
253
|
+
"Write compressed images as new sibling groups (e.g. morphology_focus:jp2k_lossless) "
|
|
254
|
+
"instead of replacing the originals in-place"
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
recompress.add_argument(
|
|
258
|
+
"--workers",
|
|
259
|
+
type=_positive_int,
|
|
260
|
+
default=os.cpu_count() or 1,
|
|
261
|
+
help="Parallel encoder workers (default: CPU count)",
|
|
262
|
+
)
|
|
263
|
+
_add_pyramid_args(recompress, with_flag=True)
|
|
264
|
+
recompress.set_defaults(func=_images_recompress)
|
|
265
|
+
|
|
266
|
+
add_pyramid = image_commands.add_parser(
|
|
267
|
+
"add-pyramid",
|
|
268
|
+
help="give single-resolution rasters a multiscale pyramid",
|
|
269
|
+
description=(
|
|
270
|
+
"Copy a store and add multiscale pyramids to rasters that have only one "
|
|
271
|
+
"resolution level, so a zoomed-out browser view does not have to read "
|
|
272
|
+
"full-resolution chunks. Codecs are left unchanged; use "
|
|
273
|
+
"'images recompress --pyramid' to do both in one pass."
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
add_pyramid.add_argument("source", help="Existing SpatialData Zarr store")
|
|
277
|
+
add_pyramid.add_argument("dest", help="Output SpatialData Zarr store")
|
|
278
|
+
add_pyramid.add_argument(
|
|
279
|
+
"--image-key",
|
|
280
|
+
metavar="KEYS",
|
|
281
|
+
help="Comma-separated image element names (default: all images)",
|
|
282
|
+
)
|
|
283
|
+
add_pyramid.add_argument(
|
|
284
|
+
"--labels",
|
|
285
|
+
action="store_true",
|
|
286
|
+
help="Also build pyramids for labels (downsampled without inventing label ids)",
|
|
287
|
+
)
|
|
288
|
+
add_pyramid.add_argument("--overwrite", action="store_true")
|
|
289
|
+
_add_pyramid_args(add_pyramid, with_flag=False)
|
|
290
|
+
add_pyramid.set_defaults(func=_images_add_pyramid)
|
|
291
|
+
|
|
292
|
+
inspect = image_commands.add_parser(
|
|
293
|
+
"inspect",
|
|
294
|
+
help="print a recompression manifest",
|
|
295
|
+
)
|
|
296
|
+
inspect.add_argument("path")
|
|
297
|
+
inspect.set_defaults(func=_images_inspect)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# --------------------------------------------------------------------------- points
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _points_list(args: argparse.Namespace) -> None:
|
|
304
|
+
from .runners import run_list_points
|
|
305
|
+
|
|
306
|
+
_run_command(lambda: run_list_points(args.zarr))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _points_morton(args: argparse.Namespace) -> None:
|
|
310
|
+
from .runners import run_morton_points
|
|
311
|
+
|
|
312
|
+
_run_command(
|
|
313
|
+
lambda: run_morton_points(
|
|
314
|
+
args.input,
|
|
315
|
+
args.output,
|
|
316
|
+
feature_key=args.feature_key,
|
|
317
|
+
row_group_size=args.row_group_size,
|
|
318
|
+
compression=args.compression,
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _points_multiscale(args: argparse.Namespace) -> None:
|
|
324
|
+
from .runners import run_multiscale_points
|
|
325
|
+
|
|
326
|
+
_run_command(
|
|
327
|
+
lambda: run_multiscale_points(
|
|
328
|
+
args.input,
|
|
329
|
+
args.output,
|
|
330
|
+
metadata_json=args.metadata_json,
|
|
331
|
+
row_group_size=args.row_group_size,
|
|
332
|
+
compression=args.compression,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _points_morton_from_zarr(args: argparse.Namespace) -> None:
|
|
338
|
+
from .runners import run_morton_points_from_zarr
|
|
339
|
+
|
|
340
|
+
_run_command(
|
|
341
|
+
lambda: run_morton_points_from_zarr(
|
|
342
|
+
args.zarr,
|
|
343
|
+
points_key=args.points_key,
|
|
344
|
+
experimental=args.experimental,
|
|
345
|
+
output=args.output,
|
|
346
|
+
output_points_key=args.output_points_key,
|
|
347
|
+
feature_key=args.feature_key,
|
|
348
|
+
overwrite=args.overwrite,
|
|
349
|
+
row_group_size=args.row_group_size,
|
|
350
|
+
compression=args.compression,
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _points_index_permutations(args: argparse.Namespace) -> None:
|
|
356
|
+
from .runners import run_write_index_permutations
|
|
357
|
+
|
|
358
|
+
condition_ids = args.conditions.split(",") if args.conditions else None
|
|
359
|
+
_run_command(
|
|
360
|
+
lambda: run_write_index_permutations(
|
|
361
|
+
args.source_zarr,
|
|
362
|
+
args.dest_zarr,
|
|
363
|
+
points_key=args.points_key,
|
|
364
|
+
max_rows=args.max_rows,
|
|
365
|
+
condition_ids=condition_ids,
|
|
366
|
+
overwrite=args.overwrite,
|
|
367
|
+
row_group_size=args.row_group_size,
|
|
368
|
+
compression=args.compression,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _points_zarr(args: argparse.Namespace) -> None:
|
|
374
|
+
from .runners import run_points_zarr
|
|
375
|
+
|
|
376
|
+
_run_command(
|
|
377
|
+
lambda: run_points_zarr(
|
|
378
|
+
args.input,
|
|
379
|
+
args.output,
|
|
380
|
+
feature_key=args.feature_key,
|
|
381
|
+
coordinates=args.coordinates.split(","),
|
|
382
|
+
points_key=args.points_key,
|
|
383
|
+
tile_size=args.tile_size,
|
|
384
|
+
n_levels=args.levels,
|
|
385
|
+
seed=args.seed,
|
|
386
|
+
chunk_rows=args.chunk_rows,
|
|
387
|
+
shard_chunks=args.shard_chunks,
|
|
388
|
+
overwrite=args.overwrite,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _points_add_zarr(args: argparse.Namespace) -> None:
|
|
394
|
+
from .runners import run_add_points_zarr
|
|
395
|
+
|
|
396
|
+
_run_command(
|
|
397
|
+
lambda: run_add_points_zarr(
|
|
398
|
+
args.zarr,
|
|
399
|
+
points_key=args.points_key,
|
|
400
|
+
tile_size=args.tile_size,
|
|
401
|
+
n_levels=args.levels,
|
|
402
|
+
seed=args.seed,
|
|
403
|
+
chunk_rows=args.chunk_rows,
|
|
404
|
+
shard_chunks=args.shard_chunks,
|
|
405
|
+
overwrite=args.overwrite,
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _add_points_zarr_args(parser: argparse.ArgumentParser) -> None:
|
|
411
|
+
parser.add_argument(
|
|
412
|
+
"--tile-size",
|
|
413
|
+
type=_positive_float,
|
|
414
|
+
default=100.0,
|
|
415
|
+
metavar="UNITS",
|
|
416
|
+
help="Level-0 tile size, doubled per level (default: 100)",
|
|
417
|
+
)
|
|
418
|
+
parser.add_argument(
|
|
419
|
+
"--levels",
|
|
420
|
+
type=_positive_int,
|
|
421
|
+
metavar="N",
|
|
422
|
+
help="Number of levels (default: until a level holds about a million points)",
|
|
423
|
+
)
|
|
424
|
+
parser.add_argument("--seed", type=int, default=0, help="Seed for the level sample (default: 0)")
|
|
425
|
+
parser.add_argument(
|
|
426
|
+
"--chunk-rows",
|
|
427
|
+
type=_positive_int,
|
|
428
|
+
default=65_536,
|
|
429
|
+
metavar="N",
|
|
430
|
+
help="Rows per chunk of a point column (default: 65536)",
|
|
431
|
+
)
|
|
432
|
+
parser.add_argument(
|
|
433
|
+
"--shard-chunks", type=_positive_int, default=64, metavar="N", help="Chunks per shard (default: 64)"
|
|
434
|
+
)
|
|
435
|
+
parser.add_argument("--overwrite", action="store_true")
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _positive_float(value: str) -> float:
|
|
439
|
+
parsed = float(value)
|
|
440
|
+
if parsed <= 0:
|
|
441
|
+
raise argparse.ArgumentTypeError("value must be positive")
|
|
442
|
+
return parsed
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _add_row_group_args(parser: argparse.ArgumentParser) -> None:
|
|
446
|
+
parser.add_argument(
|
|
447
|
+
"--row-group-size",
|
|
448
|
+
type=_positive_int,
|
|
449
|
+
default=50_000,
|
|
450
|
+
metavar="N",
|
|
451
|
+
help="Target row-group size after sentinel rows (default: 50000)",
|
|
452
|
+
)
|
|
453
|
+
parser.add_argument(
|
|
454
|
+
"--compression",
|
|
455
|
+
default="zstd",
|
|
456
|
+
help="Parquet compression codec (default: zstd)",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _add_points_commands(subparsers: argparse._SubParsersAction) -> None:
|
|
461
|
+
points = subparsers.add_parser(
|
|
462
|
+
"points",
|
|
463
|
+
help="Morton-index Points elements, or write multiscale Zarr points",
|
|
464
|
+
description=(
|
|
465
|
+
"Write browser-oriented Points artifacts: Morton-sorted Parquet with "
|
|
466
|
+
"sentinel bounding-box rows and controlled row groups, or the experimental "
|
|
467
|
+
"multiscale Zarr layout (points zarr)."
|
|
468
|
+
),
|
|
469
|
+
)
|
|
470
|
+
point_commands = points.add_subparsers(dest="points_command", required=True, metavar="command")
|
|
471
|
+
|
|
472
|
+
list_points = point_commands.add_parser(
|
|
473
|
+
"list",
|
|
474
|
+
help="list Points element keys in a store",
|
|
475
|
+
description="List Points element keys under <zarr>/points/.",
|
|
476
|
+
)
|
|
477
|
+
list_points.add_argument("zarr", metavar="ZARR", help="Path to a SpatialData Zarr store")
|
|
478
|
+
list_points.set_defaults(func=_points_list)
|
|
479
|
+
|
|
480
|
+
morton_from_zarr = point_commands.add_parser(
|
|
481
|
+
"morton-from-zarr",
|
|
482
|
+
help="Morton-sort a Points element from a store",
|
|
483
|
+
description=(
|
|
484
|
+
"Read points/<key>/points.parquet from a SpatialData Zarr store, "
|
|
485
|
+
"add morton_code_2d sentinel rows, and write Vitessce-compatible Parquet. "
|
|
486
|
+
"Defaults to in-place replacement of points/<key>/points.parquet."
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
morton_from_zarr.add_argument("zarr", metavar="ZARR", help="Path to a SpatialData Zarr store")
|
|
490
|
+
morton_from_zarr.add_argument(
|
|
491
|
+
"--experimental",
|
|
492
|
+
action="store_true",
|
|
493
|
+
help="Write to points.experimental/<key>/points.parquet instead of the canonical path",
|
|
494
|
+
)
|
|
495
|
+
morton_from_zarr.add_argument(
|
|
496
|
+
"--points-key",
|
|
497
|
+
metavar="KEY",
|
|
498
|
+
help=(
|
|
499
|
+
"Points element name under points/ (for example transcripts). "
|
|
500
|
+
"Required when the store has more than one Points element."
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
morton_from_zarr.add_argument(
|
|
504
|
+
"--output",
|
|
505
|
+
metavar="PATH",
|
|
506
|
+
help=(
|
|
507
|
+
"Output Parquet path (default: in-place on points/<key>/points.parquet, "
|
|
508
|
+
"or points.experimental/<key>/points.parquet with --experimental)"
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
morton_from_zarr.add_argument(
|
|
512
|
+
"--output-points-key",
|
|
513
|
+
metavar="KEY",
|
|
514
|
+
help=(
|
|
515
|
+
"Output Points element name under points/ (for example transcripts_morton). "
|
|
516
|
+
"Cannot be combined with --output."
|
|
517
|
+
),
|
|
518
|
+
)
|
|
519
|
+
morton_from_zarr.add_argument(
|
|
520
|
+
"--overwrite",
|
|
521
|
+
action="store_true",
|
|
522
|
+
help="Allow overwriting an existing explicit output path or output Points element.",
|
|
523
|
+
)
|
|
524
|
+
morton_from_zarr.add_argument(
|
|
525
|
+
"--feature-key",
|
|
526
|
+
metavar="COLUMN",
|
|
527
|
+
help=(
|
|
528
|
+
"Column used to derive <feature_key>_codes (default: spatialdata_attrs.feature_key "
|
|
529
|
+
"from the element zarr.json)"
|
|
530
|
+
),
|
|
531
|
+
)
|
|
532
|
+
_add_row_group_args(morton_from_zarr)
|
|
533
|
+
morton_from_zarr.set_defaults(func=_points_morton_from_zarr)
|
|
534
|
+
|
|
535
|
+
morton = point_commands.add_parser(
|
|
536
|
+
"morton",
|
|
537
|
+
help="Morton-sort points from CSV or Parquet",
|
|
538
|
+
description=(
|
|
539
|
+
"Sort x/y points by 2D Morton order, prepend sentinel bbox rows, "
|
|
540
|
+
"and write Vitessce-compatible Parquet."
|
|
541
|
+
),
|
|
542
|
+
)
|
|
543
|
+
morton.add_argument(
|
|
544
|
+
"input", metavar="INPUT", help="Input .csv, .parquet file, or directory of Parquet parts"
|
|
545
|
+
)
|
|
546
|
+
morton.add_argument("output", metavar="OUTPUT", help="Output .parquet file")
|
|
547
|
+
morton.add_argument(
|
|
548
|
+
"--feature-key",
|
|
549
|
+
metavar="COLUMN",
|
|
550
|
+
help="Column used to derive <feature_key>_codes for categorical features",
|
|
551
|
+
)
|
|
552
|
+
_add_row_group_args(morton)
|
|
553
|
+
morton.set_defaults(func=_points_morton)
|
|
554
|
+
|
|
555
|
+
multiscale = point_commands.add_parser(
|
|
556
|
+
"multiscale",
|
|
557
|
+
help="write multiscale Points Parquet with spatialdata_multiscale metadata",
|
|
558
|
+
description=(
|
|
559
|
+
"Write Points Parquet with Padua-style spatialdata_multiscale JSON "
|
|
560
|
+
"stored in the file schema metadata."
|
|
561
|
+
),
|
|
562
|
+
)
|
|
563
|
+
multiscale.add_argument(
|
|
564
|
+
"input", metavar="INPUT", help="Input .csv, .parquet file, or directory of Parquet parts"
|
|
565
|
+
)
|
|
566
|
+
multiscale.add_argument("output", metavar="OUTPUT", help="Output .parquet file")
|
|
567
|
+
multiscale.add_argument(
|
|
568
|
+
"--metadata-json",
|
|
569
|
+
metavar="PATH",
|
|
570
|
+
help="Optional spatialdata_multiscale metadata JSON (default: inferred from input)",
|
|
571
|
+
)
|
|
572
|
+
_add_row_group_args(multiscale)
|
|
573
|
+
multiscale.set_defaults(func=_points_multiscale)
|
|
574
|
+
|
|
575
|
+
points_zarr = point_commands.add_parser(
|
|
576
|
+
"zarr",
|
|
577
|
+
help="write a points store with an experimental points-zarr copy",
|
|
578
|
+
description=(
|
|
579
|
+
"Write a new SpatialData store with one standard points element and, beside "
|
|
580
|
+
"it, a copy in merfisheyes' draft points-zarr layout (levels of uniform random "
|
|
581
|
+
"samples, each in tile and feature order, with a row index). Standard readers "
|
|
582
|
+
"read the standard element and ignore the copy. Parquet input is streamed and "
|
|
583
|
+
"sorted out of core."
|
|
584
|
+
),
|
|
585
|
+
)
|
|
586
|
+
points_zarr.add_argument(
|
|
587
|
+
"input", metavar="INPUT", help="Input .csv, .parquet file, or directory of Parquet parts"
|
|
588
|
+
)
|
|
589
|
+
points_zarr.add_argument("output", metavar="OUTPUT", help="Output store directory")
|
|
590
|
+
points_zarr.add_argument(
|
|
591
|
+
"--feature-key", required=True, metavar="COLUMN", help="Categorical feature column"
|
|
592
|
+
)
|
|
593
|
+
points_zarr.add_argument(
|
|
594
|
+
"--coordinates",
|
|
595
|
+
default="x,y",
|
|
596
|
+
metavar="COLUMNS",
|
|
597
|
+
help="Comma-separated coordinate columns, two or three, written as x,y(,z) (default: x,y)",
|
|
598
|
+
)
|
|
599
|
+
points_zarr.add_argument(
|
|
600
|
+
"--points-key", default="transcripts", metavar="KEY", help="Element name under points/"
|
|
601
|
+
)
|
|
602
|
+
_add_points_zarr_args(points_zarr)
|
|
603
|
+
points_zarr.set_defaults(func=_points_zarr)
|
|
604
|
+
|
|
605
|
+
add_zarr = point_commands.add_parser(
|
|
606
|
+
"add-zarr",
|
|
607
|
+
help="add an experimental points-zarr copy beside an existing points element",
|
|
608
|
+
description=(
|
|
609
|
+
"Read points/<key>/points.parquet from a store and write its points-zarr copy "
|
|
610
|
+
"as the element's experimental.points-zarr subgroup, merging the copy into the "
|
|
611
|
+
"store's consolidated metadata. The standard element is left as it is."
|
|
612
|
+
),
|
|
613
|
+
)
|
|
614
|
+
add_zarr.add_argument("zarr", metavar="ZARR", help="Path to a SpatialData Zarr store")
|
|
615
|
+
add_zarr.add_argument("--points-key", required=True, metavar="KEY", help="Element name under points/")
|
|
616
|
+
_add_points_zarr_args(add_zarr)
|
|
617
|
+
add_zarr.set_defaults(func=_points_add_zarr)
|
|
618
|
+
|
|
619
|
+
index_permutations = point_commands.add_parser(
|
|
620
|
+
"index-permutations",
|
|
621
|
+
help="write a derivative store with transcript index sort permutations",
|
|
622
|
+
description=(
|
|
623
|
+
"Copy a SpatialData Zarr store and add sibling points elements with "
|
|
624
|
+
"different transcript sort/index layouts plus index-manifest.json."
|
|
625
|
+
),
|
|
626
|
+
)
|
|
627
|
+
index_permutations.add_argument("source_zarr", metavar="SOURCE_ZARR")
|
|
628
|
+
index_permutations.add_argument("dest_zarr", metavar="DEST_ZARR")
|
|
629
|
+
index_permutations.add_argument("--points-key", metavar="KEY")
|
|
630
|
+
index_permutations.add_argument("--max-rows", type=_positive_int, metavar="N")
|
|
631
|
+
index_permutations.add_argument(
|
|
632
|
+
"--conditions", metavar="IDS", help="Comma-separated condition ids (default: all)"
|
|
633
|
+
)
|
|
634
|
+
index_permutations.add_argument("--overwrite", action="store_true")
|
|
635
|
+
_add_row_group_args(index_permutations)
|
|
636
|
+
index_permutations.set_defaults(func=_points_index_permutations)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
# --------------------------------------------------------------------------- tables
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _tables_list(args: argparse.Namespace) -> None:
|
|
643
|
+
from .runners import run_list_tables
|
|
644
|
+
|
|
645
|
+
_run_command(lambda: run_list_tables(args.zarr))
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _tables_to_csc(args: argparse.Namespace) -> None:
|
|
649
|
+
from .runners import run_tables_to_csc
|
|
650
|
+
|
|
651
|
+
_run_command(
|
|
652
|
+
lambda: run_tables_to_csc(
|
|
653
|
+
args.source,
|
|
654
|
+
args.dest,
|
|
655
|
+
tables=args.table.split(",") if args.table else None,
|
|
656
|
+
layers=not args.no_layers,
|
|
657
|
+
densify=args.densify,
|
|
658
|
+
overwrite=args.overwrite,
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _add_tables_commands(subparsers: argparse._SubParsersAction) -> None:
|
|
664
|
+
tables = subparsers.add_parser(
|
|
665
|
+
"tables",
|
|
666
|
+
help="convert table matrices to CSC",
|
|
667
|
+
description="Rewrite AnnData table matrices for fast per-variable browser reads.",
|
|
668
|
+
)
|
|
669
|
+
table_commands = tables.add_subparsers(dest="tables_command", required=True, metavar="command")
|
|
670
|
+
|
|
671
|
+
list_tables = table_commands.add_parser(
|
|
672
|
+
"list", help="list table element keys in a store"
|
|
673
|
+
)
|
|
674
|
+
list_tables.add_argument("zarr", metavar="ZARR", help="Path to a SpatialData Zarr store")
|
|
675
|
+
list_tables.set_defaults(func=_tables_list)
|
|
676
|
+
|
|
677
|
+
to_csc = table_commands.add_parser(
|
|
678
|
+
"to-csc",
|
|
679
|
+
help="convert X (and layers) to CSC",
|
|
680
|
+
description=(
|
|
681
|
+
"Convert table matrices from CSR to CSC so that reading one variable "
|
|
682
|
+
"(gene) is a single contiguous range read instead of a scan of every row. "
|
|
683
|
+
"Values are unchanged and the result stays readable by any AnnData client."
|
|
684
|
+
),
|
|
685
|
+
)
|
|
686
|
+
to_csc.add_argument("source", metavar="SOURCE", help="SpatialData Zarr store")
|
|
687
|
+
to_csc.add_argument(
|
|
688
|
+
"dest",
|
|
689
|
+
metavar="DEST",
|
|
690
|
+
nargs="?",
|
|
691
|
+
help="Output store (default: rewrite SOURCE in place)",
|
|
692
|
+
)
|
|
693
|
+
to_csc.add_argument(
|
|
694
|
+
"--table",
|
|
695
|
+
metavar="KEYS",
|
|
696
|
+
help="Comma-separated table element names (default: all tables)",
|
|
697
|
+
)
|
|
698
|
+
to_csc.add_argument(
|
|
699
|
+
"--no-layers",
|
|
700
|
+
action="store_true",
|
|
701
|
+
help="Convert X only, leaving adata.layers untouched",
|
|
702
|
+
)
|
|
703
|
+
to_csc.add_argument(
|
|
704
|
+
"--densify",
|
|
705
|
+
action="store_true",
|
|
706
|
+
help=(
|
|
707
|
+
"Also convert dense matrices to CSC. Off by default because sparsifying "
|
|
708
|
+
"a dense matrix can make it larger."
|
|
709
|
+
),
|
|
710
|
+
)
|
|
711
|
+
to_csc.add_argument("--overwrite", action="store_true", help="Replace an existing DEST")
|
|
712
|
+
to_csc.set_defaults(func=_tables_to_csc)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
# --------------------------------------------------------------------------- codecs
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _codecs_info(_args: argparse.Namespace) -> None:
|
|
719
|
+
from .codecs.backends import backend_report
|
|
720
|
+
|
|
721
|
+
_print_json(backend_report())
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _add_codecs_commands(subparsers: argparse._SubParsersAction) -> None:
|
|
725
|
+
codecs = subparsers.add_parser(
|
|
726
|
+
"codecs",
|
|
727
|
+
help="inspect image codec backends",
|
|
728
|
+
description="Report which HTJ2K backends are installed and which one is selected.",
|
|
729
|
+
)
|
|
730
|
+
codec_commands = codecs.add_subparsers(dest="codecs_command", required=True, metavar="command")
|
|
731
|
+
|
|
732
|
+
info = codec_commands.add_parser(
|
|
733
|
+
"info",
|
|
734
|
+
help="show HTJ2K backend availability and probe results",
|
|
735
|
+
description=(
|
|
736
|
+
"Show each HTJ2K backend's availability and whether it decodes the bundled "
|
|
737
|
+
"multi-component probe codestream correctly, plus which one is selected."
|
|
738
|
+
),
|
|
739
|
+
)
|
|
740
|
+
info.set_defaults(func=_codecs_info)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
# --------------------------------------------------------------------------- tui
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _tui(args: argparse.Namespace) -> None:
|
|
747
|
+
try:
|
|
748
|
+
from .tui.app import run_tui
|
|
749
|
+
except ImportError as exc:
|
|
750
|
+
raise SystemExit(
|
|
751
|
+
"TUI dependencies are not installed. Install with: pip install 'spatialdata-js-util[tui]'"
|
|
752
|
+
) from exc
|
|
753
|
+
run_tui(initial_zarr=args.zarr)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _add_tui_command(subparsers: argparse._SubParsersAction) -> None:
|
|
757
|
+
tui = subparsers.add_parser(
|
|
758
|
+
"tui",
|
|
759
|
+
help="interactive terminal UI for every command",
|
|
760
|
+
description=(
|
|
761
|
+
"Launch the interactive workflow UI. It covers image recompression, "
|
|
762
|
+
"Points indexing, table CSC conversion, and codec backend info, with "
|
|
763
|
+
"guided forms, confirmation before any in-place write, and post-write "
|
|
764
|
+
"verification. Requires the 'tui' extra: "
|
|
765
|
+
"pip install 'spatialdata-js-util[tui]'"
|
|
766
|
+
),
|
|
767
|
+
)
|
|
768
|
+
tui.add_argument(
|
|
769
|
+
"zarr",
|
|
770
|
+
nargs="?",
|
|
771
|
+
metavar="ZARR",
|
|
772
|
+
help="Optional SpatialData Zarr store path (skips initial store picker)",
|
|
773
|
+
)
|
|
774
|
+
tui.set_defaults(func=_tui)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
778
|
+
parser = argparse.ArgumentParser(
|
|
779
|
+
prog="spatialdata-js-util",
|
|
780
|
+
description=(
|
|
781
|
+
"Read/write utilities for browser-oriented SpatialData stores: image "
|
|
782
|
+
"codecs, Morton-indexed points, and CSC table matrices."
|
|
783
|
+
),
|
|
784
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
785
|
+
epilog=_EPILOG,
|
|
786
|
+
)
|
|
787
|
+
subparsers = parser.add_subparsers(dest="group", required=True, metavar="group")
|
|
788
|
+
_add_images_commands(subparsers)
|
|
789
|
+
_add_points_commands(subparsers)
|
|
790
|
+
_add_tables_commands(subparsers)
|
|
791
|
+
_add_codecs_commands(subparsers)
|
|
792
|
+
_add_tui_command(subparsers)
|
|
793
|
+
return parser
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def main(argv: list[str] | None = None) -> None:
|
|
797
|
+
parser = build_parser()
|
|
798
|
+
args = parser.parse_args(argv)
|
|
799
|
+
args.func(args)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
if __name__ == "__main__":
|
|
803
|
+
main()
|