|
1 | 1 | import itertools |
2 | 2 | import math |
3 | 3 | import sys |
4 | | -from collections.abc import Callable, Mapping |
| 4 | +from collections.abc import Callable, Mapping, Sequence |
5 | 5 | from typing import Any, Literal |
6 | 6 |
|
7 | 7 | import hypothesis.extra.numpy as npst |
|
12 | 12 | from hypothesis.strategies import SearchStrategy |
13 | 13 |
|
14 | 14 | import zarr |
| 15 | +from zarr.abc.codec import Codec |
15 | 16 | from zarr.abc.store import ( |
16 | 17 | ByteRequest, |
17 | 18 | OffsetByteRequest, |
@@ -255,6 +256,30 @@ def shard_shapes( |
255 | 256 | return tuple(m * c for m, c in zip(multiples, chunk_shape, strict=True)) |
256 | 257 |
|
257 | 258 |
|
| 259 | +@st.composite |
| 260 | +def _sharding_codecs( |
| 261 | + draw: st.DrawFn, |
| 262 | + *, |
| 263 | + chunk_shape: tuple[int, ...], |
| 264 | + codecs: Sequence[Codec] | None = None, |
| 265 | +) -> ShardingCodec: |
| 266 | + """A ``ShardingCodec`` over ``chunk_shape`` with a drawn subchunk write order. |
| 267 | +
|
| 268 | + The inner codec chain is drawn from ``sharding_inner_codecs`` unless ``codecs`` |
| 269 | + is given, which lets a caller nest another ``ShardingCodec`` inside. |
| 270 | + """ |
| 271 | + subchunk_write_order = draw(subchunk_write_orders) |
| 272 | + inner_codecs: Sequence[Codec] = ( |
| 273 | + draw(sharding_inner_codecs, label="sharding inner codecs") if codecs is None else codecs |
| 274 | + ) |
| 275 | + return ShardingCodec( |
| 276 | + subchunk_write_order=subchunk_write_order, |
| 277 | + codecs=inner_codecs, |
| 278 | + index_codecs=[BytesCodec(), Crc32cCodec()], |
| 279 | + chunk_shape=chunk_shape, |
| 280 | + ) |
| 281 | + |
| 282 | + |
258 | 283 | @st.composite |
259 | 284 | def np_array_and_chunks( |
260 | 285 | draw: st.DrawFn, |
@@ -334,14 +359,7 @@ def arrays( |
334 | 359 | ) |
335 | 360 | event("sharded" if shard_shape is not None else "unsharded") |
336 | 361 | if shard_shape is not None: |
337 | | - subchunk_write_order = draw(subchunk_write_orders) |
338 | | - inner_codecs = draw(sharding_inner_codecs, label="sharding inner codecs") |
339 | | - serializer = ShardingCodec( |
340 | | - subchunk_write_order=subchunk_write_order, |
341 | | - codecs=inner_codecs, |
342 | | - index_codecs=[BytesCodec(), Crc32cCodec()], |
343 | | - chunk_shape=chunks_param, |
344 | | - ) |
| 362 | + serializer = draw(_sharding_codecs(chunk_shape=chunks_param)) |
345 | 363 | compressors_unsearched = None |
346 | 364 | else: |
347 | 365 | chunks_param = draw(chunk_shapes(shape=nparray.shape), label="chunk shape") |
@@ -533,6 +551,83 @@ def rectilinear_arrays( |
533 | 551 | return a |
534 | 552 |
|
535 | 553 |
|
| 554 | +# Sharded arrays need min_side >= 1: a shard must hold at least one chunk on every axis. |
| 555 | +_sharded_shapes = npst.array_shapes(max_dims=4, min_side=1, max_side=8) |
| 556 | + |
| 557 | + |
| 558 | +@st.composite |
| 559 | +def sharded_arrays( |
| 560 | + draw: st.DrawFn, |
| 561 | + *, |
| 562 | + shapes: st.SearchStrategy[tuple[int, ...]] = _sharded_shapes, |
| 563 | + nested: bool | None = None, |
| 564 | +) -> Any: |
| 565 | + """Generate a zarr v3 array whose chunks are grouped into shards. |
| 566 | +
|
| 567 | + ``arrays`` shards only a small fraction of its draws (a v3 array with a |
| 568 | + regular chunk grid, every axis larger than a chunk that is itself larger |
| 569 | + than 1, and then only half the time), so a property test that must |
| 570 | + exercise the sharding codec should draw from this strategy directly. Every |
| 571 | + draw is sharded: the chunk shape and the shard shape (an integral number of |
| 572 | + chunks per axis, possibly a single chunk) are drawn from ``shapes``, and |
| 573 | + the codec's subchunk write order and inner codec chain are drawn as in |
| 574 | + ``arrays``. ``shapes`` must generate shapes with at least one element on |
| 575 | + every axis. |
| 576 | +
|
| 577 | + ``nested`` selects one level of recursive sharding: the drawn chunks are |
| 578 | + grouped into inner shards, which are themselves grouped into the shards |
| 579 | + stored in the array, so the outer ``ShardingCodec`` wraps an inner one with |
| 580 | + its own subchunk write order. ``None`` (the default) draws it, so half the |
| 581 | + examples nest. For a nested array ``Array.chunks`` is the inner shard shape |
| 582 | + (the outer codec's chunk shape); the innermost chunk shape is the inner |
| 583 | + codec's ``chunk_shape``. |
| 584 | + """ |
| 585 | + shape = draw(shapes) |
| 586 | + chunk_shape = draw(chunk_shapes(shape=shape), label="chunk shape") |
| 587 | + serializer = draw(_sharding_codecs(chunk_shape=chunk_shape)) |
| 588 | + nest = draw(st.booleans(), label="nested sharding") if nested is None else nested |
| 589 | + if nest: |
| 590 | + # Each level's shard is an integral number of the level below's chunks. |
| 591 | + codec_chunk_shape = draw( |
| 592 | + shard_shapes(shape=shape, chunk_shape=chunk_shape), label="inner shard shape" |
| 593 | + ) |
| 594 | + serializer = draw(_sharding_codecs(chunk_shape=codec_chunk_shape, codecs=[serializer])) |
| 595 | + else: |
| 596 | + codec_chunk_shape = chunk_shape |
| 597 | + shard_shape = draw( |
| 598 | + shard_shapes(shape=shape, chunk_shape=codec_chunk_shape), label="shard shape" |
| 599 | + ) |
| 600 | + event("nested sharding" if nest else "single-level sharding") |
| 601 | + |
| 602 | + nparray = draw(numpy_arrays(shapes=st.just(shape)), label="array data") |
| 603 | + fill_value = draw(st.one_of([st.none(), npst.from_dtype(nparray.dtype)])) |
| 604 | + dim_names = draw(dimension_names(ndim=len(shape)), label="dimension names") |
| 605 | + |
| 606 | + # The shard is the array's chunk grid and the drawn codec is its serializer. |
| 607 | + # Passing ``shards=`` instead would make ``create_array`` wrap the codec in a |
| 608 | + # second ``ShardingCodec`` of the same chunk shape, hiding the drawn write |
| 609 | + # order behind a default outer one. |
| 610 | + a = zarr.create_array( |
| 611 | + store=MemoryStore(), |
| 612 | + shape=shape, |
| 613 | + chunks=shard_shape, |
| 614 | + dtype=nparray.dtype, |
| 615 | + fill_value=fill_value, |
| 616 | + dimension_names=dim_names, |
| 617 | + serializer=serializer, |
| 618 | + filters=None, |
| 619 | + compressors=None, |
| 620 | + ) |
| 621 | + assert a.shards == shard_shape |
| 622 | + assert a.chunks == codec_chunk_shape |
| 623 | + assert isinstance(a.metadata, ArrayV3Metadata) |
| 624 | + (codec,) = a.metadata.codecs |
| 625 | + assert isinstance(codec, ShardingCodec) |
| 626 | + assert codec.subchunk_write_order == serializer.subchunk_write_order |
| 627 | + a[:] = nparray |
| 628 | + return a |
| 629 | + |
| 630 | + |
536 | 631 | def is_negative_slice(idx: Any) -> bool: |
537 | 632 | return isinstance(idx, slice) and idx.step is not None and idx.step < 0 |
538 | 633 |
|
|
0 commit comments