Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 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
8c5fb70
Address flatiter review comments: scalar/bool fast paths and cleanups
antonwolfy Sep 11, 2026
554e542
Gate wrong-size bool mask flatiter test on numpy>=2.4
antonwolfy Sep 11, 2026
89e9d62
Accept an empty boolean mask in flatiter
antonwolfy Sep 11, 2026
78e333c
Tidy flatiter: extract _scalar_pos helper and minor cleanups
antonwolfy Sep 11, 2026
6f8c06b
Correct a stale comment in flatiter _validate_key
antonwolfy Sep 11, 2026
d267afe
Pin flatiter out-of-bounds behavior for an empty assignment value
antonwolfy Sep 11, 2026
1f4ea83
Merge branch 'master' into fix/SAT-8204-flatiter-indexing
antonwolfy Sep 11, 2026
156452f
Reject a multi-dimensional boolean mask in flatiter
antonwolfy Sep 11, 2026
a62a89e
Avoid a redundant full-size copy in flatiter getitem
antonwolfy Sep 11, 2026
2d82dce
Fix flatiter getitem alias check to use the allocation base
antonwolfy Sep 11, 2026
0d43c85
Respect compute-follows-data in flatiter setitem
antonwolfy Sep 11, 2026
e288078
Reword the flatiter getitem alias-check comment
antonwolfy Sep 11, 2026
bd03352
Refine flatiter CFD helper and relocate its tests
antonwolfy Sep 11, 2026
df356ae
Skip value cycling when dpnp.put already broadcasts in flatiter setitem
antonwolfy Sep 11, 2026
9a2930d
Drop dead bool guard from flatiter scalar fast paths
antonwolfy Sep 11, 2026
651bfcf
Reuse validated index in flatiter setitem, avoiding an arange rebuild
antonwolfy Sep 11, 2026
cf9503d
Match NumPy wording for the wrong-size flatiter boolean mask
antonwolfy Sep 11, 2026
92d0c68
Note flatiter assignment fixes in the changelog
antonwolfy Sep 11, 2026
a4f9eac
Add flatiter test coverage for uncovered branches
antonwolfy Sep 11, 2026
a327d46
Use cached self._size consistently in flatiter setitem
antonwolfy Sep 11, 2026
05ae718
Tidy flatiter __next__ to idiomatic form
antonwolfy Sep 11, 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 @@ -99,6 +99,7 @@ This release is compatible with NumPy 2.5.
* Fixed `dpnp.cumsum`, `dpnp.cumprod`, and their `nan`/`cumulative_*` variants (including `dpnp.tensor.cumulative_sum`/`cumulative_prod`) silently returning incorrect results when accumulating along an axis of an array with more than one row [#3063](https://github.com/IntelPython/dpnp/pull/3063)
* Fixed `dpnp.einsum` returning a result whose memory layout differs from NumPy for the default `order="K"`, and ignoring `out` and `order` for a contraction over a size-0 dimension [#3058](https://github.com/IntelPython/dpnp/pull/3058)
* Fixed operations on a boolean array whose bytes are not `0x00`/`0x01` [#3055](https://github.com/IntelPython/dpnp/pull/3055)
* Fixed `dpnp.ndarray.flat` indexing and assignment 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
251 changes: 210 additions & 41 deletions dpnp/dpnp_flatiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Comment thread
antonwolfy marked this conversation as resolved.
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling dpnp.put prevents memory overlap while NumPy allows it

In [10]: a = dpnp.arange(6)

In [11]: a_np = numpy.arange(6)

In [12]: a.flat[:-1] = a[1:]

ValueError: Arrays index overlapping segments of memory

In [13]: a_np.flat[:-1] = a_np[1:]

In [14]: a[:-1] = a[1:]  # setitem works

@antonwolfy antonwolfy Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dpnp.ndarray.flat setitem is built on dpnp.put, which rejects overlapping memory by design, so this is consistent with the rest of dpnp rather than a flat-specific gap.

Matching NumPy here isn't cleanly feasible either: NumPy's flat setitem cascades on a backward overlap (a.flat[1:] = a[:-1] -> [0,0,0,0,0,0]), which a gather/scatter put can't reproduce; the only workaround (copying the value) yields snapshot semantics that match NumPy's regular assignment but not its flat behavior.

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
Loading
Loading