Skip to content

Commit dad40c1

Browse files
authored
Merge pull request #108 from thewtex/zarr-3
zarr 3
2 parents 765c1b8 + debef90 commit dad40c1

11 files changed

Lines changed: 4594 additions & 6062 deletions

File tree

.github/copilot-instructions.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copilot Instructions for multiscale-spatial-image
2+
3+
## Project Overview
4+
5+
This library generates multiscale, chunked, multi-dimensional spatial image data
6+
structures serializable to OME-NGFF format. It's built on Xarray DataTree with
7+
spatial-image Dataset nodes, creating image pyramids for visualization and
8+
analysis.
9+
10+
## Core Architecture
11+
12+
### Key Components
13+
14+
- **MultiscaleSpatialImage**: Xarray DataTree accessor
15+
(`@register_datatree_accessor("msi")`) providing NGFF-compatible multiscale
16+
operations
17+
- **to_multiscale()**: Main entry point for creating pyramids with configurable
18+
downsampling methods
19+
- **Methods enum**: Defines downsampling algorithms (XARRAY*COARSEN, ITK*_,
20+
DASK*IMAGE*_)
21+
- **Operations module**: Provides dataset operations that skip non-dimensional
22+
nodes
23+
24+
### Data Structure Pattern
25+
26+
```python
27+
# DataTree structure: /scale0, /scale1, /scale2, etc.
28+
# Each scale contains Dataset with same variable name as input
29+
multiscale = to_multiscale(image, [2, 4]) # Creates 3 scales total
30+
multiscale['scale0'].ds # Original resolution
31+
multiscale['scale1'].ds # 2x downsampled
32+
multiscale['scale2'].ds # 8x downsampled (2*4)
33+
```
34+
35+
### Zarr Integration Patterns
36+
37+
- **Always use `dimension_separator='/'`** for NGFF compliance
38+
- Support both Zarr v2 (DirectoryStore) and v3 (LocalStore) with fallback
39+
pattern in `_data.py`
40+
- NGFF metadata automatically added in `to_zarr()` method with coordinate
41+
transformations
42+
43+
## Development Workflows
44+
45+
### Environment Management (Pixi-based)
46+
47+
```bash
48+
pixi install -a # Install all environments
49+
pixi run -e test test # Run unit tests
50+
pixi run test-notebooks # Test example notebooks
51+
pixi shell # Activate development shell
52+
```
53+
54+
### Testing Patterns
55+
56+
- **Baseline comparison testing**: Use `verify_against_baseline()` for image
57+
output validation
58+
- **Test data**: IPFS-hosted via pooch, SHA256-verified downloads
59+
- **Notebook testing**: nbmake integration tests all examples
60+
- **Multiple backends**: Tests run against ITK, dask-image, and xarray methods
61+
62+
### Adding Test Data
63+
64+
```python
65+
# Temporary add this line to generate new baseline:
66+
store_new_image(dataset_name, baseline_name, multiscale)
67+
# Remove after running tests, then update data.tar.gz and SHA256 in _data.py
68+
```
69+
70+
## Critical Patterns
71+
72+
### Dimension Handling
73+
74+
- **skip_non_dimension_nodes decorator**: Essential for operations on DataTree
75+
root nodes without dimensions
76+
- **Default chunking**: Uses 64 for 3D (with 'z'), 256 for 2D, 1 for 't'
77+
dimension
78+
- **Coordinate transforms**: Scale and translation automatically computed from
79+
coordinate spacing
80+
81+
### Multi-Backend Support
82+
83+
```python
84+
# Pattern for optional dependencies
85+
try:
86+
from zarr.storage import DirectoryStore # Zarr v2
87+
except ImportError:
88+
from zarr.storage import LocalStore # Zarr v3
89+
```
90+
91+
### Downsampling Method Dispatch
92+
93+
- Each method in `to_multiscale/` subdirectory follows `_downsample_{method}`
94+
naming
95+
- Methods handle chunking alignment via `_align_chunks()` helper
96+
- Scale factors can be uniform int or per-dimension dict:
97+
`[2, {'x': 2, 'y': 4}]`
98+
99+
## Integration Points
100+
101+
### External Dependencies
102+
103+
- **spatial-image**: Base SpatialImage input type
104+
- **xarray-datatree**: Core DataTree functionality
105+
- **OME-NGFF**: Metadata standard compliance via `multiscales` attribute
106+
- **Optional**: ITK (medical imaging), dask-image (distributed), pyimagej
107+
108+
### File Patterns
109+
110+
- `multiscale_spatial_image.py`: Core accessor class with to_zarr() and
111+
operations
112+
- `to_multiscale/`: Downsampling method implementations
113+
- `operations/`: Dataset operations with dimension-aware decorators
114+
- `examples/`: Jupyter notebooks demonstrating usage patterns
115+
116+
## Key Conventions
117+
118+
- Use `promote_attrs=True` when converting DataArrays to Datasets to preserve
119+
metadata
120+
- Coordinate names follow spatial conventions: 't' (time), 'c' (channel),
121+
'x'/'y'/'z' (space)
122+
- Error handling validates scale factors against current image dimensions before
123+
processing
124+
- NGFF axes metadata includes type classification (time/channel/space) and
125+
optional units

.github/workflows/notebook-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on: [push, pull_request]
44

55
jobs:
66
run:
7-
runs-on: ubuntu-latest
7+
runs-on: ubuntu-22.04
88
name: Test notebooks with nbmake
99
steps:
1010
- uses: actions/checkout@v4

.github/workflows/test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ jobs:
88
strategy:
99
max-parallel: 5
1010
matrix:
11-
os: [ubuntu-22.04, windows-2022, macos-12]
12-
python-version: ["3.10", "3.11", "3.12"]
11+
os: [ubuntu-24.04, windows-2022, macos-13]
12+
python-version: ["3.10", "3.11", "3.12", "3.13"]
1313

1414
steps:
1515
- uses: actions/checkout@v4
@@ -20,7 +20,7 @@ jobs:
2020
- name: Install dependencies
2121
run: |
2222
python -m pip install --upgrade pip
23-
python -m pip install -e ".[test]"
23+
python -m pip install -e ".[test,itk,dask-image]"
2424
- name: Test with pytest
2525
run: |
2626
pytest --junitxml=junit/test-results.xml

README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,6 @@ method and `assign_coords`, equivalent to `xr.Dataset` `assign_coords` method.
153153
Store as an Open Microscopy Environment-Next Generation File Format ([OME-NGFF])
154154
/ [netCDF] [Zarr] store.
155155

156-
It is highly recommended to use `dimension_separator='/'` in the construction of
157-
the Zarr stores.
158-
159-
```python
160-
store = zarr.storage.DirectoryStore('multiscale.zarr', dimension_separator='/')
161-
multiscale.to_zarr(store)
162-
```
163-
164156
**Note**: The API is under development, and it may change until 1.0.0 is
165157
released. We mean it :-).
166158

examples/ConvertPyImageJDataset.ipynb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"source": [
1010
"import sys, os\n",
1111
"!conda install --yes --prefix {sys.prefix} -c conda-forge openjdk=8\n",
12-
"os.environ['JAVA_HOME'] = os.sep.join(sys.executable.split(os.sep)[:-2] + ['jre'])\n",
12+
"# In case of being already installed through pixi it should not be set to this path.\n",
13+
"if 'JAVA_HOME' not in os.environ:\n",
14+
" os.environ['JAVA_HOME'] = os.sep.join(sys.executable.split(os.sep)[:-2] + ['jre'])\n",
1315
"!{sys.executable} -m pip install multiscale-spatial-image matplotlib zarr pyimagej"
1416
]
1517
},

multiscale_spatial_image/multiscale_spatial_image.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
import numpy as np
55
from collections.abc import MutableMapping, Hashable
66
from pathlib import Path
7-
from zarr.storage import BaseStore
7+
import zarr.storage
88
from multiscale_spatial_image.operations import (
99
transpose,
1010
reindex_data_arrays,
1111
assign_coords,
1212
)
1313

14+
# Zarr Python 3
15+
if hasattr(zarr.storage, "StoreLike"):
16+
StoreLike = zarr.storage.StoreLike
17+
else:
18+
StoreLike = Union[MutableMapping, str, Path, zarr.storage.BaseStore]
19+
1420

1521
@register_datatree_accessor("msi")
1622
class MultiscaleSpatialImage:
@@ -33,7 +39,7 @@ def __init__(self, xarray_obj: DataTree):
3339

3440
def to_zarr(
3541
self,
36-
store: Union[MutableMapping, str, Path, BaseStore],
42+
store: StoreLike,
3743
mode: str = "w",
3844
encoding=None,
3945
**kwargs,
@@ -43,7 +49,7 @@ def to_zarr(
4349
4450
Metadata is added according the OME-NGFF standard.
4551
46-
store : MutableMapping, str or Path, or zarr.storage.BaseStore
52+
store : StoreLike
4753
Store or path to directory in file system
4854
mode : {{"w", "w-", "a", "r+", None}, default: "w"
4955
Persistence mode: “w” means create (overwrite if exists); “w-” means create (fail if exists);
@@ -125,6 +131,9 @@ def to_zarr(
125131
ngff_metadata = {"multiscales": multiscales, "multiscaleSpatialImageVersion": 1}
126132
self._dt.ds = self._dt.ds.assign_attrs(**ngff_metadata)
127133

134+
# Ensure zarr v2 format for NGFF v0.4
135+
if "zarr_format" not in kwargs:
136+
kwargs["zarr_format"] = 2
128137
self._dt.to_zarr(store, mode=mode, **kwargs)
129138

130139
def transpose(self, *dims: Hashable) -> DataTree:

multiscale_spatial_image/operations/operations.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,22 @@ def transpose(ds: Dataset, *args: Any, **kwargs: Any) -> Dataset:
1515

1616
@skip_non_dimension_nodes
1717
def reindex_data_arrays(ds: Dataset, *args: Any, **kwargs: Any) -> Dataset:
18-
return ds["image"].reindex(*args, **kwargs).to_dataset()
18+
# Extract the first argument as indexers, and pass the rest as keyword arguments
19+
if args:
20+
indexers = args[0]
21+
# Map positional arguments to their parameter names
22+
reindex_kwargs = {}
23+
if len(args) > 1:
24+
reindex_kwargs["method"] = args[1]
25+
if len(args) > 2:
26+
reindex_kwargs["tolerance"] = args[2]
27+
if len(args) > 3:
28+
reindex_kwargs["copy"] = args[3]
29+
if len(args) > 4:
30+
reindex_kwargs["fill_value"] = args[4]
31+
# Add any additional keyword arguments
32+
reindex_kwargs.update(kwargs)
33+
return ds["image"].reindex(indexers, **reindex_kwargs).to_dataset()
34+
else:
35+
# Fall back to original behavior if no arguments
36+
return ds["image"].reindex(**kwargs).to_dataset()

0 commit comments

Comments
 (0)