Skip to content

fix: to_datatree reads exact level pixels and survives pickling - #87

Merged
Mr-Milk merged 2 commits into
mainfrom
claude/spatialdata-wsi-zarr-research-27c871
Oct 1, 2026
Merged

Mr-Milk merged 2 commits into
mainfrom
claude/spatialdata-wsi-zarr-research-27c871

Conversation

@Mr-Milk

@Mr-Milk Mr-Milk commented Oct 1, 2026

Copy link
Copy Markdown
Member

Summary

open_wsi(attach_images=True) builds the slide as a lazy multiscale SpatialData image through wsidata.reader.to_datatree. That path used a hand-rolled SlideZarrStore that zarr never actually opened: every chunk ran asyncio.run inside a dask.delayed lambda, and block origins used a rounded downsample. This replaces it with dask.array.map_blocks over reader.get_region.

  • Exact pixels at non-integer downsamples. Readers map level-0 offsets to the level with int(x / ds) (tiffslide, fastslide, bioformats, isyntax), so block origins now use ceil(x * ds). On GTEX-1117F-0526.svs (level 1 at downsample 4.0005), every TiffSlide level now equals one native read; before, 5.7M of the 24.8M level-1 pixels differed. OpenSlide paints at fractional offsets, so its blocks stay within 1/ds px either way.
  • Pickles. The graph no longer embeds a local lambda, so the image survives the standard pickle used by spawned workers.
  • Smaller graph. 400 tasks instead of 2000 at level 0 for that slide.
  • Same output. Node names (scale{i}/image), dims c, y, x, c = ["r", "g", "b"], y/x coords, chunks, per-level transforms and attrs = asdict(reader.properties) are unchanged. sopa.io.wsi now reads slides through open_wsi(attach_images=True).to_spatialdata(), so this layout is a downstream contract.

Why not a zarr store

spatialdata has no WSI reader or reader plugin system, and read_zarr only accepts local stores (main raises TypeError for anything else), so a virtual zarr store over a slide cannot be read by spatialdata anyway. A dask-backed DataTree already is a native image element. Keeping the reader in the map_blocks task arguments also matches the dask-task soft-link design discussed in scverse/2026_04_hackathon_padua#12, should upstream adopt it. Background: scverse/spatialdata#856, scverse/spatialdata#1161.

Tests

New in tests/test_readers.py:

  • test_to_datatree_levels_match_one_read: every level equals a native whole-level read (fails on the old store)
  • test_to_datatree_pickles (fails on the old store)
  • test_attached_image_layout: the layout sopa relies on
  • test_attached_image_is_not_written: write() never copies the attached image into the store

uv run pytest: 131 passed, 14 skipped. ruff check and ruff format --check are clean.

Build each pyramid level with dask map_blocks over reader.get_region
instead of a hand-rolled zarr store that zarr never opened. Block
origins use ceil(x * ds): readers map level-0 offsets to the level
with int(x / ds), so the old rounded downsample read a row or column
early at non-integer downsamples. The graph no longer embeds a local
lambda, so the image pickles, and it has a fifth of the tasks.

The output layout is unchanged; sopa.io.wsi depends on it.
…i-zarr-research-27c871

# Conflicts:
#	wsidata/reader/__init__.py
Copilot AI balanced review requested due to automatic review settings October 1, 2026 08:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The universal ceiling rule misaligns OpenSlide blocks at non-integer downsample factors.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Replaces the unused virtual Zarr implementation with a smaller, pickle-safe Dask-backed multiscale image reader.

Changes:

  • Reads slide levels lazily through map_blocks.
  • Adds pixel-equivalence, serialization, layout, and persistence tests.
  • Updates the public reader export.
File Description
wsidata/​reader/​_reader_datatree_zarr_v3.py Removes the virtual Zarr implementation.
wsidata/​reader/​_datatree.py Adds Dask-backed DataTree construction.
wsidata/​reader/​__init__.py Exports the new implementation.
tests/​test_readers.py Adds regression and integration coverage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread wsidata/reader/_datatree.py
@Mr-Milk
Mr-Milk merged commit 683d185 into main Oct 1, 2026
16 checks passed
@Mr-Milk
Mr-Milk deleted the claude/spatialdata-wsi-zarr-research-27c871 branch October 1, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants