Skip to content

Feature: support rectilinear chunk grid extension - #3534

Closed
jhamman wants to merge 49 commits into
zarr-developers:mainfrom
jhamman:feature/rectilinear-chunk-grid
Closed

Feature: support rectilinear chunk grid extension #3534
jhamman wants to merge 49 commits into
zarr-developers:mainfrom
jhamman:feature/rectilinear-chunk-grid

Conversation

@jhamman

@jhammanjhamman commented Oct 20, 2025

Copy link
Copy Markdown
Member

Summary

Adds support for RectilinearChunkGrid extension (Zarr v3), enabling arrays with variable chunk sizes per dimension.

Closes:#1595 | Replaces:#1483 | Related: zarr-extensions#25

Key Features

RectilinearChunkGrid

arr=zarr.create_array(
shape=(60, 100),
chunks=[[10, 20, 30], [25, 25, 25, 25]],
zarr_format=3
)
  • Zarr v3 only (not compatible with v2, sharding, or from_array())
  • Supports RLE in JSON metadata: [[10, 6]] = 6 chunks of size 10
  • Stored internally in expanded format for fast indexing

Chunk Grid Access

grid=arr.chunk_grid# Returns ChunkGrid instancegrid.chunk_shapes# ((10, 20, 30), (25, 25, 25, 25))isinstance(grid, RectilinearChunkGrid) # Type-safe checking

.chunks Property Behavior

# RegularChunkGrid: returns tuple with FutureWarning (deprecated)arr.chunks# (10, 10)# RectilinearChunkGrid: raises NotImplementedErrorarr.chunks# Use .chunk_grid instead

Design Decisions

DecisionRationale
ChunksLike as TypeAliasFlexible input types without runtime overhead
ResolvedChunkSpec as frozen dataclassNamed access, immutability, IDE support
Standalone validation functionsTestability, clear error messages, early validation
.chunks raises for rectilinearNo sensible single-tuple representation; guides users to .chunk_grid

Removed from Earlier Designs

ItemReason
RegularChunks/RectilinearChunks tuple subclassesRejected - unnecessary complexity
Named dimension access (chunks.lat)Removed per review feedback
ChunksType ABC hierarchyNot implemented - TypeAlias approach preferred

Deferred / TODO Items

ItemLocationNotes
update_shape() optional chunks parametermetadata/v3.py:483Allow specifying new chunk sizes when resizing instead of default heuristic
Validation function placementchunk_grids.py:1513-1593Reviewer suggested moving to metadata module; kept for testability

Review Focus Areas

High Priority:

  • chunk_grids.py: RectilinearChunkGrid class, ChunksLike type, RLE expansion/compression, resolve_chunk_spec()
  • metadata/v3.py: update_shape() for rectilinear resize behavior
  • indexing.py: Variable chunk indexing with binary search
  • array.py: .chunks property behavior, .chunk_grid property

Tests:

  • test_chunk_grids/test_rectilinear.py: Comprehensive unit tests
  • test_chunk_grids/test_rectilinear_integration.py: End-to-end scenarios
  • testing/strategies.py: Hypothesis strategies for property-based testing

Breaking Changes

