Chunk-aware fancy indexing for dask arrays backed by zarr
Project description
fancy-dask
Chunk-aware fancy indexing for dask arrays backed by zarr.
Dask arrays don't natively support multi-axis fancy indexing operations that NumPy users rely on. This package provides a wrapper to bridge the gap. fancy-dask provides transparent NumPy-style fancy indexing (__getitem__ / __setitem__ with integer-array and boolean-mask indices) for dask.Array objects backed by zarr stores — a combination that vanilla dask does not support natively.
The primary use-case is interactive annotation tools such as napari, where a labels layer holds a large N-D zarr array and the paint tool issues pixel-coordinate fancy indices that must be read and written efficiently without loading the entire array into memory. Other use-cases include scientific analyses with irregular sampling patterns or sparse/masked data access in large datasets, or any workflows where users want the expressiveness of NumPy indexing on large out-of-core arrays.
Features
- Fancy reads stay inside the dask graph — each touched dask block is accessed via
array.blocks[coord], preserving lazy evaluation, task fusion, and parallel scheduling. - Chunk-coalesced writes — fancy indices are grouped by zarr chunk; each chunk is read once, modified in memory, then written back. No redundant I/O.
- Parallel writes — disjoint zarr chunks are written via
ThreadPoolExecutor. - Full NumPy index semantics — integer arrays, boolean masks (1-D and N-D), slices, integers, ellipsis, and
np.newaxisare all supported, including multi-axis fancy indexing. - Napari integration —
FancyDaskLabelsLayerextendsFancyDaskwith debounced, stroke-batched writes, undo (configurable viamax_undo_steps),preserve_labelssupport, and read-after-write consistency mode for interactive annotation workflows.
Requirements
| Package | Version |
|---|---|
| Python | ≥ 3.11 (3.13 recommended) |
| zarr | ≥ 3.1 |
| dask | ≥ 2025.9 |
| numpy | ≥ 2.0 |
| napari[all] | >=0.6 (optional, for napari_integration, can work with napari[pyqt6] >=0.6) |
Installation
From PyPI
# Core install
pip install fancy-dask
# With napari (for FancyDaskLabelsLayer)
pip install fancy-dask[napari]
# With dev/test dependencies
pip install fancy-dask[dev]
# With all dependencies
pip install fancy-dask[all]
From source
git clone https://github.com/sourabhpalande/fancy-dask.git
cd fancy-dask
pip install . # or .[napari], .[dev], .[all] for extras
# For editable install (recommended for development)
pip install -e . # or .[napari], .[dev], .[all] for extras
Quick Start
Reads
import dask.array as da
import numpy as np
from fancy_dask import FancyDask
# Open a zarr-backed dask array
dask_array = da.from_zarr("path/to/data.zarr")
fancy = FancyDask(dask_array)
# Slice — forwarded to dask directly (zero overhead)
block = fancy[100:200, :, :] # lazy da.Array
# Integer fancy index along axis 0
rows = np.array([0, 50, 100, 500, 999])
result = fancy[rows, :] # lazy da.Array, shape (5, ...)
values = result.compute() # trigger I/O
# Boolean mask
mask = some_array > threshold
result = fancy[mask, :] # 1-D boolean mask on axis 0
# Multi-axis fancy index (element-wise, not outer product)
row_idx = np.array([0, 10, 20])
col_idx = np.array([5, 15, 25])
result = fancy[row_idx, col_idx, :] # shape (3, depth)
Writes
# Fancy write — chunk-coalesced, parallel
fancy[rows, :] = np.full((len(rows), width, depth), fill_value=42)
# Scalar fill
fancy[row_idx, col_idx, :] = 0
Napari Integration
import napari
import dask.array as da
from fancy_dask.napari_integration import FancyDaskLabelsLayer
labels = da.from_zarr("segmentation.zarr")
fancy_labels = FancyDaskLabelsLayer(labels, debounce_ms=100)
viewer = napari.Viewer()
layer = viewer.add_labels(fancy_labels, name="Labels")
# Connect auto stroke-batching and preserve_labels sync
fancy_labels.connect_to_layer(layer)
Workaround for Non-Zarr-Backed Dask Arrays
The fancy-dask implementation requires Dask arrays to be backed by Zarr storage. It will not work with Dask arrays backed by other storage types (e.g., NumPy, HDF5, or in-memory arrays). But you can convert these arrays to Zarr format to use fancy-dask features. For example, for an HDF5-backed Dask array:
import dask.array as da
import h5py
# Open HDF5 file (requires h5py)
hdf5_arr = da.from_array(h5py.File('data.hdf5')['dataset'], chunks=(100, 100))
# Convert to Zarr
hdf5_arr.to_zarr('mydata.zarr')
zarr_backed = da.from_zarr('mydata.zarr')
However, memory and disk space availability must be considered when using this workaround.
How It Works
Reads
FancyDask.__getitem__(fancy_indices)
↓
_preprocess_indices() # normalise / classify
↓
_map_fancy_to_dask_blocks() # searchsorted → per-block groups
↓
for each BlockGroup:
block = array.blocks[coord] # lazy da.Array
part = block[local_index_tuple] # lazy, dask-native
↓
da.concatenate(parts, axis=fancy_axis) # lazy result
↓ (if unsorted)
result[np.argsort(output_positions)] # restore input order
Writes
FancyDask.__setitem__(fancy_indices, value)
↓
_map_fancy_to_zarr_chunks() # group indices by zarr chunk
↓
for each ZarrChunkGroup (parallel via ThreadPoolExecutor):
chunk = zarr_array[zarr_slice] # one zarr read
chunk[local_index_tuple] = value[...] # numpy assignment
zarr_array[zarr_slice] = chunk # one zarr write
See docs/design.md for the full architecture and docs/performance_breakdown.md for benchmark results.
Documentation
| File | Description |
|---|---|
| docs/fancy_dask.md | FancyDask class reference, usage examples, optimizations, limitations |
| docs/napari_integration.md | FancyDaskLabelsLayer reference, napari usage, debounce tuning |
| docs/design.md | Architecture and data-structure design |
| docs/performance_breakdown.md | Benchmark results and analysis |
| docs/future_optimizations.md | Known bottlenecks and planned improvements |
Examples
| Script | Description |
|---|---|
| examples/usage_basic.py | Core read/write API walkthrough |
| examples/usage_napari.py | Napari labels layer integration |
| examples/usage_zarr_workaround.py | Zarr workaround patterns |
Run any example directly:
python examples/usage_basic.py
Running Tests
Test zarr fixtures under tests/test_data/ are created automatically when missing
or invalid, so you don't need to commit large .zarr datasets to run the suite.
# Unit tests
pytest tests/unit/
# Performance benchmarks
pytest tests/performance/ --benchmark-only
# All tests with coverage
pytest --cov=fancy_dask tests/
License
MIT
Citation
If you use this package in your research, please cite:
@software{fancy_dask,
author = {Palande, Sourabh},
title = {fancy-dask: Efficient Fancy Indexing Wrapper for Dask Arrays backed by Zarr Storage},
year = {2026},
url = {https://github.com/sourabhpalande/fancy-dask}
}
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fancy_dask-0.1.1.tar.gz.
File metadata
- Download URL: fancy_dask-0.1.1.tar.gz
- Upload date:
- Size: 38.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
90127d97c1ba893fe8ea6c79eacae8aafc3747f05425cde3e6dc21b0861da238
|
|
| MD5 |
5fb0f5b4900a99abc6894aa4aae280c5
|
|
| BLAKE2b-256 |
defb06b4408a398ded591c905f4aa07cfecf10bbb3d141d53e8ed8b95c458927
|
Provenance
The following attestation bundles were made for fancy_dask-0.1.1.tar.gz:
Publisher:
publish.yml on sourabhpalande/fancy-dask
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fancy_dask-0.1.1.tar.gz -
Subject digest:
90127d97c1ba893fe8ea6c79eacae8aafc3747f05425cde3e6dc21b0861da238 - Sigstore transparency entry: 1001228845
- Sigstore integration time:
-
Permalink:
sourabhpalande/fancy-dask@8ac553d56ae0b8044e7597ebed4dc48905924b1a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/sourabhpalande
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8ac553d56ae0b8044e7597ebed4dc48905924b1a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file fancy_dask-0.1.1-py3-none-any.whl.
File metadata
- Download URL: fancy_dask-0.1.1-py3-none-any.whl
- Upload date:
- Size: 35.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a58a97d932dac00afcbfb59f7de78381df154cce5c9991f1282c8e930f25477d
|
|
| MD5 |
ee2cef5e31b79dcb94228a81fbc5abc0
|
|
| BLAKE2b-256 |
f8a9abc7ea702c90267e7e43335868c5d6f361f4ce4fbdd5f9c3d2f7ef4eb7c5
|
Provenance
The following attestation bundles were made for fancy_dask-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on sourabhpalande/fancy-dask
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fancy_dask-0.1.1-py3-none-any.whl -
Subject digest:
a58a97d932dac00afcbfb59f7de78381df154cce5c9991f1282c8e930f25477d - Sigstore transparency entry: 1001228892
- Sigstore integration time:
-
Permalink:
sourabhpalande/fancy-dask@8ac553d56ae0b8044e7597ebed4dc48905924b1a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/sourabhpalande
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8ac553d56ae0b8044e7597ebed4dc48905924b1a -
Trigger Event:
workflow_dispatch
-
Statement type: