Repository navigation
Check the indices and values of orthogonal selections - #779
Open
FabianIsensee wants to merge 2 commits into
Open
FabianIsensee wants to merge 2 commits into
FabianIsensee wants to merge 2 commits into
Conversation
get_oselection_numpy(), set_oselection_numpy() and NDArray.oindex passed their index lists to c-blosc2 unchanged. Negative indices, which NumPy counts from the end of the axis, selected wrong data or made c-blosc2 write outside its chunk buffers, and an index equal to the axis length returned the chunk padding instead of raising. Now negative indices count from the end like in NumPy, and indices outside the axis raise IndexError before anything is read or written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiPFzsfE5i74cBSxZN13M8
set_oselection_numpy() and NDArray.oindex[...] = value handed the value's buffer to c-blosc2 with a size computed from the selection, not from the value. A value with fewer items was read past its end (the extra items came from whatever memory followed), a value of another dtype was copied as raw bytes, a value of the right size but another shape was taken in C order, and scalars and lists raised TypeError. Now a value that is not already a C-contiguous array of the selection's shape and dtype goes through NumPy's assignment into an array of that shape and dtype: it is converted, broadcast as in NumPy (leading length-1 dimensions ignored), and a shape that does not broadcast raises NumPy's ValueError before anything is written. Values that already fit are passed on without a copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiPFzsfE5i74cBSxZN13M8
This was referenced Nov 6, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NDArray.get_oselection_numpy(),set_oselection_numpy()andNDArray.oindex[...]passed their index lists to c-blosc2 unchanged, and c-blosc2'sorthogonal_selection()only rejects indices greater than the shape. Ona = blosc2.asarray(np.arange(100, dtype=np.int32).reshape(10, 10), chunks=(4, 4), blocks=(2, 2)):a.oindex[[-1], [0, 1]]returned[[81, 0]](NumPy:[[90, 91]]), anda.oindex[[0, 1], [-3]]returned values that change between runs: a negative index addresses memory before c-blosc2's buffers. Under ASan the get writes before the block mask and the set writes before the decompressed chunk (heap-buffer-overflow).a.oindex[[10], [0, 1]]returned[[0, 0]](the chunk padding) instead of raising IndexError. On an (8, 8) array with chunks (4, 4),[[0, 1], [8]]returned[[32], [40]](items of the next row of chunks) and[[8], [0, 1]]raisedRuntimeError: Error while getting orthogonal selection.a[[...]]andtake()were not affected: they normalize their indices first (_normalize_take_indices()).The first commit adds
NDArray._normalize_oselection(), called byget_oselection_numpy()andset_oselection_numpy()(whichoindexuses): negative indices count from the end of their axis like in NumPy, and an index outside[-n, n)raisesIndexError: index 10 is out of bounds for axis 0 with size 10(the first such index, as NumPy reports it) before anything is read or written. The caller's lists are not changed. It works on Python lists withmin()/max()rather than converting them to NumPy arrays, since it runs on every call; NumPy index arrays are turned into lists first (the extension copies list items faster than NumPy scalars). Index types are still not checked: float lists are normalized and range-checked like integer ones and then truncated by the extension as before, and boolean lists are still taken as indices 0/1 (NumPy rejects floats and treats booleans as masks). I left that alone here.The second commit checks the value of
set_oselection_numpy()/oindex[...] = value. The extension hands the value's buffer to c-blosc2 with a size computed from the selection, not from the value, so (same array as above,a.oindex[[0, 1], [0, 1]] = ...):[[-1, -2], [1129385424, 24270]](NumPy broadcasts the row:[[-1, -2], [-1, -2]]);1.5became[0, 1073217536]), and a value of the right size but another shape was taken in C order;TypeError: a bytes-like object is required.Now any value that is not already a C-contiguous array of the selection's shape and dtype goes through NumPy's assignment into such an array: it is converted to the array's dtype and broadcast as NumPy does for assignments (leading length-1 dimensions ignored; lists via
np.asarray(), so nested lists broadcast like arrays, as with NumPy's index arrays), and a shape that does not broadcast raises NumPy'sValueError: could not broadcast input array from shape (3,2) into shape (2,3)before anything is written. NDArray values are decompressed first. A value that already fits is passed on without a copy. It matches the value shape check ofNDArray.__setitem__in Check the value's shape in NDArray.__setitem__ and broadcast it as NumPy does #784.A companion c-blosc2 PR (Blosc/c-blosc2#864) makes
orthogonal_selection()itself reject indices outside[0, shape). This PR does not depend on it; the extension methodsget_oindex_numpy()/set_oindex_numpy(), when called directly, rely on that C check (they get a RuntimeError instead of corrupted memory once it is in).Tests in
tests/ndarray/test_ndarray.py, all against NumPy (x[np.ix_(*key)], get and set throughoindex):test_oindex_normalizes_like_numpy(negative indices,-nandn - 1, repeated indices also through negative aliases, empty lists, NumPy int64/int8/uint64 arrays, a range; the caller's indices stay unchanged),test_oindex_rejects_bad_indices(n,-n - 1, 2**63, a uint64 array, which index the message names; the array stays unchanged), andtest_oindex_random_negative_indices(1-D to 3-D, random indices in[-n, n)with repeats). On main all of them fail except the two empty-key cases. For the values:test_oindex_set_value_like_numpy(13 values: the selection's shape and dtype, float, big-endian, non-contiguous, Fortran-ordered, row and column broadcasts, a leading length-1 dimension, a scalar, flat/nested/deeper-nested lists, an NDArray) andtest_oindex_set_value_wrong_shape(6 values that NumPy rejects too, including int32 C-contiguous ones that reach the no-copy check; the array stays unchanged). 18 of the 19 fail on main and with the first commit alone (the same-shape, same-dtype value passes); dropping any one of the three no-copy conditions (shape, dtype, C-contiguity) fails 2 of them. Full suite: same results as main (only the 2 b2view tests that need aiohttp fail locally).Cost (EPYC 9554, 1 thread, alternated rounds, main built twice for the noise floor, which stayed within 1.1% for these calls):
min()/max()passes. A 2-D get of a 3x2 selection from a (10, 10) array goes from 2.51 to 3.46 us; reading 1000 indices given as a list from a 1-D array of 10**6 items from 345 to 361 us (+4.5%); the same indices as a NumPy array get faster, 411 -> 372 us, because the extension copies list items faster than NumPy scalars. Selections that take milliseconds (images, volumes, row reads; 0.4-60 ms) stay within ±2% (the ~800-1000-index ones show the +1-2% of the index passes; a noisy 20 MB box read varies by ±5-8% between rounds). The same on the previous base (abed944) and on a second machine.ndarray.py.I used Claude Code (Anthropic) to trace the bug, write the fix and the tests, and run the measurements; I reviewed the change and the numbers.
🤖 Generated with Claude Code