Skip to content

Commit 71d5bb1

Browse files
committed
refactor(chunk-grids): remove unused members of the dimension grids
Nothing in the library called `with_extent`, `ngridcells` or `_unique_edge_lengths` on `FixedDimension` / `VaryingDimension`, did an `isinstance` check against `DimensionGrid`, or used `is_regular_1d` / `is_regular_nd`. Remove them along with the tests that only exercised them. `VaryingDimension.chunk_offset` no longer maps a negative chunk index to 0, a guard left over from indexing a tuple of prefix sums. Both dimension types raise the same `IndexError` message from `index_to_chunk`, and docstrings that described the old tuple-backed behaviour are corrected. Assisted-by: ClaudeCode:claude-fable-5.1
1 parent 0344442 commit 71d5bb1

4 files changed

Lines changed: 34 additions & 194 deletions

File tree

‎changes/4479.bugfix.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Opening an array with a rectilinear chunk grid, and indexing it, now costs time and memory proportional to the size of the grid's run-length encoded metadata rather than to its number of chunks: a stored grid such as `[[1, 1000000000000]]`, a few hundred bytes of JSON, previously took seconds per ten million chunks to open and could exhaust memory. The explicit edges in `RectilinearChunkGridMetadata.chunk_shapes` and `VaryingDimension.edges` are now held as `zarr.core.common.RunLengthEdges`, an immutable run-length encoded value that is not a sequence: it has no `len`, indexing or iteration and does not compare equal to a `tuple`. Its `expand()` method yields one entry per chunk. `VaryingDimension.cumulative` is removed; `VaryingDimension.chunk_offset` gives the start of a chunk. The repr of a chunk grid shows a dimension of more than 100 chunks in its run-length encoded form.
1+
Opening an array with a rectilinear chunk grid, and indexing it, now costs time and memory proportional to the size of the grid's run-length encoded metadata rather than to its number of chunks: a stored grid such as `[[1, 1000000000000]]`, a few hundred bytes of JSON, previously took seconds per ten million chunks to open and could exhaust memory. The explicit edges in `RectilinearChunkGridMetadata.chunk_shapes` and `VaryingDimension.edges` are now held as `zarr.core.common.RunLengthEdges`, an immutable run-length encoded value that is not a sequence: it has no `len`, indexing or iteration and does not compare equal to a `tuple`. Its `expand()` method yields one entry per chunk. `VaryingDimension.cumulative` is removed; `VaryingDimension.chunk_offset` gives the start of a chunk. The repr of a chunk grid shows a dimension of more than 100 chunks in its run-length encoded form. The unused `with_extent` and `ngridcells` members of `FixedDimension` and `VaryingDimension`, and the unused helpers `zarr.core.chunk_grids.is_regular_1d` and `is_regular_nd`, are removed; both dimension types now raise the same `IndexError` message from `index_to_chunk`.

‎src/zarr/core/chunk_grids.py‎

Lines changed: 17 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
Protocol,
1515
SupportsIndex,
1616
cast,
17-
runtime_checkable,
1817
)
1918

2019
import numpy as np
@@ -53,26 +52,21 @@ class FixedDimension:
5352
size: int # chunk edge length (>= 1)
5453
extent: int # array dimension length (>= 0)
5554
nchunks: int = field(init=False, repr=False)
56-
ngridcells: int = field(init=False, repr=False)
5755

5856
def __post_init__(self) -> None:
5957
if self.size < 1:
6058
raise ValueError(f"FixedDimension size must be >= 1, got {self.size}")
6159
if self.extent < 0:
6260
raise ValueError(f"FixedDimension extent must be >= 0, got {self.extent}")
63-
n = ceildiv_int(self.extent, self.size)
64-
object.__setattr__(self, "nchunks", n)
65-
object.__setattr__(self, "ngridcells", n)
61+
object.__setattr__(self, "nchunks", ceildiv_int(self.extent, self.size))
6662

6763
def index_to_chunk(self, idx: int) -> int:
68-
if idx < 0:
69-
raise IndexError(f"Negative index {idx} is not allowed")
70-
if idx >= self.extent:
71-
raise IndexError(f"Index {idx} is out of bounds for extent {self.extent}")
64+
if idx < 0 or idx >= self.extent:
65+
raise IndexError(f"Index {idx} out of bounds for dimension with extent {self.extent}")
7266
return idx // self.size
7367

