Skip to content

build: require itkwasm-downsample 2.0.0 and repair the baseline comparisons - #628

Merged
thewtex merged 20 commits into
fideus-labs:mainfrom
vboussot:feat/itkwasm-downsample-2
Aug 11, 2026
Merged

build: require itkwasm-downsample 2.0.0 and repair the baseline comparisons#628
thewtex merged 20 commits into
fideus-labs:mainfrom
vboussot:feat/itkwasm-downsample-2

Conversation

@vboussot

@vboussot vboussot commented Aug 4, 2026

Copy link
Copy Markdown
Member

Closes #625. Closes #627.

Bumps itkwasm-downsample to 2.0.0 in both ports. Verifying a downsampling engine requires a verification apparatus that works; repairing it surfaced real bugs. The problems, and the choices made:

The baseline comparisons had been a no-op for 20 months. A path typo (8bb29ba) opened an empty store, and every comparison passed vacuously. → Path fixed; a missing baseline now skips explicitly instead of passing; comparisons work on decompressed values and parsed metadata, so the codec zarr picks per release doesn't matter.

The archive was stale. Every baseline predated the #1556 half-pixel grid shift; stores carried v2+v3 metadata side by side; a zarr2 layout nothing reads since dask ≥ 2026.1.2. → Everything regenerated, dead weight dropped (329 → 273 MB).

TensorStore wrote different stores than zarr-python. With no codecs declared it applied its own defaults (sharded, uncompressed — round trips halved chunk sizes), and requested codecs could be dropped. → Codecs always declared, compressors honoured; the four dedicated tests fail without the fix.

The TypeScript Gaussian was biased ~−1.2 on integer images. itkwasm's filter truncates in integer arithmetic; Python has cast to float32 and rounded back since c93fe01, TS never did. → Same workaround in TS, rounding half to even like np.rint. Both ports now produce bit-identical stores at every scale, data and metadata — verified.

to_ngff_zarr rewrote scales at write time and missed targets. scale_strategy="pad" (the default) re-downsampled incrementally and silently wrote (32, 64, 64) where the metadata declared the factor-3 (43, 85, 85): stores misdescribing their own geometry. Three baselines had archived exactly that. → Default is now "exact", pad stays opt-in, the three baselines were regenerated (the v0.21.0 asset is replaced in place — only this branch ever referenced it). Both writers also mutated their caller's objects; they work on shallow copies now.

Native-method baselines encode x86 floating point. Now that the comparison compares, Apple Silicon diverges on the ITK and dask-image Gaussians. → Those two comparisons skip explicitly off x86; the ITKWASM methods stay exact on every architecture — wasm is deterministic across them, and that is the guarantee that matters.

Suites: Python 813 passed / 3 skipped · TypeScript 466 passed / 0 failed.

Summary by CodeRabbit

  • New Features

    • Improved Gaussian downsampling for integer images while preserving pixel formats and safely handling rounding and value ranges.
    • Added support for modern Zarr codecs, compressor sequences, and sharded datasets.
    • Multiscale writing now defaults to exact scaling for predictable output dimensions.
    • Writers avoid modifying source images or callback state.
  • Bug Fixes

    • Improved cross-format metadata, shape, type, and pixel-value comparisons.
    • Updated compatibility with the latest downsampling release.
  • Tests

    • Expanded coverage for compression, scaling, isolation, and platform-specific results.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR updates itkwasm-downsample to 2.x, adds integer-safe Gaussian downsampling, improves Zarr v3 codec handling and writer isolation, changes the default scale strategy to "exact", and repairs Python and TypeScript baseline validation.

Changes

Downsample and Zarr alignment

