Skip to content

Commit f5ac061

Browse files
committed
create module for chunk grids, and registry
1 parent 9cb7024 commit f5ac061

8 files changed

Lines changed: 1989 additions & 1847 deletions

File tree

src/zarr/core/chunk_grids.py

Lines changed: 0 additions & 1841 deletions
This file was deleted.

src/zarr/core/chunk_grids/__init__.py

Lines changed: 506 additions & 0 deletions
Large diffs are not rendered by default.

src/zarr/core/chunk_grids/common.py

Lines changed: 456 additions & 0 deletions
Large diffs are not rendered by default.

src/zarr/core/chunk_grids/rectilinear.py

Lines changed: 868 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from __future__ import annotations
2+
3+
import itertools
4+
import operator
5+
from dataclasses import dataclass
6+
from functools import reduce
7+
from typing import TYPE_CHECKING, Any, Self
8+
9+
import numpy as np
10+
import numpy.typing as npt
11+
12+
from zarr.core.chunk_grids.common import ChunkGrid
13+
from zarr.core.common import (
14+
JSON,
15+
NamedConfig,
16+
ShapeLike,
17+
ceildiv,
18+
parse_named_configuration,
19+
parse_shapelike,
20+
)
21+
22+
if TYPE_CHECKING:
23+
from collections.abc import Iterator
24+
25+
26+
@dataclass(frozen=True)
27+
class RegularChunkGrid(ChunkGrid):
28+
chunk_shape: tuple[int, ...]
29+
30+
def __init__(self, *, chunk_shape: ShapeLike) -> None:
31+
chunk_shape_parsed = parse_shapelike(chunk_shape)
32+
33+
object.__setattr__(self, "chunk_shape", chunk_shape_parsed)
34+
35+
@classmethod
36+
def from_dict(cls, data: dict[str, JSON] | NamedConfig[str, Any]) -> Self:
37+
_, configuration_parsed = parse_named_configuration(data, "regular")
38+
39+
return cls(**configuration_parsed) # type: ignore[arg-type]
40+
41+
def to_dict(self) -> dict[str, JSON]:
42+
return {"name": "regular", "configuration": {"chunk_shape": tuple(self.chunk_shape)}}
43+
44+
def update_shape(self, new_shape: tuple[int, ...]) -> Self:
45+
return self
46+
47+
def all_chunk_coords(self, array_shape: tuple[int, ...]) -> Iterator[tuple[int, ...]]:
48+
return itertools.product(
49+
*(range(ceildiv(s, c)) for s, c in zip(array_shape, self.chunk_shape, strict=False))
50+
)
51+
52+
def get_nchunks(self, array_shape: tuple[int, ...]) -> int:
53+
return reduce(
54+
operator.mul,
55+
itertools.starmap(ceildiv, zip(array_shape, self.chunk_shape, strict=True)),
56+
1,
57+
)
58+
59+
def get_chunk_shape(
60+
self, array_shape: tuple[int, ...], chunk_coord: tuple[int, ...]
61+
) -> tuple[int, ...]:
62+
"""
63+
Get the shape of a specific chunk.
64+
65+
For RegularChunkGrid, all chunks have the same shape except possibly
66+
the last chunk in each dimension.
67+
"""
68+
return tuple(
69+
int(min(self.chunk_shape[i], array_shape[i] - chunk_coord[i] * self.chunk_shape[i]))
70+
for i in range(len(array_shape))
71+
)
72+
73+
def get_chunk_start(
74+
self, array_shape: tuple[int, ...], chunk_coord: tuple[int, ...]
75+
) -> tuple[int, ...]:
76+
"""
77+
Get the starting position of a chunk in the array.
78+
79+
For RegularChunkGrid, this is simply chunk_coord * chunk_shape.
80+
"""
81+
return tuple(
82+
coord * size for coord, size in zip(chunk_coord, self.chunk_shape, strict=False)
83+
)
84+
85+
def array_index_to_chunk_coord(
86+
self, array_shape: tuple[int, ...], array_index: tuple[int, ...]
87+
) -> tuple[int, ...]:
88+
"""
89+
Map an array index to chunk coordinates.
90+
91+
For RegularChunkGrid, this is simply array_index // chunk_shape.
92+
"""
93+
return tuple(
94+
0 if size == 0 else idx // size
95+
for idx, size in zip(array_index, self.chunk_shape, strict=False)
96+
)
97+
98+
def array_indices_to_chunk_dim(
99+
self, array_shape: tuple[int, ...], dim: int, indices: npt.NDArray[np.intp]
100+
) -> npt.NDArray[np.intp]:
101+
"""
102+
Vectorized mapping of array indices to chunk coordinates along one dimension.
103+
104+
For RegularChunkGrid, this is simply indices // chunk_size.
105+
"""
106+
chunk_size = self.chunk_shape[dim]
107+
if chunk_size == 0:
108+
return np.zeros_like(indices)
109+
return indices // chunk_size
110+
111+
def chunks_per_dim(self, array_shape: tuple[int, ...], dim: int) -> int:
112+
"""
113+
Get the number of chunks along a specific dimension.
114+
115+
For RegularChunkGrid, this is ceildiv(array_shape[dim], chunk_shape[dim]).
116+
"""
117+
return ceildiv(array_shape[dim], self.chunk_shape[dim])
118+
119+
def get_chunk_grid_shape(self, array_shape: tuple[int, ...]) -> tuple[int, ...]:
120+
"""
121+
Get the shape of the chunk grid (number of chunks along each dimension).
122+
123+
For RegularChunkGrid, this is computed using ceildiv for each dimension.
124+
"""
125+
return tuple(
126+
ceildiv(array_len, chunk_len)
127+
for array_len, chunk_len in zip(array_shape, self.chunk_shape, strict=False)
128+
)

src/zarr/core/metadata/v3.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
ChunkGrid,
2929
RectilinearChunkGrid,
3030
RegularChunkGrid,
31+
parse_chunk_grid_from_dict,
3132
)
3233
from zarr.core.chunk_key_encodings import (
3334
ChunkKeyEncoding,
@@ -233,7 +234,7 @@ def __init__(
233234
"""
234235

235236
shape_parsed = parse_shapelike(shape)
236-
chunk_grid_parsed = ChunkGrid.from_dict(chunk_grid)
237+
chunk_grid_parsed = parse_chunk_grid_from_dict(chunk_grid)
237238
chunk_key_encoding_parsed = parse_chunk_key_encoding(chunk_key_encoding)
238239
dimension_names_parsed = parse_dimension_names(dimension_names)
239240
# Note: relying on a type method is numpy-specific

src/zarr/registry.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,20 @@
2222
)
2323
from zarr.abc.numcodec import Numcodec
2424
from zarr.core.buffer import Buffer, NDBuffer
25+
from zarr.core.chunk_grids import ChunkGrid
2526
from zarr.core.chunk_key_encodings import ChunkKeyEncoding
2627
from zarr.core.common import JSON
2728

2829
__all__ = [
2930
"Registry",
3031
"get_buffer_class",
32+
"get_chunk_grid_class",
3133
"get_chunk_key_encoding_class",
3234
"get_codec_class",
3335
"get_ndbuffer_class",
3436
"get_pipeline_class",
3537
"register_buffer",
38+
"register_chunk_grid",
3639
"register_chunk_key_encoding",
3740
"register_codec",
3841
"register_ndbuffer",
@@ -63,6 +66,7 @@ def register(self, cls: type[T], qualname: str | None = None) -> None:
6366
__pipeline_registry: Registry[CodecPipeline] = Registry()
6467
__buffer_registry: Registry[Buffer] = Registry()
6568
__ndbuffer_registry: Registry[NDBuffer] = Registry()
69+
__chunk_grid_registry: Registry[ChunkGrid] = Registry()
6670
__chunk_key_encoding_registry: Registry[ChunkKeyEncoding] = Registry()
6771

6872
"""
@@ -103,6 +107,11 @@ def _collect_entrypoints() -> list[Registry[Any]]:
103107
data_type_registry._lazy_load_list.extend(entry_points.select(group="zarr.data_type"))
104108
data_type_registry._lazy_load_list.extend(entry_points.select(group="zarr", name="data_type"))
105109

110+
__chunk_grid_registry.lazy_load_list.extend(entry_points.select(group="zarr.chunk_grid"))
111+
__chunk_grid_registry.lazy_load_list.extend(
112+
entry_points.select(group="zarr", name="chunk_grid")
113+
)
114+
106115
__chunk_key_encoding_registry.lazy_load_list.extend(
107116
entry_points.select(group="zarr.chunk_key_encoding")
108117
)
@@ -125,6 +134,7 @@ def _collect_entrypoints() -> list[Registry[Any]]:
125134
__pipeline_registry,
126135
__buffer_registry,
127136
__ndbuffer_registry,
137+
__chunk_grid_registry,
128138
__chunk_key_encoding_registry,
129139
]
130140

@@ -156,6 +166,10 @@ def register_buffer(cls: type[Buffer], qualname: str | None = None) -> None:
156166
__buffer_registry.register(cls, qualname)
157167

158168

169+
def register_chunk_grid(key: str, cls: type) -> None:
170+
__chunk_grid_registry.register(cls, key)
171+
172+
159173
def register_chunk_key_encoding(key: str, cls: type) -> None:
160174
__chunk_key_encoding_registry.register(cls, key)
161175

@@ -296,6 +310,16 @@ def get_ndbuffer_class(reload_config: bool = False) -> type[NDBuffer]:
296310
)
297311