7468
def chunk_offset(self, chunk_ix: int) -> int:
75-
"""Byte-aligned start position of chunk *chunk_ix* in array coordinates.
69+
"""Start position of chunk *chunk_ix* in array coordinates.
7670
7771
Does not validate *chunk_ix* — callers must ensure it is in
7872
``[0, nchunks)``. Use ``ChunkGrid.__getitem__`` for safe access.
@@ -95,34 +89,12 @@ def data_size(self, chunk_ix: int) -> int:
9589
"""
9690
return max(0, min(self.size, self.extent - chunk_ix * self.size))
9791

98-
@property
99-
def _unique_edge_lengths(self) -> Iterable[int]:
100-
"""Distinct chunk edge lengths for this dimension.
101-
102-
Used by shard validation to check that every unique edge length
103-
is divisible by the inner chunk size. O(1) for fixed dimensions
104-
since there is only one edge length.
105-
"""
106-
return (self.size,)
107-
10892
def indices_to_chunks(self, indices: npt.NDArray[np.intp]) -> npt.NDArray[np.intp]:
10993
return indices // self.size
11094

111-
def with_extent(self, new_extent: int) -> FixedDimension:
112-
"""Re-bind to *new_extent* without modifying edges.
113-
114-
Used when constructing a grid from existing metadata where edges
115-
are already correct. Raises on
116-
``VaryingDimension`` if edges don't cover the new extent.
117-
"""
118-
return FixedDimension(size=self.size, extent=new_extent)
119-
12095
def resize(self, new_extent: int) -> FixedDimension:
121-
"""Adapt for a user-initiated array resize, growing edges if needed.
122-
123-
For ``FixedDimension`` this is identical to ``with_extent`` since
124-
regular grids don't store explicit edges.
125-
"""
96+
"""Adapt for a user-initiated array resize: the chunk size is kept and
97+
re-bound to *new_extent*."""
12698
return FixedDimension(size=self.size, extent=new_extent)
12799

128100
@property
@@ -159,7 +131,6 @@ class VaryingDimension:
159131
edges: RunLengthEdges # per-chunk edge lengths (all > 0), run-length encoded
160132
extent: int # array dimension length (may be < sum(edges) after resize)
161133
nchunks: int = field(init=False, repr=False) # cached at construction
162-
ngridcells: int = field(init=False, repr=False) # cached at construction
163134

164135
def __init__(self, edges: Sequence[int] | RunLengthEdges, extent: int) -> None:
165136
rle = RunLengthEdges.from_edges(edges)
@@ -176,34 +147,33 @@ def __init__(self, edges: Sequence[int] | RunLengthEdges, extent: int) -> None:
176147
# Cache nchunks: number of chunks that overlap [0, extent)
177148
n = 0 if extent == 0 else rle.index_at(extent - 1) + 1
178149
object.__setattr__(self, "nchunks", n)
179-
object.__setattr__(self, "ngridcells", rle.num_edges)
180150

181151
def index_to_chunk(self, idx: int) -> int:
182152
if idx < 0 or idx >= self.extent:
183153
raise IndexError(f"Index {idx} out of bounds for dimension with extent {self.extent}")
184154
return self.edges.index_at(idx)
185155

186156
def chunk_offset(self, chunk_ix: int) -> int:
187-
"""Start position of chunk *chunk_ix* in array coordinates.
157+
"""Start position of chunk *chunk_ix* in array coordinates; for
158+
``chunk_ix == edges.num_edges``, the sum of all edges.
188159
189-
Does not validate *chunk_ix* — callers must ensure it is in
190-
``[0, ngridcells)``. Use ``ChunkGrid.__getitem__`` for safe access.
160+
Raises ``IndexError`` for any other *chunk_ix* outside
161+
``[0, edges.num_edges)``.
191162
"""
192-
return self.edges.offset_of(chunk_ix) if chunk_ix > 0 else 0
163+
return self.edges.offset_of(chunk_ix)
193164

