dyna-zarr 0.0.2__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.
- dyna_zarr-0.0.2/LICENSE +21 -0
- dyna_zarr-0.0.2/PKG-INFO +226 -0
- dyna_zarr-0.0.2/README.md +182 -0
- dyna_zarr-0.0.2/pyproject.toml +68 -0
- dyna_zarr-0.0.2/setup.cfg +4 -0
- dyna_zarr-0.0.2/src/dyna_zarr/__init__.py +43 -0
- dyna_zarr-0.0.2/src/dyna_zarr/codecs.py +201 -0
- dyna_zarr-0.0.2/src/dyna_zarr/dynamic_array.py +609 -0
- dyna_zarr-0.0.2/src/dyna_zarr/io.py +1098 -0
- dyna_zarr-0.0.2/src/dyna_zarr/rechunk.py +233 -0
- dyna_zarr-0.0.2/src/dyna_zarr/reindex.py +104 -0
- dyna_zarr-0.0.2/src/dyna_zarr/tiff_reader.py +35 -0
- dyna_zarr-0.0.2/src/dyna_zarr/utils.py +67 -0
- dyna_zarr-0.0.2/src/dyna_zarr.egg-info/PKG-INFO +226 -0
- dyna_zarr-0.0.2/src/dyna_zarr.egg-info/SOURCES.txt +30 -0
- dyna_zarr-0.0.2/src/dyna_zarr.egg-info/dependency_links.txt +1 -0
- dyna_zarr-0.0.2/src/dyna_zarr.egg-info/requires.txt +29 -0
- dyna_zarr-0.0.2/src/dyna_zarr.egg-info/top_level.txt +1 -0
- dyna_zarr-0.0.2/tests/test_creation.py +79 -0
- dyna_zarr-0.0.2/tests/test_io_laziness.py +233 -0
- dyna_zarr-0.0.2/tests/test_io_read.py +179 -0
- dyna_zarr-0.0.2/tests/test_io_roundtrip.py +232 -0
- dyna_zarr-0.0.2/tests/test_io_with_operations.py +258 -0
- dyna_zarr-0.0.2/tests/test_io_write.py +221 -0
- dyna_zarr-0.0.2/tests/test_neighborhood.py +122 -0
- dyna_zarr-0.0.2/tests/test_new_ops.py +94 -0
- dyna_zarr-0.0.2/tests/test_numpy_protocol.py +130 -0
- dyna_zarr-0.0.2/tests/test_rechunk.py +208 -0
- dyna_zarr-0.0.2/tests/test_reductions.py +153 -0
- dyna_zarr-0.0.2/tests/test_reindex.py +58 -0
- dyna_zarr-0.0.2/tests/test_scan.py +80 -0
- dyna_zarr-0.0.2/tests/test_transform_correctness.py +234 -0
dyna_zarr-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bugra Oezdemir
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dyna_zarr-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dyna-zarr
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A lightweight library for lazy operations on Zarr arrays without task graph overhead
|
|
5
|
+
Author-email: Bugra Oezdemir <bugraa.ozdemir@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bugraoezdemir/dyna_zarr
|
|
8
|
+
Project-URL: Documentation, https://dyna-zarr.readthedocs.io
|
|
9
|
+
Keywords: zarr,lazy,array,dask-alternative
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: zarr>=3.0.0
|
|
21
|
+
Requires-Dist: numpy>=1.20.0
|
|
22
|
+
Requires-Dist: scipy>=1.6.0
|
|
23
|
+
Requires-Dist: tensorstore
|
|
24
|
+
Requires-Dist: tifffile
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
28
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
29
|
+
Requires-Dist: isort>=5.0; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Requires-Dist: sphinx>=5.0; extra == "docs"
|
|
34
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
|
|
35
|
+
Provides-Extra: gpu
|
|
36
|
+
Requires-Dist: cupy-cuda12x[ctk]; extra == "gpu"
|
|
37
|
+
Provides-Extra: gpu-cu11
|
|
38
|
+
Requires-Dist: cupy-cuda11x[ctk]; extra == "gpu-cu11"
|
|
39
|
+
Provides-Extra: gpu-cu12
|
|
40
|
+
Requires-Dist: cupy-cuda12x[ctk]; extra == "gpu-cu12"
|
|
41
|
+
Provides-Extra: gpu-cu13
|
|
42
|
+
Requires-Dist: cupy-cuda13x[ctk]; extra == "gpu-cu13"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# dyna-zarr
|
|
46
|
+
|
|
47
|
+
A lightweight, dask-free Python library for lazy, memory-bounded operations on large Zarr (and TIFF) arrays, with an optional GPU path.
|
|
48
|
+
|
|
49
|
+
## Overview
|
|
50
|
+
|
|
51
|
+
dyna-zarr is a thin, pull-based array layer over [Zarr](https://zarr-python.readthedocs.io/). Instead of building a task graph, every operation is a lazy *transform* whose `read(key)` maps an output slice back to a bounded input read, ending at a direct zarr/TensorStore read. Slicing a result pulls only that region through the whole operation chain, so no intermediates are materialized.
|
|
52
|
+
|
|
53
|
+
The practical consequence is memory-boundedness. When you stream a result to disk with `io.write`, the array is processed region by region, so peak RAM is a function of the region and worker budget rather than the array size. This makes it possible to read, transform, and write arrays far larger than memory.
|
|
54
|
+
|
|
55
|
+
## Memory-boundedness
|
|
56
|
+
|
|
57
|
+
There are two ways to run a lazy result, with different memory behavior:
|
|
58
|
+
|
|
59
|
+
- `io.write(result, path)` streams the result to disk region by region. Peak RAM is roughly `region_size_mb * max_workers`, independent of the array size. This is the memory-bounded path.
|
|
60
|
+
- `result.compute()` returns a single in-memory NumPy array. It materializes the whole result by design (mirroring `dask.array.compute`), so it is not memory-bounded. Use it only for results that fit in RAM.
|
|
61
|
+
|
|
62
|
+
Every operation is memory-bounded on the `io.write` path except `median`, `argmin`, and `argmax`, which are flagged in the operations catalog below.
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- **Pull-based and lazy.** Operations defer until `.compute()` (materialize) or `io.write` (stream to disk).
|
|
67
|
+
- **Memory-bounded streaming.** Region-wise `io.write` with per-worker memory and worker-count knobs. Even reshape, flatten, and rechunk of incompatibly-chunked data stay bounded, by staging through disk.
|
|
68
|
+
- **NumPy-like.** Operator overloads, array methods (`.astype`, `.clip`, `.round`), and the NumPy ufunc protocol (`np.sqrt(a)`, `np.add(a, 2)`) all work on a `DynamicArray`.
|
|
69
|
+
- **Rich op set.** About 90 operations: pointwise ufuncs, streaming reductions, neighborhood (halo) filters, structural reshaping, differences, and array creation.
|
|
70
|
+
- **Multi-format I/O.** Read TIFF, Zarr v2, and Zarr v3 (local, S3/GCS, HTTP); write Zarr v2/v3 with optional sharding.
|
|
71
|
+
- **Optional GPU.** Run an op chain on CUDA via CuPy, with a single host-to-device transfer per region.
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install dyna-zarr
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Optional GPU support (pick the extra matching your CUDA toolkit from `nvidia-smi`):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install "dyna-zarr[gpu-cu12]" # CUDA 12.x ([gpu] is an alias for this)
|
|
83
|
+
pip install "dyna-zarr[gpu-cu11]" # CUDA 11.x
|
|
84
|
+
pip install "dyna-zarr[gpu-cu13]" # CUDA 13.x (e.g. Blackwell)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quick start
|
|
88
|
+
|
|
89
|
+
### Read
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from dyna_zarr import io
|
|
93
|
+
|
|
94
|
+
arr = io.read("image.tiff") # TIFF via tifffile's zarr bridge
|
|
95
|
+
arr = io.read("array_v2.zarr") # Zarr v2
|
|
96
|
+
arr = io.read("array_v3.zarr") # Zarr v3 (also s3://, gs://, http://)
|
|
97
|
+
|
|
98
|
+
print(arr.shape, arr.dtype, arr.chunks)
|
|
99
|
+
|
|
100
|
+
data = arr.compute() # materialize the whole array
|
|
101
|
+
region = arr[10:20, 50:150, 100:200].compute() # pull just this region
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Write (memory-bounded streaming)
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from dyna_zarr import io, Codecs
|
|
108
|
+
|
|
109
|
+
io.write(arr, "out_v3.zarr", zarr_format=3)
|
|
110
|
+
io.write(arr, "out.zarr", chunks=(64, 64, 64), zarr_format=3)
|
|
111
|
+
io.write(arr, "out.zarr", dtype="float32", zarr_format=3) # cast on write
|
|
112
|
+
io.write(arr, "out.zarr", compressor=Codecs(compressor="zstd", clevel=5), zarr_format=3)
|
|
113
|
+
|
|
114
|
+
# memory and parallelism controls (peak RAM is roughly region_size_mb * max_workers)
|
|
115
|
+
io.write(arr, "out.zarr", region_size_mb=64, max_workers=4)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Lazy operation chains
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from dyna_zarr import io, operations as ops
|
|
122
|
+
|
|
123
|
+
arr = io.read("input.zarr")
|
|
124
|
+
|
|
125
|
+
result = ops.sqrt(ops.clip(ops.abs(arr), 0, 1)) # nothing computed yet
|
|
126
|
+
io.write(result, "output.zarr", zarr_format=3) # streamed, region by region
|
|
127
|
+
# ...or result.compute() to materialize
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### NumPy-like interface
|
|
131
|
+
|
|
132
|
+
A `DynamicArray` behaves like a NumPy or dask array. Operators, methods, and ufuncs are all lazy:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import numpy as np
|
|
136
|
+
|
|
137
|
+
masked = (arr > 3) & (arr < 100) # elementwise operators build a lazy mask
|
|
138
|
+
scaled = (arr.astype("float32") / 255).clip(0, 1)
|
|
139
|
+
out = np.sqrt(np.abs(arr)) # NumPy ufunc protocol dispatches to lazy ops
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Neighborhood filters
|
|
143
|
+
|
|
144
|
+
Neighborhood (halo) filters wrap `scipy.ndimage`. Each read pulls its own halo, so results are chunk-invariant and exact, and stay memory-bounded when streamed.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
import numpy as np
|
|
148
|
+
from dyna_zarr import io, operations as ops
|
|
149
|
+
|
|
150
|
+
img = io.read("volume.zarr") # e.g. (z, y, x)
|
|
151
|
+
|
|
152
|
+
# LoG filtering
|
|
153
|
+
log = ops.gaussian_laplace(img, sigma=2)
|
|
154
|
+
io.write(log, "log.zarr", zarr_format=3) # halo handled per region
|
|
155
|
+
|
|
156
|
+
# median denoise
|
|
157
|
+
denoised = ops.median_filter(img, size=3)
|
|
158
|
+
io.write(denoised, "denoised.zarr")
|
|
159
|
+
|
|
160
|
+
# a custom per-plane kernel
|
|
161
|
+
kernel = np.ones((1, 3, 3), dtype="float32") / 9 # 3x3 mean within each z-plane
|
|
162
|
+
blurred = ops.convolve(img, kernel)
|
|
163
|
+
io.write(blurred, "blurred.zarr")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Operations catalog
|
|
167
|
+
|
|
168
|
+
Every operation is lazy, and memory-bounded on the `io.write` path except `median`, `argmin`, and `argmax` (see Memory-boundedness). All are available flat on `dyna_zarr.operations`, and also grouped by category submodule.
|
|
169
|
+
|
|
170
|
+
- **Pointwise / ufuncs.** `abs`, `negative`, `sign`, `sqrt`, `square`, `exp`, `log`, `log2`, `log10`, `floor`, `ceil`, `reciprocal`, `round`, `clip`, `astype`; binary `add`, `subtract`, `multiply`, `divide`, `floor_divide`, `mod`, `power`, `maximum`, `minimum`; comparisons `greater(_equal)`, `less(_equal)`, `equal`, `not_equal`; logical `and`, `or`, `xor`, `not`; `where`, `isin`, `digitize`.
|
|
171
|
+
- **Reductions.** Streaming and memory-bounded: `min`, `max`, `sum`, `prod`, `mean`, `any`, `all`, `var`, `std`, `histogram` (with `axis=` and `keepdims=`). Not fully bounded (hold the full reduced axis): `median`, `argmin`, `argmax`.
|
|
172
|
+
- **Neighborhood (halo/overlap).** `gaussian_filter`, `uniform_filter`, `median_filter`, `minimum_filter`, `maximum_filter`, `grey_erosion`, `grey_dilation`, `convolve`, `correlate`, `laplace`, `gaussian_laplace`, `gaussian_gradient_magnitude`.
|
|
173
|
+
- **Structural.** `concatenate`, `stack`, `transpose`, `swap_axes`, `reshape`, `flatten`, `squeeze`, `expand_dims`, `pad`, `tile`, `roll`, `flip`, `rot90`, `slice_array`.
|
|
174
|
+
- **Differences.** `diff`, `gradient`.
|
|
175
|
+
- **Scan (prefix, along one axis).** `cumsum`, `cumprod`, `cummax`, `cummin`. Streamed with a bounded carry on the `io.write` path, so memory-bounded despite the sequential dependency.
|
|
176
|
+
- **Creation.** `zeros`, `ones`, `full`, `empty`, `random` (and the `*_like` variants). `random` is position-deterministic, so the result is independent of chunking.
|
|
177
|
+
- **Primitives.** `map_blocks` (pointwise), `map_overlap` (neighborhood with a halo), `reduce` (streaming). Use these to build your own ops.
|
|
178
|
+
|
|
179
|
+
## Memory-bounded reshape, flatten, and rechunk
|
|
180
|
+
|
|
181
|
+
C-order reshape and flatten conflict with n-dimensional chunk layout, so a naive implementation blows up. dyna-zarr stages these through disk (a Rechunker-style two-phase, read-once/write-once copy), so peak RAM stays a function of the per-worker budget rather than the array size. When you `io.write` an outermost `reshape` or `flatten`, this path is used automatically:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from dyna_zarr import io, operations as ops
|
|
185
|
+
|
|
186
|
+
arr = io.read("big_4d.zarr") # e.g. 5 GB, awkward chunks
|
|
187
|
+
io.write(ops.flatten(arr), "flat.zarr", region_size_mb=128, max_workers=2)
|
|
188
|
+
io.write(ops.reshape(arr, (a, b)), "reshaped.zarr") # (a, b) is any target shape of the same size
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## GPU (optional)
|
|
192
|
+
|
|
193
|
+
With a CuPy install, run a chain on the GPU. Setting `device='cuda'` on a terminal call (`compute` or `io.write`) makes device-inheriting ops run on the GPU. A single host-to-device transfer happens at the first CUDA op and the data stays resident up the chain. Results are returned or written from the host.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
result = ops.gaussian_filter(arr, sigma=3)
|
|
197
|
+
out = result.compute(device="cuda") # whole chain on the GPU
|
|
198
|
+
io.write(result, "out.zarr", device="cuda") # per-region GPU compute, streamed write
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Relationship to dask
|
|
202
|
+
|
|
203
|
+
dyna-zarr is not a general replacement for `dask.array`. It targets one job: memory-bounded read, transform, and write of large Zarr/TIFF arrays.
|
|
204
|
+
|
|
205
|
+
The core idea is to drop the task graph. Because every operation is a pull-based chain, where each output slice maps back to a bounded input read, there is no graph to build and no scheduler to run it. That keeps the engine small, keeps peak RAM bounded by `region_size_mb * max_workers` on the `io.write` path, and avoids scheduling overhead, which makes the read-transform-write pipeline efficient.
|
|
206
|
+
|
|
207
|
+
The tradeoff is that only operations that fit this slice-pushdown model belong in the chain: pointwise math, neighborhood/halo filters, streaming reductions, and structural reshaping. These are operations that are commonly used in image processing, which is what dyna-zarr is mainly built for. Operations that would need a global, data-dependent graph do not fit directly, and a few that do (such as non-associative reductions) trade extra reads or memory to stay correct.
|
|
208
|
+
|
|
209
|
+
Two more differences worth knowing:
|
|
210
|
+
|
|
211
|
+
- **Single machine, for now.** Parallelism today is threaded I/O within one process, plus the optional GPU path. There is no cluster or distributed execution yet; better and process-based parallelism is a possible future direction.
|
|
212
|
+
- **Narrower surface.** About 90 operations today, extended where the slice-pushdown model permits. Binary ops also need equal-shaped operands (no general broadcasting between differently shaped lazy arrays yet).
|
|
213
|
+
|
|
214
|
+
## Core components
|
|
215
|
+
|
|
216
|
+
- `io.read(source)` reads TIFF, Zarr v2, or Zarr v3 (local or remote) into a `DynamicArray`.
|
|
217
|
+
- `io.write(array, path, ...)` streams a `DynamicArray` to Zarr v2/v3 (chunks, sharding, compression, dtype cast, `region_size_mb`, `max_workers`, `device`).
|
|
218
|
+
- `operations` is the lazy op set above.
|
|
219
|
+
- `DynamicArray` is the pull-based lazy array (slicing, `.compute()`, operators, `.astype`/`.clip`/`.round`, ufunc protocol).
|
|
220
|
+
- `Codecs` is the compression configuration for Zarr v2 and v3.
|
|
221
|
+
|
|
222
|
+
## Requirements
|
|
223
|
+
|
|
224
|
+
- Python 3.11 or newer
|
|
225
|
+
- zarr 3.0.0+, numpy 1.20+, scipy 1.6+, tensorstore, tifffile
|
|
226
|
+
- Optional: CuPy (via the `gpu-cuXX` extras) for the GPU path
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# dyna-zarr
|
|
2
|
+
|
|
3
|
+
A lightweight, dask-free Python library for lazy, memory-bounded operations on large Zarr (and TIFF) arrays, with an optional GPU path.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
dyna-zarr is a thin, pull-based array layer over [Zarr](https://zarr-python.readthedocs.io/). Instead of building a task graph, every operation is a lazy *transform* whose `read(key)` maps an output slice back to a bounded input read, ending at a direct zarr/TensorStore read. Slicing a result pulls only that region through the whole operation chain, so no intermediates are materialized.
|
|
8
|
+
|
|
9
|
+
The practical consequence is memory-boundedness. When you stream a result to disk with `io.write`, the array is processed region by region, so peak RAM is a function of the region and worker budget rather than the array size. This makes it possible to read, transform, and write arrays far larger than memory.
|
|
10
|
+
|
|
11
|
+
## Memory-boundedness
|
|
12
|
+
|
|
13
|
+
There are two ways to run a lazy result, with different memory behavior:
|
|
14
|
+
|
|
15
|
+
- `io.write(result, path)` streams the result to disk region by region. Peak RAM is roughly `region_size_mb * max_workers`, independent of the array size. This is the memory-bounded path.
|
|
16
|
+
- `result.compute()` returns a single in-memory NumPy array. It materializes the whole result by design (mirroring `dask.array.compute`), so it is not memory-bounded. Use it only for results that fit in RAM.
|
|
17
|
+
|
|
18
|
+
Every operation is memory-bounded on the `io.write` path except `median`, `argmin`, and `argmax`, which are flagged in the operations catalog below.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Pull-based and lazy.** Operations defer until `.compute()` (materialize) or `io.write` (stream to disk).
|
|
23
|
+
- **Memory-bounded streaming.** Region-wise `io.write` with per-worker memory and worker-count knobs. Even reshape, flatten, and rechunk of incompatibly-chunked data stay bounded, by staging through disk.
|
|
24
|
+
- **NumPy-like.** Operator overloads, array methods (`.astype`, `.clip`, `.round`), and the NumPy ufunc protocol (`np.sqrt(a)`, `np.add(a, 2)`) all work on a `DynamicArray`.
|
|
25
|
+
- **Rich op set.** About 90 operations: pointwise ufuncs, streaming reductions, neighborhood (halo) filters, structural reshaping, differences, and array creation.
|
|
26
|
+
- **Multi-format I/O.** Read TIFF, Zarr v2, and Zarr v3 (local, S3/GCS, HTTP); write Zarr v2/v3 with optional sharding.
|
|
27
|
+
- **Optional GPU.** Run an op chain on CUDA via CuPy, with a single host-to-device transfer per region.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install dyna-zarr
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Optional GPU support (pick the extra matching your CUDA toolkit from `nvidia-smi`):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install "dyna-zarr[gpu-cu12]" # CUDA 12.x ([gpu] is an alias for this)
|
|
39
|
+
pip install "dyna-zarr[gpu-cu11]" # CUDA 11.x
|
|
40
|
+
pip install "dyna-zarr[gpu-cu13]" # CUDA 13.x (e.g. Blackwell)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
### Read
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from dyna_zarr import io
|
|
49
|
+
|
|
50
|
+
arr = io.read("image.tiff") # TIFF via tifffile's zarr bridge
|
|
51
|
+
arr = io.read("array_v2.zarr") # Zarr v2
|
|
52
|
+
arr = io.read("array_v3.zarr") # Zarr v3 (also s3://, gs://, http://)
|
|
53
|
+
|
|
54
|
+
print(arr.shape, arr.dtype, arr.chunks)
|
|
55
|
+
|
|
56
|
+
data = arr.compute() # materialize the whole array
|
|
57
|
+
region = arr[10:20, 50:150, 100:200].compute() # pull just this region
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Write (memory-bounded streaming)
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from dyna_zarr import io, Codecs
|
|
64
|
+
|
|
65
|
+
io.write(arr, "out_v3.zarr", zarr_format=3)
|
|
66
|
+
io.write(arr, "out.zarr", chunks=(64, 64, 64), zarr_format=3)
|
|
67
|
+
io.write(arr, "out.zarr", dtype="float32", zarr_format=3) # cast on write
|
|
68
|
+
io.write(arr, "out.zarr", compressor=Codecs(compressor="zstd", clevel=5), zarr_format=3)
|
|
69
|
+
|
|
70
|
+
# memory and parallelism controls (peak RAM is roughly region_size_mb * max_workers)
|
|
71
|
+
io.write(arr, "out.zarr", region_size_mb=64, max_workers=4)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Lazy operation chains
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from dyna_zarr import io, operations as ops
|
|
78
|
+
|
|
79
|
+
arr = io.read("input.zarr")
|
|
80
|
+
|
|
81
|
+
result = ops.sqrt(ops.clip(ops.abs(arr), 0, 1)) # nothing computed yet
|
|
82
|
+
io.write(result, "output.zarr", zarr_format=3) # streamed, region by region
|
|
83
|
+
# ...or result.compute() to materialize
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### NumPy-like interface
|
|
87
|
+
|
|
88
|
+
A `DynamicArray` behaves like a NumPy or dask array. Operators, methods, and ufuncs are all lazy:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import numpy as np
|
|
92
|
+
|
|
93
|
+
masked = (arr > 3) & (arr < 100) # elementwise operators build a lazy mask
|
|
94
|
+
scaled = (arr.astype("float32") / 255).clip(0, 1)
|
|
95
|
+
out = np.sqrt(np.abs(arr)) # NumPy ufunc protocol dispatches to lazy ops
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Neighborhood filters
|
|
99
|
+
|
|
100
|
+
Neighborhood (halo) filters wrap `scipy.ndimage`. Each read pulls its own halo, so results are chunk-invariant and exact, and stay memory-bounded when streamed.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import numpy as np
|
|
104
|
+
from dyna_zarr import io, operations as ops
|
|
105
|
+
|
|
106
|
+
img = io.read("volume.zarr") # e.g. (z, y, x)
|
|
107
|
+
|
|
108
|
+
# LoG filtering
|
|
109
|
+
log = ops.gaussian_laplace(img, sigma=2)
|
|
110
|
+
io.write(log, "log.zarr", zarr_format=3) # halo handled per region
|
|
111
|
+
|
|
112
|
+
# median denoise
|
|
113
|
+
denoised = ops.median_filter(img, size=3)
|
|
114
|
+
io.write(denoised, "denoised.zarr")
|
|
115
|
+
|
|
116
|
+
# a custom per-plane kernel
|
|
117
|
+
kernel = np.ones((1, 3, 3), dtype="float32") / 9 # 3x3 mean within each z-plane
|
|
118
|
+
blurred = ops.convolve(img, kernel)
|
|
119
|
+
io.write(blurred, "blurred.zarr")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Operations catalog
|
|
123
|
+
|
|
124
|
+
Every operation is lazy, and memory-bounded on the `io.write` path except `median`, `argmin`, and `argmax` (see Memory-boundedness). All are available flat on `dyna_zarr.operations`, and also grouped by category submodule.
|
|
125
|
+
|
|
126
|
+
- **Pointwise / ufuncs.** `abs`, `negative`, `sign`, `sqrt`, `square`, `exp`, `log`, `log2`, `log10`, `floor`, `ceil`, `reciprocal`, `round`, `clip`, `astype`; binary `add`, `subtract`, `multiply`, `divide`, `floor_divide`, `mod`, `power`, `maximum`, `minimum`; comparisons `greater(_equal)`, `less(_equal)`, `equal`, `not_equal`; logical `and`, `or`, `xor`, `not`; `where`, `isin`, `digitize`.
|
|
127
|
+
- **Reductions.** Streaming and memory-bounded: `min`, `max`, `sum`, `prod`, `mean`, `any`, `all`, `var`, `std`, `histogram` (with `axis=` and `keepdims=`). Not fully bounded (hold the full reduced axis): `median`, `argmin`, `argmax`.
|
|
128
|
+
- **Neighborhood (halo/overlap).** `gaussian_filter`, `uniform_filter`, `median_filter`, `minimum_filter`, `maximum_filter`, `grey_erosion`, `grey_dilation`, `convolve`, `correlate`, `laplace`, `gaussian_laplace`, `gaussian_gradient_magnitude`.
|
|
129
|
+
- **Structural.** `concatenate`, `stack`, `transpose`, `swap_axes`, `reshape`, `flatten`, `squeeze`, `expand_dims`, `pad`, `tile`, `roll`, `flip`, `rot90`, `slice_array`.
|
|
130
|
+
- **Differences.** `diff`, `gradient`.
|
|
131
|
+
- **Scan (prefix, along one axis).** `cumsum`, `cumprod`, `cummax`, `cummin`. Streamed with a bounded carry on the `io.write` path, so memory-bounded despite the sequential dependency.
|
|
132
|
+
- **Creation.** `zeros`, `ones`, `full`, `empty`, `random` (and the `*_like` variants). `random` is position-deterministic, so the result is independent of chunking.
|
|
133
|
+
- **Primitives.** `map_blocks` (pointwise), `map_overlap` (neighborhood with a halo), `reduce` (streaming). Use these to build your own ops.
|
|
134
|
+
|
|
135
|
+
## Memory-bounded reshape, flatten, and rechunk
|
|
136
|
+
|
|
137
|
+
C-order reshape and flatten conflict with n-dimensional chunk layout, so a naive implementation blows up. dyna-zarr stages these through disk (a Rechunker-style two-phase, read-once/write-once copy), so peak RAM stays a function of the per-worker budget rather than the array size. When you `io.write` an outermost `reshape` or `flatten`, this path is used automatically:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from dyna_zarr import io, operations as ops
|
|
141
|
+
|
|
142
|
+
arr = io.read("big_4d.zarr") # e.g. 5 GB, awkward chunks
|
|
143
|
+
io.write(ops.flatten(arr), "flat.zarr", region_size_mb=128, max_workers=2)
|
|
144
|
+
io.write(ops.reshape(arr, (a, b)), "reshaped.zarr") # (a, b) is any target shape of the same size
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## GPU (optional)
|
|
148
|
+
|
|
149
|
+
With a CuPy install, run a chain on the GPU. Setting `device='cuda'` on a terminal call (`compute` or `io.write`) makes device-inheriting ops run on the GPU. A single host-to-device transfer happens at the first CUDA op and the data stays resident up the chain. Results are returned or written from the host.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
result = ops.gaussian_filter(arr, sigma=3)
|
|
153
|
+
out = result.compute(device="cuda") # whole chain on the GPU
|
|
154
|
+
io.write(result, "out.zarr", device="cuda") # per-region GPU compute, streamed write
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Relationship to dask
|
|
158
|
+
|
|
159
|
+
dyna-zarr is not a general replacement for `dask.array`. It targets one job: memory-bounded read, transform, and write of large Zarr/TIFF arrays.
|
|
160
|
+
|
|
161
|
+
The core idea is to drop the task graph. Because every operation is a pull-based chain, where each output slice maps back to a bounded input read, there is no graph to build and no scheduler to run it. That keeps the engine small, keeps peak RAM bounded by `region_size_mb * max_workers` on the `io.write` path, and avoids scheduling overhead, which makes the read-transform-write pipeline efficient.
|
|
162
|
+
|
|
163
|
+
The tradeoff is that only operations that fit this slice-pushdown model belong in the chain: pointwise math, neighborhood/halo filters, streaming reductions, and structural reshaping. These are operations that are commonly used in image processing, which is what dyna-zarr is mainly built for. Operations that would need a global, data-dependent graph do not fit directly, and a few that do (such as non-associative reductions) trade extra reads or memory to stay correct.
|
|
164
|
+
|
|
165
|
+
Two more differences worth knowing:
|
|
166
|
+
|
|
167
|
+
- **Single machine, for now.** Parallelism today is threaded I/O within one process, plus the optional GPU path. There is no cluster or distributed execution yet; better and process-based parallelism is a possible future direction.
|
|
168
|
+
- **Narrower surface.** About 90 operations today, extended where the slice-pushdown model permits. Binary ops also need equal-shaped operands (no general broadcasting between differently shaped lazy arrays yet).
|
|
169
|
+
|
|
170
|
+
## Core components
|
|
171
|
+
|
|
172
|
+
- `io.read(source)` reads TIFF, Zarr v2, or Zarr v3 (local or remote) into a `DynamicArray`.
|
|
173
|
+
- `io.write(array, path, ...)` streams a `DynamicArray` to Zarr v2/v3 (chunks, sharding, compression, dtype cast, `region_size_mb`, `max_workers`, `device`).
|
|
174
|
+
- `operations` is the lazy op set above.
|
|
175
|
+
- `DynamicArray` is the pull-based lazy array (slicing, `.compute()`, operators, `.astype`/`.clip`/`.round`, ufunc protocol).
|
|
176
|
+
- `Codecs` is the compression configuration for Zarr v2 and v3.
|
|
177
|
+
|
|
178
|
+
## Requirements
|
|
179
|
+
|
|
180
|
+
- Python 3.11 or newer
|
|
181
|
+
- zarr 3.0.0+, numpy 1.20+, scipy 1.6+, tensorstore, tifffile
|
|
182
|
+
- Optional: CuPy (via the `gpu-cuXX` extras) for the GPU path
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=65", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools]
|
|
6
|
+
packages = ["dyna_zarr"]
|
|
7
|
+
|
|
8
|
+
[tool.setuptools.package-dir]
|
|
9
|
+
"" = "src"
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "dyna-zarr"
|
|
13
|
+
version = "0.0.2"
|
|
14
|
+
description = "A lightweight library for lazy operations on Zarr arrays without task graph overhead"
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
requires-python = ">=3.11"
|
|
17
|
+
license = {text = "MIT"}
|
|
18
|
+
authors = [
|
|
19
|
+
{name = "Bugra Oezdemir", email = "bugraa.ozdemir@gmail.com"},
|
|
20
|
+
]
|
|
21
|
+
keywords = ["zarr", "lazy", "array", "dask-alternative"]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 3 - Alpha",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Scientific/Engineering",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
dependencies = [
|
|
33
|
+
"zarr>=3.0.0",
|
|
34
|
+
"numpy>=1.20.0",
|
|
35
|
+
"scipy>=1.6.0",
|
|
36
|
+
"tensorstore",
|
|
37
|
+
"tifffile"
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=7.0",
|
|
43
|
+
"pytest-cov>=4.0",
|
|
44
|
+
"black>=23.0",
|
|
45
|
+
"isort>=5.0",
|
|
46
|
+
"mypy>=1.0",
|
|
47
|
+
"ruff>=0.1.0",
|
|
48
|
+
]
|
|
49
|
+
docs = [
|
|
50
|
+
"sphinx>=5.0",
|
|
51
|
+
"sphinx-rtd-theme>=1.0",
|
|
52
|
+
]
|
|
53
|
+
# GPU execution (optional). CuPy ships CUDA-version-specific wheels and pip CANNOT
|
|
54
|
+
# auto-detect your CUDA, so pick the extra that matches `nvidia-smi` (CUDA Version):
|
|
55
|
+
# pip install dyna-zarr[gpu-cu11] # CUDA 11.x
|
|
56
|
+
# pip install dyna-zarr[gpu-cu12] # CUDA 12.x
|
|
57
|
+
# pip install dyna-zarr[gpu-cu13] # CUDA 13.x (e.g. Blackwell)
|
|
58
|
+
# `[gpu]` is a convenience alias for CUDA 12.x. The [ctk] extra ships the NVRTC headers
|
|
59
|
+
# CuPy needs to JIT kernels (without it every kernel raises "Failed to find CUDA headers").
|
|
60
|
+
# Auto-detect alternative (may lag new CUDA releases): pip install cupy-wheel
|
|
61
|
+
gpu = ["cupy-cuda12x[ctk]"]
|
|
62
|
+
gpu-cu11 = ["cupy-cuda11x[ctk]"]
|
|
63
|
+
gpu-cu12 = ["cupy-cuda12x[ctk]"]
|
|
64
|
+
gpu-cu13 = ["cupy-cuda13x[ctk]"]
|
|
65
|
+
|
|
66
|
+
[project.urls]
|
|
67
|
+
Homepage = "https://github.com/bugraoezdemir/dyna_zarr"
|
|
68
|
+
Documentation = "https://dyna-zarr.readthedocs.io"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dyna_zarr: Lightweight lazy operations on Zarr arrays without task graph overhead.
|
|
3
|
+
|
|
4
|
+
A thin layer around zarr.Array that enables lazy/dynamic array processing,
|
|
5
|
+
similar to dask.array but without the task graph overhead, optimized for
|
|
6
|
+
large-scale (terabyte+) Zarr datasets.
|
|
7
|
+
|
|
8
|
+
Includes efficient TIFF reading via tifffile's zarr bridge with concurrent access support.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.0.2"
|
|
12
|
+
__author__ = "EuBI-Biohub"
|
|
13
|
+
|
|
14
|
+
# Import core classes
|
|
15
|
+
from .dynamic_array import DynamicArray, slice_array
|
|
16
|
+
from .codecs import Codecs
|
|
17
|
+
|
|
18
|
+
# Import operations (now a package/module of flat ops) and io namespaces
|
|
19
|
+
from . import operations
|
|
20
|
+
from .io import io
|
|
21
|
+
|
|
22
|
+
# Optional: expose tifffile utilities if available
|
|
23
|
+
try:
|
|
24
|
+
from .io import read_file
|
|
25
|
+
from .tiff_reader import read_tiff_lazy, open_tiff_zarr
|
|
26
|
+
__all__ = [
|
|
27
|
+
"DynamicArray",
|
|
28
|
+
"operations",
|
|
29
|
+
"io",
|
|
30
|
+
"slice_array",
|
|
31
|
+
"Codecs",
|
|
32
|
+
"read_file",
|
|
33
|
+
"read_tiff_lazy",
|
|
34
|
+
"open_tiff_zarr",
|
|
35
|
+
]
|
|
36
|
+
except ImportError:
|
|
37
|
+
__all__ = [
|
|
38
|
+
"DynamicArray",
|
|
39
|
+
"operations",
|
|
40
|
+
"io",
|
|
41
|
+
"slice_array",
|
|
42
|
+
"Codecs",
|
|
43
|
+
]
|