diff --git a/.github/workflows/konfai_apps_ci.yml b/.github/workflows/konfai_apps_ci.yml
index 3a82cb35..9443f9b4 100644
--- a/.github/workflows/konfai_apps_ci.yml
+++ b/.github/workflows/konfai_apps_ci.yml
@@ -41,7 +41,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
- python-version: ["3.10", "3.11", "3.12", "3.13"]
+ python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
@@ -70,9 +70,6 @@ jobs:
run: |
pip install -e ./konfai-apps
- - name: Pin gloo to the loopback interface on macOS
- if: runner.os == 'macOS'
- run: echo "GLOO_SOCKET_IFNAME=lo0" >> "$GITHUB_ENV"
- name: Run apps tests
run: |
python -m pytest -q konfai-apps/tests
diff --git a/.github/workflows/konfai_ci.yml b/.github/workflows/konfai_ci.yml
index 2e9c6300..4f9fc6c7 100644
--- a/.github/workflows/konfai_ci.yml
+++ b/.github/workflows/konfai_ci.yml
@@ -32,7 +32,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
- python-version: ["3.10", "3.11", "3.12", "3.13"]
+ python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
diff --git a/.github/workflows/konfai_mcp_ci.yml b/.github/workflows/konfai_mcp_ci.yml
index c5ae51a4..cc0961c9 100644
--- a/.github/workflows/konfai_mcp_ci.yml
+++ b/.github/workflows/konfai_mcp_ci.yml
@@ -25,7 +25,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13"]
+ python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
diff --git a/.github/workflows/konfai_studio_ci.yml b/.github/workflows/konfai_studio_ci.yml
index 12250213..356c2dd4 100644
--- a/.github/workflows/konfai_studio_ci.yml
+++ b/.github/workflows/konfai_studio_ci.yml
@@ -25,7 +25,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13"]
+ python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index fd0f945a..10c26738 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -114,7 +114,7 @@ jobs:
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
- python-version: "3.10"
+ python-version: "3.11"
- name: Set up Node
if: matrix.name == 'konfai-studio'
diff --git a/README.md b/README.md
index 7a546272..8bb3a6e4 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
From images on disk to reproducible experiments, production inference, and reusable clinical applications.
-
+
diff --git a/apps/impact_reg/README.md b/apps/impact_reg/README.md
index 9f8d0a6a..e53b6eb6 100644
--- a/apps/impact_reg/README.md
+++ b/apps/impact_reg/README.md
@@ -1,6 +1,6 @@
[](https://www.apache.org/licenses/LICENSE-2.0)
[](https://pypi.org/project/impact_reg_konfai/)
-[](https://www.python.org/)
+[](https://www.python.org/)
[](https://github.com/fideus-labs/KonfAI/actions/workflows/konfai_ci.yml)
[](https://www.arxiv.org/abs/2508.09823)
diff --git a/apps/impact_reg/pyproject.toml b/apps/impact_reg/pyproject.toml
index c8edeae9..f985b5ba 100644
--- a/apps/impact_reg/pyproject.toml
+++ b/apps/impact_reg/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version", "dependencies"]
description = "Fast and lightweight CLI for pairwise registration workflows with IMPACT-Reg presets through the KonfAI runtime."
readme = "README.md"
license = { file = "LICENSE" }
-requires-python = ">=3.10"
+requires-python = ">=3.11"
authors = [
{ name = "Valentin Boussot", email = "boussot.v@gmail.com" }
diff --git a/apps/impact_reg/tests/unit/test_displacement_field_io.py b/apps/impact_reg/tests/unit/test_displacement_field_io.py
index 93843273..44001078 100644
--- a/apps/impact_reg/tests/unit/test_displacement_field_io.py
+++ b/apps/impact_reg/tests/unit/test_displacement_field_io.py
@@ -38,7 +38,7 @@
)
from konfai.utils.errors import TransformError # noqa: E402
from konfai.utils.ITK import read_displacement_field # noqa: E402
-from konfai.utils.ome_zarr import _zarr_v3_available, is_displacement_field, write_ome_zarr # noqa: E402
+from konfai.utils.ome_zarr import is_displacement_field, write_ome_zarr # noqa: E402
def _write_displacement_field(field: "sitk.Image", dest: Path) -> None:
@@ -59,13 +59,6 @@ def _write_displacement_field(field: "sitk.Image", dest: Path) -> None:
sitk.WriteImage(field, str(dest))
-# The OME-Zarr side of these tests writes an RFC-5 field, a zarr v3 store that zarr 2.x
-# (Python 3.10) cannot write.
-pytestmark = pytest.mark.skipif(
- not _zarr_v3_available(),
- reason="NGFF RFC-5 displacement fields need a zarr v3 store (zarr>=3, Python>=3.11)",
-)
-
SPACING = (1.5, 1.5, 2.0)
ORIGIN = (7.0, -3.0, 10.0)
# A 90 deg in-plane rotation: non-identity, so a Direction dropped on the store round-trip changes
diff --git a/apps/impact_reg/tests/unit/test_orchestration.py b/apps/impact_reg/tests/unit/test_orchestration.py
index bbdc9e28..24615b99 100644
--- a/apps/impact_reg/tests/unit/test_orchestration.py
+++ b/apps/impact_reg/tests/unit/test_orchestration.py
@@ -219,9 +219,6 @@ def test_register_reads_a_store_moving_against_an_itk_field(tmp_path: Path) -> N
per group, so a caller's OME-Zarr moving registers against the ``.mha`` field every published
preset declares."""
ome_zarr = pytest.importorskip("konfai.utils.ome_zarr")
- if not ome_zarr._zarr_v3_available():
- pytest.skip("writing an OME-Zarr moving needs zarr 3")
-
volume = np.arange(8**3, dtype=np.float32).reshape(8, 8, 8)
moving = tmp_path / "moving.ome.zarr"
ome_zarr.write_ome_zarr(moving, volume[None], spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0))
diff --git a/apps/impact_seg/README.md b/apps/impact_seg/README.md
index 71ce9258..87703409 100644
--- a/apps/impact_seg/README.md
+++ b/apps/impact_seg/README.md
@@ -1,6 +1,6 @@
[](https://www.apache.org/licenses/LICENSE-2.0)
[](https://pypi.org/project/impact_seg_konfai/)
-[](https://www.python.org/)
+[](https://www.python.org/)
[](https://github.com/fideus-labs/KonfAI/actions/workflows/konfai_ci.yml)
[](https://www.arxiv.org/abs/2508.09823)
diff --git a/apps/impact_seg/pyproject.toml b/apps/impact_seg/pyproject.toml
index be1a1eeb..74b4da35 100644
--- a/apps/impact_seg/pyproject.toml
+++ b/apps/impact_seg/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version", "dependencies"]
description = "Fast and lightweight CLI for anatomical segmentation with 2.5D U-Net models using a residual encoder within the KonfAI framework."
readme = "README.md"
license = { file = "LICENSE" }
-requires-python = ">=3.10"
+requires-python = ">=3.11"
authors = [
{ name = "Valentin Boussot", email = "boussot.v@gmail.com" }
diff --git a/apps/impact_synth/README.md b/apps/impact_synth/README.md
index f9402413..fd2b8249 100644
--- a/apps/impact_synth/README.md
+++ b/apps/impact_synth/README.md
@@ -1,6 +1,6 @@
[](https://www.apache.org/licenses/LICENSE-2.0)
[](https://pypi.org/project/impact_synth_konfai/)
-[](https://www.python.org/)
+[](https://www.python.org/)
[](https://github.com/fideus-labs/KonfAI/actions/workflows/konfai_ci.yml)
[](https://arxiv.org/abs/2510.21358)
diff --git a/apps/impact_synth/pyproject.toml b/apps/impact_synth/pyproject.toml
index bf43a77a..dd5a5b8a 100644
--- a/apps/impact_synth/pyproject.toml
+++ b/apps/impact_synth/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version", "dependencies"]
description = "Fast and lightweight CLI for synthetic CT generation using IMPACT-Synth models within the KonfAI framework."
readme = "README.md"
license = { file = "LICENSE" }
-requires-python = ">=3.10"
+requires-python = ">=3.11"
authors = [
{ name = "Valentin Boussot", email = "boussot.v@gmail.com" }
diff --git a/apps/mrsegmentator/README.md b/apps/mrsegmentator/README.md
index 3c88b075..dfd63263 100644
--- a/apps/mrsegmentator/README.md
+++ b/apps/mrsegmentator/README.md
@@ -1,6 +1,6 @@
[](https://www.apache.org/licenses/LICENSE-2.0)
[](https://pypi.org/project/mrsegmentator-konfai/)
-[](https://www.python.org/)
+[](https://www.python.org/)
[](https://github.com/fideus-labs/KonfAI/actions/workflows/konfai_ci.yml)
[](https://www.arxiv.org/abs/2508.09823)
diff --git a/apps/mrsegmentator/pyproject.toml b/apps/mrsegmentator/pyproject.toml
index 188fee51..7fc221c5 100644
--- a/apps/mrsegmentator/pyproject.toml
+++ b/apps/mrsegmentator/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version", "dependencies"]
description = "Fast and lightweight MRSegmentator CLI powered by the KonfAI framework."
readme = "README.md"
license = { file = "LICENSE" }
-requires-python = ">=3.10"
+requires-python = ">=3.11"
authors = [
{ name = "Valentin Boussot", email = "boussot.v@gmail.com" }
diff --git a/apps/totalsegmentator/README.md b/apps/totalsegmentator/README.md
index cdc03d36..fcca9e22 100644
--- a/apps/totalsegmentator/README.md
+++ b/apps/totalsegmentator/README.md
@@ -1,6 +1,6 @@
[](https://www.apache.org/licenses/LICENSE-2.0)
[](https://pypi.org/project/totalsegmentator-konfai/)
-[](https://www.python.org/)
+[](https://www.python.org/)
[](https://github.com/fideus-labs/KonfAI/actions/workflows/konfai_ci.yml)
[](https://github.com/fideus-labs/KonfAI/actions/workflows/konfai_apps_ci.yml)
[](https://www.arxiv.org/abs/2508.09823)
diff --git a/apps/totalsegmentator/pyproject.toml b/apps/totalsegmentator/pyproject.toml
index cd13085d..098328dc 100644
--- a/apps/totalsegmentator/pyproject.toml
+++ b/apps/totalsegmentator/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version", "dependencies"]
description = "Fast and lightweight TotalSegmentator CLI powered by the KonfAI framework."
readme = "README.md"
license = { file = "LICENSE" }
-requires-python = ">=3.10"
+requires-python = ">=3.11"
authors = [
{ name = "Valentin Boussot", email = "boussot.v@gmail.com" }
diff --git a/docs/source/development.md b/docs/source/development.md
index a6609b5f..2ac453ab 100644
--- a/docs/source/development.md
+++ b/docs/source/development.md
@@ -12,7 +12,7 @@ cloned KonfAI checkout.**
## Prerequisites
-- **Python 3.10 or later**: the minimum version declared in `pyproject.toml`
+- **Python 3.11 or later**: the minimum version declared in `pyproject.toml`
- **Pixi**: install once with:
```bash
@@ -140,7 +140,7 @@ pixi run test -- tests/unit/test_config.py -v
### What CI runs
The GitHub Actions workflow in `.github/workflows/konfai_ci.yml` runs `pytest`
-across Python `3.10` to `3.13` on Linux, macOS, and Windows.
+across Python `3.11` to `3.13` on Linux, macOS, and Windows.
### The konfai-apps test suite
diff --git a/docs/source/getting-started/installation.md b/docs/source/getting-started/installation.md
index 9f784fb7..ca331ba7 100644
--- a/docs/source/getting-started/installation.md
+++ b/docs/source/getting-started/installation.md
@@ -1,6 +1,6 @@
# Installation
-KonfAI needs **Python 3.10 or newer**. This is the line most people want:
+KonfAI needs **Python 3.11 or newer**. This is the line most people want:
```bash
python -m pip install "konfai[imaging]"
diff --git a/docs/source/index.rst b/docs/source/index.rst
index bb6ef713..a9122acf 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -24,7 +24,7 @@ KonfAI
pip install "konfai[imaging]"
Apache-2.0
- Python 3.10+
+ Python 3.11+
diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst
index 17a0fe5b..745d1a6f 100644
--- a/docs/source/quickstart.rst
+++ b/docs/source/quickstart.rst
@@ -17,7 +17,7 @@ first, so it is the shortest path to seeing KonfAI work.
Install
-------
-You need Python 3.10 or newer. A GPU makes it faster, and every command below
+You need Python 3.11 or newer. A GPU makes it faster, and every command below
works with ``--cpu 1`` instead of ``--gpu 0``.
.. code-block:: bash
diff --git a/konfai-apps/konfai_apps/app_server.py b/konfai-apps/konfai_apps/app_server.py
index 361aeb59..15ead45a 100644
--- a/konfai-apps/konfai_apps/app_server.py
+++ b/konfai-apps/konfai_apps/app_server.py
@@ -570,7 +570,7 @@ async def sse_log_stream(job: Job):
while True:
try:
line = await asyncio.wait_for(job.log_q.get(), timeout=SSE_HEARTBEAT_S)
- except asyncio.TimeoutError:
+ except TimeoutError:
# Keep the connection alive during quiet periods without signalling
# completion. Stop only once the job itself has terminated (guards
# against a completion marker dropped from a saturated queue).
diff --git a/konfai-apps/pyproject.toml b/konfai-apps/pyproject.toml
index e7f24073..549f7035 100644
--- a/konfai-apps/pyproject.toml
+++ b/konfai-apps/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version", "dependencies"]
description = "Standalone KonfAI Apps package with local and remote app execution"
readme = "README.md"
license = { file = "LICENSE" }
-requires-python = ">=3.10"
+requires-python = ">=3.11"
authors = [
{ name = "Valentin Boussot", email = "boussot.v@gmail.com" }
diff --git a/konfai-mcp/konfai_mcp/catalog.py b/konfai-mcp/konfai_mcp/catalog.py
index e1f1985a..f5a5670d 100644
--- a/konfai-mcp/konfai_mcp/catalog.py
+++ b/konfai-mcp/konfai_mcp/catalog.py
@@ -29,7 +29,6 @@
import importlib
import inspect
import os
-import types
from typing import Any
from konfai_mcp.classpaths import public_module
@@ -115,10 +114,6 @@ def _list_subclasses(module_path: str, base_name: str) -> list[dict[str, Any]]:
base = getattr(module, base_name)
components: list[dict[str, Any]] = []
for name, obj in inspect.getmembers(module, inspect.isclass):
- # A subscripted builtin generic (konfai.data.geometry.SpatialStages) passes isclass on
- # Python 3.10 but is not a class there, and issubclass refuses it.
- if isinstance(obj, types.GenericAlias):
- continue
if obj is base or not issubclass(obj, base):
continue
if inspect.isabstract(obj) or name.startswith("_"):
diff --git a/konfai-mcp/konfai_mcp/server_experiments.py b/konfai-mcp/konfai_mcp/server_experiments.py
index 0eb7aa8c..c3bf16f6 100644
--- a/konfai-mcp/konfai_mcp/server_experiments.py
+++ b/konfai-mcp/konfai_mcp/server_experiments.py
@@ -18,7 +18,7 @@
import tempfile
from dataclasses import dataclass
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Literal, cast
@@ -62,7 +62,7 @@ class SessionService(DatasetInspectionMixin, MetricsServiceMixin):
def _isoformat(self, timestamp: float | None) -> str | None:
if timestamp is None:
return None
- return datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat()
+ return datetime.fromtimestamp(timestamp, tz=UTC).isoformat()
def session_name(self) -> str:
return self.workspace_layout.current_session or "default"
diff --git a/konfai-mcp/pyproject.toml b/konfai-mcp/pyproject.toml
index 3772e0c3..479a5c58 100644
--- a/konfai-mcp/pyproject.toml
+++ b/konfai-mcp/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version"]
description = "Standalone MCP server package for KonfAI experimentation workflows"
readme = "README.md"
license = "Apache-2.0"
-requires-python = ">=3.10"
+requires-python = ">=3.11"
authors = [
{ name = "Valentin Boussot", email = "boussot.v@gmail.com" }
diff --git a/konfai-mcp/tests/test_mcp_server_apps.py b/konfai-mcp/tests/test_mcp_server_apps.py
index 18fe4371..841f43c8 100644
--- a/konfai-mcp/tests/test_mcp_server_apps.py
+++ b/konfai-mcp/tests/test_mcp_server_apps.py
@@ -859,8 +859,8 @@ async def scenario() -> dict[str, dict]:
properties = schemas[name].get("properties", {})
missing = required_params - set(properties)
assert not missing, f"{name} lost parameters on the wire: {missing}"
- # pydantic places a union param's description inside anyOf on some versions (the 3.10 leg):
- # documented means present anywhere in the property's subtree.
+ # pydantic places a union param's description inside anyOf on some versions: documented
+ # means present anywhere in the property's subtree.
undocumented = [p for p in required_params if '"description"' not in json.dumps(properties[p])]
assert not undocumented, f"{name} has undocumented parameters: {undocumented}"
diff --git a/konfai/data/case_reduction.py b/konfai/data/case_reduction.py
index b25bae63..c20305fc 100644
--- a/konfai/data/case_reduction.py
+++ b/konfai/data/case_reduction.py
@@ -113,6 +113,12 @@ class ReductionPlan:
#: whatever the cohort's size, because ``_fold`` accumulates the members one after another, so
#: only one chain is ever replaying.
chain_multiple: float = 0.0
+ #: The source window ONE member's region pulls (:attr:`~konfai.data.patching.BlockReads`
+ #: ``widest_pull``). For a chain that resamples, a region's source is not the region -- a
+ #: rotated or scaled map reaches a box around it -- and the chain holds that box while it
+ #: produces the region. Charged ONCE, like the reads below: the members are folded in turn, so
+ #: one chain is pulling at a time. Zero for a chain whose region is its own source.
+ pull_bytes: int = 0
#: What ONE member's region makes the store decode ABOVE the window it asked for
#: (:meth:`~konfai.data.patching.DatasetManager.region_reads`). A chunked backend decodes whole
#: blocks, so below one stored block this is the SAME figure at every height: it is charged flat
@@ -180,15 +186,15 @@ def peak_bytes(self) -> int:
# one region holds, so the peak is the same whether there are one or two passes.
member_bytes = self._region_bytes(self.source_channels or self.channels)
members = self.buffered_regions * member_bytes
- # The chain replaying a member holds its own buffers beside the region it lands, and it is
- # the members' own width it holds them at. Once: the members are accumulated in turn.
- # Plus what the store decodes to serve ONE member's region: the chain's buffers are built
- # over the window it asked for, the decode materialises the blocks that window falls in, and
- # the two are resident together. One read is in flight at a time (the members accumulate in
- # turn), so it is charged once.
+ # Beside the buffered regions, the one replaying chain holds three things at once: the
+ # source window it pulled, its own working buffers, and what the store decoded above that
+ # window. The buffers are built over the larger of the window and the region it lands, the
+ # sweep prices its blocks the same way, and all three are charged once because the members
+ # are folded in turn -- one chain pulls, allocates and decodes at a time.
return int(
members * (1 + self.working_multiple)
- + self.chain_multiple * member_bytes
+ + self.chain_multiple * max(member_bytes, self.pull_bytes)
+ + self.pull_bytes
+ self.region_bytes
+ self.read_bytes
)
@@ -359,14 +365,6 @@ def fit_budget(self, budget_bytes: float | None, cap: int | None = None) -> None
if not budget_bytes or budget_bytes <= 0:
return
plan = self.plan()
- # What the store decodes does NOT fall with the height: below one stored block the same
- # blocks are decoded whatever the region asks for, and the fold simply asks more often. It
- # is taken off the allowance whole, never divided by the rows -- dividing it is what made a
- # small budget cut the regions, decode the same bytes twice as often, and hold MORE.
- flat = plan.read_bytes
- row_bytes = (plan.peak_bytes - flat) / max(1, plan.slab_rows)
- if row_bytes <= 0:
- return
# Past the plateau a taller region reads no less and holds more, so that is the ceiling
# when the caller names none; a chain that cannot price one falls back to the output height.
ceiling = int(cap) if cap is not None else self._plateau_rows(plan)
@@ -377,8 +375,41 @@ def fit_budget(self, budget_bytes: float | None, cap: int | None = None) -> None
allowance = budget_share("regions", budget_bytes) or 0.0
if self.keeps_folds(plan):
allowance -= self._folded_output_bytes(plan)
- allowance -= flat
- self.slab_rows = max(1, min(ceiling, int(max(row_bytes, allowance) / row_bytes)))
+ self.slab_rows = self._tallest_affordable(ceiling, allowance)
+
+ def _tallest_affordable(self, ceiling: int, allowance: float) -> int:
+ """The tallest region up to ``ceiling`` whose PRICED plan fits ``allowance``.
+
+ Bisected on the price itself rather than extrapolated from one height, because none of what
+ a region costs scales with its rows: a chain's source window is not its region (a halo is a
+ constant, a rotated map's box grows with the diagonal), and what a chunked store decodes
+ does not fall with the height at all -- below one stored block the same blocks are decoded
+ whatever the region asks for. A straight line through one sample sized a fold at 2.6x the
+ budget it printed.
+
+ One row when nothing fits: the plan then reports a peak above the budget and the workflow
+ refuses, which is the only honest answer, there being no whole-volume path to fall back to.
+ """
+ ceiling = max(1, int(ceiling))
+ if self._priced_peak(ceiling) <= allowance:
+ return ceiling
+ low, high = 1, ceiling
+ while low < high:
+ middle = (low + high + 1) // 2
+ if self._priced_peak(middle) <= allowance:
+ low = middle
+ else:
+ high = middle - 1
+ return low
+
+ def _priced_peak(self, rows: int) -> int:
+ """What the plan prices at ``rows``, leaving the height the sizing is working from alone."""
+ held = self.slab_rows
+ try:
+ self.slab_rows = rows
+ return self.plan().peak_bytes
+ finally:
+ self.slab_rows = held
def keeps_folds(self, plan: ReductionPlan) -> bool:
"""Whether the stat pass hands its folds to the write pass instead of re-folding them.
@@ -528,6 +559,7 @@ def _needs_stat_pass(self) -> bool:
def plan(self) -> ReductionPlan:
reference = self.reference
+ pull_bytes, read_bytes = self._member_read_bytes(int(reference.base_shape[0]))
return ReductionPlan(
output=self.reduce.output,
cases=[manager.name for manager in self.managers],
@@ -542,24 +574,30 @@ def plan(self) -> ReductionPlan:
working_multiple=float(self.operator.working_multiple_for(len(self.managers))),
# The worst member's, because the fold is paced by whichever chain holds the most.
chain_multiple=max((float(manager.working_multiple()) for manager in self.managers), default=0.0),
- read_bytes=self._member_read_bytes(int(reference.base_shape[0])),
+ pull_bytes=pull_bytes,
+ read_bytes=read_bytes,
stat_pass=self._needs_stat_pass(),
unbounded=self._unbounded_members(),
refusal=self._first_refusal(),
)
- def _member_read_bytes(self, channels: int) -> int:
- """What the widest member's region makes its store decode, at the current height.
+ def _member_read_bytes(self, channels: int) -> tuple[int, int]:
+ """What one member's region costs its store at the current height, in bytes: the source
+ window it pulls, and what the store decodes above that window.
- The fold is paced by whichever member decodes the most, and one read is in flight at a
- time. ``None`` from a manager (a chain that cannot answer) contributes nothing: the peak
- then says what it did before, which is what the run-time probe is there to correct.
+ The fold is paced by whichever member costs the most, and one read is in flight at a time,
+ so both are the widest member's and both are charged once. ``None`` from a manager (a chain
+ that cannot answer) contributes nothing: the peak then says what it did before, which is
+ what the run-time probe is there to correct.
"""
from konfai.data.patching import _SWEEP_ELEMENT_BYTES
reads = [manager.region_reads(self.slab_rows) for manager in self.managers]
- widest = max((excess for read in reads if read is not None for excess, _total in [read]), default=0)
- return int(widest * max(1, channels) * _SWEEP_ELEMENT_BYTES)
+ present = [read for read in reads if read is not None]
+ element = max(1, channels) * _SWEEP_ELEMENT_BYTES
+ pull = max((read.widest_pull for read in present), default=0)
+ excess = max((read.widest_excess for read in present), default=0)
+ return int(pull * element), int(excess * element)
# --------------------------------------------------------------- execution
diff --git a/konfai/data/materialize.py b/konfai/data/materialize.py
index 3e293b37..f487d4f7 100644
--- a/konfai/data/materialize.py
+++ b/konfai/data/materialize.py
@@ -28,22 +28,10 @@
"""
import contextlib
-import sys
import warnings
from collections.abc import Iterable, Iterator, Sequence
from dataclasses import dataclass
-
-if sys.version_info >= (3, 11):
- from enum import StrEnum
-else: # Python 3.10 has no StrEnum: the same contract, a str whose text is its value
- from enum import Enum
-
- class StrEnum(str, Enum):
- def __str__(self) -> str:
- return str(self.value)
-
- __format__ = str.__format__
-
+from enum import StrEnum
import numpy as np
import torch
diff --git a/konfai/data/patching/manager.py b/konfai/data/patching/manager.py
index f34aad01..c2768a72 100644
--- a/konfai/data/patching/manager.py
+++ b/konfai/data/patching/manager.py
@@ -1352,16 +1352,16 @@ def _source_extents(spatial: Sequence[int], plans: Sequence["_ReadStagePlan"]) -
window the landing has no extent for."""
return [int(extent) for extent in plans[0].in_shape] if plans else [int(extent) for extent in spatial]
- def region_reads(self, rows: int, a: int = 0) -> tuple[int, int] | None:
- """What a decomposition into ``rows``-row regions costs this chain in source voxels:
- ``(excess, total)``. ``None`` when the chain cannot stream.
+ def region_reads(self, rows: int, a: int = 0) -> "BlockReads | None":
+ """What a decomposition into ``rows``-row regions costs this chain in source voxels.
+ ``None`` when the chain cannot stream.
- ``excess`` is what the widest region's read materialises ABOVE the window it asked for --
- a chunked store decodes whole blocks, so a window is served by the block-aligned hull that
- covers it. Zero on a store that serves exactly what it is asked for, which is what makes
- this safe to charge on top of a price already counting the window.
-
- ``total`` is what all the regions read together, the figure a caller compares heights on.
+ The same aggregates the sweep sizes against (:class:`BlockReads`): ``widest_pull``, the
+ source window one region materialises -- which for a chain that resamples is not the region
+ and is what a fold must hold while it produces one; ``widest_excess``, what the store
+ decodes above that window, a chunked one serving a window by the block-aligned hull that
+ covers it; and ``total``, what all the regions read together, the figure a caller compares
+ heights on.
Closed form, from the chain's own pull maps and the store's metadata: no voxel is read.
"""
@@ -1371,8 +1371,7 @@ def region_reads(self, rows: int, a: int = 0) -> tuple[int, int] | None:
segment = segments[-1]
spatial = [int(extent) for extent in segment.landing]
tile = [max(1, min(int(rows), spatial[0])), *spatial[1:]]
- reads = self.block_reads(spatial, tile, segment.plans)
- return (reads.widest_excess, reads.total)
+ return self.block_reads(spatial, tile, segment.plans)
def _grid_rows(self, cap: int) -> list[int]:
"""The heights that land on the store's block grid, up to ``cap``.
diff --git a/konfai/data/transform/io.py b/konfai/data/transform/io.py
index 0760a098..06edc064 100644
--- a/konfai/data/transform/io.py
+++ b/konfai/data/transform/io.py
@@ -35,7 +35,7 @@ class Save(Transform):
where the levels are derived once the last region has landed.
``downsample_method`` names how the coarse levels are derived, and its default is
- ``ITKWASM_BIN_SHRINK`` (block averaging), NOT ngff-zarr's own ``ITKWASM_GAUSSIAN``. Measured on a
+ ``DASK_BIN_SHRINK`` (block averaging), NOT ngff-zarr's own ``ITKWASM_GAUSSIAN``. Measured on a
real volume, the Gaussian holds a 0.9998 correlation while crushing peak intensity by 20 %.
"""
diff --git a/konfai/network/network/base.py b/konfai/network/network/base.py
index 05dfa450..290010d8 100644
--- a/konfai/network/network/base.py
+++ b/konfai/network/network/base.py
@@ -46,6 +46,40 @@ def is_full(self) -> bool:
return len(self.patch.get_patch_slices(0)) == self.index
+#: A patched forward yields every layer twice over: once per patch, and once assembled. The two are
+#: told apart by a marker inside the layer's NAME, because the name is what a checkpoint key and an
+#: ``outputs_criterions`` key are written against -- it cannot move out of the string without moving
+#: both. The marker stays in band; the reading of it does not, and lives in the four helpers below.
+_ACCUMULATED = ";accu;"
+
+
+def mark_accumulated(name: str) -> str:
+ """``name`` as a patched forward yields it, for a layer an accumulator is assembling."""
+ return f"{_ACCUMULATED}{name}"
+
+
+def is_accumulated(name: str) -> bool:
+ """Whether ``name`` names a patch of a layer rather than the assembled layer."""
+ return _ACCUMULATED in name
+
+
+def strip_accumulated(name: str) -> str:
+ """``name`` as the model declares it, with any marker taken out."""
+ return name.replace(_ACCUMULATED, "")
+
+
+def accumulator_owner(name: str) -> str:
+ """The module whose accumulator assembles ``name``: the last segment of the path before the
+ INNERMOST marker, empty for the network itself.
+
+ Innermost because a nested network marks the names it yields and its parent prefixes its own
+ path onto them, so a layer of a sub-network carries a marker per level and the accumulator is
+ the one closest to the layer.
+ """
+ head = name.rsplit(_ACCUMULATED, 2)[-2] if is_accumulated(name) else ""
+ return head.rstrip(".").rsplit(".", 1)[-1]
+
+
def batched_step(
optimizer_class: type[torch.optim.Optimizer], parameters: list[torch.nn.parameter.Parameter]
) -> Callable[..., torch.optim.Optimizer]:
diff --git a/konfai/network/network/measure.py b/konfai/network/network/measure.py
index e53e85bd..0b5d652a 100644
--- a/konfai/network/network/measure.py
+++ b/konfai/network/network/measure.py
@@ -26,6 +26,7 @@
import torch
from konfai.metric.schedulers import Scheduler
+from konfai.network.network.base import strip_accumulated
from konfai.network.network.loaders import CriterionsAttr, TargetCriterionsLoader
from konfai.utils.dataset import Attribute
from konfai.utils.errors import ConfigError, MeasureError
@@ -194,7 +195,7 @@ def init(self, model: torch.nn.Module, group_dest: list[str]) -> None:
modules.append(i)
for output_group in self.outputs_criterions.keys():
- if output_group.replace(";accu;", "") not in modules:
+ if strip_accumulated(output_group) not in modules:
raise MeasureError(
f"The output group '{output_group}' defined in 'outputs_criterions' "
"does not correspond to any module in the model.",
diff --git a/konfai/network/network/network.py b/konfai/network/network/network.py
index cdf0c061..3ec1309f 100644
--- a/konfai/network/network/network.py
+++ b/konfai/network/network/network.py
@@ -24,14 +24,8 @@
from collections.abc import Callable, Iterable, Iterator, Sequence
from contextlib import nullcontext
from functools import partial
-from typing import Any
+from typing import Any, Self
-from konfai.utils.dataset import Attribute
-
-try:
- from typing import Self # type: ignore[attr-defined] # Python โฅ 3.11
-except ImportError:
- from typing_extensions import Self # Python โค 3.10
import torch
from torch._jit_internal import _copy_to_script_wrapper
from torch.utils.checkpoint import checkpoint
@@ -39,10 +33,18 @@
from konfai import konfai_root
from konfai.data.data_manager import BatchSample
from konfai.data.patching import Accumulator, ModelPatch
-from konfai.network.network.base import NetState, PatchIndexed
+from konfai.network.network.base import (
+ NetState,
+ PatchIndexed,
+ accumulator_owner,
+ is_accumulated,
+ mark_accumulated,
+ strip_accumulated,
+)
from konfai.network.network.loaders import LRSchedulersLoader, OptimizerLoader, TargetCriterionsLoader
from konfai.network.network.measure import Measure
from konfai.utils.clock import SweepClock
+from konfai.utils.dataset import Attribute
from konfai.utils.errors import ConfigError
from konfai.utils.runtime import State, get_device, get_gpu_memory
@@ -366,7 +368,7 @@ def named_forward(
),
):
for ob in self._modulesArgs[name].out_branch:
- if ob in module._modulesArgs[k.split(".")[0].replace(";accu;", "")].out_branch:
+ if ob in module._modulesArgs[strip_accumulated(k.split(".")[0])].out_branch:
tmp.append(ob)
branchs[ob] = out
yield name + "." + k, out
@@ -880,7 +882,7 @@ def named_forward(
buffer = []
for i, patch_input in enumerate(patch_iterator):
for name, output_layer in super().named_forward(*patch_input, attributes=attributes):
- yield f";accu;{name}", output_layer
+ yield mark_accumulated(name), output_layer
buffer.append((name.split(".")[0], output_layer))
if len(buffer) == 2:
if buffer[0][0] != buffer[1][0]:
@@ -923,7 +925,7 @@ def get_layers(
it = 0
debug = "KONFAI_DEBUG" in os.environ
for name_tmp, output_layer in self.named_forward(*inputs, attributes=attributes):
- name = name_tmp.replace(";accu;", "")
+ name = strip_accumulated(name_tmp)
if debug:
if "KONFAI_DEBUG_LAST_LAYER" in os.environ:
os.environ["KONFAI_DEBUG_LAST_LAYER"] = (
@@ -937,13 +939,9 @@ def get_layers(
)
it += 1
if name in layers_name or name_tmp in layers_name:
- if ";accu;" in name_tmp:
+ if is_accumulated(name_tmp):
if name not in output_layer_patch_indexed:
- network_name = (
- name_tmp.split(".;accu;")[-2].split(".")[-1]
- if ".;accu;" in name_tmp
- else name_tmp.split(";accu;")[-2].split(".")[-1]
- )
+ network_name = accumulator_owner(name_tmp)
module = self
network = None
if network_name == "":
@@ -976,7 +974,7 @@ def get_layers(
yield name_tmp, output_layer, None
if name in layers_name:
- if ";accu;" in name_tmp:
+ if is_accumulated(name_tmp):
yield name, output_layer, output_layer_patch_indexed[name]
output_layer_patch_indexed[name].index += 1
if output_layer_patch_indexed[name].is_full():
diff --git a/konfai/utils/ome_zarr.py b/konfai/utils/ome_zarr.py
index 6c4c419d..226a9e53 100644
--- a/konfai/utils/ome_zarr.py
+++ b/konfai/utils/ome_zarr.py
@@ -23,8 +23,8 @@
1. map between KonfAI's channel-first ``C[Z]YX`` arrays / ``(x, y, z)`` geometry
and ngff-zarr's ``NgffImage`` (axis-named ``scale``/``translation``), and
2. round-trip KonfAI's full ``Attribute`` sidecar (including the ``Direction``
- matrix, which OME-NGFF cannot express) through a single ``konfai`` group
- attribute, read/written with ``zarr``.
+ matrix, which OME-NGFF cannot express) through a single ``konfai`` root
+ attribute, carried by ngff-zarr beside the OME metadata.
Reads are lazy: ``ngff-zarr`` exposes the array as a chunked store, so slicing
only materialises the requested patch.
@@ -38,7 +38,6 @@
import dataclasses
import itertools
import operator
-import shutil
import tempfile
import threading
from collections import OrderedDict
@@ -99,9 +98,24 @@ def _native_byteorder(array: np.ndarray) -> np.ndarray:
# NGFF RFC-5 types the component axis of a vector field, so a displacement field says what it is on
# disk. Those types only exist from NGFF 0.6 (zarr v3); 0.4 (zarr v2 layout) stays the default
-# everywhere else, being the version portable across the whole CI matrix.
+# everywhere else, being the version external OME-Zarr readers most widely accept.
_DISPLACEMENT_AXIS_TYPE = "displacement"
+# EVERY field store holds its components in the spec's order -- the OUTPUT axes' (dz, dy, dx for a
+# zyx store) -- there is no second layout. KonfAI's own convention stays ITK's (dx, dy, dz)
+# everywhere in memory; the two orders meet only at this backend's read/write boundary, where the
+# components are flipped. An axis-aligned grid is additionally declared a ``displacements``
+# transformation mapping the physical coordinate system onto itself through the level-0 array,
+# which is what makes it APPLICABLE by a spec reader rather than merely labelled; a grid carrying
+# a rotation cannot be declared (RFC-5 maps a field's array to space by scale and translation
+# alone) and keeps only the typed axis, its Direction in the sidecar, the marker below saying how
+# its components are ordered. A typed store with NEITHER the entry NOR the marker is a pre-1.9
+# layout whose components are ITK-ordered: reading it under one convention or the other would be a
+# guess with a plausible registration either way, so it is refused by name.
+_PHYSICAL_CS = "physical"
+_FIELD_COMPONENTS_KEY = "field_components"
+_FIELD_COMPONENTS = "output-axes"
+
_RFC5_VERSION = "0.6"
_DEFAULT_VERSION = "0.4"
@@ -112,21 +126,11 @@ def _native_byteorder(array: np.ndarray) -> np.ndarray:
CHUNK_SPATIAL_TILE = 128
CHUNK_TARGET_BYTES = 32 << 20
-
-def _zarr_v3_available() -> bool:
- """Whether the installed zarr can write a v3 store, which NGFF >= 0.5 (RFC-5) requires.
-
- RFC-5 axis types live only in NGFF 0.6, a zarr v3 layout, and zarr-python 3 needs Python >= 3.11 --
- so on Python 3.10 (zarr 2.x) a displacement field cannot be written. This is the capability the
- RFC-5 write actually depends on: ``coordinateSystems`` (checked in ``_type_component_axis``) tracks
- the ngff-zarr version, not the zarr one, so it alone lets a 2.x store through to an opaque failure.
- """
- if not _ZARR_AVAILABLE:
- return False
- try:
- return int(zarr.__version__.split(".")[0]) >= 3
- except (AttributeError, ValueError):
- return False
+#: zarr v2 stores keep byte-shuffled blosc-lz4 (what every 1.8.2 store carries) rather than the
+#: zarrista writer's zstd-0 default: measured on a CT-like uint16 volume, zstd-0 costs +19 % disk
+#: and ~+11 % on the streamed read sweep, because without the shuffle a uint16's high bytes break
+#: every run the compressor could fold.
+_V2_COMPRESSOR = {"id": "blosc", "cname": "lz4", "clevel": 5, "shuffle": 1, "blocksize": 0}
def _require_zarr() -> None:
@@ -146,37 +150,16 @@ def _require_ngff_zarr() -> None:
)
-def _require_zarr_v3_for_rfc5() -> None:
- """Both write paths type a component axis, so both need the same capability check.
-
- Raised rather than silently downgraded to an untyped 0.4 store: a caller asking for a displacement
- field is asking for the one property that makes it readable as a transform, and a store that
- quietly is not one gets found out much later, by a reader that took its three channels for an
- image.
- """
- if not _zarr_v3_available():
- raise DatasetManagerError(
- "Writing an NGFF RFC-5 displacement field needs a zarr v3 store, i.e. "
- "zarr-python >= 3 (Python >= 3.11); this environment has zarr 2.",
- "Install it with: pip install 'zarr>=3' on Python >= 3.11.",
- )
-
-
def _read_konfai_attributes(store_path: str | Path) -> dict[str, Any]:
- """KonfAI's proprietary ``Attribute`` sidecar from the store, if present.
-
- A copy of a memoised read: it is metadata, and a streamed run asks for it once per region.
+ """KonfAI's proprietary ``Attribute`` sidecar: the ``konfai`` key ngff-zarr carries back beside
+ the OME metadata. A copy of a memoised parse: it is metadata, and a streamed run asks for it
+ once per region.
"""
- return dict(_konfai_attributes(str(store_path)))
-
-
-@lru_cache(maxsize=8)
-def _konfai_attributes(store_path: str) -> dict[str, Any]:
try:
- group = zarr.open_group(store_path, mode="r")
- return dict(dict(group.attrs).get(_KONFAI_ATTR_KEY, {}).get("attributes", {}))
- except (KeyError, OSError, ValueError, TypeError):
+ root = _multiscales(str(store_path)).root_attributes or {}
+ except Exception:
return {}
+ return dict(root.get(_KONFAI_ATTR_KEY, {}).get("attributes", {}))
def _from_ngff_zarr(store_path: str | Path) -> Any:
@@ -196,13 +179,21 @@ def _from_ngff_zarr(store_path: str | Path) -> Any:
@lru_cache(maxsize=8)
-def _load_image(store_path: str, level: int) -> Any:
- """Return the ``NgffImage`` for ``level`` of an OME-Zarr store, memoised per (store, level).
+def _multiscales(store_path: str) -> Any:
+ """ngff-zarr's multiscales for a store, memoised per path: the images, their metadata, and the
+ root attributes beside them, all from one parse.
A streamed run reads one patch per call, and re-parsing the NGFF metadata and rebuilding the
- lazy array graph per patch is pure per-read overhead: the image object is lazy (no voxel data),
- so a handful of them is cheap to keep. The key is the path alone, so anything that puts a
- different store at a path already read must call ``clear_ome_zarr_cache()``: see there.
+ lazy array graph per patch is pure per-read overhead: the object is lazy (no voxel data), so a
+ handful of them is cheap to keep. The key is the path alone, so anything that puts a different
+ store at a path already read must call ``clear_ome_zarr_cache()``: see there.
+ """
+ _require_ngff_zarr()
+ return _from_ngff_zarr(store_path)
+
+
+def _load_image(store_path: str, level: int) -> Any:
+ """Return the ``NgffImage`` for ``level`` of an OME-Zarr store, off the memoised parse.
``@N`` selects among the levels a store offers, so a single-level store has nothing to select: its
one level is read whatever ``N`` says (as every other backend does: ``SitkFile`` ignores
@@ -210,9 +201,8 @@ def _load_image(store_path: str, level: int) -> Any:
three-level mask beside a four-level image is a real mismatch (it silently pairs 160 ยตm against
320 ยตm), and quietly falling back to level 0 would hide it.
"""
- _require_ngff_zarr()
try:
- multiscales = _from_ngff_zarr(store_path)
+ multiscales = _multiscales(store_path)
except (KeyError, IndexError, OSError, TypeError, ValueError) as exc:
raise DatasetManagerError(
f"Cannot open OME-Zarr store '{store_path}' (level {level}).",
@@ -255,10 +245,8 @@ def clear_ome_zarr_cache(store_path: str | Path | None = None) -> None:
a hit serves the previous store's axes and geometry against the new store's voxels. That reads as
a shape mismatch when the two differ, and as nothing at all when they do not.
"""
- _load_image.cache_clear()
- _level_path.cache_clear()
+ _multiscales.cache_clear()
_level_array.cache_clear()
- _konfai_attributes.cache_clear()
if _CHUNK_CACHE is not None:
_CHUNK_CACHE.forget(None if store_path is None else store_identity(store_path))
@@ -270,22 +258,52 @@ def is_displacement_field(store_path: str | Path) -> bool:
read from the store itself: the producer does not have to be trusted, and no sidecar convention
(a filename, an attribute) has to be agreed on separately.
- A store that predates RFC-5, or an ngff-zarr too old to model it, simply answers False: an
- unreadable or absent store is not a displacement field either, so this never raises.
+ A store that predates RFC-5 simply answers False: an unreadable or absent store is not a
+ displacement field either, so this never raises.
"""
if not _NGFF_ZARR_AVAILABLE:
return False
try:
- metadata = _from_ngff_zarr(store_path).metadata
+ image = _multiscales(str(store_path)).images[0]
except Exception:
# "Not a displacement field" is the only answer this owes: it is asked purely to decide HOW to
# read an entry, and an absent or unreadable store is not one either.
return False
- return any(
- axis.name == "c" and axis.type == _DISPLACEMENT_AXIS_TYPE
- for system in getattr(metadata, "coordinateSystems", None) or []
- for axis in system.axes
- )
+ return _has_displacement_axis(image)
+
+
+def _has_displacement_axis(image: Any) -> bool:
+ """Whether the image's component axis is typed as an RFC-5 displacement."""
+ return (image.axes_types or {}).get("c") == _DISPLACEMENT_AXIS_TYPE
+
+
+def _component_flip(store_path: str) -> bool:
+ """Whether the store holds its components in the spec's order, to flip back to ITK's on read.
+
+ Every field store this backend writes does -- marked by the ``displacements`` entry when the
+ grid could be declared, by the sidecar's ``field_components`` when it could not. A store that
+ types its axis and carries neither is a pre-1.9 layout whose components are ITK-ordered, and it
+ is refused rather than read: under either convention the guess yields a plausible field with
+ dx and dz possibly exchanged, which is the silent kind of wrong.
+ """
+ if not _NGFF_ZARR_AVAILABLE:
+ return False
+ try:
+ multiscales = _multiscales(store_path)
+ except Exception:
+ return False
+ if any(entry.type == "displacements" for entry in multiscales.metadata.coordinateTransformations or []):
+ return True
+ root = (multiscales.root_attributes or {}).get(_KONFAI_ATTR_KEY) or {}
+ if root.get(_FIELD_COMPONENTS_KEY) == _FIELD_COMPONENTS:
+ return True
+ if _has_displacement_axis(multiscales.images[0]):
+ raise DatasetManagerError(
+ f"'{store_path}' types its component axis but declares no component order: a"
+ " displacement field written by KonfAI < 1.9, whose components are ITK-ordered.",
+ "Rewrite it from its source transform with KonfAI >= 1.9, or read it with the release that wrote it.",
+ )
+ return False
def _canonical_shape(dims: Sequence[str], shape: Sequence[int]) -> list[int]:
@@ -639,11 +657,10 @@ def place(coords: tuple) -> None:
map_over_rank_pool(place, wanted)
-@lru_cache(maxsize=8)
def _level_path(store_path: str, level: int) -> str | None:
- """The zarr path of one level, from the store's multiscales metadata, memoised beside the image."""
+ """The zarr path of one level, from the store's memoised multiscales metadata."""
try:
- datasets = _from_ngff_zarr(store_path).metadata.datasets
+ datasets = _multiscales(store_path).metadata.datasets
return str(datasets[level if len(datasets) > 1 else 0].path)
except Exception:
return None
@@ -761,10 +778,21 @@ def read_ome_zarr_data_slice(
level: int = 0,
timepoint: int = 0,
) -> tuple[np.ndarray, dict[str, Any]]:
- """Read a KonfAI channel-first ``C[Z]YX`` patch from an OME-Zarr store (lazy)."""
+ """Read a KonfAI channel-first ``C[Z]YX`` patch from an OME-Zarr store (lazy).
+
+ A conformant displacement store holds its components in RFC-5 order; the channel selection is
+ remapped and the patch flipped back, so every caller keeps receiving ITK's (dx, dy, dz)
+ whatever layout the store holds.
+ """
image = _load_image(str(store_path), level)
dims = [str(axis).lower() for axis in image.dims]
canonical_shape = _canonical_shape(dims, image.data.shape)
+ flipped = _component_flip(str(store_path))
+ if flipped:
+ start, stop, step = slices[0].indices(canonical_shape[0])
+ if step != 1:
+ raise DatasetManagerError("A displacement store's component axis takes unit-step selections.")
+ slices = (slice(canonical_shape[0] - stop, canonical_shape[0] - start), *slices[1:])
index = _store_index(dims, canonical_shape, slices, timepoint)
patch = _read_level_window(str(store_path), level, image, index)
remaining = [axis for axis, selection in zip(dims, index, strict=True) if not isinstance(selection, int)]
@@ -772,6 +800,10 @@ def read_ome_zarr_data_slice(
patch = np.transpose(patch, [remaining.index(axis) for axis in wanted])
if "c" not in remaining:
patch = patch[np.newaxis]
+ elif flipped:
+ # Contiguous, not a reversed view: a negative stride is refused by torch.from_numpy, and
+ # every patch this returns is about to become a tensor.
+ patch = np.ascontiguousarray(patch[::-1])
metadata = {
"axes": dims,
@@ -812,18 +844,20 @@ def _spatial_geometry(
def _downsample_method(downsample_method: str | None) -> Any:
- """Resolve a downsampling method name to ngff-zarr's enum, defaulting to BIN_SHRINK.
+ """Resolve a downsampling method name to ngff-zarr's enum, defaulting to DASK_BIN_SHRINK.
NOT ngff-zarr's own default, which is ``ITKWASM_GAUSSIAN``: a pyramid is indexed by position and
read as "the same image, coarser", so a level that has been smoothed is a change of pixels that
no reader can see. Measured on a real volume, the gaussian keeps a 0.9998 correlation while
crushing the peak intensity by 20 %: the shape of difference that passes a sanity check and
- resurfaces months later. ``ITKWASM_BIN_SHRINK`` is a plain block mean, so a caller that already
- downsamples by averaging blocks gets the same voxels from this writer.
+ resurfaces months later. ``DASK_BIN_SHRINK`` is a plain block mean with ITK's own BinShrink
+ semantics (aligned windows, remainder trimmed, integers rounded half up), computed lazily with
+ a bounded peak: it takes any extent and chunk layout the streamed writer leaves, where the wasm
+ variant traps on blocks past 2.5 GiB and on tails no chunking can avoid.
"""
_require_ngff_zarr()
if downsample_method is None:
- return ngff_zarr.Methods.ITKWASM_BIN_SHRINK
+ return ngff_zarr.Methods.DASK_BIN_SHRINK
try:
return ngff_zarr.Methods[downsample_method]
except KeyError:
@@ -898,35 +932,15 @@ def write_ome_zarr(
append_ome_zarr_levels(store_path, scale_factors, downsample_method=downsample_method)
-def _type_component_axis(multiscales: Any, axis_type: str) -> None:
- """Type the ``c`` axis of every coordinate system, in place.
-
- The axis type is set on the RFC-5 coordinate systems rather than on the ``NgffImage``, because
- ``to_multiscales`` derives the axes itself and hardcodes ``type="channel"`` for a ``c`` dim --
- tagging the image is a dead assignment on a non-frozen dataclass, and the store comes out an
- ordinary 3-channel image with no error raised anywhere.
-
- Coordinate systems are also the capability check: an ngff-zarr too old to model RFC-5 has no
- ``coordinateSystems`` on its metadata, and would otherwise write a silently untyped store.
- """
- systems = getattr(multiscales.metadata, "coordinateSystems", None)
- if not systems:
- raise DatasetManagerError(
- f"Writing a '{axis_type}' field needs NGFF RFC-5 coordinate systems, which this ngff-zarr cannot model.",
- "Upgrade it with: pip install 'ngff-zarr>=0.38'",
- )
- for system in systems:
- for axis in system.axes:
- if axis.name == "c":
- axis.type = axis_type
-
-
-def _write_skeleton(store_path: str | Path, multiscales: Any, version: str) -> None:
- """ngff-zarr's metadata for the store, written in place. ngff-zarr (>= 0.44) writes local
+def _write_skeleton(store_path: str | Path, multiscales: Any, version: str, **kwargs: Any) -> None:
+ """The store's metadata and empty arrays, written in place (``to_ngff_zarr(metadata_only=True)``
+ describes every level and creates its array without computing a voxel). ngff-zarr writes local
directories only, so a remote root gets the skeleton written locally and uploaded through the
- root's own filesystem: a few bytes of metadata, before the array is created underneath it."""
+ root's own filesystem: metadata documents only, before a chunk lands."""
if not uri.is_uri(store_path):
- ngff_zarr.to_ngff_zarr(str(store_path), multiscales, overwrite=True, version=version)
+ ngff_zarr.to_ngff_zarr(
+ str(store_path), multiscales, overwrite=True, version=version, metadata_only=True, **kwargs
+ )
return
filesystem = uri.filesystem(store_path)
_, target = uri.split_scheme(str(store_path))
@@ -940,11 +954,107 @@ def _write_skeleton(store_path: str | Path, multiscales: Any, version: str) -> N
filesystem.makedirs(target, exist_ok=True)
with tempfile.TemporaryDirectory() as scratch:
local = Path(scratch) / "skeleton"
- ngff_zarr.to_ngff_zarr(str(local), multiscales, overwrite=True, version=version)
+ ngff_zarr.to_ngff_zarr(str(local), multiscales, overwrite=True, version=version, metadata_only=True, **kwargs)
for file in sorted(path for path in local.rglob("*") if path.is_file()):
filesystem.put_file(str(file), uri.join(target, file.relative_to(local).as_posix()))
+def _grid_is_axis_aligned(attributes: dict[str, Any] | None) -> bool:
+ """Whether the field's grid carries no rotation.
+
+ RFC-5 maps a field's array to space by scale and translation alone, so only an axis-aligned
+ grid can be declared a ``displacements`` transformation; an oriented one keeps the label-only
+ layout, its Direction in the sidecar.
+
+ ``Attribute`` versions its keys (``Direction_0``, ``Direction_1``, ...), and the sidecar dict
+ arrives here verbatim: the LATEST version is the grid the store describes.
+ """
+ versions = {
+ key: value
+ for key, value in (attributes or {}).items()
+ if key == "Direction" or (key.startswith("Direction_") and key.removeprefix("Direction_").isdigit())
+ }
+ if not versions:
+ return True
+ value = versions[max(versions, key=lambda key: int(key.rsplit("_", 1)[-1]) if "_" in key else -1)]
+ flat = np.asarray(
+ str(value).replace("[", " ").replace("]", " ").split() if isinstance(value, str) else value, dtype=np.float64
+ ).ravel()
+ side = round(len(flat) ** 0.5)
+ return side * side == len(flat) and bool(np.allclose(flat.reshape(side, side), np.eye(side)))
+
+
+def _declare_displacements_transform(multiscales: Any) -> None:
+ """Mark the store as an RFC-5 ``displacements`` transformation, in place.
+
+ What makes the field APPLICABLE by a spec reader rather than merely labelled: a spatial
+ ``physical`` coordinate system, and a ``displacements`` entry mapping it onto itself through
+ the level-0 array. The components must then follow the output axes' order, which is
+ ``_ComponentFlippedWriter``'s half of the contract.
+ """
+ from ngff_zarr.v06.zarr_metadata import Axis, CoordinateSystem, CoordinateSystemIdentifier, Displacements
+
+ spatial = [str(dim) for dim in multiscales.images[0].dims if dim in _SPATIAL]
+ physical = CoordinateSystem(name=_PHYSICAL_CS, axes=[Axis(name=name, type="space", unit=None) for name in spatial])
+ reference = CoordinateSystemIdentifier(name=_PHYSICAL_CS)
+ entry = Displacements(
+ input=reference, output=reference, path=multiscales.metadata.datasets[0].path, interpolation="linear"
+ )
+ multiscales.metadata = dataclasses.replace(
+ multiscales.metadata,
+ coordinateSystems=[*multiscales.metadata.coordinateSystems, physical],
+ coordinateTransformations=[entry],
+ )
+
+
+class _ComponentFlippedWriter:
+ """The level-0 array of a conformant displacement store, taking ITK-ordered components.
+
+ Every producer in KonfAI hands fields in ITK's component order (dx, dy, dz); the store holds
+ the spec's (dz, dy, dx). Flipping at this boundary keeps the two conventions from ever
+ meeting: no producer knows about the spec, no store holds a private order. A value of lower
+ rank than the array (a scalar fill) has no component identity and broadcasts as it stands.
+ """
+
+ def __init__(self, array: Any) -> None:
+ self._array = array
+ self._channels = int(array.shape[0])
+
+ @property
+ def shape(self) -> tuple[int, ...]:
+ return tuple(self._array.shape)
+
+ @property
+ def chunks(self) -> tuple[int, ...]:
+ return tuple(self._array.chunks)
+
+ @property
+ def dtype(self) -> Any:
+ return self._array.dtype
+
+ def _remap(self, key: Any) -> tuple[Any, bool]:
+ """The store-side selection for a caller's ITK-side one, and whether values need flipping."""
+ if key is Ellipsis:
+ return key, True
+ first, *rest = key if isinstance(key, tuple) else (key,)
+ if isinstance(first, int):
+ return (self._channels - 1 - first, *rest), False
+ start, stop, step = first.indices(self._channels)
+ if step != 1:
+ raise DatasetManagerError("A displacement store's component axis takes unit-step selections.")
+ return (slice(self._channels - stop, self._channels - start), *rest), True
+
+ def __setitem__(self, key: Any, value: Any) -> None:
+ target, flip = self._remap(key)
+ data = np.asarray(value)
+ self._array[target] = np.flip(data, axis=0) if flip and data.ndim == self._array.ndim else data
+
+ def __getitem__(self, key: Any) -> np.ndarray:
+ source, flip = self._remap(key)
+ data = np.asarray(self._array[source])
+ return np.ascontiguousarray(np.flip(data, axis=0)) if flip else data
+
+
def create_ome_zarr_store(
store_path: str | Path,
shape: Sequence[int],
@@ -965,17 +1075,10 @@ def create_ome_zarr_store(
ngff-zarr writes that metadata, exactly as it does for the whole-array path, so both paths describe
a store the same way: ``displacement_field`` included, which is the point of routing it through
ngff-zarr at all. A field too large to assemble in memory is written region by region, so this is
- the ONLY path a real one takes, and until it could type its component axis a DVF came out
- self-describing exactly when it was small enough not to need to be.
-
- It describes the store from a one-voxel stand-in rather than from an array of the target shape.
- With a single resolution level the metadata does not depend on the extent at all (axes and
- coordinate transformations come from ``dims``, ``scale`` and ``translation``), and the two come
- out byte-identical, verified for 0.4 and 0.6. Handing ngff-zarr the real shape instead costs a
- pass over every chunk of an array that is entirely zeros (~44 ms per chunk, ~33 s for a 13.6 GiB
- field) to write no bytes at all. The real array is then created underneath that metadata, which is
- also what makes its chunking exactly the caller's: the region grid is the one thing ngff-zarr
- cannot infer, and a store whose chunks straddle it turns every region write into a
+ the ONLY path a real one takes. The KonfAI sidecar rides along as a root attribute beside the OME
+ keys, and the array is described from a LAZY zeros of the real shape: ``metadata_only`` creates it
+ without computing a voxel, chunked exactly as the caller says: the region grid is the one thing
+ ngff-zarr cannot infer, and a store whose chunks straddle it turns every region write into a
read-modify-write.
"""
clear_ome_zarr_cache(store_path)
@@ -994,135 +1097,34 @@ def create_ome_zarr_store(
chunks = [min(shape[0], max(1, CHUNK_TARGET_BYTES // max(1, tile_bytes))), *spatial_chunks]
chunks = tuple(chunks)
- stand_in = dask.array.zeros((shape[0], *(1,) * len(spatial_axes)), dtype=np.dtype(dtype))
- image = ngff_zarr.to_ngff_image(stand_in, dims=dims, scale=scale, translation=translation)
- multiscales = ngff_zarr.to_multiscales(image, scale_factors=[])
+ data = dask.array.zeros(tuple(shape), dtype=np.dtype(dtype), chunks=chunks)
+ image = ngff_zarr.to_ngff_image(data, dims=dims, scale=scale, translation=translation)
version = _DEFAULT_VERSION
if displacement_field:
- _require_zarr_v3_for_rfc5()
- _type_component_axis(multiscales, _DISPLACEMENT_AXIS_TYPE)
+ # Typing the component axis (NGFF RFC-5, so version 0.6) is what lets the store say on disk
+ # that its channels are a displacement rather than an ordinary 3-channel image.
+ image.axes_types = {"c": _DISPLACEMENT_AXIS_TYPE}
version = _RFC5_VERSION
- # version is explicit because to_ngff_zarr defaults to 0.5, which zarr-python 2 cannot write.
- _write_skeleton(store_path, multiscales, version)
+ multiscales = ngff_zarr.to_multiscales(image, scale_factors=[], chunks=chunks, cache=False)
+ if displacement_field:
+ # One layout for every field: components in the spec's order. The entry when the grid can
+ # be declared; the sidecar marker either way, so the reader never has to guess.
+ if _grid_is_axis_aligned(attributes):
+ _declare_displacements_transform(multiscales)
+ multiscales.root_attributes = {
+ _KONFAI_ATTR_KEY: {"attributes": dict(attributes or {}), _FIELD_COMPONENTS_KEY: _FIELD_COMPONENTS}
+ }
+ elif attributes:
+ multiscales.root_attributes = {_KONFAI_ATTR_KEY: {"attributes": dict(attributes)}}
+ # version is explicit because to_ngff_zarr defaults to 0.5; 0.4 stays the portable default. A
+ # v3 (RFC-5) store takes the writer's own codec chain, which is what 1.8.2 wrote there too.
+ compression = {} if displacement_field else {"compressor": _V2_COMPRESSOR}
+ _write_skeleton(store_path, multiscales, version, **compression)
# The level-0 key comes from the metadata rather than a literal: ngff-zarr builds it from the
# image name, so "scale0/image" is its convention to change, not ours to hardcode.
- group = zarr.open_group(str(store_path), mode="r+")
- create_array = getattr(group, "create_array", None) or group.create_dataset
- array = create_array(
- multiscales.metadata.datasets[0].path,
- shape=tuple(shape),
- chunks=chunks,
- dtype=np.dtype(dtype),
- fill_value=0,
- overwrite=True,
- )
- # Sidecar last: to_ngff_zarr(overwrite=True) reopens the root with mode="w" and drops every
- # attribute it finds, so writing this first loses Direction, and losing Direction is silent,
- # the reader falls back to identity and returns a plausibly-oriented volume.
- if attributes:
- group.attrs[_KONFAI_ATTR_KEY] = {"attributes": dict(attributes)}
- # ngff-zarr leaves a consolidated index behind, and readers trust it over the arrays themselves --
- # so until it is rebuilt the store still advertises the one-voxel stand-in, whatever is on disk.
- # Last, so that the sidecar written just above is part of what gets indexed.
- zarr.consolidate_metadata(str(store_path))
- return array
-
-
-def _bin_shrink_multiscales(image: Any, scale_factors: Sequence[int], out_chunks: Any) -> Any:
- """The BIN_SHRINK pyramid computed by ``dask.array.coarsen``, in ngff-zarr's own clothes.
-
- NOT ngff-zarr's ``ITKWASM_BIN_SHRINK``, for a reason found on real rounds: that method hands
- the 32-bit wasm sandbox one BLOCK at a time, the sandbox traps near 2.5 GiB -- a whole ExaSPIM
- volume as one block -- and it also traps on any multi-block layout whose extent does not divide
- the factor (514 rows @4: every chunking leaves an offending tail, so NO layout is safe). The
- engine's own streamed writes hit both.
-
- ``coarsen(np.mean, trim_excess)`` over factor-aligned blocks is the same statistic -- the mean
- of each aligned ``factor**rank`` window, the global remainder dropped -- computed lazily with a
- bounded peak, no sandbox, no layout constraint. The cast back to the payload dtype truncates,
- which is the ``static_cast`` ITK's own BinShrink performs.
-
- The METADATA stays ngff-zarr's: each level's dataset entry is taken from a single-level
- ``to_multiscales`` call on that level's image and repathed, so the axes, transform spelling and
- version handling remain theirs, not a private replica that drifts when they move.
- """
- dims = list(image.dims)
- spatial = [dim for dim in dims if dim in ("z", "y", "x")]
- images = [image]
- previous, previous_absolute = image, 1
- for absolute in scale_factors:
- factor = int(absolute) // previous_absolute
- if factor * previous_absolute != int(absolute) or factor < 1:
- raise DatasetManagerError(
- f"scale_factors {list(scale_factors)} (relative to level 0) do not form a ladder: each"
- " factor must be an integer multiple of the previous one.",
- "Each declared factor shrinks the level above it and must be 2 or more: [2, 2] or [4, 4].",
- )
- data = previous.data
- trim = tuple(
- slice(0, (int(data.shape[axis]) // factor) * factor) if dims[axis] in spatial else slice(None)
- for axis in range(len(dims))
- )
- for axis in range(len(dims)):
- if dims[axis] in spatial and int(data.shape[axis]) < factor:
- raise DatasetManagerError(
- f"scale factor {int(absolute)} shrinks axis '{dims[axis]}' (extent"
- f" {int(data.shape[axis])}) to nothing at this level.",
- "Stop the ladder before the factor outgrows the smallest axis.",
- )
- # coarsen folds within blocks, so every spatial chunk must divide the factor; on the
- # trimmed extent a factor-multiple chunk size guarantees the tail does too.
- chunk = max(factor, (256 // factor) * factor)
- working = data[trim].rechunk({axis: chunk if dims[axis] in spatial else -1 for axis in range(len(dims))})
- coarsened = dask.array.coarsen(
- np.mean, working, {axis: factor for axis in range(len(dims)) if dims[axis] in spatial}
- )
- if not np.issubdtype(np.dtype(data.dtype), np.floating):
- # Round to nearest, half up, BEFORE the cast: astype truncates toward zero, and ITK's
- # BinShrink (the reference for these levels) rounds -- a 0.5 window mean writes 1.
- # Truncation would shift every integer level ~half an LSB down, silently and uniformly.
- coarsened = dask.array.floor(coarsened + 0.5)
- coarsened = coarsened.astype(data.dtype)
- # The centre-of-voxel convention ngff-zarr itself applies: the coarse voxel's centre sits
- # half the spacing delta past the fine one's. Getting this wrong composes every level a
- # fraction of a voxel apart and still looks like an image.
- scale = {dim: previous.scale[dim] * (factor if dim in spatial else 1) for dim in previous.scale}
- translation = {
- dim: previous.translation[dim] + (0.5 * (factor - 1) * previous.scale[dim] if dim in spatial else 0.0)
- for dim in previous.translation
- }
- previous = dataclasses.replace(previous, data=coarsened, scale=scale, translation=translation)
- previous_absolute = int(absolute)
- images.append(previous)
-
- if out_chunks is not None:
- images = [
- dataclasses.replace(level, data=level.data.rechunk(tuple(out_chunks)))
- if hasattr(level.data, "rechunk")
- else level
- for level in images
- ]
- assembled = ngff_zarr.to_multiscales(image, scale_factors=[], chunks=out_chunks, cache=False)
- datasets = []
- for index, level in enumerate(images):
- level_multiscales = ngff_zarr.to_multiscales(level, scale_factors=[], chunks=out_chunks, cache=False)
- dataset = level_multiscales.metadata.datasets[0]
- path = f"scale{index}/{image.name}"
- for transform_sequence in dataset.coordinateTransformations or []:
- if getattr(transform_sequence, "input", None) is not None and hasattr(transform_sequence.input, "path"):
- transform_sequence.input.path = path
- dataset.path = path
- datasets.append(dataset)
- assembled.images = images
- assembled.metadata = dataclasses.replace(assembled.metadata, datasets=datasets)
- # Both OFF, deliberately: to_ngff_zarr RE-DERIVES every level whose index it can, whenever
- # scale_factors, method and chunks are all set -- to_multiscales records its default method
- # (the gaussian) even when asked for no levels. The levels' data never go through it (see
- # append_ome_zarr_levels), but its metadata write must not try to derive them either.
- assembled.scale_factors = []
- assembled.method = None
- return assembled
+ array = zarr.open_group(str(store_path), mode="r+")[multiscales.metadata.datasets[0].path]
+ return _ComponentFlippedWriter(array) if displacement_field else array
def append_ome_zarr_levels(
@@ -1135,16 +1137,13 @@ def append_ome_zarr_levels(
The companion of :func:`create_ome_zarr_store`: a store written region by region cannot be given
``scale_factors`` up front, because no level exists until the last region lands. This derives the
- pyramid afterwards, from what is on disk, and grafts it BESIDE level 0.
-
- Level 0 is not rewritten, not moved, not read back whole: each coarser level is computed lazily
- from it and stored straight into a new array of the same group, so the cost is one pass over
- level 0 into a level 16x smaller (measured 55 s -> ~10 s on a 4.9 GB store), with a chunk-sized
- peak. The multiscales metadata that names every level is still ngff-zarr's: it is described from
- one-voxel stand-ins (the metadata does not depend on the extent) into a scratch store and copied
- onto the group's attributes, so the axes, transforms and version spelling remain theirs. The
- KonfAI attribute sidecar is untouched, being a key beside theirs; a displacement field keeps its
- typed component axis through the same call that types it at creation.
+ pyramid afterwards, from what is on disk, and grafts it BESIDE level 0
+ (``to_ngff_zarr(start_level=1)``): level 0 is not rewritten, not moved, not read back whole; each
+ coarser level is computed lazily from the one before it with a chunk-sized peak, and the
+ multiscales metadata that names every level lands last, so an interrupted call leaves a store
+ that still reads exactly as its level 0. The KonfAI attribute sidecar rides along as the root
+ attributes ngff-zarr read back beside the OME keys; a displacement field keeps its typed
+ component axis the same way.
"""
if uri.is_uri(store_path):
raise DatasetManagerError(
@@ -1156,60 +1155,59 @@ def append_ome_zarr_levels(
return
store = Path(store_path)
clear_ome_zarr_cache(store)
- field = is_displacement_field(store)
- base = _from_ngff_zarr(store).images[0]
- stored_chunks = tuple(int(size) for size in base.data.chunksize)
- if downsample_method in (None, "ITKWASM_BIN_SHRINK"):
- multiscales = _bin_shrink_multiscales(base, _level_zero_scale_factors(scale_factors), stored_chunks)
- else:
- multiscales = ngff_zarr.to_multiscales(
- base,
- scale_factors=_level_zero_scale_factors(scale_factors),
- method=_downsample_method(downsample_method),
- chunks=stored_chunks,
- cache=False,
- )
- version = _DEFAULT_VERSION
- if field:
- _require_zarr_v3_for_rfc5()
- _type_component_axis(multiscales, _DISPLACEMENT_AXIS_TYPE)
- version = _RFC5_VERSION
-
- group = zarr.open_group(str(store), mode="r+")
- create_array = getattr(group, "create_array", None) or group.create_dataset
- for level, dataset in zip(multiscales.images[1:], multiscales.metadata.datasets[1:], strict=True):
- data = level.data.rechunk(stored_chunks)
- array = create_array(
- dataset.path, shape=data.shape, chunks=stored_chunks, dtype=data.dtype, fill_value=0, overwrite=True
+ multiscales = _from_ngff_zarr(store)
+ base = multiscales.images[0]
+ factors = _level_zero_scale_factors(scale_factors)
+ _refuse_factors_outgrowing_an_axis(base, factors)
+ derived = ngff_zarr.to_multiscales(
+ base,
+ scale_factors=factors,
+ method=_downsample_method(downsample_method),
+ chunks=tuple(int(size) for size in base.data.chunksize),
+ cache=False,
+ )
+ derived.root_attributes = multiscales.root_attributes
+ if multiscales.metadata.coordinateTransformations:
+ # A conformant field keeps its ``displacements`` entry (and the coordinate system it names)
+ # through the append: the entry references level 0, which this never rewrites.
+ declared = {system.name for system in derived.metadata.coordinateSystems}
+ derived.metadata = dataclasses.replace(
+ derived.metadata,
+ coordinateSystems=[
+ *derived.metadata.coordinateSystems,
+ *(s for s in multiscales.metadata.coordinateSystems if s.name not in declared),
+ ],
+ coordinateTransformations=multiscales.metadata.coordinateTransformations,
)
- # Aligned chunks: each zarr chunk is written by exactly one task, so no lock is needed.
- dask.array.store(data, array, lock=False)
-
- # The metadata LAST, so an interrupted call leaves a store that still reads as its level 0
- # (unreferenced arrays beside it are overwritten by the next call).
- rank = base.data.ndim - 1
- described = dataclasses.replace(
- multiscales,
- images=[
- dataclasses.replace(
- level, data=dask.array.zeros((level.data.shape[0], *(1,) * rank), dtype=level.data.dtype)
- )
- for level in multiscales.images
- ],
- scale_factors=[],
- method=None,
+ field = _has_displacement_axis(base)
+ # The coarse levels take level 0's own compressor, so the store stays uniform whatever wrote
+ # it; a v3 store carries a codec chain instead and keeps the writer's default.
+ level_zero = zarr.open_group(str(store), mode="r")[multiscales.metadata.datasets[0].path]
+ compressor = level_zero.metadata.to_dict().get("compressor")
+ ngff_zarr.to_ngff_zarr(
+ str(store),
+ derived,
+ overwrite=False,
+ version=_RFC5_VERSION if field else _DEFAULT_VERSION,
+ start_level=1,
+ **({"compressor": compressor} if compressor else {}),
)
- scratch = store.with_name(f"{store.name}.describing")
- shutil.rmtree(scratch, ignore_errors=True)
- try:
- ngff_zarr.to_ngff_zarr(str(scratch), described, overwrite=True, version=version)
- group.attrs.update(dict(zarr.open_group(str(scratch), mode="r").attrs))
- finally:
- shutil.rmtree(scratch, ignore_errors=True)
- zarr.consolidate_metadata(str(store))
clear_ome_zarr_cache(store)
+def _refuse_factors_outgrowing_an_axis(base: Any, factors: Sequence[int]) -> None:
+ """A factor that shrinks an axis to nothing is refused by name: ngff-zarr would fall back to
+ deriving that level from level 0 and write an empty array where a consumer's ``@N`` resolves."""
+ for absolute in factors:
+ for axis, dim in enumerate(base.dims):
+ if dim in _SPATIAL and int(base.data.shape[axis]) // int(absolute) == 0:
+ raise DatasetManagerError(
+ f"scale factor {int(absolute)} shrinks axis '{dim}' (extent"
+ f" {int(base.data.shape[axis])}) to nothing at this level.",
+ "Stop the ladder before the factor outgrows the smallest axis.",
+ )
+
+
def get_ome_zarr_info(store_path: str | Path, level: int = 0) -> dict[str, Any]:
"""OME-Zarr metadata, without reading pixel data.
@@ -1226,9 +1224,11 @@ def get_ome_zarr_info(store_path: str | Path, level: int = 0) -> dict[str, Any]:
image = _load_image(str(store_path), level)
dims = [str(axis).lower() for axis in image.dims]
try:
- n_levels = len(_from_ngff_zarr(store_path).images)
+ n_levels = len(_multiscales(str(store_path)).images)
except (OSError, TypeError, ValueError):
n_levels = 1
+ scale = _ordered(dict(image.scale), dims)
+ translation = _ordered(dict(image.translation), dims)
return {
"axes": dims,
"shape": list(image.data.shape),
@@ -1239,17 +1239,12 @@ def get_ome_zarr_info(store_path: str | Path, level: int = 0) -> dict[str, Any]:
"canonical_shape": _canonical_shape(dims, image.data.shape),
"chunks": list(getattr(image.data, "chunks", []) or []),
"dtype": str(image.data.dtype),
- "scale": _ordered(dict(image.scale), dims),
- "translation": _ordered(dict(image.translation), dims),
+ "scale": scale,
+ "translation": translation,
# Keyed by axis name, so no caller has to know which of the two orders it is holding.
"geometry": {
- axis: {"scale": float(scale), "translation": float(translation)}
- for axis, scale, translation in zip(
- dims,
- _ordered(dict(image.scale), dims),
- _ordered(dict(image.translation), dims),
- strict=True,
- )
+ axis: {"scale": float(value), "translation": float(offset)}
+ for axis, value, offset in zip(dims, scale, translation, strict=True)
},
"n_levels": n_levels,
"attributes": _read_konfai_attributes(store_path),
diff --git a/konfai/utils/runtime/distributed.py b/konfai/utils/runtime/distributed.py
index 7b53dc1f..c95d0303 100644
--- a/konfai/utils/runtime/distributed.py
+++ b/konfai/utils/runtime/distributed.py
@@ -516,8 +516,6 @@ def apply_cpu_thread_budget(world_size: int | None = None) -> None:
try:
import zarr
- from konfai.utils.ome_zarr import _zarr_v3_available
-
# A THIRD of the share: a pipelined sweep runs three of these at once, the decode of the
# region being read, the assembly of the one before it, the encode of the one being written.
# Measured with the chain off the reading thread: 24 cores, ExaSPIM 513x1331x1776 through a stored affine, two runs per point,
@@ -531,7 +529,7 @@ def apply_cpu_thread_budget(world_size: int | None = None) -> None:
# costs the reader its own throughput (read busy 4.4-4.6 s at 8, 5.1-5.2 s at 24). A share
# of a few cores keeps them all, up to four: a third of it is one chunk in flight, and on a
# remote root that is the whole of the read's parallelism.
- if _zarr_v3_available(): # 2.x has no config object, and no async reader to share the cores with
+ if hasattr(zarr, "config"): # 2.x has no config object, and no async reader to share the cores with
zarr.config.set({"async.concurrency": max(min(cores, 4), cores // 3)})
except ImportError:
pass
diff --git a/pixi.lock b/pixi.lock
index aa186cb6..bd493e15 100644
--- a/pixi.lock
+++ b/pixi.lock
@@ -52,7 +52,6 @@ environments:
- pypi: https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl
@@ -66,6 +65,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl
@@ -124,13 +124,13 @@ environments:
- pypi: ./
- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl
- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl
@@ -181,13 +181,13 @@ environments:
- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl
@@ -265,9 +265,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/12/c6/906a1c94a9e0d12b6cbc8a36d6fe284d06298d3d3eda14934d21609845ab/itkwasm_downsample-1.8.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl
@@ -307,6 +305,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl
@@ -317,7 +316,6 @@ environments:
- pypi: https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/5c/1f/1957d07725e6abdaef43b05c30a797fa2a2e7093ced9dbd750d796921a5d/ngff_zarr-0.40.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl
@@ -387,10 +385,12 @@ environments:
- pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl
@@ -436,12 +436,14 @@ environments:
- pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/f0/48/3d07340e3256b2cdf441b883da4fd2f7ab89d4894da94a4c627ba8b2f9eb/zarrista-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
@@ -483,9 +485,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl
- - pypi: https://files.pythonhosted.org/packages/12/c6/906a1c94a9e0d12b6cbc8a36d6fe284d06298d3d3eda14934d21609845ab/itkwasm_downsample-1.8.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl
@@ -516,10 +516,12 @@ environments:
- pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/41/d8/f37480ebc669af91ebab0b01ab649413446bd5cce365f3f50b71d05be2af/zarrista-0.1.0-cp311-abi3-macosx_11_0_arm64.whl
- pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl
@@ -528,7 +530,6 @@ environments:
- pypi: https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl
- pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5b/0b/a81b5daf5adea482ecb68d9615f6a348486ab4d8e980a915d4420e57ee4d/wasmtime-45.0.0-py3-none-macosx_11_0_arm64.whl
- - pypi: https://files.pythonhosted.org/packages/5c/1f/1957d07725e6abdaef43b05c30a797fa2a2e7093ced9dbd750d796921a5d/ngff_zarr-0.40.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl
@@ -598,8 +599,10 @@ environments:
- pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl
- pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl
@@ -644,6 +647,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
- pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl
@@ -683,10 +687,8 @@ environments:
- pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/12/c6/906a1c94a9e0d12b6cbc8a36d6fe284d06298d3d3eda14934d21609845ab/itkwasm_downsample-1.8.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/13/a8/734725bb703c5fabb687f79c79e51249475212b3eb37771ac4a4ac9b487f/onnx-1.22.0-cp312-abi3-win_amd64.whl
- - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl
@@ -721,6 +723,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/46/e0/60466c6d712dad2cf807df315e39863e91609ffd1064ecb835994460bbda/pydicom-3.0.2-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl
@@ -728,7 +731,6 @@ environments:
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl
- - pypi: https://files.pythonhosted.org/packages/5c/1f/1957d07725e6abdaef43b05c30a797fa2a2e7093ced9dbd750d796921a5d/ngff_zarr-0.40.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl
@@ -788,11 +790,14 @@ environments:
- pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/af/2e/59f3ab896f5a8c76a3d009c40015a32660b1b05b0ff055efcee3aa977caa/zarrista-0.1.0-cp311-abi3-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/b5/2b/2cca4261bab978eb3d9f1aa32fe7dcd9021e193bdc88b3dcf0bb366e9f91/itkwasm_downsample_wasi-1.8.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl
@@ -839,6 +844,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl
@@ -884,7 +890,6 @@ environments:
- pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl
@@ -902,6 +907,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl
@@ -979,7 +985,6 @@ environments:
- pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl
@@ -990,6 +995,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl
@@ -1059,7 +1065,6 @@ environments:
- pypi: https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl
@@ -1070,6 +1075,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl
@@ -1995,8 +2001,8 @@ packages:
- simpleitk>=2.0 ; extra == 'imaging'
- h5py ; extra == 'imaging'
- pydicom ; extra == 'imaging'
- - zarr ; extra == 'imaging'
- - ngff-zarr>=0.38 ; extra == 'imaging'
+ - zarr>=3 ; extra == 'imaging'
+ - ngff-zarr>=0.45 ; extra == 'imaging'
- dask ; extra == 'imaging'
- vtk ; extra == 'vtk'
- lpips ; extra == 'lpips'
@@ -2006,8 +2012,8 @@ packages:
- torchvision ; extra == 'fid'
- submitit ; extra == 'cluster'
- pydicom ; extra == 'dicom'
- - zarr ; extra == 'omezarr'
- - ngff-zarr>=0.38 ; extra == 'omezarr'
+ - zarr>=3 ; extra == 'omezarr'
+ - ngff-zarr>=0.45 ; extra == 'omezarr'
- dask ; extra == 'omezarr'
- s3fs ; extra == 's3'
- aiohttp ; extra == 's3'
@@ -2031,8 +2037,8 @@ packages:
- simpleitk>=2.0 ; extra == 'dev'
- h5py ; extra == 'dev'
- pydicom ; extra == 'dev'
- - zarr ; extra == 'dev'
- - ngff-zarr>=0.38 ; extra == 'dev'
+ - zarr>=3 ; extra == 'dev'
+ - ngff-zarr>=0.45 ; extra == 'dev'
- dask ; extra == 'dev'
- scikit-image ; extra == 'dev'
- onnx ; extra == 'dev'
@@ -2058,7 +2064,7 @@ packages:
- sphinx-copybutton ; extra == 'dev'
- sphinx-togglebutton ; extra == 'dev'
- sphinx-tabs ; extra == 'dev'
- requires_python: '>=3.10'
+ requires_python: '>=3.11'
- pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl
name: distlib
version: 0.4.3
@@ -2338,15 +2344,6 @@ packages:
version: 1.5.5
sha256: 0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/12/c6/906a1c94a9e0d12b6cbc8a36d6fe284d06298d3d3eda14934d21609845ab/itkwasm_downsample-1.8.1-py3-none-any.whl
- name: itkwasm-downsample
- version: 1.8.1
- sha256: f1315d32a1d80a610c682c3a32ab41aa46b584f91f17a839a7c0b6e3796e60c7
- requires_dist:
- - itkwasm-downsample-emscripten ; sys_platform == 'emscripten'
- - itkwasm-downsample-wasi ; sys_platform != 'emscripten'
- - itkwasm>=1.0b145
- requires_python: '>=3.8'
- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl
name: filelock
version: 3.29.4
@@ -2363,11 +2360,6 @@ packages:
- ml-dtypes>=0.5.4
- pillow ; extra == 'reference'
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl
- name: typing-extensions
- version: 4.15.0
- sha256: f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
- requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl
name: absl-py
version: 2.4.0
@@ -2921,6 +2913,13 @@ packages:
requires_dist:
- referencing>=0.31.0
requires_python: '>=3.9'
+- pypi: https://files.pythonhosted.org/packages/41/d8/f37480ebc669af91ebab0b01ab649413446bd5cce365f3f50b71d05be2af/zarrista-0.1.0-cp311-abi3-macosx_11_0_arm64.whl
+ name: zarrista
+ version: 0.1.0
+ sha256: 28ea1359ec791e84e1438e38e3047a964a3e7d59f56f122255a44ec2b368f33f
+ requires_dist:
+ - zarr-metadata>=0.4
+ requires_python: '>=3.11'
- pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl
name: id
version: 1.6.1
@@ -2997,6 +2996,11 @@ packages:
- pylibjpeg[rle] ; extra == 'pixeldata'
- python-gdcm ; extra == 'pixeldata'
requires_python: '>=3.10'
+- pypi: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl
+ name: typing-extensions
+ version: 4.16.0
+ sha256: 481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8
+ requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl
name: imageio
version: 2.37.3
@@ -3316,89 +3320,6 @@ packages:
version: 0.15.2
sha256: d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5
requires_python: '>=3.7'
-- pypi: https://files.pythonhosted.org/packages/5c/1f/1957d07725e6abdaef43b05c30a797fa2a2e7093ced9dbd750d796921a5d/ngff_zarr-0.40.0-py3-none-any.whl
- name: ngff-zarr
- version: 0.40.0
- sha256: d8b07b4bc27a198190cfa98b292b5f1a5b7fb88ff3a72e401308347abb852866
- requires_dist:
- - dask[array]>=2026.1.2
- - importlib-resources
- - itkwasm-downsample>=1.8.0
- - itkwasm>=1.0b183
- - numpy
- - platformdirs
- - psutil ; sys_platform != 'emscripten'
- - rich
- - rich-argparse
- - typing-extensions
- - zarr
- - adlfs ; extra == 'all'
- - aiohttp ; extra == 'all'
- - dask-image ; extra == 'all'
- - dask[distributed] ; extra == 'all'
- - deepdiff ; extra == 'all'
- - fsspec ; extra == 'all'
- - furo>=2024.7.18,<2025 ; extra == 'all'
- - gcsfs ; extra == 'all'
- - imagecodecs>=2025.3.30,<2026.5.0 ; extra == 'all'
- - imageio ; extra == 'all'
- - itk-filtering>=5.3.0 ; extra == 'all'
- - itk-io>=5.3.0 ; extra == 'all'
- - itkwasm-image-io ; extra == 'all'
- - jsonschema ; extra == 'all'
- - liffile ; extra == 'all'
- - myst-parser>=3.0.1,<4 ; extra == 'all'
- - nibabel ; extra == 'all'
- - pooch ; extra == 'all'
- - pytest>=6 ; extra == 'all'
- - requests ; extra == 'all'
- - s3fs ; extra == 'all'
- - sphinx-autobuild>=2024.4.16,<2025 ; extra == 'all'
- - sphinx-autodoc2>=0.5.0,<0.6 ; extra == 'all'
- - sphinx-copybutton>=0.5.2,<0.6 ; extra == 'all'
- - sphinx-design>=0.6.0,<0.7 ; extra == 'all'
- - sphinx>=7.4.7,<8 ; extra == 'all'
- - sphinxext-opengraph>=0.9.1,<0.10 ; extra == 'all'
- - tensorstore ; extra == 'all'
- - tifffile>=2024.7.24 ; extra == 'all'
- - adlfs ; extra == 'cli'
- - aiohttp ; extra == 'cli'
- - dask-image ; extra == 'cli'
- - dask[distributed] ; extra == 'cli'
- - fsspec ; extra == 'cli'
- - gcsfs ; extra == 'cli'
- - imagecodecs>=2025.3.30,<2026.5.0 ; extra == 'cli'
- - imageio ; extra == 'cli'
- - itk-filtering>=5.3.0 ; extra == 'cli'
- - itk-io>=5.3.0 ; extra == 'cli'
- - itkwasm-image-io ; extra == 'cli'
- - liffile ; extra == 'cli'
- - nibabel ; extra == 'cli'
- - requests ; extra == 'cli'
- - s3fs ; extra == 'cli'
- - tifffile>=2024.7.24 ; extra == 'cli'
- - dask-image ; extra == 'dask-image'
- - furo>=2024.7.18,<2025 ; extra == 'docs'
- - myst-parser>=3.0.1,<4 ; extra == 'docs'
- - sphinx-autobuild>=2024.4.16,<2025 ; extra == 'docs'
- - sphinx-autodoc2>=0.5.0,<0.6 ; extra == 'docs'
- - sphinx-copybutton>=0.5.2,<0.6 ; extra == 'docs'
- - sphinx-design>=0.6.0,<0.7 ; extra == 'docs'
- - sphinx>=7.4.7,<8 ; extra == 'docs'
- - sphinxext-opengraph>=0.9.1,<0.10 ; extra == 'docs'
- - itk-filtering>=5.3.0 ; extra == 'itk'
- - adlfs ; extra == 'remote'
- - aiohttp ; extra == 'remote'
- - fsspec ; extra == 'remote'
- - gcsfs ; extra == 'remote'
- - requests ; extra == 'remote'
- - s3fs ; extra == 'remote'
- - tensorstore ; extra == 'tensorstore'
- - deepdiff ; extra == 'test'
- - pooch ; extra == 'test'
- - pytest>=6 ; extra == 'test'
- - jsonschema ; extra == 'validate'
- requires_python: '>=3.10'
- pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl
name: charset-normalizer
version: 3.4.7
@@ -4642,6 +4563,13 @@ packages:
- pytest ; extra == 'testing'
- tox ; extra == 'testing'
requires_python: '>=3.8'
+- pypi: https://files.pythonhosted.org/packages/af/2e/59f3ab896f5a8c76a3d009c40015a32660b1b05b0ff055efcee3aa977caa/zarrista-0.1.0-cp311-abi3-win_amd64.whl
+ name: zarrista
+ version: 0.1.0
+ sha256: 5723fc1a4960392ba220ae17dcf8830805c653f86e9f1933396175062af0cb3d
+ requires_dist:
+ - zarr-metadata>=0.4
+ requires_python: '>=3.11'
- pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl
name: h5py
version: 3.16.0
@@ -4669,6 +4597,13 @@ packages:
version: 0.3.0
sha256: 841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780
requires_python: '>=3.8'
+- pypi: https://files.pythonhosted.org/packages/b2/1a/f5e892909992c91e18b05f45237391ce0af54732a7cb393a79607fea0806/zarr_metadata-0.5.0-py3-none-any.whl
+ name: zarr-metadata
+ version: 0.5.0
+ sha256: 9d0128cee0356def01b0c23cbd9006874452ce1b34489c06f30b3883129a3336
+ requires_dist:
+ - typing-extensions>=4.16
+ requires_python: '>=3.11'
- pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
name: ast-serialize
version: 0.5.0
@@ -4731,6 +4666,15 @@ packages:
- pytest-timeout ; extra == 'testing'
- requests ; extra == 'testing'
requires_python: '>=3.10'
+- pypi: https://files.pythonhosted.org/packages/b3/f6/b16f524cd61d4347b87882350a00869b99d52c1830618a323bd06eb77d0b/itkwasm_downsample-2.0.2-py3-none-any.whl
+ name: itkwasm-downsample
+ version: 2.0.2
+ sha256: e4aad62f586d05ac51fdfe644be21174ceb67a5f9dc10332361bb6f89afdfdfb
+ requires_dist:
+ - itkwasm-downsample-emscripten ; sys_platform == 'emscripten'
+ - itkwasm-downsample-wasi ; sys_platform != 'emscripten'
+ - itkwasm>=1.0b185
+ requires_python: '>=3.8'
- pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl
name: psutil
version: 7.2.2
@@ -5581,6 +5525,13 @@ packages:
- pyreadline ; python_full_version < '3.8' and sys_platform == 'win32'
- pyreadline3 ; python_full_version >= '3.8' and sys_platform == 'win32'
requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*'
+- pypi: https://files.pythonhosted.org/packages/f0/48/3d07340e3256b2cdf441b883da4fd2f7ab89d4894da94a4c627ba8b2f9eb/zarrista-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl
+ name: zarrista
+ version: 0.1.0
+ sha256: a2840000a75fc8367bf44e751c0ef2200138c31c6d859ad03a1dc27059293153
+ requires_dist:
+ - zarr-metadata>=0.4
+ requires_python: '>=3.11'
- pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl
name: stack-data
version: 0.6.3
@@ -5700,6 +5651,76 @@ packages:
- ruff>=0.12.0 ; extra == 'dev'
- cython-lint>=0.12.2 ; extra == 'dev'
requires_python: '>=3.12'
+- pypi: https://files.pythonhosted.org/packages/f7/90/011498cb18e1e3343207efc66e78c819323f3971e47bdb72b800bbd29f0d/ngff_zarr-0.45.0-py3-none-any.whl
+ name: ngff-zarr
+ version: 0.45.0
+ sha256: 9860a09986a938f52881d0def6ed96aee2837ca808c358fd1db011866d6bacc0
+ requires_dist:
+ - dask[array]>=2026.1.2
+ - importlib-resources
+ - itkwasm-downsample>=2.0.2
+ - itkwasm>=1.0b183
+ - numcodecs
+ - numpy
+ - packaging
+ - platformdirs
+ - psutil ; sys_platform != 'emscripten'
+ - rich
+ - rich-argparse
+ - typing-extensions
+ - zarrista
+ - dask-image ; extra == 'all'
+ - dask[distributed] ; extra == 'all'
+ - deepdiff ; extra == 'all'
+ - furo>=2024.7.18,<2025 ; extra == 'all'
+ - imagecodecs>=2025.3.30,<2026.5.0 ; extra == 'all'
+ - imageio ; extra == 'all'
+ - itk-filtering>=5.3.0 ; extra == 'all'
+ - itk-io>=5.3.0 ; extra == 'all'
+ - itkwasm-image-io ; extra == 'all'
+ - jsonschema ; extra == 'all'
+ - liffile ; extra == 'all'
+ - myst-parser>=3.0.1,<4 ; extra == 'all'
+ - nibabel ; extra == 'all'
+ - obstore ; extra == 'all'
+ - pooch ; extra == 'all'
+ - pytest>=6 ; extra == 'all'
+ - sphinx-autobuild>=2024.4.16,<2025 ; extra == 'all'
+ - sphinx-autodoc2>=0.5.0,<0.6 ; extra == 'all'
+ - sphinx-copybutton>=0.5.2,<0.6 ; extra == 'all'
+ - sphinx-design>=0.6.0,<0.7 ; extra == 'all'
+ - sphinx>=7.4.7,<8 ; extra == 'all'
+ - sphinxext-opengraph>=0.9.1,<0.10 ; extra == 'all'
+ - tifffile>=2024.7.24 ; extra == 'all'
+ - zarr ; extra == 'all'
+ - dask-image ; extra == 'cli'
+ - dask[distributed] ; extra == 'cli'
+ - imagecodecs>=2025.3.30,<2026.5.0 ; extra == 'cli'
+ - imageio ; extra == 'cli'
+ - itk-filtering>=5.3.0 ; extra == 'cli'
+ - itk-io>=5.3.0 ; extra == 'cli'
+ - itkwasm-image-io ; extra == 'cli'
+ - liffile ; extra == 'cli'
+ - nibabel ; extra == 'cli'
+ - obstore ; extra == 'cli'
+ - tifffile>=2024.7.24 ; extra == 'cli'
+ - dask-image ; extra == 'dask-image'
+ - furo>=2024.7.18,<2025 ; extra == 'docs'
+ - myst-parser>=3.0.1,<4 ; extra == 'docs'
+ - sphinx-autobuild>=2024.4.16,<2025 ; extra == 'docs'
+ - sphinx-autodoc2>=0.5.0,<0.6 ; extra == 'docs'
+ - sphinx-copybutton>=0.5.2,<0.6 ; extra == 'docs'
+ - sphinx-design>=0.6.0,<0.7 ; extra == 'docs'
+ - sphinx>=7.4.7,<8 ; extra == 'docs'
+ - sphinxext-opengraph>=0.9.1,<0.10 ; extra == 'docs'
+ - itk-filtering>=5.3.0 ; extra == 'itk'
+ - obstore ; extra == 'remote'
+ - deepdiff ; extra == 'test'
+ - pooch ; extra == 'test'
+ - pytest>=6 ; extra == 'test'
+ - zarr ; extra == 'test'
+ - jsonschema ; extra == 'validate'
+ requires_python: '>=3.11'
- pypi: https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl
name: debugpy
version: 1.8.21
diff --git a/pyproject.toml b/pyproject.toml
index 718f24cc..a83971e8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version"]
description = "A declarative execution engine for reproducible medical-imaging workflows"
readme = "README.md"
license = { file = "LICENSE" }
-requires-python = ">=3.10"
+requires-python = ">=3.11"
authors = [
{ name = "Valentin Boussot", email = "boussot.v@gmail.com" }
@@ -22,7 +22,6 @@ classifiers = [
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
@@ -58,7 +57,7 @@ itk = ["SimpleITK>=2.0"]
hdf5 = ["h5py"]
monitoring = ["nvidia-ml-py"]
tensorboard = ["tensorboard"]
-imaging = ["SimpleITK>=2.0", "h5py", "pydicom", "zarr", "ngff-zarr>=0.38", "dask"]
+imaging = ["SimpleITK>=2.0", "h5py", "pydicom", "zarr>=3", "ngff-zarr>=0.45", "dask"]
vtk = ["vtk"]
lpips = ["lpips"]
smp = ["segmentation-models-pytorch"]
@@ -66,7 +65,7 @@ ssim = ["scikit-image"]
fid = ["scipy", "torchvision"]
cluster = ["submitit"]
dicom = ["pydicom"]
-omezarr = ["zarr", "ngff-zarr>=0.38", "dask"]
+omezarr = ["zarr>=3", "ngff-zarr>=0.45", "dask"]
# Reading a dataset root from S3: s3fs is the fsspec implementation, aiohttp the transport
# aiobotocore drives it over. fsspec itself already comes with the OME-Zarr extra, through dask.
s3 = ["s3fs", "aiohttp"]
@@ -91,8 +90,8 @@ dev = [
"SimpleITK>=2.0",
"h5py",
"pydicom",
- "zarr",
- "ngff-zarr>=0.38",
+ "zarr>=3",
+ "ngff-zarr>=0.45",
"dask",
"scikit-image",
"onnx",
@@ -143,7 +142,7 @@ platforms = ["linux-64", "osx-arm64", "win-64"]
konfai = { path = ".", editable = true }
[tool.pixi.dependencies]
-python = ">=3.10,<3.14"
+python = ">=3.11,<3.14"
pip = "*"
[tool.pixi.environments]
@@ -170,8 +169,8 @@ h5py = "*"
tensorboard = "*"
nvidia-ml-py = "*"
pydicom = "*"
-zarr = "*"
-ngff-zarr = ">=0.38"
+zarr = ">=3"
+ngff-zarr = ">=0.45"
dask = "*"
scikit-image = "*"
@@ -191,7 +190,7 @@ check = { depends-on = ["lint", "format-check", "test", "test-apps"], descriptio
# Feature: lint
# ---------------------------------------------------------------------------
[tool.pixi.feature.lint.dependencies]
-python = ">=3.10,<3.14"
+python = ">=3.11,<3.14"
[tool.pixi.feature.lint.pypi-dependencies]
ruff = "==0.15.2"
@@ -205,7 +204,7 @@ pre-commit-run = { cmd = "pre-commit run --all-files", description = "Run pre-co
# Feature: docs
# ---------------------------------------------------------------------------
[tool.pixi.feature.docs.dependencies]
-python = ">=3.10,<3.14"
+python = ">=3.11,<3.14"
[tool.pixi.feature.docs.pypi-dependencies]
sphinx = ">=7.0"
@@ -255,7 +254,7 @@ check_untyped_defs = true
# No `python_version`: mypy analyses as the interpreter it runs on, which is the only version whose
# stubs are installed. Pinning it to the floor of `requires-python` makes mypy refuse to parse
# NumPy's stubs (they use `type` statements) and a parse error inside a dependency stops the whole
-# analysis. Syntax newer than 3.10 is caught by ruff instead, which infers its target from
+# analysis. Syntax newer than 3.11 is caught by ruff instead, which infers its target from
# `requires-python` (rule set `UP`).
# ---------------------------------------------------------------------------
diff --git a/studio/pyproject.toml b/studio/pyproject.toml
index 3436db0c..09d215af 100644
--- a/studio/pyproject.toml
+++ b/studio/pyproject.toml
@@ -9,7 +9,7 @@ name = "konfai-studio"
dynamic = ["version", "dependencies"]
description = "KonfAI Studio - a chatbot web UI driving the konfai-mcp server (BFF + front)"
readme = "README.md"
-requires-python = ">=3.10"
+requires-python = ">=3.11"
license = { text = "Apache-2.0" }
[project.optional-dependencies]
diff --git a/studio/tests/test_live_feed.py b/studio/tests/test_live_feed.py
index 40206078..6bc9f18c 100644
--- a/studio/tests/test_live_feed.py
+++ b/studio/tests/test_live_feed.py
@@ -63,9 +63,8 @@ async def drain() -> None:
break
try:
- # wait_for, not asyncio.timeout: the package supports 3.10 and the latter is 3.11+.
await asyncio.wait_for(drain(), timeout=2)
- except (TimeoutError, asyncio.TimeoutError, asyncio.CancelledError):
+ except (TimeoutError, asyncio.CancelledError):
pass
finally:
await stream.aclose()
@@ -657,7 +656,7 @@ async def drain() -> None:
try:
await asyncio.wait_for(drain(), timeout=3)
- except (TimeoutError, asyncio.TimeoutError, asyncio.CancelledError):
+ except (TimeoutError, asyncio.CancelledError):
pass
finally:
await stream.aclose()
diff --git a/tests/unit/oracle_support.py b/tests/unit/oracle_support.py
index acdee0c3..42c4b4d8 100644
--- a/tests/unit/oracle_support.py
+++ b/tests/unit/oracle_support.py
@@ -29,7 +29,6 @@
"""
import inspect
-import types
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
@@ -613,11 +612,8 @@ def builtin_transforms() -> list[type[Transform]]:
"""Every concrete transform class KonfAI ships."""
return [
cls
- # A subscripted builtin generic (SpatialStages) passes isclass on Python 3.10 but is not a
- # class there, and issubclass refuses it.
for _, cls in inspect.getmembers(transform_module, inspect.isclass)
- if not isinstance(cls, types.GenericAlias)
- and issubclass(cls, Transform)
+ if issubclass(cls, Transform)
and cls.__module__.startswith(transform_module.__name__)
and not inspect.isabstract(cls)
]
@@ -748,8 +744,7 @@ def builtin_augmentations() -> list[type[DataAugmentation]]:
return [
cls
for _, cls in inspect.getmembers(augmentation_module, inspect.isclass)
- if not isinstance(cls, types.GenericAlias)
- and issubclass(cls, DataAugmentation)
+ if issubclass(cls, DataAugmentation)
and cls.__module__ == augmentation_module.__name__
and not inspect.isabstract(cls)
]
diff --git a/tests/unit/test_case_reduction.py b/tests/unit/test_case_reduction.py
index 6ea349eb..8dda475d 100644
--- a/tests/unit/test_case_reduction.py
+++ b/tests/unit/test_case_reduction.py
@@ -20,6 +20,7 @@
never assembled, that a cases which does not agree on its grid is refused before anything is read,
and that a chain continues after the reduction."""
+import dataclasses
import itertools
import time
import weakref
@@ -847,6 +848,40 @@ def test_the_plan_prices_what_a_member_chain_holds_beside_its_region(tmp_path: P
)
+def test_the_plan_prices_the_source_window_a_member_pulls(tmp_path: Path) -> None:
+ """A member's region is not its source: a chain that resamples reaches a box around it, and the
+ fold holds that box while it produces the region.
+
+ The fold priced the landed regions, the operator and the output, and charged nothing for the
+ pull -- so the sizing rested on a figure the run walked straight past. Measured on the bench's
+ own ``Mean +resample x5`` at a 256 MiB budget: the plan printed 127.63 MiB and the run peaked at
+ 1332 MiB; charged, the same row peaks at 754 MiB for a plan of 115 MiB, and the run that cannot
+ fit is refused instead of overshooting.
+ """
+ resampled = _run(
+ tmp_path / "resampled", [Resample(spacing=[1.0, 1.0, 2.0])], Reduce(operator="Mean", output="t"), []
+ )[0]
+ plan = resampled.plan()
+ assert plan.pull_bytes > 0, "a resampling member pulls a source window, and one is resident"
+ assert plan.peak_bytes - dataclasses.replace(plan, pull_bytes=0).peak_bytes >= plan.pull_bytes, (
+ "the peak the sizing is derived from must carry the window, or it is not a bound"
+ )
+
+
+def test_a_budget_no_region_fits_is_refused_rather_than_cut_to_one_row(tmp_path: Path) -> None:
+ """Below one row there is nothing to cut, and no whole-volume path to fall back to: the sizing
+ stops at one row and the plan reports a peak above the budget, which is what the workflow
+ refuses on. Sizing it to something that does not fit and running anyway is what a linear
+ extrapolation through one height used to do."""
+ resampled = _run(tmp_path / "tight", [Resample(spacing=[1.0, 1.0, 2.0])], Reduce(operator="Mean", output="t"), [])[
+ 0
+ ]
+ resampled.fit_budget(4096)
+
+ assert resampled.slab_rows == 1, "one row is the floor, and the plan then says it does not fit"
+ assert resampled.plan().peak_bytes > 4096, "the peak must exceed the budget for the refusal to fire"
+
+
def test_a_generous_budget_stops_at_the_plateau_and_never_below_the_slab_floor(tmp_path: Path) -> None:
"""The budget is a CEILING, not a target: past the height where a chain reads no fewer source
voxels, a taller region only holds more, so the sizing stops there however much memory it is
diff --git a/tests/unit/test_imaging_formats.py b/tests/unit/test_imaging_formats.py
index dea90fb2..1456f151 100644
--- a/tests/unit/test_imaging_formats.py
+++ b/tests/unit/test_imaging_formats.py
@@ -312,22 +312,6 @@ def test_raises_without_zarr(self) -> None:
with pytest.raises(DatasetManagerError, match="zarr is required"):
ome_zarr._require_zarr()
- def test_displacement_field_raises_without_zarr_v3(self, tmp_path: Path) -> None:
- """An RFC-5 field is an NGFF >= 0.5 store, which only zarr v3 can write. The check must fire
- before ngff-zarr is handed the version, so what surfaces on zarr 2.x names the requirement
- instead of the ValueError ngff-zarr raises from inside its writer."""
- pytest.importorskip("zarr")
- from konfai.utils import ome_zarr
-
- field = np.zeros((3, 2, 3, 4), dtype=np.float32)
- with patch.object(ome_zarr, "_zarr_v3_available", lambda: False):
- with pytest.raises(DatasetManagerError, match="zarr v3"):
- ome_zarr.write_ome_zarr(tmp_path / "DVF.ome.zarr", field, displacement_field=True)
-
- # The guard is specific to RFC-5: an ordinary image is a 0.4 store and stays writable.
- ome_zarr.write_ome_zarr(tmp_path / "Image.ome.zarr", field)
- assert (tmp_path / "Image.ome.zarr").is_dir()
-
class TestDatasetImagingBackends:
def test_ome_zarr_dataset_round_trip_and_patch_read(self, tmp_path: Path) -> None:
diff --git a/tests/unit/test_ome_zarr_data_surface.py b/tests/unit/test_ome_zarr_data_surface.py
index ed8edb4a..05ed2336 100644
--- a/tests/unit/test_ome_zarr_data_surface.py
+++ b/tests/unit/test_ome_zarr_data_surface.py
@@ -113,13 +113,13 @@ def test_a_stepped_selection_falls_back_instead_of_returning_a_wrong_window(tmp_
def test_the_konfai_sidecar_is_read_once_and_not_once_per_region(tmp_path: Path) -> None:
"""It is metadata, and a streamed run asks for it once per region: re-opening the store each
time is a read it never needed."""
- from konfai.utils.ome_zarr import _konfai_attributes, clear_ome_zarr_cache
+ from konfai.utils.ome_zarr import _multiscales, clear_ome_zarr_cache
store = _big_endian_store(tmp_path / "big_endian.ome.zarr", _volume())
clear_ome_zarr_cache()
for row in range(4):
read_ome_zarr_data_slice(store, (slice(None), slice(row, row + 1), slice(None), slice(None)))
- assert _konfai_attributes.cache_info() == (3, 1, 8, 1), "one read of the sidecar, three regions"
+ assert _multiscales.cache_info().misses == 1, "one parse of the store, however many regions"
def test_info_publishes_the_shape_the_reader_indexes(tmp_path: Path) -> None:
diff --git a/tests/unit/test_omezarr_displacement_field.py b/tests/unit/test_omezarr_displacement_field.py
index 62578275..a7ae4a15 100644
--- a/tests/unit/test_omezarr_displacement_field.py
+++ b/tests/unit/test_omezarr_displacement_field.py
@@ -34,19 +34,12 @@
from konfai.utils.dataset import DISPLACEMENT_FIELD_ATTRIBUTE, Attribute, Dataset # noqa: E402
from konfai.utils.ome_zarr import ( # noqa: E402
- _zarr_v3_available,
+ append_ome_zarr_levels,
clear_ome_zarr_cache,
is_displacement_field,
write_ome_zarr,
)
-# RFC-5 fields are a zarr v3 store (NGFF >= 0.5), which zarr 2.x (Python 3.10) cannot write: the
-# feature does not exist there.
-pytestmark = pytest.mark.skipif(
- not _zarr_v3_available(),
- reason="NGFF RFC-5 displacement fields need a zarr v3 store (zarr>=3, Python>=3.11)",
-)
-
SPACING = (1.5, 1.5, 2.0)
ORIGIN = (7.0, -3.0, 10.0)
# A 90 deg in-plane rotation: an identity matrix would let a dropped Direction pass unnoticed.
@@ -167,3 +160,144 @@ def test_a_field_streamed_region_by_region_is_still_a_field(tmp_path: Path) -> N
clear_ome_zarr_cache()
assert is_displacement_field(tmp_path / "dataset" / "case" / "DVF.ome.zarr")
assert isinstance(dataset.read_transform("DVF", "case"), sitk.DisplacementFieldTransform)
+
+
+def _axis_aligned_field_transform() -> "sitk.DisplacementFieldTransform":
+ """The conformant case: a field whose grid carries no rotation."""
+ values = np.arange(4 * 5 * 6 * 3, dtype=np.float64).reshape(4, 5, 6, 3)
+ field = sitk.GetImageFromArray(values, isVector=True)
+ field.SetSpacing(SPACING)
+ field.SetOrigin(ORIGIN)
+ return sitk.DisplacementFieldTransform(sitk.Cast(field, sitk.sitkVectorFloat64))
+
+
+def test_an_axis_aligned_field_is_an_applicable_rfc5_transformation(tmp_path: Path) -> None:
+ """The store is not merely labelled, a spec reader can APPLY it.
+
+ ngff-zarr reads the ``displacements`` entry the writer declares, rebuilds a native ITK
+ transform from the store alone -- no KonfAI in the loop -- and that transform maps points
+ exactly as the SimpleITK original. This is the whole meaning of conformance: the mistakes it
+ rules out (component order, value frame) are silent and produce plausible registrations.
+ """
+ itk = pytest.importorskip("itk")
+ import dataclasses
+
+ import ngff_zarr
+
+ original = _axis_aligned_field_transform()
+ _store(tmp_path).write("case", "DVF", original)
+ store = next(tmp_path.rglob("*.ome.zarr"))
+
+ multiscales = ngff_zarr.from_ngff_zarr(str(store))
+ entries = multiscales.metadata.coordinateTransformations or []
+ assert [entry.type for entry in entries] == ["displacements"]
+
+ wasm = ngff_zarr.ngff_displacement_field_to_itk_transform(entries[0], multiscales, ["z", "y", "x"])
+ native = itk.transform_from_dict(dataclasses.asdict(wasm[0]))
+ native = native[0] if isinstance(native, (list, tuple)) else native
+ for point in ((9.0, -1.0, 12.0), (7.5, -2.5, 11.0), (10.0, 0.0, 14.0)):
+ assert tuple(native.TransformPoint(point)) == pytest.approx(original.TransformPoint(point))
+
+
+def test_a_conformant_store_holds_spec_ordered_components(tmp_path: Path) -> None:
+ """On disk the components follow the output axes (dz, dy, dx); through KonfAI's reader they
+ come back in ITK's (dx, dy, dz). Both at once, or one side is silently wrong: the store for
+ every spec reader, the flip for every KonfAI consumer."""
+ import zarr
+ from konfai.data import read_ome_zarr_data_slice
+
+ values = np.arange(3 * 4 * 5 * 6, dtype=np.float32).reshape(3, 4, 5, 6) / 100.0
+ attributes = Attribute()
+ attributes["Spacing"] = np.asarray(SPACING)
+ attributes["Origin"] = np.asarray(ORIGIN)
+ attributes[DISPLACEMENT_FIELD_ATTRIBUTE] = "true"
+
+ dataset = _store(tmp_path)
+ stream = dataset.open_data_stream("DVF", "case", list(values.shape), values.dtype, attributes)
+ assert stream is not None
+ with stream:
+ for z in range(0, values.shape[1], 2):
+ stream.write_slice(
+ (slice(0, 3), slice(z, z + 2), slice(0, values.shape[2]), slice(0, values.shape[3])),
+ values[:, z : z + 2],
+ )
+ clear_ome_zarr_cache()
+ store = next(tmp_path.rglob("*.ome.zarr"))
+
+ stored = zarr.open_group(str(store), mode="r")["scale0/image"][...]
+ np.testing.assert_array_equal(stored, values[::-1], err_msg="the store must hold (dz, dy, dx)")
+
+ back, _ = read_ome_zarr_data_slice(store, tuple(slice(None) for _ in values.shape))
+ np.testing.assert_array_equal(back, values, err_msg="the reader must hand back (dx, dy, dz)")
+ one, _ = read_ome_zarr_data_slice(store, (slice(0, 1), *[slice(None)] * 3))
+ np.testing.assert_array_equal(one[0], values[0], err_msg="a partial component read must remap too")
+
+
+def test_an_oriented_field_keeps_the_label_only_layout(tmp_path: Path) -> None:
+ """A rotated grid has no RFC-5 spelling (scale and translation cannot carry a direction), so
+ no ``displacements`` entry that would promise a reader an application it cannot make -- but the
+ LAYOUT is the same one every field shares: components in the spec's order, marked as such, the
+ Direction in the sidecar. One convention on disk, whatever the grid."""
+ import ngff_zarr
+ import zarr
+
+ original = _displacement_field_transform() # carries the 90-degree DIRECTION
+ _store(tmp_path).write("case", "DVF", original)
+ store = next(tmp_path.rglob("*.ome.zarr"))
+
+ multiscales = ngff_zarr.from_ngff_zarr(str(store))
+ assert not (multiscales.metadata.coordinateTransformations or [])
+ assert is_displacement_field(store)
+ stored = zarr.open_group(str(store), mode="r")["scale0/image"][...]
+ itk_order = np.moveaxis(sitk.GetArrayFromImage(original.GetDisplacementField()), -1, 0)
+ np.testing.assert_array_equal(stored, itk_order[::-1], err_msg="oriented fields share the spec order")
+
+
+def test_a_pre_19_store_is_refused_by_name(tmp_path: Path) -> None:
+ """A store with the typed axis but neither the ``displacements`` entry nor the component-order
+ marker is every store this backend wrote before 1.9, and its components are ITK-ordered. Read
+ under either convention it is a guess -- a plausible field with dx and dz possibly exchanged --
+ so it is refused with the layout named, not read with one."""
+ import dask.array
+ import ngff_zarr
+ from konfai.data import read_ome_zarr_data_slice
+
+ values = np.arange(3 * 4 * 5 * 6, dtype=np.float32).reshape(3, 4, 5, 6)
+ image = ngff_zarr.to_ngff_image(
+ dask.array.from_array(values, chunks=values.shape),
+ dims=["c", "z", "y", "x"],
+ scale={"c": 1.0, "z": SPACING[2], "y": SPACING[1], "x": SPACING[0]},
+ translation={"c": 0.0, "z": ORIGIN[2], "y": ORIGIN[1], "x": ORIGIN[0]},
+ )
+ image.axes_types = {"c": "displacement"}
+ multiscales = ngff_zarr.to_multiscales(image, scale_factors=[], cache=False)
+ store = tmp_path / "legacy.ome.zarr"
+ ngff_zarr.to_ngff_zarr(str(store), multiscales, overwrite=True, version="0.6")
+ clear_ome_zarr_cache()
+
+ with pytest.raises(DatasetManagerError, match=r"KonfAI < 1\.9"):
+ read_ome_zarr_data_slice(store, tuple(slice(None) for _ in values.shape))
+
+
+@pytest.mark.parametrize("oriented", [False, True], ids=["axis-aligned", "oriented"])
+def test_appending_levels_keeps_the_field_a_field(tmp_path: Path, oriented: bool) -> None:
+ """A pyramid grafted onto a field store rewrites the multiscales document, which is where the
+ typed component axis and the ``displacements`` entry live. Both must survive, or the store
+ comes back an ordinary 3-channel image and nothing says so."""
+ import ngff_zarr
+
+ transform = _displacement_field_transform() if oriented else _axis_aligned_field_transform()
+ _store(tmp_path).write("case", "DVF", transform)
+ store = next(tmp_path.rglob("*.ome.zarr"))
+ before = ngff_zarr.from_ngff_zarr(str(store)).metadata
+
+ append_ome_zarr_levels(store, [2])
+ clear_ome_zarr_cache()
+
+ after = ngff_zarr.from_ngff_zarr(str(store)).metadata
+ assert [dataset.path for dataset in after.datasets] == ["scale0/image", "scale1/image"]
+ assert is_displacement_field(store)
+ # The entry an axis-aligned field declares keeps naming level 0, which the append never
+ # rewrites; an oriented one has none to keep.
+ assert (after.coordinateTransformations or []) == (before.coordinateTransformations or [])
+ assert {system.name for system in after.coordinateSystems} == {system.name for system in before.coordinateSystems}
diff --git a/tests/unit/test_omezarr_store_creation.py b/tests/unit/test_omezarr_store_creation.py
index 570fc595..f2968a45 100644
--- a/tests/unit/test_omezarr_store_creation.py
+++ b/tests/unit/test_omezarr_store_creation.py
@@ -35,7 +35,6 @@
import zarr
from konfai.utils.ome_zarr import (
- _zarr_v3_available,
clear_ome_zarr_cache,
create_ome_zarr_store,
get_ome_zarr_info,
@@ -159,10 +158,6 @@ def test_creating_a_store_writes_no_pixel_bytes(tmp_path: Path) -> None:
assert written < metadata_only, f"{written} bytes written for an untouched store"
-@pytest.mark.skipif(
- not _zarr_v3_available(),
- reason="NGFF RFC-5 displacement fields need a zarr v3 store (zarr>=3, Python>=3.11)",
-)
def test_a_streamed_displacement_field_says_that_it_is_one(tmp_path: Path) -> None:
"""A field written region by region must be as self-describing as one written whole.
@@ -187,3 +182,20 @@ def test_a_store_is_not_a_field_unless_it_was_asked_to_be(tmp_path: Path) -> Non
clear_ome_zarr_cache()
assert not is_displacement_field(tmp_path / "image.ome.zarr")
+
+
+def test_stores_keep_the_byte_shuffled_blosc_compressor(tmp_path: Path) -> None:
+ """The zarrista writer's default is zstd-0; measured on a CT-like uint16 volume that costs
+ +19 % of disk and ~+11 % on the streamed read against the byte-shuffled lz4 every 1.8.2 store
+ carries, so the v2 compressor is pinned, and an appended level takes level 0's own."""
+ from konfai.utils.ome_zarr import append_ome_zarr_levels, write_ome_zarr
+
+ volume = np.arange(1 * 16 * 16 * 16, dtype=np.uint16).reshape(1, 16, 16, 16)
+ store = tmp_path / "v.ome.zarr"
+ write_ome_zarr(store, volume, spacing=[1.0, 1.0, 1.0], origin=[0.0, 0.0, 0.0])
+ append_ome_zarr_levels(store, [4])
+ group = zarr.open_group(str(store), mode="r")
+ for key in ("scale0/image", "scale1/image"):
+ compressor = group[key].metadata.to_dict().get("compressor")
+ assert compressor is not None and compressor["id"] == "blosc", (key, compressor)
+ assert compressor["cname"] == "lz4" and compressor["shuffle"] == 1, (key, compressor)
diff --git a/tests/unit/test_runtime.py b/tests/unit/test_runtime.py
index cee992c4..fa9f0d96 100644
--- a/tests/unit/test_runtime.py
+++ b/tests/unit/test_runtime.py
@@ -33,7 +33,6 @@
from konfai.predictor import Predictor
from konfai.trainer import Trainer
from konfai.utils.errors import ConfigError
-from konfai.utils.ome_zarr import _zarr_v3_available
from konfai.utils.runtime import (
DistributedObject,
State,
@@ -845,7 +844,7 @@ def test_zarr_keeps_a_small_share_whole(monkeypatch, cores: int, expected: int)
"""A third of a 24-core share is the measured point; a third of four cores is one chunk in
flight, which on a remote root is the whole of the read's parallelism."""
zarr = pytest.importorskip("zarr")
- if not _zarr_v3_available(): # 2.x has no config object, and no async reader to share the cores with
+ if not hasattr(zarr, "config"): # 2.x has no config object, and no async reader to share the cores with
pytest.skip("zarr 2.x has no async reader to size")
previous = zarr.config.get("async.concurrency")
try:
diff --git a/tests/unit/test_sweep_tiling.py b/tests/unit/test_sweep_tiling.py
index ebc2620c..9fdc771d 100644
--- a/tests/unit/test_sweep_tiling.py
+++ b/tests/unit/test_sweep_tiling.py
@@ -430,10 +430,10 @@ def test_a_region_read_is_charged_only_for_what_the_block_grid_adds(
a region under one stored block decodes the block anyway and simply wastes more of it."""
source, _volume = _sheared_fixture(tmp_path)
manager = _manager(source, [Save(f"{tmp_path / 'out'}:h5")])
- assert manager.region_reads(6)[0] == 0, "a store with no block grid adds nothing"
+ assert manager.region_reads(6).widest_excess == 0, "a store with no block grid adds nothing"
_chunked(monkeypatch, (16, 128, 128))
manager._read_granularity = patching_module._UNRESOLVED
- assert manager.region_reads(16)[0] < manager.region_reads(4)[0], (
+ assert manager.region_reads(16).widest_excess < manager.region_reads(4).widest_excess, (
"a region under one stored block wastes more of the block it decodes, not less"
)
diff --git a/tests/unit/test_transformer_workflow.py b/tests/unit/test_transformer_workflow.py
index 8916460c..a7c65d41 100644
--- a/tests/unit/test_transformer_workflow.py
+++ b/tests/unit/test_transformer_workflow.py
@@ -1730,10 +1730,10 @@ def _write_snapshot_cohort(tmp_path: Path) -> Path:
CT -> C (Clip -> Expand -> Brightness -> Write /out_c:h5): EXPAND 3 case(s) -> 9 cop(ies): 5 STREAM (shared read pass), 1 STREAM (own pass), 0 WHOLE-VOLUME, 3 SKIP (copy already written)
(1 cop(ies)) own pass: the only copy of this case still to write; a shared pass with one member is its own sweep.
CT -> D (Clip -> Reduce -> Write /out_d:h5): REDUCE 3 case(s) -> 1 output 'atlas': REDUCE
- 4.5 resident region(s) of 4 row(s) = 0.00 GiB (incremental accumulator)
- reads: 1 of 3 member(s) sit on nii.gz, which decodes the whole volume behind every region read: 2 decodes per member (one per region), 2 in all
+ 4.5 resident region(s) of 3 row(s) = 0.00 GiB (incremental accumulator)
+ reads: 1 of 3 member(s) sit on nii.gz, which decodes the whole volume behind every region read: 3 decodes per member (one per region), 3 in all
put a Save ...:h5 before the Reduce so each member is materialized on a bounded store first
- peak ~= 72.00 KiB vs the regions' share of the budget, 80.00 KiB of 160.00 KiB per rank
+ peak ~= 66.00 KiB vs the regions' share of the budget, 80.00 KiB of 160.00 KiB per rank
cases: CASE_000, CASE_001, CASE_002
CT -> E (Clip -> Standardize -> Reduce -> Write /out_e:h5): REDUCE 3 case(s) -> 1 output 'atlas': REFUSED
case 'CASE_000': stage 1 'Standardize' needs whole-volume statistics, but an earlier stage changes the values: the stored volume's statistic is not this stage's input.
diff --git a/tests/unit/test_warp.py b/tests/unit/test_warp.py
index 47c8be50..ffcce329 100644
--- a/tests/unit/test_warp.py
+++ b/tests/unit/test_warp.py
@@ -29,18 +29,10 @@
from konfai.data.transform import LocalityKind, Resample, Save
from konfai.utils.dataset import Attribute, Dataset
from konfai.utils.errors import TransformError
-from konfai.utils.ome_zarr import _zarr_v3_available
from oracle_support import geometry, manager
pytest.importorskip("SimpleITK")
-# A field records its bound in an RFC-5 store, which is zarr v3 (NGFF >= 0.5): and zarr 2.x, the
-# newest release for Python 3.10, cannot write one. Everything else here reads an h5 field.
-_needs_rfc5 = pytest.mark.skipif(
- not _zarr_v3_available(),
- reason="a displacement field's bound is recorded in a zarr v3 store (zarr>=3, Python>=3.11)",
-)
-
SPACING = (2.0, 1.0, 1.0) # (x, y, z) SimpleITK order