194165
def chunk_size(self, chunk_ix: int) -> int:
195166
"""Buffer size for codec processing.
196167
197-
Does not validate *chunk_ix* — callers must ensure it is in
198-
``[0, ngridcells)``. Use ``ChunkGrid.__getitem__`` for safe access.
168+
Raises ``IndexError`` for *chunk_ix* outside ``[0, edges.num_edges)``.
199169
"""
200170
return self.edges.size_of(chunk_ix)
201171

202172
def data_size(self, chunk_ix: int) -> int:
203-
"""Valid data region within the buffer — clipped at extent.
173+
"""Valid data region within the buffer — clipped at extent; 0 for an
174+
edge that lies entirely past the extent.
204175
205-
Does not validate *chunk_ix* — callers must ensure it is in
206-
``[0, ngridcells)``. Use ``ChunkGrid.__getitem__`` for safe access.
176+
Raises ``IndexError`` for *chunk_ix* outside ``[0, edges.num_edges)``.
207177
"""
208178
return max(0, min(self.edges.size_of(chunk_ix), self.extent - self.chunk_offset(chunk_ix)))
209179

@@ -215,38 +185,14 @@ def _data_sizes(self) -> tuple[int, ...]:
215185
last = self.nchunks - 1
216186
return (*itertools.islice(self.edges.expand(), last), self.data_size(last))
217187

218-
@property
219-
def _unique_edge_lengths(self) -> Iterable[int]:
220-
"""Distinct chunk edge lengths for this dimension, in order of first use.
221-
222-
Used by shard validation to check that every unique edge length
223-
is divisible by the inner chunk size. Reads one size per run of
224-
equal edges, not one per chunk.
225-
"""
226-
return tuple(dict.fromkeys(self.edges.sizes))
227-
228188
def indices_to_chunks(self, indices: npt.NDArray[np.intp]) -> npt.NDArray[np.intp]:
229189
return self.edges.indices_at(indices)
230190

231-
def with_extent(self, new_extent: int) -> VaryingDimension:
232-
"""Re-bind to *new_extent* without modifying edges.
233-
234-
Used when constructing a grid from existing metadata where edges
235-
are already correct. Raises if the
236-
existing edges don't cover *new_extent*.
237-
"""
238-
edge_sum = self.edges.total
239-
if edge_sum < new_extent:
240-
raise ValueError(
241-
f"VaryingDimension edge sum {edge_sum} is less than new extent {new_extent}"
242-
)
243-
return VaryingDimension(self.edges, extent=new_extent)
244-
245191
def resize(self, new_extent: int) -> VaryingDimension:
246192
"""Adapt for a user-initiated array resize, growing edges if needed.
247193
248-
Unlike ``with_extent``, this never fails — if *new_extent* exceeds
249-
the current edge sum, a new chunk is appended to cover the gap.
194+
If *new_extent* exceeds the current edge sum, a new chunk is
195+
appended to cover the gap.
250196
Shrinking preserves all edges (the spec allows trailing edges
251197
beyond the array extent).
252198
"""
@@ -263,24 +209,18 @@ def _size_repr(self) -> str:
263209
return _edges_repr(self.edges)
264210

265211

266-
@runtime_checkable
267212
class DimensionGrid(Protocol):
268213
"""Structural interface shared by FixedDimension and VaryingDimension."""
269214

270215
@property
271216
def nchunks(self) -> int: ...
272217
@property
273-
def ngridcells(self) -> int: ...
274-
@property
275218
def extent(self) -> int: ...
276219
def index_to_chunk(self, idx: int) -> int: ...
277220
def chunk_offset(self, chunk_ix: int) -> int: ...
278221
def chunk_size(self, chunk_ix: int) -> int: ...
279222
def data_size(self, chunk_ix: int) -> int: ...
280223
def indices_to_chunks(self, indices: npt.NDArray[np.intp]) -> npt.NDArray[np.intp]: ...
281-
@property
282-
def _unique_edge_lengths(self) -> Iterable[int]: ...
283-
def with_extent(self, new_extent: int) -> DimensionGrid: ...
284224
def resize(self, new_extent: int) -> DimensionGrid: ...
285225
@property
286226
def _size_repr(self) -> str: ...
@@ -365,28 +305,6 @@ def _chunk_list(chunks: object) -> list[Any]:
365305
) from None
366306

367307

368-
def is_regular_1d(dim_chunks: Sequence[int]) -> bool:
369-
"""Check if a single dimension's chunk sizes represent a regular grid.
370-
371-
A regular dimension has either all chunks the same size, or all
372-
but the last chunk the same size with the last chunk smaller
373-
(boundary chunk).
374-
"""
375-
if len(dim_chunks) <= 1:
376-
return True
377-
first = dim_chunks[0]
378-
for c in dim_chunks[1:-1]:
379-
if c != first:
380-
return False
381-
# Last chunk must be the same size or a smaller boundary chunk
382-
return dim_chunks[-1] <= first
383-
384-
385-
def is_regular_nd(chunks: Iterable[Sequence[int]]) -> bool:
386-
"""Check if an N-dimensional chunk specification represents a regular grid."""
387-
return all(is_regular_1d(d) for d in chunks)
388-
389-
390308
@dataclass(frozen=True)
391309
class ChunkGrid:
392310
"""