Layer / File(s) Summary
Integer Gaussian handling
ts/src/methods/itkwasm-shared.ts, ts/src/methods/itkwasm-browser.ts, ts/src/methods/itkwasm-node.ts, ts/deno.json, py/pyproject.toml
Integer images use float32 during Gaussian filtering and return to the original integer type with rounding and clamping. Python and Deno target downsample 2.x.
Zarr writer codecs and isolation
py/ngff_zarr/to_multiscales.py, py/ngff_zarr/to_ngff_zarr.py
Zarr v3 writes use explicit bytes and compression codecs. Compressor sequences are supported. Multiscale inputs and callback lists are copied before writing.
Versioned baseline validation
py/test/_data.py, py/test/test_baseline_guard.py, py/test/test_to_ngff_zarr_sharding.py, py/test/test_to_ngff_zarr_v3_compression.py, py/test/test_to_ngff_zarr_dask_image.py, py/test/test_to_ngff_zarr_itk.py
Baseline data and paths are updated. Comparisons validate metadata and decompressed arrays. Missing baselines skip explicitly, and architecture-dependent Gaussian tests skip on non-x86 systems.
Scale and writer regression tests
py/test/test_non_power_of_2_scale_factors.py, py/test/test_writer_isolation.py
Tests cover the "exact" default, explicit "pad" behavior, compressor preservation, backend parity, and input-state isolation.
TypeScript baseline comparison
ts/test/baseline_comparison_test.ts
Baseline checks compare image metadata and pixel values synchronously. The MR-head test uses default anatomical orientation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GaussianMethod
  participant castImageToFloat32
  participant GaussianFilter
  participant castImageToIntegerType
  GaussianMethod->>castImageToFloat32: convert integer image
  castImageToFloat32->>GaussianFilter: provide float32 image
  GaussianFilter->>castImageToIntegerType: return filtered float image
  castImageToIntegerType->>GaussianMethod: restore integer component type
Loading
sequenceDiagram
  participant to_ome_zarr
  participant _to_ngff_zarr_impl
  participant TensorStore
  participant ZarrStore
  to_ome_zarr->>_to_ngff_zarr_impl: pass exact scale strategy
  _to_ngff_zarr_impl->>TensorStore: pass copied multiscale state and compressors
  TensorStore->>ZarrStore: write array data and codec metadata
  ZarrStore->>_to_ngff_zarr_impl: complete Zarr array write
Loading

Possibly related PRs

Suggested reviewers: thewtex

Poem

A rabbit guards each pixel bright,
Floats it through the filter right.
Zarr codecs stack in rows,
Exact scales guide data flows.
Baselines compare, callbacks sleep—
Safe copied state the burrow keeps.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the dependency upgrade and baseline comparison repairs, which are the primary changes.
Linked Issues check ✅ Passed The changes update version 2.0.0, repair baseline paths and comparisons, regenerate test data, and cover related compatibility refactoring.
Out of Scope Changes check ✅ Passed The additional codec, scale-strategy, writer-isolation, integer-handling, and architecture changes support the linked upgrade and baseline objectives.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@vboussot
vboussot marked this pull request as ready for review August 5, 2026 11:31
@vboussot vboussot changed the title build: require itkwasm-downsample 2.0.0 in both ports build: require itkwasm-downsample 2.0.0 and repair the baseline comparisons Aug 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@py/ngff_zarr/to_multiscales.py`:
- Around line 503-506: Update the NgffImage handling in to_multiscales so the
shallow-copied image receives its own computed_callbacks list before caching can
append cleanup callbacks. Preserve the existing conversion path for
non-NgffImage inputs and avoid mutating the caller’s callback list across
repeated calls.

In `@py/ngff_zarr/to_ngff_zarr.py`:
- Around line 308-336: Update create_compression_codec to detect and serialize
native Zarr v3 codecs through their to_dict() method before attempting the
existing codec_id-based handling. Ensure native codecs such as
zarr.codecs.BloscCodec are returned as the configured compression codec, so the
codec selection in the surrounding writer does not fall back to
default_compression.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b47e5315-3825-41a1-8052-4bc3a5cbbc97

📥 Commits

Reviewing files that changed from the base of the PR and between d2a83b6 and aecf25f.

📒 Files selected for processing (5)
  • py/ngff_zarr/to_multiscales.py
  • py/ngff_zarr/to_ngff_zarr.py
  • py/test/_data.py
  • py/test/test_to_ngff_zarr_sharding.py
  • py/test/test_to_ngff_zarr_v3_compression.py

Comment thread py/ngff_zarr/to_multiscales.py Outdated
Comment thread py/ngff_zarr/to_ngff_zarr.py Outdated
@vboussot
vboussot force-pushed the feat/itkwasm-downsample-2 branch from 2b7bf54 to c49d6dd Compare August 5, 2026 14:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
py/ngff_zarr/to_ngff_zarr.py (2)

337-340: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve the full compressors contract.

The TensorStore path reduces a Zarr v3 codec sequence to one codec and treats compressors=None as default Zstd. It also replaces unsupported codecs with default Zstd. Preserve the complete sequence and explicit no-compression sentinel, or reject unsupported sequences instead of changing the stored encoding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py/ngff_zarr/to_ngff_zarr.py` around lines 337 - 340, Update the codec
construction around create_compression_codec and inner_codecs to preserve the
full compressors sequence and its ordering. Treat compressors=None as the
explicit no-compression sentinel, and do not substitute default_compression or
silently replace unsupported codecs; reject unsupported codec sequences instead
while keeping supported codecs unchanged.

