-
Notifications
You must be signed in to change notification settings - Fork 29
Fix dpnp.ndarray.flat indexing edge cases
#3045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
7809332
0bf9d53
6c1e5b2
be8f6a0
fe66c93
575c6b1
ee7488c
1dbcb7f
6bc35e6
3a9333a
f70a68a
eb5804e
63b0fb7
5c430b1
7c87de9
338b6df
6c446ec
9d48d82
e314321
2f7e6e7
8c5fb70
554e542
89e9d62
78e333c
6f8c06b
d267afe
1f4ea83
156452f
a62a89e
2d82dce
0d43c85
e288078
bd03352
df356ae
9a2930d
651bfcf
cf9503d
92d0c68
a4f9eac
a327d46
05ae718
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,63 +28,232 @@ | |
|
|
||
| """Implementation of flatiter.""" | ||
|
|
||
| import numpy | ||
|
|
||
| import dpnp | ||
| import dpnp.tensor as dpt | ||
|
|
||
| from .dpnp_array import dpnp_array | ||
|
|
||
|
|
||
| class flatiter: | ||
| """Flat iterator object to iterate over arrays.""" | ||
| """ | ||
| Flat iterator object to iterate over arrays. | ||
|
|
||
| A flat iterator is returned by :obj:`dpnp.ndarray.flat` for any array. It | ||
| allows iterating over the array as if it were a 1-D array, either in a | ||
| for-loop or by calling its ``next`` method. | ||
|
|
||
| Iteration is done in row-major, C-style order (the last index varying the | ||
| fastest). The iterator can also be indexed using basic slicing or advanced | ||
| indexing. | ||
|
|
||
| For full documentation refer to :obj:`numpy.flatiter`. | ||
|
|
||
| See Also | ||
| -------- | ||
| :obj:`dpnp.ndarray.flat` : Return a flat iterator over an array. | ||
| :obj:`dpnp.ndarray.flatten` : Return a flattened copy of an array. | ||
|
|
||
| Examples | ||
| -------- | ||
| >>> import dpnp as np | ||
| >>> x = np.arange(6).reshape(2, 3) | ||
| >>> for item in x.flat: | ||
| ... print(item) | ||
| 0 | ||
| 1 | ||
| 2 | ||
| 3 | ||
| 4 | ||
| 5 | ||
|
|
||
| >>> x.flat[2:4] | ||
| array([2, 3]) | ||
|
|
||
| """ | ||
|
|
||
| def __init__(self, X): | ||
| if type(X) is not dpnp.ndarray: | ||
| def __init__(self, a): | ||
| if not isinstance(a, dpnp_array): | ||
| raise TypeError( | ||
| "Argument must be of type dpnp.ndarray, got {}".format(type(X)) | ||
| f"An array must be of type dpnp.ndarray, but got {type(a)}" | ||
| ) | ||
| self.arr_ = X | ||
| self.size_ = X.size | ||
| self.i_ = 0 | ||
|
|
||
| def _multiindex(self, i): | ||
| nd = self.arr_.ndim | ||
| if nd == 0: | ||
| if i == 0: | ||
| return () | ||
| raise KeyError | ||
| elif nd == 1: | ||
| return (i,) | ||
| sh = self.arr_.shape | ||
| i_ = i | ||
| multi_index = [0] * nd | ||
| for k in reversed(range(1, nd)): | ||
| si = sh[k] | ||
| q = i_ // si | ||
| multi_index[k] = i_ - q * si | ||
| i_ = q | ||
| multi_index[0] = i_ | ||
| return tuple(multi_index) | ||
| self._arr = a | ||
| self._size = a.size | ||
| self._i = 0 | ||
|
|
||
| def _validate_key(self, key): | ||
| """ | ||
| Validate `key` as a flat iterator index. | ||
|
|
||
| Return the array of flat positions for an integer-array key, or | ||
| ``None`` when the caller has to resolve the positions itself. | ||
|
|
||
| """ | ||
| # Ellipsis/slice/tuple need no validation here | ||
| if key is Ellipsis or isinstance(key, (slice, tuple)): | ||
| return None | ||
|
|
||
| # a genuine scalar int (not bool, not an array): bounds-checked later | ||
| if ( | ||
| not isinstance(key, bool) | ||
| and callable(getattr(key, "__index__", None)) | ||
| and not hasattr(key, "ndim") | ||
| ): | ||
| return None | ||
|
|
||
| if isinstance(key, dpnp_array): | ||
| idx = key | ||
| elif isinstance(key, dpt.usm_ndarray): | ||
| idx = dpnp_array._create_from_usm_ndarray(key) | ||
| else: | ||
| try: | ||
| idx = numpy.asarray(key) | ||
| except (TypeError, ValueError): | ||
| return None # let regular indexing raise | ||
|
|
||
| if dpnp.issubdtype(idx.dtype, dpnp.bool): | ||
| if idx.ndim > 1: | ||
| raise IndexError( | ||
| "too many indices for flat iterator: flat iterator is " | ||
| f"1-dimensional, but {idx.ndim} were indexed" | ||
| ) | ||
|
|
||
| # only a 1-D boolean ndarray mask is valid; reject scalars/lists | ||
| if idx.ndim == 1 and not isinstance(key, list): | ||
| # an empty mask selects nothing; otherwise sizes must match | ||
| if idx.size not in (0, self._size): | ||
| raise IndexError( | ||
| "boolean index did not match indexed flat iterator " | ||
| f"along axis 0; size of axis is {self._size} but size " | ||
| f"of corresponding boolean axis is {idx.size}" | ||
| ) | ||
| return None | ||
| raise IndexError("boolean indices for iterators are not supported") | ||
|
|
||
| if not dpnp.issubdtype(idx.dtype, dpnp.integer) or idx.size == 0: | ||
| return None | ||
|
|
||
| # fancy int indices wrap instead of raising, so bounds-check | ||
| size = self._size | ||
| hi, lo = int(idx.max()), int(idx.min()) | ||
| if hi >= size: | ||
| raise IndexError(f"index {hi} is out of bounds for size {size}") | ||
| if lo < -size: | ||
| raise IndexError(f"index {lo} is out of bounds for size {size}") | ||
| return idx | ||
|
|
||
| def _prepare_key(self, key): | ||
| # normalize a 1-D iterator index (unwrap a 1-elem tuple; reject None | ||
| # and longer tuples) and return it with the validated positions or None | ||
| if isinstance(key, tuple) and len(key) == 1: | ||
| key = key[0] | ||
| if key is None or (isinstance(key, tuple) and len(key) > 1): | ||
| raise IndexError( | ||
| "only integers, slices (`:`), ellipsis (`...`) and integer " | ||
| "or boolean arrays are valid indices" | ||
| ) | ||
| return key, self._validate_key(key) | ||
|
|
||
| def _scalar_pos(self, key): | ||
| # normalize a scalar flat index (wrap negatives) and bounds-check it | ||
| pos = key + self._size if key < 0 else key | ||
| if not 0 <= pos < self._size: | ||
| raise IndexError( | ||
| f"index {key} is out of bounds for size {self._size}" | ||
| ) | ||
| return pos | ||
|
|
||
| def _as_dpnp_array(self, x): | ||
| # coerce to a dpnp array without overriding a device array's queue | ||
| if isinstance(x, dpnp_array): | ||
| return x | ||
| if isinstance(x, dpt.usm_ndarray): | ||
| return dpnp_array._create_from_usm_ndarray(x) | ||
| return dpnp.asarray( | ||
| x, sycl_queue=self._arr.sycl_queue, usm_type=self._arr.usm_type | ||
| ) | ||
|
|
||
| def __getitem__(self, key): | ||
| idx = getattr(key, "__index__", None) | ||
| if not callable(idx): | ||
| raise TypeError(key) | ||
| i = idx() | ||
| mi = self._multiindex(i) | ||
| return self.arr_.__getitem__(mi) | ||
| key, _ = self._prepare_key(key) | ||
|
|
||
| if isinstance(key, int): | ||
| # scalar fast path: index directly instead of flattening the array | ||
| # (a bool key was already rejected in _prepare_key) | ||
| pos = self._scalar_pos(key) | ||
| return self._arr[numpy.unravel_index(pos, self._arr.shape)].copy() | ||
|
|
||
| res = dpnp.reshape(self._arr, -1)[key] | ||
| # basic indexing may alias the source, copy if shares the source buffer | ||
| # pylint: disable=protected-access | ||
| src = self._arr.get_array().usm_data._pointer | ||
| if res.get_array().usm_data._pointer == src: | ||
| res = res.copy() | ||
| return res | ||
|
|
||
| def __setitem__(self, key, val): | ||
| idx = getattr(key, "__index__", None) | ||
| if not callable(idx): | ||
| raise TypeError(key) | ||
| i = idx() | ||
| mi = self._multiindex(i) | ||
| return self.arr_.__setitem__(mi, val) | ||
| key, idx = self._prepare_key(key) | ||
|
|
||
| if isinstance(key, tuple) and len(key) == 0: | ||
| # NumPy rejects arr.flat[()] = val | ||
| raise IndexError( | ||
| "Assigning to a flat iterator with a 0-D index is not " | ||
| "supported" | ||
| ) | ||
|
|
||
| a = self._arr | ||
| exec_q = a.sycl_queue | ||
| usm_type = a.usm_type | ||
|
|
||
| # resolve key to flat positions; an integer-array key is already | ||
| # validated (idx), and dpnp.put resolves its negative positions | ||
| if isinstance(key, int): | ||
| # scalar fast path: avoid building a full index array | ||
| # (a bool key was already rejected in _prepare_key) | ||
| pos = self._scalar_pos(key) | ||
| idx = dpnp.asarray(pos, sycl_queue=exec_q, usm_type=usm_type) | ||
| elif isinstance(key, slice): | ||
| # slice fast path: build only the selected positions | ||
| start, stop, step = key.indices(self._size) | ||
| idx = dpnp.arange( | ||
| start, stop, step, sycl_queue=exec_q, usm_type=usm_type | ||
| ) | ||
| elif hasattr(key, "dtype") and dpnp.issubdtype(key.dtype, dpnp.bool): | ||
| # boolean mask fast path | ||
| mask = self._as_dpnp_array(key) | ||
| idx = dpnp.nonzero(mask)[0] | ||
| elif idx is None: | ||
| # ellipsis, empty tuple or an unrecognized key: let regular | ||
| # indexing resolve the positions and raise on an invalid key | ||
| flat_index = dpnp.arange( | ||
| self._size, sycl_queue=exec_q, usm_type=usm_type | ||
| ) | ||
| idx = flat_index[key] | ||
|
|
||
| if not dpnp.isscalar(val): | ||
| val = self._as_dpnp_array(val) | ||
| if idx.ndim == 0 and val.ndim != 0: | ||
| # a scalar index targets a single item, reject an array value | ||
| raise ValueError("Error setting single item of array.") | ||
|
|
||
| val = val.ravel() | ||
| n = idx.size | ||
| if val.size not in (0, 1, n): | ||
| # put broadcasts size-0/1 values; otherwise cycle over selection | ||
| val = val[ | ||
| dpnp.arange(n, sycl_queue=exec_q, usm_type=usm_type) | ||
| % val.size | ||
| ] | ||
|
|
||
| dpnp.put(a, idx, val) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Matching NumPy here isn't cleanly feasible either: NumPy's flat setitem cascades on a backward overlap ( Given that, I'd prefer to keep raising a clear error rather than add a masked copy that still diverges. |
||
|
|
||
| def __iter__(self): | ||
| return self | ||
|
|
||
| def __next__(self): | ||
| if self.i_ < self.size_: | ||
| val = self.__getitem__(self.i_) | ||
| self.i_ = self.i_ + 1 | ||
| if self._i < self._size: | ||
| val = self[self._i] | ||
| self._i += 1 | ||
| return val | ||
| else: | ||
| raise StopIteration | ||
Uh oh!
There was an error while loading. Please reload this page.