From 7765addafc292b19dafe147b2b5ec893c7399c01 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 11 Mar 2026 15:36:14 +0100 Subject: [PATCH 1/5] Calculation of out_strides does not depend on offset --- dpnp/dpnp_iface_indexing.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dpnp/dpnp_iface_indexing.py b/dpnp/dpnp_iface_indexing.py index db70f1fd238..2319b92978f 100644 --- a/dpnp/dpnp_iface_indexing.py +++ b/dpnp/dpnp_iface_indexing.py @@ -725,17 +725,15 @@ def diagonal(a, offset=0, axis1=0, axis2=1): # Compute shape, strides and offset of the resulting diagonal array # based on the input offset + out_strides = a_straides[:-2] + (st_n + st_m,) 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 return dpnp_array( From 983ff84e4019c1f82488a0a4ef3d8c7c8224cc0f Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 11 Mar 2026 15:37:57 +0100 Subject: [PATCH 2/5] Add tests to cover the issue --- dpnp/tests/test_indexing.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/dpnp/tests/test_indexing.py b/dpnp/tests/test_indexing.py index b6cae0733d4..3247448daa8 100644 --- a/dpnp/tests/test_indexing.py +++ b/dpnp/tests/test_indexing.py @@ -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, @@ -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", @@ -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) @@ -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: @@ -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 @@ -115,6 +118,29 @@ 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("dtype", 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, dtype, shape, offset): + a = generate_random_numpy_array(shape=shape, dtype=dtype) + 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) + class TestExtins: @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) From 383c39cafedc5b05171e0445a169cb2355d0fd77 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 11 Mar 2026 16:03:57 +0100 Subject: [PATCH 3/5] Use unified way to calculate shape, strides and offset --- dpnp/dpnp_iface_indexing.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/dpnp/dpnp_iface_indexing.py b/dpnp/dpnp_iface_indexing.py index 2319b92978f..2a90f6cff63 100644 --- a/dpnp/dpnp_iface_indexing.py +++ b/dpnp/dpnp_iface_indexing.py @@ -719,22 +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 - out_strides = a_straides[:-2] + (st_n + st_m,) - if offset == 0: - out_shape = a_shape[:-2] + (min(n, m),) - out_offset = 0 - elif 0 < offset < m: - out_shape = a_shape[:-2] + (min(n, m - offset),) - out_offset = st_m // a.itemsize * offset - else: - out_shape = a_shape[:-2] + (0,) - 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 From 7845b3cd9a009fb39ba44bc27f4dd10b97741908 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 11 Mar 2026 16:06:24 +0100 Subject: [PATCH 4/5] Add more tests to cover different use cases --- dpnp/tests/test_indexing.py | 41 ++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/dpnp/tests/test_indexing.py b/dpnp/tests/test_indexing.py index 3247448daa8..27f34f6288b 100644 --- a/dpnp/tests/test_indexing.py +++ b/dpnp/tests/test_indexing.py @@ -118,7 +118,7 @@ def test_errors(self): assert_raises(ValueError, a.diagonal, axis1=1, axis2=1) assert_raises(ValueError, a.diagonal, axis1=1, axis2=-1) - @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) @pytest.mark.parametrize( "shape, offset", [ @@ -129,8 +129,8 @@ def test_errors(self): ((3, 3, 4), 5), # 3D array, offset >= m ], ) - def test_empty_strides(self, dtype, shape, offset): - a = generate_random_numpy_array(shape=shape, dtype=dtype) + 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) @@ -141,6 +141,41 @@ def test_empty_strides(self, dtype, shape, offset): 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)) From b3d712d797eb0e4c5e68a5927c2a49a288387ac2 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 11 Mar 2026 16:15:32 +0100 Subject: [PATCH 5/5] Add PR to the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61cde1ddfef..799a3e6db1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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