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 diff --git a/dpnp/dpnp_iface_indexing.py b/dpnp/dpnp_iface_indexing.py index db70f1fd238..2a90f6cff63 100644 --- a/dpnp/dpnp_iface_indexing.py +++ b/dpnp/dpnp_iface_indexing.py @@ -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 diff --git a/dpnp/tests/test_indexing.py b/dpnp/tests/test_indexing.py index b6cae0733d4..27f34f6288b 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,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))