‎tests/test_metadata/test_v3.py‎

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from tests.conftest import Expect, ExpectFail
1212
from tests.test_metadata.conftest import minimal_metadata_dict_v3
1313
from zarr.core.buffer import default_buffer_prototype
14-
from zarr.core.chunk_grids import ChunkGrid, is_regular_1d, is_regular_nd
14+
from zarr.core.chunk_grids import ChunkGrid
1515
from zarr.core.config import config
1616
from zarr.core.dtype import Float64, UInt8
1717
from zarr.core.group import GroupMetadata, parse_node_type
@@ -108,47 +108,6 @@ def test_parse_codecs_unknown_raises(monkeypatch: pytest.MonkeyPatch) -> None:
108108
parse_codecs([{"name": "unknown"}])
109109

110110

111-
# ---------------------------------------------------------------------------
112-
# Chunk-grid regularity helpers
113-
# ---------------------------------------------------------------------------
114-
115-
_REGULAR_1D_CASES: list[Expect[list[int], bool]] = [
116-
Expect(input=[], output=True, id="empty"),
117-
Expect(input=[10], output=True, id="single-chunk"),
118-
Expect(input=[10, 10, 10], output=True, id="all-equal"),
119-
Expect(input=[10, 10, 10, 7], output=True, id="smaller-boundary"),
120-
Expect(input=[10, 10, 10, 10], output=True, id="exact-multiple"),
121-
Expect(input=[10, 5, 10], output=False, id="middle-mismatch"),
122-
Expect(input=[10, 10, 10, 12], output=False, id="last-larger"),
123-
# The first chunk anchors the size; later mismatches in the middle fail
124-
# before the boundary check.
125-
Expect(input=[5, 10, 5], output=False, id="middle-larger"),
126-
]
127-
128-
129-
@pytest.mark.parametrize("case", _REGULAR_1D_CASES, ids=lambda c: c.id)
130-
def test_is_regular_1d_sequence(case: Expect[list[int], bool]) -> None:
131-
"""`is_regular_1d` accepts plain Python sequences."""
132-
assert is_regular_1d(case.input) is case.output
133-
assert is_regular_1d(tuple(case.input)) is case.output
134-
135-
136-
@pytest.mark.parametrize(
137-
"case",
138-
[
139-
Expect(input=[[10, 10, 10], [5, 5]], output=True, id="all-regular"),
140-
Expect(input=[[10, 10, 7], [5, 5, 5, 3]], output=True, id="all-regular-with-boundary"),
141-
Expect(input=[[10, 10, 10], [5, 8, 5]], output=False, id="second-dim-irregular"),
142-
Expect(input=[[10, 5, 10], [5, 5]], output=False, id="first-dim-irregular"),
143-
Expect(input=[], output=True, id="zero-dims"),
144-
],
145-
ids=lambda c: c.id,
146-
)
147-
def test_is_regular_nd_sequence(case: Expect[list[list[int]], bool]) -> None:
148-
"""`is_regular_nd` returns True iff every per-dim spec is regular."""
149-
assert is_regular_nd(case.input) is case.output
150-
151-
152111
def test_create_chunk_grid_metadata_unknown_dimension_type() -> None:
153112
"""`create_chunk_grid_metadata` rejects dimension grids it does not recognize."""
154113
grid = ChunkGrid(dimensions=(object(),)) # type: ignore[arg-type]

0 commit comments

Comments
 (0)