Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7809332
Align dpnp.flatiter indexing edge cases with NumPy
antonwolfy Aug 27, 2026
0bf9d53
Accept dpnp.ndarray subclasses in flatiter via isinstance check
antonwolfy Aug 27, 2026
6c1e5b2
Add flatiter indexing tests aligned with NumPy
antonwolfy Aug 27, 2026
be8f6a0
Cycle values in flatiter setitem and enable cupy iterate tests
antonwolfy Aug 27, 2026
fe66c93
Add CHANGELOG entry for flatiter indexing fix
antonwolfy Aug 27, 2026
575c6b1
Improve dpnp.flatiter and dpnp.ndarray.flat docstrings
antonwolfy Aug 28, 2026
ee7488c
Add NumPy reference to dpnp.ndarray.flat docstring
antonwolfy Aug 28, 2026
1dbcb7f
Avoid full index array for scalar and slice flatiter assignment
antonwolfy Aug 31, 2026
6bc35e6
Catch out-of-bounds index wrapped in a tuple in flatiter
antonwolfy Aug 31, 2026
3a9333a
Bounds-check flat index on the host to avoid device transfers
antonwolfy Aug 31, 2026
f70a68a
Reject an array value assigned to a single flatiter item
antonwolfy Sep 3, 2026
eb5804e
Correct NumPy version gating of flatiter tests
antonwolfy Sep 3, 2026
63b0fb7
Use dpnp_array consistently in flatiter type checks
antonwolfy Sep 3, 2026
5c430b1
Cover negative-step slice in flatiter getitem test
antonwolfy Sep 3, 2026
7c87de9
Reject a multi-element index tuple in flatiter
antonwolfy Sep 3, 2026
338b6df
Narrow index-conversion exception handling in flatiter
antonwolfy Sep 3, 2026
6c446ec
Consolidate flatiter key normalization into a single helper
antonwolfy Sep 3, 2026
9d48d82
Reject non-mask boolean indices in flatiter
antonwolfy Sep 3, 2026
e314321
Gate tuple-index flatiter tests and harden the iteration test
antonwolfy Sep 3, 2026
2f7e6e7
Un-gate tuple-index flatiter tests (valid since NumPy 2.0)
antonwolfy Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ This release is compatible with NumPy 2.5.
* Fixed `astype` casting an out-of-range floating point value to a signed narrow integer type saturating to the destination min/max instead of wrapping like NumPy, generalizing the earlier unsigned-only fix [#3033](https://github.com/IntelPython/dpnp/pull/3033)
* Fixed `dpnp.insert` silently ignoring out-of-bounds negative indices in a multi-element `obj`, so a mix of in-bounds and out-of-bounds indices now consistently raises `IndexError` [#3041](https://github.com/IntelPython/dpnp/pull/3041)
* Fixed a per-call `sycl::queue` leak in `usm_ndarray::get_queue()`/`get_device()` [#3042](https://github.com/IntelPython/dpnp/pull/3042)
* Fixed `dpnp.ndarray.flat` indexing edge cases, adding support for slices, ellipsis, and integer/boolean array indices [#3045](https://github.com/IntelPython/dpnp/pull/3045)

### Security

Expand Down
34 changes: 31 additions & 3 deletions dpnp/dpnp_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,9 +1332,37 @@ def flags(self):
@property
def flat(self):
"""
Return a flat iterator, or set a flattened version of self to value.
A 1-D iterator over the array.

""" # noqa: D200
This is a :obj:`dpnp.flatiter` instance, which acts similarly to, but
is not a subclass of, Python's built-in iterator object.

For full documentation refer to :obj:`numpy.ndarray.flat`.

See Also
--------
:obj:`dpnp.flatiter` : Flat iterator object to iterate over arrays.
:obj:`dpnp.ndarray.flatten` : Return a flattened copy of the array.

Examples
--------
>>> import dpnp as np
>>> x = np.arange(1, 7).reshape(2, 3)
>>> x
array([[1, 2, 3],
[4, 5, 6]])
>>> x.flat[3]
array(4)
>>> x.T.flat[3]
array(5)

An assignment example:

>>> x.flat[[1, 4]] = 1; x
array([[1, 1, 3],
[4, 1, 6]])

"""

return dpnp.flatiter(self)

Expand Down Expand Up @@ -1367,7 +1395,7 @@ def flatten(self, /, order="C"):
See Also
--------
:obj:`dpnp.ravel` : Return a flattened array.
:obj:`dpnp.flat` : A 1-D flat iterator over the array.
:obj:`dpnp.ndarray.flat` : A 1-D flat iterator over the array.

Examples
--------
Expand Down
198 changes: 157 additions & 41 deletions dpnp/dpnp_flatiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,63 +28,179 @@

"""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 = a
self._size = a.size
self._i = 0

def _validate_key(self, key):
# Ellipsis/slice/tuple need no validation here
if key is Ellipsis or isinstance(key, (slice, tuple)):
return

# a genuine scalar int (not bool): regular indexing checks it
if not isinstance(key, bool) and (
isinstance(key, int)
or (
callable(getattr(key, "__index__", None))
and not hasattr(key, "ndim")
)
):
return

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 # let regular indexing raise

if dpnp.issubdtype(idx.dtype, dpnp.bool):
# only a boolean ndarray mask is valid; reject bool scalars/lists
if idx.ndim > 0 and not isinstance(key, (bool, list, tuple)):
return
raise IndexError("boolean indices for iterators are not supported")

if not dpnp.issubdtype(idx.dtype, dpnp.integer) or idx.size == 0:
return

# 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}")

def _normalize_key(self, key):
# 1-D iterator: unwrap a 1-elem tuple; reject None and longer tuples
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"
)
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._validate_key(key)
return key

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._normalize_key(key)

# flat always yields a copy, never a view
return dpnp.reshape(self._arr, -1)[key].copy()

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 = self._normalize_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
if isinstance(key, int) and not isinstance(key, bool):
Comment thread
antonwolfy marked this conversation as resolved.
# fast path for a scalar index: avoid building a full index array
pos = key + a.size if key < 0 else key
if not 0 <= pos < a.size:
raise IndexError(
f"index {key} is out of bounds for size {a.size}"
)
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(a.size)
idx = dpnp.arange(
start, stop, step, sycl_queue=exec_q, usm_type=usm_type
)
else:
flat_index = dpnp.arange(
a.size, sycl_queue=exec_q, usm_type=usm_type
)
idx = flat_index[key]

if not dpnp.isscalar(val):
val = dpnp.asarray(val, sycl_queue=exec_q, usm_type=usm_type)
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 0 < val.size != n:
# cycles the values over the selection
val = val[
dpnp.arange(n, sycl_queue=exec_q, usm_type=usm_type)
% val.size
]

dpnp.put(a, idx, val)

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.__getitem__(self._i)
self._i = self._i + 1
return val
else:
raise StopIteration
Loading
Loading