None. Fully backward compatible.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/user-guide/*.md
  • Changes documented as a new file in changes/
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

@github-actionsgithub-actionsBot added the needs release notes Automatically applied to PRs which haven't added release notes label Oct 20, 2025
@codecov

codecovBot commented Oct 20, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.85533% with 133 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.65%. Comparing base (b712f96) to head (41db2dc).
⚠️ Report is 10 commits behind head on main.

Files with missing linesPatch %Lines
src/zarr/core/chunk_grids.py74.49%76 Missing ⚠️
src/zarr/core/array.py75.00%20 Missing ⚠️
src/zarr/core/indexing.py85.07%20 Missing ⚠️
src/zarr/testing/strategies.py89.28%9 Missing ⚠️
src/zarr/core/metadata/v2.py72.72%6 Missing ⚠️
src/zarr/core/_info.py0.00%1 Missing ⚠️
src/zarr/core/metadata/v3.py90.00%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #3534 +/- ##
==========================================
+ Coverage 60.94% 61.65% +0.71% 
==========================================
Files 86 86 Lines 10268 10769 +501 ==========================================
+ Hits 6258 6640 +382 - Misses 4010 4129 +119 
Files with missing linesCoverage Δ
src/zarr/api/asynchronous.py72.20% <ø> (ø)
src/zarr/api/synchronous.py36.61% <ø> (ø)
src/zarr/core/group.py70.27% <ø> (ø)
src/zarr/core/_info.py51.80% <0.00%> (ø)
src/zarr/core/metadata/v3.py59.91% <90.00%> (+1.90%)⬆️
src/zarr/core/metadata/v2.py60.31% <72.72%> (+2.17%)⬆️
src/zarr/testing/strategies.py94.18% <89.28%> (-3.66%)⬇️
src/zarr/core/array.py67.99% <75.00%> (-0.12%)⬇️
src/zarr/core/indexing.py70.19% <85.07%> (+0.73%)⬆️
src/zarr/core/chunk_grids.py70.70% <74.49%> (+8.40%)⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actionsgithub-actionsBot removed the needs release notes Automatically applied to PRs which haven't added release notes label Oct 20, 2025
Comment threaddocs/user-guide/arrays.md
Comment threaddocs/user-guide/arrays.md Outdated
Comment threadsrc/zarr/core/chunk_grids.py Outdated


@dataclass(frozen=True)
class RectilinearChunkGrid(ChunkGrid):

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.

thoughts on just calling this class Rectilinear, and renaming the RegularChunkGrid to Regular? We could keep around a RegularChunkGrid class for compatibility. But I feel like people know these are chunk grids when they import them

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

50/50. I think the more descriptive class is useful when looking at a tracebacks. Plus, this is currently in .core so its not meant to be used directly by users.

Comment threadsrc/zarr/core/chunk_grids.py Outdated
Comment threadsrc/zarr/core/chunk_grids.py Outdated
Comment threadsrc/zarr/core/indexing.py
Comment threadsrc/zarr/testing/strategies.py Outdated
Comment threadtests/test_properties.py Outdated
@given(data=st.data())
async def test_basic_indexing(data: st.DataObject) -> None:
zarray = data.draw(simple_arrays())
@given(data=st.data(), zarray=st.one_of([simple_arrays(), complex_chunked_arrays()]))

@dcheriandcherianOct 27, 2025

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.

Because the search space for the standard arrays strategy is so large, i made a different one complex_chunked_arrays that purely checks different chunk grids
with simple_arrays() we are only spending 10% of our time trying RectilinearChunkGrid so using this approach. We should boost number of examples too.

Comment on lines +668 to +669
2. **Not compatible with sharding**: You cannot use variable chunking together with
the sharding feature. Arrays must use either variable chunking or sharding, but not both.

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.

I hope this is a temporary limitation! There's a natural extension of rectilinear chunk grids to rectilinear shard grids.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

@maxrjones

Copy link
Copy Markdown
Member

I tried this out in virtual tiff (virtual-zarr/virtual-tiff#69) and virtualizarr (zarr-developers/VirtualiZarr#877) and am super excited about this feature. I expect that it'll unlock a lot of downstream development and absolutely support releasing it this week as experimental. I think that the experimental status is already effectively documented.

My only additional comment beyond my two reviews earlier today is that I'm not convinced of the value of deprecating .chunks for RegularChunkGrid, but wouldn't want that concern to block merging.

Thanks for your work on this @jhamman!

jhammanand others added 6 commits February 25, 2026 11:07
Add vectorized array_indices_to_chunk_dim to eliminate loops
Resolve conflicts in src/zarr/core/array.py: take main's refactored
helper functions (_nchunks_initialized, _resize) which are compatible
with RectilinearChunkGrid.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix _info() crash for RectilinearChunkGrid arrays by catching
NotImplementedError from metadata.chunks (displays "<variable>")
- Fix spurious FutureWarning in _nchunks_initialized() by using
array.metadata.chunks instead of array.chunks
- Add empty-chunk validation in _normalize_rectilinear_chunks to reject
chunk specs where the last chunk(s) contain no valid data
- Remove FutureWarning deprecation on .chunks for RegularChunkGrid per
reviewer feedback; .chunks still raises NotImplementedError for
RectilinearChunkGrid
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…arr-python into feature/rectilinear-chunk-grid


@dataclass(frozen=True)
class RegularChunkGrid(ChunkGrid):

@d-v-bd-v-bMar 11, 2026

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.

I don't think we actually need a regular chunk grid class, since the rectilinear chunk grid is more general.

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.

I disagree. Downstream libraries may want to use this info to possibly use a fastpath.

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.

How would that work? Because a rectilinear chunk grid can be regular. So if a library uses RegularChunkGrid to trigger a fastpath, they also need to inspect the could-be-regular chunks of the RectilinearChunkGrid as well, and then we are back to the RegularChunkGrid having no use.

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.

and the information about the chunk grid an array is using is already contained in the metadata for that array. clients should feel more comfortable inspecting that document than importing internal utility classes from zarr python.

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.

they also need to inspect

They don't need to. This stuff can be expensive. My input here comes from many years of trying to write optimized dask code. Having to inspect the damned chunks tuple with potentially 10 million elements in Python is quite slow and annoying. We should strive to preserve such context through strong types.

Here is an example: https://github.com/pydata/xarray/pull/9808/changes . This core problem there was that we normalized zarr's nice tuple[int, ...] to dask-style tuple[tuple[int, ...]] and then later added a "nice performance warning" that slowed everything down because of the need to parse through half a million identical ints. 🤦🏾‍♂️

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.

They don't need to. This stuff can be expensive.

Inspecting the chunk_grid attribute of the metadata document will never be expensive. We are not going to put 10 million elements in JSON.

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.

for context, my suggestion is the removal of the RegularChunkGridclass, not the removal of the information that the chunk grid is regular. Those are different things.



@dataclass(frozen=True)
class RectilinearChunkGrid(ChunkGrid):

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.

many of the methods on this class take the array shape as a parameter. For an array with a constant shape, this is redundant, so we should probably define the array shape as an attribute of this class.

},
}

def update_shape(self, new_shape: tuple[int, ...]) -> Self:

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.

the name of this method is confusing -- it makes me think the chunk grid has a shape attribute. And I feel like we should allow people to define the chunking of the new part of the array. so maybe resize would be a better name in that case

dtype: ZDTypeLike | None = None,
data: np.ndarray[Any, np.dtype[Any]] | None = None,
chunks: tuple[int, ...] | Literal["auto"] = "auto",
chunks: ChunksLike = "auto",

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.

we declare rectilinear chunks and regular chunks both via the chunks attribute, but accessing the chunks attribute of the array raises an exception in the rectilinear case. This is a deviation from the way the rest of the parameters in this function work -- they are all attributes on the array class, and none of them raise exceptions when you access them. The closest thing is shards being None, but that's consistent with the signature of this function.

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.

also -- we need a way for someone to say "these chunks are regular, and I want to use the regular chunk grid" vs "these chunks are regular, and I want to use the rectilinear chunk grid". I don't think the signature right now supports this.

"The `chunks` property is not supported for arrays with variable chunk sizes "
"(RectilinearChunkGrid). Use `chunk_grid` instead to access chunk information."
)
raise NotImplementedError(msg)

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.

I'd like "accessing some attributes raises exceptions" to be a last resort, even if it means breaking public API. What if we copied dask, and made chunks a tuple of tuples of edge lengths, one per axis?

@dcheriandcherianMar 12, 2026

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.

Everyone using Zarr today expects chunks to be a tuple[int]. I think it's significantly worse to silently break that expectation

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.

yes, it would be a breaking change, and so of course we can't do it silently, but it might also be a welcome change to any downstream library that uses dask with zarr, so IMO we should consider this as a possibility for how we want the zarr array API to eventually look

@dataclass(frozen=True, kw_only=True)
class ArrayV2Metadata(Metadata):
shape: tuple[int, ...]
chunks: tuple[int, ...]

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.

why are we removing this? that's very confusing, from a "model the v2 metadata document" pov, where chunks is an attribute

@maxrjones

Copy link
Copy Markdown
Member

I've been building on the work in this PR and the design discussion here to explore an alternative internal architecture for rectilinear chunk grids. The result is a polished proof of concept branch with a design doc, comprehensive tests, and working downstream POCs for xarray, cubed, VirtualiZarr, and virtual-tiff.

Branch:maxrjones#5
Design doc:https://github.com/maxrjones/zarr-python/blob/poc/unified-chunk-grid/docs/design/chunk-grid.md

The approach reuses several components from this PR (RLE helpers, validation logic, test patterns) and tries to address the design feedback raised here.

While the existing PR would deliver the long-requested rectilinear chunks feature, I would appreciate at least consideration of the alternative design because I think it has some clean abstractions that could enable future extensions. For example, it would be quite simple to add support for repeating chunk grids.

The key architectural differences:

  • Per-dimension decomposition (FixedDimension / VaryingDimension behind a DimensionGrid protocol) instead of monolithic grid classes. Callers use polymorphic dispatch rather than isinstance branching.
  • Self-describing grids. Extent stored per-dimension at construction, so grid[coord] works without passing array_shape everywhere.
  • Single ChunkGrid class with is_regular property instead of RegularChunkGrid + RectilinearChunkGrid, with a RegularChunkGrid deprecation shim for backwards compatibility.
  • Explicit opt-in via feature flag. Rectilinear chunks are gated behind zarr.config.set({"array.rectilinear_chunks": True}), disabled by default. This lets us ship the feature experimentally without committing to API stability, and gives a clear path to promotion once downstream usage validates the design.

The design doc covers the rationale for these choices, addresses all the review feedback from this PR, and documents what was intentionally deferred (metadata/array separation, resize chunks parameter, tile encoding).

I'd suggest reviewing the design doc first to see if the approach makes sense. If it does, I can split the implementation into 5 incremental PRs detailed at the bottom of the design doc.

Thanks, @jhamman, for the foundational work in this PR! I really appreciate everything you've done here and found it essential for making architectural trade-offs concrete.

I hope y'all will consider the alternative architecture in my branch/design doc.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

benchmarkCode will be benchmarked in a CI job.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@jhamman@keewis@tomwhite@d-v-b@tinaok@maxrjones@shoyer@dcherian