Skip to content

Commit 39fb609

Browse files
selmanozleyend-v-b
andauthored
fix: unsigned index arrays raise IndexError because np.diff wraps and uint64 promotes (#4286)
* fix: array comparison and add test for unsorted indexing with unsigned dtypes * rewrite comment * fix: resolve IndexError in integer array indexing with unsigned dtypes --------- Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
1 parent 7db1c74 commit 39fb609

3 files changed

Lines changed: 25 additions & 2 deletions

File tree

‎changes/4286.bugfix.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fixed integer array indexing with unsigned index dtypes. An unsorted index such as
2+
`np.array([3, 0], dtype="uint8")` spanning more than one chunk raised `IndexError`, because
3+
the order check used `np.diff`, which wraps on unsigned dtypes and misclassified a
4+
descending selection as increasing. Separately, a `uint64` index raised `IndexError` on both
5+
`array[...]` and `array.vindex[...]` — sorted or not — because `uint64` promotes to
6+
`float64` against a signed chunk offset. Index arrays are now cast to `intp`.

‎src/zarr/core/indexing.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -715,8 +715,8 @@ class Order(Enum):
715715

716716
@staticmethod
717717
def check(a: npt.NDArray[Any]) -> Order:
718-
diff = np.diff(a)
719-
diff_positive = diff >= 0
718+
# compare, don't subtract: np.diff wraps on unsigned dtypes
719+
diff_positive = a[1:] >= a[:-1]
720720
n_diff_positive = np.count_nonzero(diff_positive)
721721
all_increasing = n_diff_positive == len(diff_positive)
722722
any_increasing = n_diff_positive > 0
@@ -769,6 +769,8 @@ def __init__(
769769
dim_sel = np.asanyarray(dim_sel)
770770
if not is_integer_array(dim_sel, 1):
771771
raise IndexError("integer arrays in an orthogonal selection must be 1-dimensional only")
772+
# uint64 promotes to float against the signed chunk offset
773+
dim_sel = dim_sel.astype(np.intp, copy=False)
772774

773775
nitems = len(dim_sel)
774776
g = dim_grid
@@ -1207,6 +1209,11 @@ def __init__(
12071209
"(coordinate) array per dimension of the target array, "
12081210
f"got {selection!r}"
12091211
)
1212+
# keep indices integral: uint64 against a signed offset promotes to float
1213+
selection_normalized = cast(
1214+
"CoordinateSelectionNormalized",
1215+
tuple(np.asarray(s, dtype=np.intp) for s in selection_normalized),
1216+
)
12101217

12111218
# Optimization for a single sorted, in-bounds, 1-D integer coordinate array over a
12121219
# regular (fixed-size) chunk grid. The general path below makes several full passes over

‎tests/test_indexing.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,16 @@ def test_orthogonal_indexing_edge_cases(store: StorePath) -> None:
939939
assert_array_equal(expect, actual)
940940

941941

942+
@pytest.mark.parametrize("dtype", ["int8", "int64", "uint8", "uint16", "uint32", "uint64"])
943+
def test_unsorted_index_unsigned_dtype(store: StorePath, dtype: str) -> None:
944+
a = np.arange(8).reshape(4, 2)
945+
z = zarr_array_from_numpy_array(store, a, chunk_shape=(2, 1))
946+
rows = np.array([3, 0], dtype=dtype)
947+
948+
assert_array_equal(a[[3, 0], :], z[rows, :])
949+
assert_array_equal(a[[3, 0], [1, 0]], z.vindex[rows, np.array([1, 0], dtype=dtype)])
950+
951+
942952
def _test_set_orthogonal_selection(
943953
v: npt.NDArray[np.int_], a: npt.NDArray[Any], z: Array, selection: OrthogonalSelection
944954
) -> None:

0 commit comments

Comments
 (0)