Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -76,6 +76,7 @@ Also, that release drops support for Python 3.9, making Python 3.10 the minimum
* Resolved an issue causing `dpnp.linspace` to return an incorrect output shape when inputs were passed as arrays [#2712](https://github.com/IntelPython/dpnp/pull/2712)
* Resolved an issue where `dpnp` always returns the base allocation pointer, when the view start is expected [#2651](https://github.com/IntelPython/dpnp/pull/2651)
* Fixed an issue causing an exception in `dpnp.geomspace` and `dpnp.logspace` when called with explicit `device` keyword but any input array is allocated on another device [#2723](https://github.com/IntelPython/dpnp/pull/2723)
* Resolved an issue with strides calculation in `dpnp.diagonal` to return correct values for empty diagonals [#2814](https://github.com/IntelPython/dpnp/pull/2814)

### Security

Expand Down
28 changes: 11 additions & 17 deletions dpnp/dpnp_iface_indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,24 +719,18 @@ def diagonal(a, offset=0, axis1=0, axis2=1):
offset = -offset

a_shape = a.shape
a_straides = a.strides
a_strides = a.strides
n, m = a_shape[-2:]
st_n, st_m = a_straides[-2:]

# Compute shape, strides and offset of the resulting diagonal array
# based on the input offset
if offset == 0:
out_shape = a_shape[:-2] + (min(n, m),)
out_strides = a_straides[:-2] + (st_n + st_m,)
out_offset = 0
elif 0 < offset < m:
out_shape = a_shape[:-2] + (min(n, m - offset),)
out_strides = a_straides[:-2] + (st_n + st_m,)
out_offset = st_m // a.itemsize * offset
else:
out_shape = a_shape[:-2] + (0,)
out_strides = a_straides[:-2] + (a.itemsize,)
out_offset = 0
st_n, st_m = a_strides[-2:]

# Compute the diagonal array as a view:
# - stride: sum of row and column strides (diag advances in both dimensions)
# - shape: determined by diagonal size using max(0, min(n, m - offset))
# - offset: starting position in buffer for non-zero offsets
diag_size = max(0, min(n, m - offset))
out_shape = a_shape[:-2] + (diag_size,)
out_strides = a_strides[:-2] + (st_n + st_m,)
out_offset = st_m // a.itemsize * offset

return dpnp_array(
out_shape, buffer=a, strides=out_strides, offset=out_offset
Expand Down
69 changes: 65 additions & 4 deletions dpnp/tests/test_indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from dpnp.exceptions import AxisError, ExecutionPlacementError

from .helper import (
generate_random_numpy_array,
get_abs_array,
get_all_dtypes,
get_array,
Expand All @@ -44,7 +45,9 @@ def wrapped(a, axis, **kwargs):


class TestDiagonal:
@pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True))
@pytest.mark.parametrize(
"dtype", get_all_dtypes(no_none=True, no_bool=True)
)
@pytest.mark.parametrize("offset", [-3, -1, 0, 1, 3])
@pytest.mark.parametrize(
"shape",
Expand All @@ -58,7 +61,7 @@ class TestDiagonal:
"(2, 2, 2, 3)",
],
)
def test_diagonal_offset(self, shape, dtype, offset):
def test_offset(self, shape, dtype, offset):
a = numpy.arange(numpy.prod(shape), dtype=dtype).reshape(shape)
a_dp = dpnp.array(a)
expected = numpy.diagonal(a, offset)
Expand All @@ -74,7 +77,7 @@ def test_diagonal_offset(self, shape, dtype, offset):
((4, 3, 5, 2), [(0, 1), (1, 2), (2, 3), (0, 3)]),
],
)
def test_diagonal_axes(self, shape, axis_pairs, dtype):
def test_axes(self, shape, axis_pairs, dtype):
a = numpy.arange(numpy.prod(shape), dtype=dtype).reshape(shape)
a_dp = dpnp.array(a)
for axis1, axis2 in axis_pairs:
Expand All @@ -91,7 +94,7 @@ def test_linalg_diagonal(self, offset):
result = dpnp.linalg.diagonal(a_dp, offset=offset)
assert_array_equal(expected, result)

def test_diagonal_errors(self):
def test_errors(self):
a = dpnp.arange(12).reshape(3, 4)

# unsupported type
Expand All @@ -115,6 +118,64 @@ def test_diagonal_errors(self):
assert_raises(ValueError, a.diagonal, axis1=1, axis2=1)
assert_raises(ValueError, a.diagonal, axis1=1, axis2=-1)

@pytest.mark.parametrize("dt", get_all_dtypes(no_none=True))
@pytest.mark.parametrize(
"shape, offset",
[
((2, 5), 5), # offset >= m
((2, 5), 10), # offset >> m
((4, 5), 6), # offset >= m
((2, 5), -5), # negative offset >= n
((3, 3, 4), 5), # 3D array, offset >= m
],
)
def test_empty_strides(self, dt, shape, offset):
a = generate_random_numpy_array(shape=shape, dtype=dt)
ia = dpnp.array(a)

expected = numpy.diagonal(a, offset)
result = dpnp.diagonal(ia, offset)

# Check both shape and strides match NumPy
assert expected.shape == result.shape
assert expected.strides == result.strides
assert_array_equal(expected, result)

@pytest.mark.parametrize("dt", get_all_dtypes(no_none=True))
def test_view(self, dt):
a = generate_random_numpy_array(shape=(3, 4), dtype=dt)
a = dpnp.array(a)
ia = a.copy()

diag = dpnp.diagonal(a)
diag[1] = 17 # modify a diagonal element
ia[1, 1] = 17 # do the same in original copy of the array

assert (a == ia).all()

@pytest.mark.parametrize("dt", get_all_dtypes(no_none=True))
@pytest.mark.parametrize(
"slice_spec, offset",
[
((slice(None), slice(None, None, 2)), 0), # skip columns
((slice(None, None, 2), slice(None)), 1), # skip rows
((slice(None, None, 2), slice(None, None, 2)), 0), # skip both
],
)
def test_noncontiguous(self, dt, slice_spec, offset):
a = generate_random_numpy_array(shape=(4, 6), dtype=dt)
a_sliced = a[slice_spec]
ia = dpnp.array(a)
ia_sliced = ia[slice_spec]

expected = numpy.diagonal(a_sliced, offset=offset)
result = dpnp.diagonal(ia_sliced, offset=offset)

# Check strides match for non-contiguous arrays
assert expected.shape == result.shape
assert expected.strides == result.strides
assert_array_equal(expected, result)


class TestExtins:
@pytest.mark.parametrize("dt", get_all_dtypes(no_none=True))
Expand Down
Loading