|
4 | 4 |
|
5 | 5 | import numpy as np |
6 | 6 | import pytest |
7 | | -from hypothesis import assume, given |
| 7 | +from hypothesis import assume, given, settings |
8 | 8 | from hypothesis import strategies as st |
9 | 9 |
|
10 | 10 | import zarr_indexing |
@@ -955,25 +955,125 @@ def test_independent_components_scatter_through_lazy_array(reader_kind: str) -> |
955 | 955 | np.testing.assert_array_equal(view.result(), source[a, b, c, :]) |
956 | 956 |
|
957 | 957 |
|
958 | | -@given(data=st.data()) |
959 | | -def test_component_dependency_graph_matches_pointwise_oracle(data: st.DataObject) -> None: |
960 | | - shape = tuple(data.draw(st.lists(st.integers(1, 3), min_size=0, max_size=3))) |
| 958 | +class SignedGrid: |
| 959 | + """An unbounded grid with translated boundaries and signed chunk identifiers.""" |
| 960 | + |
| 961 | + def __init__(self, size: int, origin: int) -> None: |
| 962 | + self.size = size |
| 963 | + self.origin = origin |
| 964 | + |
| 965 | + def index_to_chunk(self, index: int) -> int: |
| 966 | + return (index - self.origin) // self.size |
| 967 | + |
| 968 | + def indices_to_chunks( |
| 969 | + self, indices: np.ndarray[Any, np.dtype[np.intp]] |
| 970 | + ) -> np.ndarray[Any, np.dtype[np.intp]]: |
| 971 | + return (indices - self.origin) // self.size |
| 972 | + |
| 973 | + def chunk_offset(self, chunk: int) -> int: |
| 974 | + return self.origin + chunk * self.size |
| 975 | + |
| 976 | + def chunk_size(self, chunk: int) -> int: |
| 977 | + return self.size |
| 978 | + |
| 979 | + |
| 980 | +@settings(max_examples=300) |
| 981 | +@given(data=st.data(), shape=st.lists(st.integers(0, 3), min_size=0, max_size=3)) |
| 982 | +def test_component_dependency_graph_matches_pointwise_oracle( |
| 983 | + data: st.DataObject, shape: list[int] |
| 984 | +) -> None: |
| 985 | + """Catch lost duplicates, signed chunk collisions, and incorrect request origins. |
| 986 | +
|
| 987 | + Enumerate the transform's small domain directly: no planner intersection, |
| 988 | + grouping, or dependency helpers contribute to the expected mapping. |
| 989 | + """ |
| 990 | + origin = tuple(data.draw(st.integers(-4, 4)) for _ in shape) |
961 | 991 | output_rank = data.draw(st.integers(1, 5)) |
962 | | - maps = [] |
| 992 | + affine_axes = {axis for axis in range(len(shape)) if data.draw(st.booleans())} |
| 993 | + maps: list[ArrayMap | ConstantMap | DimensionMap] = [ |
| 994 | + DimensionMap( |
| 995 | + axis, offset=data.draw(st.integers(-3, 3)), stride=data.draw(st.integers(-2, 2)) |
| 996 | + ) |
| 997 | + for axis in sorted(affine_axes) |
| 998 | + ] |
963 | 999 | for _ in range(output_rank): |
964 | | - dependencies = data.draw(st.lists(st.booleans(), min_size=len(shape), max_size=len(shape))) |
| 1000 | + if data.draw(st.booleans()): |
| 1001 | + maps.append(ConstantMap(data.draw(st.integers(-3, 3)))) |
| 1002 | + continue |
| 1003 | + # Reserve affine axes for one DimensionMap each. Unsupported shared |
| 1004 | + # affine dependencies are exercised explicitly in the error properties. |
| 1005 | + dependencies = [ |
| 1006 | + axis not in affine_axes and data.draw(st.booleans()) for axis in range(len(shape)) |
| 1007 | + ] |
965 | 1008 | array_shape = tuple( |
966 | 1009 | size if dependent else 1 for size, dependent in zip(shape, dependencies, strict=True) |
967 | 1010 | ) |
968 | 1011 | count = int(np.prod(array_shape)) |
969 | | - values = data.draw(st.lists(st.integers(0, 3), min_size=count, max_size=count)) |
970 | | - maps.append(ArrayMap(np.array(values, dtype=np.intp).reshape(array_shape))) |
971 | | - transform = IndexTransform(IndexDomain.from_shape(shape), tuple(maps)) |
972 | | - grids = dimension_grids_from_chunks((2,) * output_rank, (4,) * output_rank) |
973 | | - partition = plan_chunks(transform, grids).partition() |
| 1012 | + # A small value range produces repeated storage points at distinct request positions. |
| 1013 | + values = data.draw(st.lists(st.integers(-2, 2), min_size=count, max_size=count)) |
| 1014 | + maps.append( |
| 1015 | + ArrayMap( |
| 1016 | + np.array(values, dtype=np.intp).reshape(array_shape), |
| 1017 | + offset=data.draw(st.integers(-3, 3)), |
| 1018 | + stride=data.draw(st.integers(-2, 2)), |
| 1019 | + ) |
| 1020 | + ) |
| 1021 | + grids = [SignedGrid(data.draw(st.integers(1, 3)), data.draw(st.integers(-3, 3))) for _ in maps] |
| 1022 | + transform = IndexTransform( |
| 1023 | + IndexDomain(origin, tuple(lo + size for lo, size in zip(origin, shape, strict=True))), |
| 1024 | + tuple(maps), |
| 1025 | + ) |
| 1026 | + expected = {point: _storage_of(transform, point) for point in _points(transform.domain)} |
| 1027 | + expected_chunks = { |
| 1028 | + tuple(grid.index_to_chunk(value) for grid, value in zip(grids, storage, strict=True)) |
| 1029 | + for storage in expected.values() |
| 1030 | + } |
| 1031 | + partition = plan_chunks(transform, tuple(grids)).partition() |
974 | 1032 | rows = list(partition) |
975 | 1033 | _check_projections(transform, rows) |
| 1034 | + assert {row.chunk_coords for row in rows} == expected_chunks |
| 1035 | + assert len(rows) == len(expected_chunks) |
976 | 1036 | assert partition.chunk_coords().tolist() == [list(row.chunk_coords) for row in rows] |
| 1037 | + reconstructed = [] |
| 1038 | + for row in rows: |
| 1039 | + assert row.chunk_domain.inclusive_min == tuple( |
| 1040 | + grid.chunk_offset(chunk) for grid, chunk in zip(grids, row.chunk_coords, strict=True) |
| 1041 | + ) |
| 1042 | + for cell in _points(row.cell_transform.domain): |
| 1043 | + request = _storage_of(row.cell_transform, cell) |
| 1044 | + storage = tuple( |
| 1045 | + local + grid.chunk_offset(chunk) |
| 1046 | + for local, grid, chunk in zip( |
| 1047 | + _storage_of(row.chunk_transform, cell), grids, row.chunk_coords, strict=True |
| 1048 | + ) |
| 1049 | + ) |
| 1050 | + reconstructed.append((request, storage)) |
| 1051 | + # A list comparison retains multiplicity: repeating one position cannot hide a missing one. |
| 1052 | + assert sorted(reconstructed) == sorted(expected.items()) |
| 1053 | + |
| 1054 | + |
| 1055 | +@given(origin=st.integers(-4, 4), size=st.integers(2, 5), stride=st.sampled_from([-2, -1, 1, 2])) |
| 1056 | +def test_generated_shared_affine_dependency_is_rejected( |
| 1057 | + origin: int, size: int, stride: int |
| 1058 | +) -> None: |
| 1059 | + transform = IndexTransform( |
| 1060 | + IndexDomain((origin,), (origin + size,)), |
| 1061 | + (DimensionMap(0, stride=stride), DimensionMap(0, offset=3)), |
| 1062 | + ) |
| 1063 | + with pytest.raises(ValueError, match="read input axis 0"): |
| 1064 | + list(plan_chunks(transform, (SignedGrid(2, -1), SignedGrid(3, 1)))) |
| 1065 | + |
| 1066 | + |
| 1067 | +@given(origin=st.integers(-4, 4), size=st.integers(2, 5), stride=st.sampled_from([-2, -1, 1, 2])) |
| 1068 | +def test_generated_mixed_affine_array_dependency_is_rejected( |
| 1069 | + origin: int, size: int, stride: int |
| 1070 | +) -> None: |
| 1071 | + transform = IndexTransform( |
| 1072 | + IndexDomain((origin,), (origin + size,)), |
| 1073 | + (DimensionMap(0, stride=stride), ArrayMap(np.zeros(size, dtype=np.intp))), |
| 1074 | + ) |
| 1075 | + with pytest.raises(NotImplementedError, match="also bound by a slice map"): |
| 1076 | + list(plan_chunks(transform, (SignedGrid(2, -1), SignedGrid(3, 1)))) |
977 | 1077 |
|
978 | 1078 |
|
979 | 1079 | @pytest.mark.parametrize("stride", [0, 1, 2, -2]) |
|
0 commit comments