343-351: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add required index_codecs to sharding_indexed codec configuration.

Zarr v3 specification requires index_codecs as a mandatory configuration member within the sharding_indexed codec. Current code at lines 343–351 omits this field. Add index_codecs with a bytes codec (little-endian) followed by a CRC32C checksum codec, matching the specification recommendation:

Suggested change
if internal_chunk_shape:
    codecs.append(
        {
            "name": "sharding_indexed",
            "configuration": {
                "chunk_shape": internal_chunk_shape,
                "codecs": inner_codecs,
                "index_codecs": [
                    {"name": "bytes", "configuration": {"endian": "little"}},
                    {"name": "crc32c"},
                ],
            },
        }
    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py/ngff_zarr/to_ngff_zarr.py` around lines 343 - 351, Update the
sharding_indexed configuration in the internal_chunk_shape branch to include the
required index_codecs list, using a little-endian bytes codec followed by a
crc32c codec; preserve the existing chunk_shape and inner_codecs entries.
🧹 Nitpick comments (1)
py/test/test_baseline_guard.py (1)

14-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use an absolute import for _data.

Replace from ._data import ... with the package-qualified absolute import used by this repository. The current import depends on pytest collecting py/test as a package.

As per coding guidelines, Python files under py/ must use absolute imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py/test/test_baseline_guard.py` at line 14, Update the imports in
test_baseline_guard.py to replace the relative ._data import with the
repository’s package-qualified absolute import, preserving the existing
store_equals and verify_against_baseline symbols.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/typescript.yml:
- Around line 30-34: Pin the release workflow’s deno-version setting to "2.8.3"
instead of the floating v2.x version, keeping Deno formatting and build behavior
consistent with the test and lint workflows.

In `@py/ngff_zarr/to_ngff_zarr.py`:
- Around line 592-599: Update _is_bytes_codec to handle string codec entries
before object and dictionary introspection, returning true when the string is
"bytes"; alternatively, reject string codec entries consistently at validation.
Preserve existing detection for codec objects and dictionaries so the
TensorStore codec chain creates only one bytes codec.
- Around line 272-291: The Blosc codec evolution in the codec serialization
branch updates typesize without synchronizing shuffle. Update this logic to use
compressor.evolve_from_array_spec() before to_dict(), or manually set both
fields so multi-byte dtypes use the evolved typesize and "shuffle" while
preserving explicit caller settings; add coverage for default and explicitly
provided typesize/shuffle configurations.

---

Outside diff comments:
In `@py/ngff_zarr/to_ngff_zarr.py`:
- Around line 337-340: Update the codec construction around
create_compression_codec and inner_codecs to preserve the full compressors
sequence and its ordering. Treat compressors=None as the explicit no-compression
sentinel, and do not substitute default_compression or silently replace
unsupported codecs; reject unsupported codec sequences instead while keeping
supported codecs unchanged.
- Around line 343-351: Update the sharding_indexed configuration in the
internal_chunk_shape branch to include the required index_codecs list, using a
little-endian bytes codec followed by a crc32c codec; preserve the existing
chunk_shape and inner_codecs entries.

---

Nitpick comments:
In `@py/test/test_baseline_guard.py`:
- Line 14: Update the imports in test_baseline_guard.py to replace the relative
._data import with the repository’s package-qualified absolute import,
preserving the existing store_equals and verify_against_baseline symbols.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d3b2680-9ecc-4f97-87c6-17f297c1a94e

📥 Commits

Reviewing files that changed from the base of the PR and between aecf25f and 2b7bf54.

📒 Files selected for processing (5)
  • .github/workflows/typescript.yml
  • py/ngff_zarr/to_multiscales.py
  • py/ngff_zarr/to_ngff_zarr.py
  • py/test/_data.py
  • py/test/test_baseline_guard.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • py/ngff_zarr/to_multiscales.py
  • py/test/_data.py

Comment thread .github/workflows/typescript.yml Outdated
Comment thread py/ngff_zarr/to_ngff_zarr.py Outdated
Comment thread py/ngff_zarr/to_ngff_zarr.py
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@vboussot
vboussot marked this pull request as draft August 5, 2026 14:39
@vboussot
vboussot force-pushed the feat/itkwasm-downsample-2 branch from c49d6dd to 57c2bfb Compare August 5, 2026 14:41
Closes fideus-labs#625.

Brings in the information-only output handling
(InsightSoftwareConsortium/ITK-Wasm#1545), the origin fix
(InsightSoftwareConsortium/ITK-Wasm#1556), and the resample-bounding-box
pipeline fideus-labs#626 needs.

#1556 shifts the downsample and downsample-label-image grid by half a coarse
pixel to match downsample-bin-shrink, which was already correct. ngff-zarr
already wrote that shift into its per-scale translation, so pixels and
metadata now agree. Every baseline predates it and had to be regenerated.
verify_against_baseline read from baseline/v{version}/ while the writer uses
baseline/zarr{N}/v{version}/. The missing segment opened an empty store, and
store_equals walks the baseline's keys, so an empty baseline compared equal to
anything. Every comparison had been passing vacuously since 8bb29ba.

Turning them back on showed the comparison itself was too strict: chunks were
compared as stored, so compressed. zarr picks the codec and changed its
default between releases, zstd until 3.2 and blosc/lz4 from 3.3, which gives
different bytes for identical data. Compare decompressed arrays instead, parse
the metadata rather than diffing its bytes, and ignore which codec zarr chose.

That also widens coverage: nested .zattrs and .zgroup previously matched
neither branch and were not compared at all, which is where the axes and the
RFC-4 orientation live.

A missing baseline now skips explicitly rather than passing, with tests
pinning that down.
Regenerated every baseline, since #1556 shifted the downsampling grid by half
a coarse pixel and the stored ones all predate it.

The old archive also carried stores holding both Zarr v2 and v3 metadata side
by side, 28 files where a clean write produces 18, and a baseline/zarr2/
layout nothing reads. Dropping those takes it from 329 MB to 273 MB, and the
baselines from 274 MB to 56 MB. The flat layout the TypeScript suite reads is
preserved.

Six tests across three files also verified against
cthead1/2_4/RFC3_GAUSSIAN.zarr while writing two different stores: the rfc2
tests use the default chunks, the sharding and compression tests pass
chunks=(64,64). Whichever regenerated it last won, so no archive could satisfy
all of them. The chunks=(64,64) group now has its own name.
The TensorStore spec only set "codecs" when a compressor or sharding was
requested. With neither, the key was omitted and TensorStore applied its own
defaults, which shard the array and drop compression, so the same call gave a
different store depending on the backend:

  zarr-python : chunk_grid [130,128,128], codecs [bytes, zstd]
  tensorstore : chunk_grid [130,128,128], codecs [sharding_indexed],
                inner chunk [65,64,64], no compression

Reading such a store back reports the inner chunk, so a round trip halved the
chunk size on every axis. A supplied codec was dropped too: the helper keyed
off codec_id, which native Zarr v3 codecs lack, the writer read only
"compressor" and never the "compressors" the API takes, and blosc's typesize
came from to_dict() rather than the dtype.

to_multiscales also replaced ngff_image.data on the object it was handed, so a
caller's image came back rechunked and results depended on what ran before.
Shallow-copy it, with its own computed_callbacks so the cache cleanup does not
pile up once per call.

The baseline comparison works on decompressed values and so covers none of
this, hence the dedicated tests. All four fail without these changes.
@vboussot
vboussot force-pushed the feat/itkwasm-downsample-2 branch from 57c2bfb to 6121aad Compare August 5, 2026 15:58
itkwasm-downsample's Gaussian filter loses precision in integer arithmetic
(a float 128.0 comes back 127), a systematic ~-1.2 bias on uint8/int16.
The Python port has cast to float32 and rounded back since c93fe01; mirror
it in both the Node and browser paths, rounding half to even to match
np.rint. Package outputs are otherwise bit-identical between ports.
@itk-wasm/compare-images throws on any 3D image with a non-identity
direction, even compared to itself; compare geometry and pixels directly
instead, mirroring the Python port's store_equals. The rebuilt archive
carries RFC-4 orientation, so stop stripping it on the test side.
…ller

to_ngff_zarr's task-graph optimization re-downsamples each scale from the
one it just wrote. With scale_strategy="pad", the default, targets that no
integer factor reaches are silently missed: for MR-head with factors
[2, 3, 4], to_multiscales computes (43, 85, 85) at scale 2 but the store
receives (32, 64, 64) under datasets metadata that still declares the
factor-3 scale, so the store misdescribes its own geometry by a third.
Default to "exact", which falls back to the precomputed image whenever the
incremental path cannot hit the target; pad stays available.

The write loop also swapped the caller's image data for on-disk views and
replaced entries in the caller's multiscales. Work on a shallow copy with
per-image computed_callbacks, like to_multiscales since 6121aad.
cthead1 2_3, 2th_cthead1 2_3 and MR-head 2_3_4 were archived through the
pad-mode write: 64 pixels wide at the factor-3 level where the metadata
says 85. With the exact default both ports produce the same store
bit-for-bit; regenerated in the zarr3 layout and the flat layout the
TypeScript suite reads.

The testing-data-v0.21.0 release asset is replaced in place (20 files
changed, none added or removed) since only this branch ever referenced
it; CI runs on commits before this one will no longer reproduce.
The ITK and dask-image Gaussian baselines encode x86 floating point; now
that the comparison compares, Apple Silicon diverges on both. Skip those
two comparisons off x86 rather than loosening them to a tolerance.
@vboussot
vboussot marked this pull request as ready for review August 5, 2026 22:53
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@vboussot

vboussot commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
py/test/test_to_ngff_zarr_dask_image.py (1)

11-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated _on_x86 architecture predicate in two test modules. Both files define the same expression platform.machine().lower() in ("x86_64", "amd64"). The shared root cause is a missing helper. Put the predicate in py/test/_data.py next to the other shared test infrastructure, and import it. A future change to the accepted architecture list then applies to both tests.

  • py/test/test_to_ngff_zarr_dask_image.py#L11-L11: remove the local _on_x86 definition and the platform import, then import the shared predicate from ._data.
  • py/test/test_to_ngff_zarr_itk.py#L10-L10: remove the local _on_x86 definition and the platform import, then import the same shared predicate from ._data.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py/test/test_to_ngff_zarr_dask_image.py` at line 11, Move the shared _on_x86
architecture predicate into py/test/_data.py alongside the existing test
infrastructure, then import it from ._data in both
py/test/test_to_ngff_zarr_dask_image.py lines 11-11 and
py/test/test_to_ngff_zarr_itk.py lines 10-10; remove each local definition and
its platform import.
py/test/_data.py (1)

174-188: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_drop_codecs also hides codecs that this library does choose.

The recursion removes "compressor", "compressors", and "codecs" at every depth. That covers the zarr-chosen default, which is the stated goal. It also removes the sharding codec entry and any explicit compressors passed by a caller. test_to_ngff_zarr_v3_compression.py and test_to_ngff_zarr_sharding.py pass explicit codec settings, so the baseline comparison no longer confirms those settings.

Those two tests already assert the codec entries directly from zarr.json, so coverage is not lost today. Consider narrowing the removal to the top-level "codecs" entry that carries the bytes-to-bytes default, so caller-selected codecs stay in the comparison.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py/test/_data.py` around lines 174 - 188, Update _drop_codecs to remove only
the top-level "codecs" entry representing zarr’s bytes-to-bytes default, while
preserving nested sharding codec data and explicit caller-provided
compressor/codecs settings in recursive structures. Keep the existing list and
non-dict recursion behavior, and adjust the filtering logic to distinguish the
root node from nested nodes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@py/ngff_zarr/to_ngff_zarr.py`:
- Around line 619-626: The compressors handling around _write_with_tensorstore
currently keeps only the first non-bytes codec; preserve the full filtered
sequence instead. Update the serialization path, including sharding_indexed
metadata, to retain every codec after the bytes codec in its original order, and
add a regression test asserting codec names and ordering in zarr.json.

In `@py/test/_data.py`:
- Around line 275-279: After creating the baseline store and before comparing it
with the test store, use store_keys, store_contents, and _array_paths to derive
the baseline array paths and assert that the result is non-empty. Keep the
existing missing-directory skip in place, and make the assertion identify the
baseline path and indicate that an empty baseline would pass vacuously.
- Around line 267-271: Add the missing baseline/zarr2/ entries to the testing
archive for the versions and datasets used by verify_against_baseline, ensuring
the expected directory structure matches baseline/zarr3/ so
baseline_path.is_dir() succeeds for Zarr 2 comparisons.

In `@ts/src/methods/itkwasm-shared.ts`:
- Around line 289-294: Update isIntegerComponentType to test whether
componentType is an own property of INTEGER_COMPONENT_RANGES rather than using
the in operator, while preserving the existing string-type guard.

In `@ts/test/baseline_comparison_test.ts`:
- Around line 115-120: Update the diagnostic loop around testData, baselineData,
and componentType to handle int64 and uint64 values as bigint before subtracting
or comparing against numeric zero. Use bigint-safe difference tracking, or skip
mean-difference accumulation for bigint data while preserving difference counts
and firstDiffIndex behavior.

---

Nitpick comments:
In `@py/test/_data.py`:
- Around line 174-188: Update _drop_codecs to remove only the top-level "codecs"
entry representing zarr’s bytes-to-bytes default, while preserving nested
sharding codec data and explicit caller-provided compressor/codecs settings in
recursive structures. Keep the existing list and non-dict recursion behavior,
and adjust the filtering logic to distinguish the root node from nested nodes.

In `@py/test/test_to_ngff_zarr_dask_image.py`:
- Line 11: Move the shared _on_x86 architecture predicate into py/test/_data.py
alongside the existing test infrastructure, then import it from ._data in both
py/test/test_to_ngff_zarr_dask_image.py lines 11-11 and
py/test/test_to_ngff_zarr_itk.py lines 10-10; remove each local definition and
its platform import.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0b805c71-e7d7-4599-831e-ec0125b64497

📥 Commits

Reviewing files that changed from the base of the PR and between 5bab919 and 2631868.

⛔ Files ignored due to path filters (2)
  • py/pixi.lock is excluded by !**/*.lock
  • ts/deno.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • py/ngff_zarr/to_multiscales.py
  • py/ngff_zarr/to_ngff_zarr.py
  • py/pyproject.toml
  • py/test/_data.py
  • py/test/test_baseline_guard.py
  • py/test/test_non_power_of_2_scale_factors.py
  • py/test/test_to_ngff_zarr_dask_image.py
  • py/test/test_to_ngff_zarr_itk.py
  • py/test/test_to_ngff_zarr_sharding.py
  • py/test/test_to_ngff_zarr_v3_compression.py
  • py/test/test_writer_isolation.py
  • ts/deno.json
  • ts/src/methods/itkwasm-browser.ts
  • ts/src/methods/itkwasm-node.ts
  • ts/src/methods/itkwasm-shared.ts
  • ts/test/baseline_comparison_test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • py/ngff_zarr/to_multiscales.py
  • py/pyproject.toml
  • py/test/test_baseline_guard.py
  • py/test/test_to_ngff_zarr_sharding.py
  • ts/deno.json
  • py/test/test_to_ngff_zarr_v3_compression.py

Comment thread py/ngff_zarr/to_ngff_zarr.py Outdated
Comment thread py/test/_data.py Outdated
Comment thread py/test/_data.py
Comment thread ts/src/methods/itkwasm-shared.ts
Comment thread ts/test/baseline_comparison_test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@py/test/_data.py`:
- Around line 290-293: Update verify_against_baseline in
py/test/_data.py:290-293 to accept an already-written test store and compare it
directly with baseline_store, removing the internal default MemoryStore write.
In py/test/test_to_ngff_zarr_sharding.py:59-61, 81-83, 103-105, and 142-144,
pass each scalar-, tuple-, mapping-sharded, and TensorStore output; in
py/test/test_to_ngff_zarr_v3_compression.py:66-68 and 127-129, pass each
compressed output to verify_against_baseline.
- Around line 238-264: The store_equals logic in py/test/_data.py#L238-L264 must
reject any mismatch between baseline_keys and test_keys before metadata or array
comparison, reporting unexpected test-store keys as well as missing keys. Update
py/test/test_baseline_guard.py#L28-L39 so the empty-versus-populated assertion
expects False and add coverage proving an extra test-store key is rejected.

In `@py/test/test_to_ngff_zarr_dask_image.py`:
- Around line 14-18: Apply the existing _on_x86 skip marker to both Dask
Gaussian tests, including test_gaussian_isotropic_scale_factors_two_components,
or move it to module scope so it covers both tests. Preserve the current skip
reason and ensure tests using Methods.DASK_IMAGE_GAUSSIAN are skipped on non-x86
architectures.

In `@py/test/test_writer_isolation.py`:
- Around line 71-73: Update the assertion in the test around _scale0_metadata to
locate the blosc codec and verify its cname is "zlib", clevel is 5, and
serialized shuffle is "shuffle", rather than checking only that "blosc" appears
in names.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 309cd5e3-9c8f-4c30-b0de-bb71c8d198a5

📥 Commits

Reviewing files that changed from the base of the PR and between 5bab919 and 2631868.

⛔ Files ignored due to path filters (2)
  • py/pixi.lock is excluded by !**/*.lock
  • ts/deno.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • py/ngff_zarr/to_multiscales.py
  • py/ngff_zarr/to_ngff_zarr.py
  • py/pyproject.toml
  • py/test/_data.py
  • py/test/test_baseline_guard.py
  • py/test/test_non_power_of_2_scale_factors.py
  • py/test/test_to_ngff_zarr_dask_image.py
  • py/test/test_to_ngff_zarr_itk.py
  • py/test/test_to_ngff_zarr_sharding.py
  • py/test/test_to_ngff_zarr_v3_compression.py
  • py/test/test_writer_isolation.py
  • ts/deno.json
  • ts/src/methods/itkwasm-browser.ts
  • ts/src/methods/itkwasm-node.ts
  • ts/src/methods/itkwasm-shared.ts
  • ts/test/baseline_comparison_test.ts

Comment thread py/test/_data.py
Comment thread py/test/_data.py
Comment thread py/test/test_to_ngff_zarr_dask_image.py
Comment thread py/test/test_writer_isolation.py Outdated
Three review findings that measure as real:

- A native Zarr v3 codec passed as compressor on the sharding path was
  silently swapped for the zstd default: _numcodecs_to_zarr_v3_codec
  returned None and the caller substituted ZstdCodec(). Pass it through
  unchanged.
- A constructor-defaulted BloscCodec serialized with shuffle=bitshuffle
  where zarr-python evolves shuffle from the dtype at write time, so the
  two backends wrote different configurations for the same request.
  Mirror zarr's _tunable_attrs evolution. The compressor test now
  compares full codec configurations between backends, not just names,
  and covers the defaulted case.
- _is_bytes_codec did not recognize the codec given as the string
  "bytes", so a string codec chain picked "bytes" as the compressor.
store_equals only walked the baseline's keys, so an extra array in the
test store passed unnoticed; require the key sets to match in both
directions. A baseline directory that exists but holds no arrays (a
truncated extraction) also compared vacuously; demand at least one
array. The guard test now pins both.
'constructor' passed the `in` prototype-chain check in
isIntegerComponentType; use an own-property check. The comparison loop
now compares raw values, so bigint-backed arrays work, and converts to
Number only for the diagnostics.
The Python 3.10 matrix resolves zarr 2.18 (zarr 3 requires 3.11), where
zarr.codecs has no BloscCodec: building the codec inside a parametrize
decorator broke collection for the whole suite. Construct it lazily in
the test, imported from zarr.codecs.blosc, and drop the zarr_format
kwarg zarr 2 does not know from the extra-key guard case.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates both the Python and TypeScript implementations to require itkwasm-downsample 2.0.0, and makes baseline verification meaningful again by comparing decompressed array values and parsed metadata (instead of bytewise store contents), with regenerated baselines and targeted regression tests to catch backend-default and geometry mismatches.

Changes:

  • Bumped itkwasm-downsample to 2.0.0 (Python + TypeScript) and updated lockfiles accordingly.
  • Reworked baseline comparison to be non-vacuous and robust to codec defaults by comparing decoded arrays + normalized metadata; missing baselines now explicitly skip.
  • Fixed/standardized downsampling behavior and writer behavior (exact scale strategy by default, Gaussian integer workaround in TS, and isolation from caller-owned objects) and added regression tests for these invariants.

Reviewed changes

Copilot reviewed 16 out of 18 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
ts/test/baseline_comparison_test.ts Replaces ITK-Wasm compare helper with strict element-wise equality aligned with Python’s baseline semantics; updates orientation assumptions.
ts/src/methods/itkwasm-shared.ts Adds integer Gaussian float32 workaround helpers (cast + rint-half-to-even + clamp) for cross-port bit-identical output.
ts/src/methods/itkwasm-node.ts Applies the integer Gaussian workaround in the Node downsampling path.
ts/src/methods/itkwasm-browser.ts Applies the integer Gaussian workaround in the browser downsampling path.
ts/deno.lock Locks updated npm dependencies (including @itk-wasm/downsample@2.0.0).
ts/deno.json Bumps @itk-wasm/downsample import to ^2.0.0.
py/test/test_writer_isolation.py Adds regression tests ensuring TensorStore vs zarr-python parity and that writers don’t mutate caller-owned objects.
py/test/test_to_ngff_zarr_v3_compression.py Updates baseline name/path to match regenerated baseline layout.
py/test/test_to_ngff_zarr_sharding.py Updates baseline name/path to match regenerated baseline layout.
py/test/test_to_ngff_zarr_itk.py Skips native ITK Gaussian baseline checks off x86 due to platform floating-point divergence.
py/test/test_to_ngff_zarr_dask_image.py Skips dask-image/scipy Gaussian baseline checks off x86 due to platform floating-point divergence.
py/test/test_non_power_of_2_scale_factors.py Updates tests to reflect scale_strategy="exact" as the default; keeps "pad" as opt-in.
py/test/test_baseline_guard.py Adds explicit tests preventing vacuous baseline passes (missing/empty baselines and keyset mismatches).
py/test/_data.py Fixes baseline pathing, enforces non-empty baseline stores, and compares decoded arrays + normalized metadata (dropping codec fields).
py/pyproject.toml Requires itkwasm-downsample >= 2.0.0.
py/pixi.lock Updates locked Python artifacts for itkwasm_downsample / wasi companion to 2.0.0.
py/ngff_zarr/to_ngff_zarr.py Switches default scale_strategy to "exact", improves TensorStore codec handling, and avoids mutating caller multiscales.
py/ngff_zarr/to_multiscales.py Avoids mutating caller NgffImage (shallow copy + independent callbacks list).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread py/ngff_zarr/to_ngff_zarr.py
Reverts the default back to "pad"; "exact" stays available opt-in. Updates the
default-behaviour test and the parameter docs accordingly.
The 2_3 and 2_3_4 baselines were regenerated to the exact geometry, but
"pad" stayed the default because it downsamples from the previous level
instead of the full-resolution source. Measured on a 1024 cubed volume
with factors 2, 3 and 4, that is 13 s against 25 s, so the default is
worth keeping.

The three baseline tests that use non-power-of-2 factors now ask for the
exact strategy explicitly. Pad's own geometry is already asserted in
test_non_power_of_2_scale_factors.py, so nothing loses coverage, and the
TypeScript port has no strategy and always writes the exact geometry, so
the shared baselines stay in parity.

Also puts the sentence about a store misdescribing its own geometry back
on "pad", where it belongs.
… order

Merging main brought fideus-labs#623, which normalizes generated axes to the spec
order (t, c, z, y, x). The brain_two_components DASK_IMAGE_GAUSSIAN
baseline in the v0.21.0 testing-data archive was generated before that
change, with the component axis last, so the key sets no longer match.
Regenerate that baseline against the merged code and pin the updated
archive.
@vboussot
vboussot requested a review from thewtex August 10, 2026 15:44

@thewtex thewtex left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looking good!

A request inline.

Comment thread ts/src/methods/itkwasm-shared.ts
Requested in review: castImage carries the optimized conversion paths.
The integer restore still rounds half-to-even and clamps first, since
castImage truncates raw floats and the values must match the Python
port's np.clip(np.rint(...)). The rounded intermediate is float64 so
every uint32/int32 value survives the trip exactly.

@thewtex thewtex left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

💯

The decorator covered only the single-component test; both compare
scipy Gaussian baselines generated on x86_64.
A supplied compressors sequence was reduced to its first non-bytes
codec. Keep the whole bytes-to-bytes chain in order after the leading
bytes codec, treat an explicit empty chain as no compression to match
zarr-python, and apply the default zstd only when no codec option is
given. Cover default, empty, bytes-only and multi-codec chains.
@thewtex
thewtex merged commit 72fb653 into fideus-labs:main Aug 11, 2026
50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Baseline verification in the Python test suite is a no-op Update itkwasm-downsample to 2.0.0

3 participants