Skip to content

Commit 8db6b73

Browse files
authored
Allow negative step in index (#928)
1 parent 384cd09 commit 8db6b73

3 files changed

Lines changed: 40 additions & 11 deletions

File tree

.github/workflows/array-api-tests.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,6 @@ jobs:
122122
array_api_tests/test_signatures.py::test_array_method_signature[__setitem__]
123123
array_api_tests/test_signatures.py::test_array_method_signature[to_device]
124124
125-
# (getitem with negative step size is not implemented)
126-
array_api_tests/test_array_object.py::test_getitem
127125
# (indexing with more than one integer index is not implemented)
128126
array_api_tests/test_array_object.py::test_getitem_arrays_and_ints_1
129127
array_api_tests/test_array_object.py::test_getitem_arrays_and_ints_2

cubed/core/indexing.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,19 @@ def index(x, key):
4343
if n:
4444
where_newaxis[i] -= n
4545
idx = ndindex.Tuple(*(ia for ia in idx.args if not isinstance(ia, ndindex.Newaxis)))
46-
selection = idx.raw
46+
selection = list(idx.raw)
47+
48+
# Use trick from xarray for negative step values
49+
where_negative_step = []
50+
for i, ia in enumerate(idx.args):
51+
if isinstance(ia, ndindex.Slice) and ia.step < 0:
52+
where_negative_step.append(i)
53+
pos_slice = _convert_slice_with_negative_step(selection[i], x.shape[i])
54+
selection[i] = pos_slice
55+
where_negative_step = tuple(where_negative_step)
56+
selection = tuple(selection)
4757

4858
# Check selection is supported
49-
if any(ia.step < 1 for ia in idx.args if isinstance(ia, ndindex.Slice)):
50-
raise NotImplementedError(f"Slice step must be >= 1: {key}")
5159
if not all(
5260
isinstance(ia, (ndindex.Integer, ndindex.Slice, ndindex.IntegerArray))
5361
for ia in idx.args
@@ -140,6 +148,11 @@ def selection_function(out_key):
140148
if chunks != merged_chunks:
141149
out = merge_chunks(out, merged_chunks)
142150

151+
if len(where_negative_step) > 0:
152+
from cubed.array_api.manipulation_functions import flip
153+
154+
out = flip(out, axis=where_negative_step)
155+
143156
for axis in where_newaxis:
144157
from cubed.array_api.manipulation_functions import expand_dims
145158

@@ -148,6 +161,16 @@ def selection_function(out_key):
148161
return out
149162

150163

164+
def _convert_slice_with_negative_step(key: slice, size: int) -> slice:
165+
"""Convert a slice with a negative step to one with a positive
166+
step, which must then be followed by a flip.
167+
"""
168+
# see https://github.com/pydata/xarray/blob/99ee8c6ca54057a9b994d7685f36236f2d5a69d9/xarray/core/indexing.py#L1056
169+
start, stop, step = key.indices(size)
170+
exact_stop = range(start, stop, step)[-1]
171+
return slice(exact_stop, start + 1, -step)
172+
173+
151174
def _index_num_input_blocks(
152175
idx: ndindex.Tuple, in_chunksizes, out_chunksizes, numblocks
153176
):

cubed/tests/test_array_api.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ def test_index_2d_no_op(spec, ind):
345345
(20, 8, slice(5, 18, 3), ((5,),)),
346346
# step is bigger than chunks
347347
(50, 5, slice(3, 50, 7), ((5, 2),)),
348+
# negative step
349+
(20, 4, slice(14, 3, -2), ((4, 2),)),
348350
],
349351
)
350352
def test_index_1d_step(spec, shape, chunks, ind, new_chunks_expected):
@@ -364,6 +366,18 @@ def test_index_1d_step(spec, shape, chunks, ind, new_chunks_expected):
364366
(slice(3, 14, 2), slice(3, 14, 3)),
365367
((4, 2), (3, 1),),
366368
),
369+
(
370+
(20, 20),
371+
(4, 4),
372+
(slice(14, 3, -2), slice(3, 14, 3)),
373+
((4, 2), (3, 1),),
374+
),
375+
(
376+
(20, 20),
377+
(4, 4),
378+
(slice(14, 3, -2), slice(14, 3, -3)),
379+
((4, 2), (4,),),
380+
),
367381
],
368382
)
369383
# fmt: on
@@ -389,12 +403,6 @@ def test_index_zero_dim(shape, chunks, ind):
389403
assert_array_equal(b.compute(), np.ones(shape)[ind])
390404

391405

392-
def test_index_slice_unsupported_step(spec):
393-
a = xp.arange(12, chunks=(4,), spec=spec)
394-
with pytest.raises(NotImplementedError):
395-
a[::-1]
396-
397-
398406
@pytest.mark.parametrize("axis", [0, 1, -1, -2])
399407
@skip_if_cupy # ndindex with a cupy.ndarray
400408
def test_take(spec, axis):

0 commit comments

Comments
 (0)