298312

313+
def get_chunk_grid_class(key: str) -> type[ChunkGrid]:
314+
__chunk_grid_registry.lazy_load(use_entrypoint_name=True)
315+
if key not in __chunk_grid_registry:
316+
raise KeyError(
317+
f"Chunk grid '{key}' not found in registered chunk grids: "
318+
f"{list(__chunk_grid_registry)}."
319+
)
320+
return __chunk_grid_registry[key]
321+
322+
299323
def get_chunk_key_encoding_class(key: str) -> type[ChunkKeyEncoding]:
300324
__chunk_key_encoding_registry.lazy_load(use_entrypoint_name=True)
301325
if key not in __chunk_key_encoding_registry:

tests/test_chunk_grids/test_rectilinear.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def test_rectilinear_from_dict_spec_example() -> None:
130130
},
131131
}
132132

133-
grid = RectilinearChunkGrid._from_dict(metadata) # type: ignore[arg-type]
133+
grid = RectilinearChunkGrid.from_dict(metadata) # type: ignore[arg-type]
134134

135135
assert grid.chunk_shapes == (
136136
(2, 2, 2),
@@ -151,7 +151,7 @@ def test_rectilinear_from_dict_invalid_kind() -> None:
151151
},
152152
}
153153
with pytest.raises(ValueError, match="Only 'inline' kind is supported"):
154-
RectilinearChunkGrid._from_dict(metadata) # type: ignore[arg-type]
154+
RectilinearChunkGrid.from_dict(metadata) # type: ignore[arg-type]
155155

156156

157157
def test_rectilinear_from_dict_missing_chunk_shapes() -> None:
@@ -163,7 +163,7 @@ def test_rectilinear_from_dict_missing_chunk_shapes() -> None:
163163
},
164164
}
165165
with pytest.raises(ValueError, match="must contain 'chunk_shapes'"):
166-
RectilinearChunkGrid._from_dict(metadata) # type: ignore[arg-type]
166+
RectilinearChunkGrid.from_dict(metadata) # type: ignore[arg-type]
167167

168168

169169
def test_rectilinear_to_dict() -> None:
@@ -234,7 +234,7 @@ def test_rectilinear_roundtrip() -> None:
234234
"""Test that to_dict and from_dict are inverses"""
235235
original = RectilinearChunkGrid(chunk_shapes=[[1, 2, 3], [4, 5]])
236236
metadata = original.to_dict()
237-
reconstructed = RectilinearChunkGrid._from_dict(metadata)
237+
reconstructed = RectilinearChunkGrid.from_dict(metadata)
238238

239239
assert reconstructed.chunk_shapes == original.chunk_shapes
240240

@@ -331,7 +331,7 @@ def test_roundtrip_with_compression() -> None:
331331
assert metadata["configuration"]["chunk_shapes"] == [[[10, 6]], [[5, 5]]] # type: ignore[call-overload,index]
332332

333333
# Deserialize from dict
334-
grid2 = RectilinearChunkGrid._from_dict(metadata)
334+
grid2 = RectilinearChunkGrid.from_dict(metadata)
335335

336336
# Verify the expanded chunk_shapes match
337337
assert grid2.chunk_shapes == grid1.chunk_shapes

0 commit comments

Comments
 (0)