Skip to content

Commit 6028669

Browse files
committed
Added fancy indexing with slices with negative steps
1 parent 3a85376 commit 6028669

2 files changed

Lines changed: 59 additions & 46 deletions

File tree

src/blosc2/ndarray.py

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,9 +1511,9 @@ def get_fselection_numpy(self, key: list | np.ndarray) -> np.ndarray:
15111511
out_shape = _slice.newshape(shape)
15121512
_slice = _slice.raw
15131513
# now all indices are slices or arrays of integers (or booleans)
1514-
# moreover, all arrays are consecutive (otherwise an error is raised)
1515-
if builtins.any(k.step < 0 for k in _slice if isinstance(k, slice)):
1516-
raise ValueError("Fancy indexing not supported for slices with negative steps.")
1514+
# # moreover, all arrays are consecutive (otherwise an error is raised)
1515+
# if builtins.any(k.step < 0 for k in _slice if isinstance(k, slice)):
1516+
# raise ValueError("Fancy indexing not supported for slices with negative steps.")
15171517

15181518
if np.all([isinstance(s, (slice, np.ndarray)) for s in _slice]) and np.all(
15191519
[s.dtype is not bool for s in _slice if isinstance(s, np.ndarray)]
@@ -1523,7 +1523,21 @@ def get_fselection_numpy(self, key: list | np.ndarray) -> np.ndarray:
15231523
# ------| arrs |------
15241524
arridxs = [i for i, s in enumerate(_slice) if isinstance(s, np.ndarray)]
15251525
begin, end = arridxs[0], arridxs[-1] + 1
1526-
flat_shape = tuple((i.stop - i.start + (i.step - 1)) // i.step for i in _slice[:begin])
1526+
1527+
start, stop, step, _ = get_ndarray_start_stop(begin, _slice[:begin], self.shape[:begin])
1528+
prior_tuple = tuple(
1529+
slice(s, st, stp) for s, st, stp in zip(start, stop, step, strict=True)
1530+
) # convert to start and stop +ve
1531+
start, stop, step, _ = get_ndarray_start_stop(
1532+
len(self.shape[end:]), _slice[end:], self.shape[end:]
1533+
)
1534+
post_tuple = tuple(
1535+
slice(s, st, stp) for s, st, stp in zip(start, stop, step, strict=True)
1536+
) # convert to start and stop +ve
1537+
1538+
flat_shape = tuple(
1539+
(i.stop - i.start - i.step // builtins.abs(i.step)) // i.step + 1 for i in prior_tuple
1540+
)
15271541
idx_dim = np.prod(_slice[begin].shape)
15281542

15291543
# TODO: find a nicer way to do the copy maybe
@@ -1532,11 +1546,11 @@ def get_fselection_numpy(self, key: list | np.ndarray) -> np.ndarray:
15321546
arr[:, i] = s.reshape(-1) # have to do a copy
15331547

15341548
flat_shape += (idx_dim,)
1535-
flat_shape += tuple((i.stop - i.start + (i.step - 1)) // i.step for i in _slice[end:])
1549+
flat_shape += tuple(
1550+
(i.stop - i.start - i.step // builtins.abs(i.step)) // i.step + 1 for i in post_tuple
1551+
)
15361552
# out_shape could have new dims if indexing arrays are not all 1D
15371553
# (we have just flattened them so need to handle accordingly)
1538-
prior_tuple = _slice[:begin]
1539-
post_tuple = _slice[end:]
15401554
divider = chunks[begin:end]
15411555
chunked_arr = arr // divider
15421556
if arr.shape[-1] == 1: # 1D chunks, can avoid loading whole chunks
@@ -1592,7 +1606,7 @@ def get_fselection_numpy(self, key: list | np.ndarray) -> np.ndarray:
15921606
)
15931607
for cpost_tuple in product(*cpost_slices):
15941608
out_post_selection, post_selection, loc_post_selection = _get_selection(
1595-
cpost_tuple, post_tuple, chunks[end:] if end is not None else []
1609+
cpost_tuple, post_tuple, chunks[end:]
15961610
)
15971611
locbegin, locend = _get_local_slice(
15981612
prior_selection, post_selection, (chunk_begin, chunk_end)
@@ -1806,13 +1820,10 @@ def __setitem__( # noqa : C901
18061820
raise ValueError("Cannot mix non-unit steps and None indexing for __setitem__.")
18071821
chunks = self.chunks
18081822
shape = self.shape
1809-
pos_key = tuple(
1810-
slice(s, st, stp) if stp > 0 else slice(st + 1, s + 1, -stp)
1811-
for s, st, stp in zip(start, stop, step, strict=True)
1812-
) # get positive steps
18131823
_slice = tuple(slice(s, st, stp) for s, st, stp in zip(start, stop, step, strict=True))
1814-
# this will work only for positive steps
1815-
intersecting_chunks = [slice_to_chunktuple(s, c) for s, c in zip(pos_key, chunks, strict=True)]
1824+
intersecting_chunks = [
1825+
slice_to_chunktuple(s, c) for s, c in zip(_slice, chunks, strict=True)
1826+
] # internally handles negative steps
18161827
intersecting_chunks = [
18171828
(0,) if i == () else i for i in intersecting_chunks
18181829
] # special case of dims with 0 length
@@ -1826,12 +1837,13 @@ def updater(sel_idx):
18261837
return value[sel_idx]
18271838

18281839
for c in product(*intersecting_chunks):
1829-
sel_idx, _, sub_idx = _get_selection(c, _slice, chunks, load_full=True)
1840+
sel_idx, glob_selection, sub_idx = _get_selection(c, _slice, chunks)
18301841
sel_idx = tuple(s for s, m in zip(sel_idx, mask, strict=True) if not m)
18311842
sub_idx = tuple(s if not m else k.start for s, m, k in zip(sub_idx, mask, key_, strict=True))
1832-
locstart, locstop = (
1833-
tuple(c_ * cs for c_, cs in zip(c, chunks, strict=True)),
1834-
tuple((c_ + 1) * cs for c_, cs in zip(c, chunks, strict=True)),
1843+
locstart, locstop = _get_local_slice(
1844+
glob_selection,
1845+
(),
1846+
((), ()), # switches start and stop for negative steps
18351847
)
18361848
chunk = np.empty(
18371849
tuple(sp - st for st, sp in zip(locstart, locstop, strict=True)), dtype=self.dtype
@@ -1870,6 +1882,8 @@ def __len__(self) -> int:
18701882
"""Returns the length of the first dimension of the array.
18711883
This is equivalent to ``self.shape[0]``.
18721884
"""
1885+
if self.shape == ():
1886+
raise TypeError("len() of unsized object")
18731887
return self.shape[0]
18741888

18751889
def get_chunk(self, nchunk: int) -> bytes:
@@ -4761,13 +4775,18 @@ def slice_to_chunktuple(s, n):
47614775
out: tuple
47624776
"""
47634777
start, stop, step = s.start, s.stop, s.step
4778+
if step < 0:
4779+
temp = stop
4780+
stop = start + 1
4781+
start = temp + 1
4782+
step = -step # get positive steps
47644783
if step > n:
47654784
return tuple((start + k * step) // n for k in range(ceiling(stop - start, step)))
47664785
else:
47674786
return tuple(range(start // n, ceiling(stop, n)))
47684787

47694788

4770-
def _get_selection(ctuple, ptuple, chunks, load_full=False):
4789+
def _get_selection(ctuple, ptuple, chunks):
47714790
# we assume that at least one element of chunk intersects with the slice
47724791
# (as a consequence of only looping over intersecting chunks)
47734792
# ptuple is global slice, ctuple is chunk coords (in units of chunks)
@@ -4803,7 +4822,7 @@ def _get_selection(ctuple, ptuple, chunks, load_full=False):
48034822
# selection relative to coordinates of out (necessarily out_step = 1 as we work through out chunk-by-chunk of self)
48044823
# when added n + 1 elements
48054824
# ps.start = pt.start + step * (n+1) => n = (ps.start - pt.start - sign) // step
4806-
# hence, out_start = n + 1 or shape(out) - 1 - (n + 1) if step < 0
4825+
# hence, out_start = n + 1
48074826
# ps.stop = pt.start + step * (out_stop - 1) + k, k in [step, -1] or [1, step]
48084827
# => out_stop = (ps.stop - pt.start - sign) // step + 1
48094828
out_pselection = ()
@@ -4823,39 +4842,34 @@ def _get_selection(ctuple, ptuple, chunks, load_full=False):
48234842
)
48244843
i += 1
48254844

4826-
if load_full:
4827-
4828-
def my_checker_f(x): # handle case -1 to get full chunk
4829-
return x if x >= 0 else None
4830-
4831-
loc_selection = tuple(
4832-
slice(s.start - i * csize, my_checker_f(s.stop - i * csize), s.step)
4833-
for i, s, csize in zip(
4834-
ctuple, pselection, chunks, strict=True
4835-
) # if s.step < 0 then we match with out coords no problem
4836-
) # local coords of full chunk
4837-
else:
4838-
loc_selection = tuple(
4839-
slice(0, s.stop - s.start, s.step) if s.step > 0 else slice(s.start - s.stop, None, s.step)
4840-
for s in pselection
4841-
) # local coords of loaded part of chunk
4845+
loc_selection = tuple( # is s.stop is None, get whole chunk so s.start - 0
4846+
slice(0, s.stop - s.start, s.step)
4847+
if s.step > 0
4848+
else slice(s.start if s.stop == -1 else s.start - s.stop, None, s.step)
4849+
for s in pselection
4850+
) # local coords of loaded part of chunk
48424851

48434852
return out_pselection, pselection, loc_selection
48444853

48454854

48464855
def _get_local_slice(prior_selection, post_selection, chunk_bounds):
48474856
chunk_begin, chunk_end = chunk_bounds
4857+
# +1 for negative steps as have to include start (exclude stop)
48484858
locbegin = np.hstack(
48494859
(
4850-
[s.start for s in prior_selection],
4860+
[s.start if s.step > 0 else s.stop + 1 for s in prior_selection],
48514861
chunk_begin,
4852-
[s.start for s in post_selection],
4862+
[s.start if s.step > 0 else s.stop + 1 for s in post_selection],
48534863
),
48544864
casting="unsafe",
48554865
dtype="int64",
48564866
)
48574867
locend = np.hstack(
4858-
([s.stop for s in prior_selection], chunk_end, [s.stop for s in post_selection]),
4868+
(
4869+
[s.stop if s.step > 0 else s.start + 1 for s in prior_selection],
4870+
chunk_end,
4871+
[s.stop if s.step > 0 else s.start + 1 for s in post_selection],
4872+
),
48594873
casting="unsafe",
48604874
dtype="int64",
48614875
)

tests/ndarray/test_ndarray.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -362,13 +362,12 @@ def test_fancy_index(c):
362362
np.testing.assert_allclose(b, n)
363363

364364
# indices and negative slice steps
365-
# TODO: these currently fail
366-
# b = arr[row, d//2::-1]
367-
# n = nparr[row, d//2::-1]
368-
# np.testing.assert_allclose(b, n)
369-
# b = arr[row, d//2::-3]
370-
# n = nparr[row, d//2::-3]
371-
# np.testing.assert_allclose(b, n)
365+
b = arr[row, d // 2 :: -1]
366+
n = nparr[row, d // 2 :: -1]
367+
np.testing.assert_allclose(b, n)
368+
b = arr[M // 2 :: -4, row, d // 2 :: -3]
369+
n = nparr[M // 2 :: -4, row, d // 2 :: -3]
370+
np.testing.assert_allclose(b, n)
372371

373372
# Transposition test (3rd example is transposed)
374373
b1 = arr[:, [0, 1], 0]

0 commit comments

Comments
 (0)