From 5324b330144c74442052bb3fffab4dfbae65899f Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Tue, 21 May 2024 15:28:06 +0200 Subject: [PATCH 1/2] Get rid of call_origin in dpnp.put --- dpnp/dpnp_iface_indexing.py | 111 +++++++++--------- tests/test_indexing.py | 8 +- .../cupy/indexing_tests/test_insert.py | 18 +-- 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/dpnp/dpnp_iface_indexing.py b/dpnp/dpnp_iface_indexing.py index a87bcdc36557..a9bee48248ca 100644 --- a/dpnp/dpnp_iface_indexing.py +++ b/dpnp/dpnp_iface_indexing.py @@ -669,7 +669,7 @@ def nonzero(a): [2, 1]]) A common use for ``nonzero`` is to find the indices of an array, where - a condition is ``True.`` Given an array `a`, the condition `a` > 3 is + a condition is ``True``. Given an array `a`, the condition `a` > 3 is a boolean array and since ``False`` is interpreted as ``0``, ``np.nonzero(a > 3)`` yields the indices of the `a` where the condition is true. @@ -728,25 +728,33 @@ def place(x, mask, vals, /): return call_origin(numpy.place, x, mask, vals, dpnp_inplace=True) -# pylint: disable=redefined-outer-name -def put(a, indices, vals, /, *, axis=None, mode="wrap"): +def put(a, ind, v, /, *, axis=None, mode="wrap"): """ Puts values of an array into another array along a given axis. For full documentation refer to :obj:`numpy.put`. - Limitations - ----------- - Parameters `a` and `indices` are supported either as :class:`dpnp.ndarray` - or :class:`dpctl.tensor.usm_ndarray`. - Parameter `indices` is supported as 1-D array of integer data type. - Parameter `vals` must be broadcastable to the shape of `indices` - and has the same data type as `a` if it is as :class:`dpnp.ndarray` - or :class:`dpctl.tensor.usm_ndarray`. - Parameter `mode` is supported with ``wrap``, the default, and ``clip`` - values. - Parameter `axis` is supported as integer only. - Otherwise the function will be executed sequentially on CPU. + Parameters + ---------- + a : {dpnp.ndarray, usm_ndarray} + The array the values will be put into. + ind : {array_like} + Target indices, interpreted as integers. + v : {scalar, array_like} + Values to be put into `a`. Must be broadcastable to the result shape + ``a.shape[:axis] + ind.shape + a.shape[axis+1:]``. + axis {None, int}, optional + The axis along which the values will be placed. If `a` is 1-D array, + this argument is optional. + Default: ``None``. + mode : {'wrap', 'clip'}, optional + Specifies how out-of-bounds indices will behave. + + - 'wrap': clamps indices to (``-n <= i < n``), then wraps negative + indices. + - 'clip': clips indices to (``0 <= i < n``). + + Default: ``'wrap'``. See Also -------- @@ -766,49 +774,46 @@ def put(a, indices, vals, /, *, axis=None, mode="wrap"): Examples -------- >>> import dpnp as np - >>> x = np.arange(5) - >>> indices = np.array([0, 1]) - >>> np.put(x, indices, [-44, -55]) - >>> x - array([-44, -55, 2, 3, 4]) + >>> a = np.arange(5) + >>> np.put(a, [0, 2], [-44, -55]) + >>> a + array([-44, 1, -55, 3, 4]) - >>> x = np.arange(5) - >>> indices = np.array([22]) - >>> np.put(x, indices, -5, mode='clip') - >>> x + >>> a = np.arange(5) + >>> np.put(a, 22, -5, mode='clip') + >>> a array([ 0, 1, 2, 3, -5]) """ - if dpnp.is_supported_array_type(a) and dpnp.is_supported_array_type( - indices - ): - if indices.ndim != 1 or not dpnp.issubdtype( - indices.dtype, dpnp.integer - ): - pass - elif mode not in ("clip", "wrap"): - pass - elif axis is not None and not isinstance(axis, int): - raise TypeError(f"`axis` must be of integer type, got {type(axis)}") - # TODO: remove when #1382(dpctl) is solved - elif dpnp.is_supported_array_type(vals) and a.dtype != vals.dtype: - pass - else: - if axis is None and a.ndim > 1: - a = dpnp.reshape(a, -1) - dpt_array = dpnp.get_usm_ndarray(a) - dpt_indices = dpnp.get_usm_ndarray(indices) - dpt_vals = ( - dpnp.get_usm_ndarray(vals) - if isinstance(vals, dpnp_array) - else vals - ) - return dpt.put( - dpt_array, dpt_indices, dpt_vals, axis=axis, mode=mode - ) + dpnp.check_supported_arrays_type(a) + + if not dpnp.is_supported_array_type(ind): + ind = dpnp.asarray(ind, sycl_queue=a.sycl_queue, usm_type=a.usm_type) + ind = ind.ravel() + + if not dpnp.is_supported_array_type(v): + v = dpnp.asarray( + v, dtype=a.dtype, sycl_queue=a.sycl_queue, usm_type=a.usm_type + ) + if v.size == 0: + return + + if not (axis is None or isinstance(axis, int)): + raise TypeError(f"`axis` must be of integer type, got {type(axis)}") + + if axis is None and a.ndim > 1: + a = dpnp.ravel(a) + + if mode not in ("wrap", "clip"): + raise ValueError( + f"clipmode must be one of 'clip' or 'wrap' (got '{mode}')" + ) - return call_origin(numpy.put, a, indices, vals, mode, dpnp_inplace=True) + usm_a = dpnp.get_usm_ndarray(a) + usm_ind = dpnp.get_usm_ndarray(ind) + usm_v = dpnp.get_usm_ndarray(v) + dpt.put(usm_a, usm_ind, usm_v, axis=axis, mode=mode) # pylint: disable=redefined-outer-name @@ -1186,7 +1191,7 @@ def triu_indices(n, k=0, m=None): ------- inds : tuple, shape(2) of ndarrays, shape(`n`) The indices for the triangle. The returned tuple contains two arrays, - each with the indices along one dimension of the array. Can be used + each with the indices along one dimension of the array. Can be used to slice a ndarray of shape(`n`, `n`). """ diff --git a/tests/test_indexing.py b/tests/test_indexing.py index 6e3d884eac91..ff01cee642e0 100644 --- a/tests/test_indexing.py +++ b/tests/test_indexing.py @@ -597,10 +597,6 @@ def test_put_1d(indices, vals, array_dtype, indices_dtype, mode): ind = numpy.array(indices, dtype=indices_dtype) iind = dpnp.array(ind) - # TODO: remove when #1382(dpctl) is solved - if dpnp.is_supported_array_type(vals): - vals = dpnp.astype(vals, ia.dtype) - numpy.put(a, ind, vals, mode=mode) dpnp.put(ia, iind, vals, mode=mode) assert_array_equal(a, ia) @@ -653,17 +649,15 @@ def test_put_2d_ind(): @pytest.mark.parametrize( "shape", [ - (0,), (3,), (4,), ], ids=[ - "(0,)", "(3,)", "(4,)", ], ) -@pytest.mark.parametrize("mode", ["clip", "wrap"], ids=["clip", "wrap"]) +@pytest.mark.parametrize("mode", ["clip", "wrap"]) def test_put_invalid_shape(shape, mode): a = dpnp.arange(7) ind = dpnp.array([2]) diff --git a/tests/third_party/cupy/indexing_tests/test_insert.py b/tests/third_party/cupy/indexing_tests/test_insert.py index 28d25c487b0c..a79956e2f141 100644 --- a/tests/third_party/cupy/indexing_tests/test_insert.py +++ b/tests/third_party/cupy/indexing_tests/test_insert.py @@ -70,24 +70,23 @@ def test_place_shape_unmatch_error(self, dtype): "shape": [(7,), (2, 3), (4, 3, 2)], "mode": ["raise", "wrap", "clip"], # The vals shape of array must be broadcastable to the shape of indices - "n_vals": [1, 4], + "n_vals": [0, 1, 4], } ) ) -@pytest.mark.usefixtures("allow_fall_back_on_numpy") class TestPut(unittest.TestCase): @testing.for_all_dtypes() @testing.numpy_cupy_array_equal() def test_put(self, xp, dtype): a = testing.shaped_arange(self.shape, xp, dtype) # Take care so that actual indices don't overlap. - if self.mode in ("raise"): - inds = xp.array([2, -1, 3, 0]) + if self.mode == "raise": + pytest.skip("'raise' mode is not supported") # `wrap` mode in dpctl.tensor.put is different from numpy.put (#1365): # numpy`s `wrap` mode wraps indices around for cyclic operations # while dpctl`s `wrap` mode restricts indices to stay within the array bounds (-n <= i < n). - elif self.mode in ("wrap"): - inds = xp.array([2, -1, 3, -6]) + elif self.mode == "wrap": + inds = xp.array([2, -1, 3, 0]) else: inds = xp.array([2, -8, 3, 7]) vals = testing.shaped_random((self.n_vals,), xp, dtype) @@ -103,13 +102,13 @@ def test_put(self, xp, dtype): ) ) class TestPutScalars(unittest.TestCase): - @pytest.mark.usefixtures("allow_fall_back_on_numpy") @testing.numpy_cupy_array_equal() def test_put_index_scalar(self, xp): dtype = cupy.float32 a = testing.shaped_arange(self.shape, xp, dtype) inds = 4 - vals = testing.shaped_random((4,), xp, dtype) + # The vals shape of array must be broadcastable to the shape of indices + vals = testing.shaped_random((1,), xp, dtype) xp.put(a, inds, vals) return a @@ -131,8 +130,8 @@ def test_put_values_scalar(self, xp): } ) ) -@pytest.mark.usefixtures("allow_fall_back_on_numpy") class TestPutRaises(unittest.TestCase): + @pytest.mark.skip("'raise' mode is not supported") @testing.for_all_dtypes() def test_put_inds_underflow_error(self, dtype): for xp in (numpy, cupy): @@ -142,6 +141,7 @@ def test_put_inds_underflow_error(self, dtype): with pytest.raises(IndexError): xp.put(a, inds, vals, mode="raise") + @pytest.mark.skip("'raise' mode is not supported") @testing.for_all_dtypes() def test_put_inds_overflow_error(self, dtype): for xp in (numpy, cupy): From 7a92e8e99df1212f1d1f59182d7a4d9df3e1e711 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 22 May 2024 13:47:48 +0200 Subject: [PATCH 2/2] Extended tests for dpnp.put --- dpnp/dpnp_iface_indexing.py | 13 +- tests/helper.py | 8 -- tests/test_indexing.py | 267 +++++++++++++++++++++--------------- 3 files changed, 165 insertions(+), 123 deletions(-) diff --git a/dpnp/dpnp_iface_indexing.py b/dpnp/dpnp_iface_indexing.py index fd7c5c641f59..11d9804272ad 100644 --- a/dpnp/dpnp_iface_indexing.py +++ b/dpnp/dpnp_iface_indexing.py @@ -797,8 +797,12 @@ def put(a, ind, v, /, *, axis=None, mode="wrap"): dpnp.check_supported_arrays_type(a) if not dpnp.is_supported_array_type(ind): - ind = dpnp.asarray(ind, sycl_queue=a.sycl_queue, usm_type=a.usm_type) - ind = ind.ravel() + ind = dpnp.asarray( + ind, dtype=dpnp.intp, sycl_queue=a.sycl_queue, usm_type=a.usm_type + ) + elif not dpnp.issubdtype(ind.dtype, dpnp.integer): + ind = dpnp.astype(ind, dtype=dpnp.intp, casting="safe") + ind = dpnp.ravel(ind) if not dpnp.is_supported_array_type(v): v = dpnp.asarray( @@ -810,8 +814,9 @@ def put(a, ind, v, /, *, axis=None, mode="wrap"): if not (axis is None or isinstance(axis, int)): raise TypeError(f"`axis` must be of integer type, got {type(axis)}") + in_a = a if axis is None and a.ndim > 1: - a = dpnp.ravel(a) + a = dpnp.ravel(in_a) if mode not in ("wrap", "clip"): raise ValueError( @@ -822,6 +827,8 @@ def put(a, ind, v, /, *, axis=None, mode="wrap"): usm_ind = dpnp.get_usm_ndarray(ind) usm_v = dpnp.get_usm_ndarray(v) dpt.put(usm_a, usm_ind, usm_v, axis=axis, mode=mode) + if in_a is not a: + in_a[:] = a.reshape(in_a.shape, copy=False) # pylint: disable=redefined-outer-name diff --git a/tests/helper.py b/tests/helper.py index a081fbf060b1..2db95fdeb771 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -91,14 +91,6 @@ def get_integer_dtypes(): return [dpnp.int32, dpnp.int64] -def get_integer_dtypes(): - """ - Build a list of integer types supported by DPNP. - """ - - return [dpnp.int32, dpnp.int64] - - def get_complex_dtypes(device=None): """ Build a list of complex types supported by DPNP based on device capabilities. diff --git a/tests/test_indexing.py b/tests/test_indexing.py index ff01cee642e0..89ea4ce39b3f 100644 --- a/tests/test_indexing.py +++ b/tests/test_indexing.py @@ -148,6 +148,161 @@ def test_array_method(self, dtype): assert_array_equal(a.nonzero(), ia.nonzero()) +class TestPut: + @pytest.mark.parametrize("a_dt", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize( + "indices", [[0, 2], [-5, 4]], ids=["[0, 2]", "[-5, 4]"] + ) + @pytest.mark.parametrize("ind_dt", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize( + "vals", + [0, [1, 2], (2, 2), dpnp.array([1, 2])], + ids=["0", "[1, 2]", "(2, 2)", "dpnp.array([1,2])"], + ) + @pytest.mark.parametrize("mode", ["clip", "wrap"]) + def test_input_1d(self, a_dt, indices, ind_dt, vals, mode): + a = numpy.array([-2, -1, 0, 1, 2], dtype=a_dt) + b = numpy.copy(a) + ia = dpnp.array(a) + ib = dpnp.array(b) + + ind = numpy.array(indices, dtype=ind_dt) + if ind_dt == dpnp.bool and ind.all(): + ind[0] = False # to get rid of duplicate indices + iind = dpnp.array(ind) + + if numpy.can_cast(ind_dt, numpy.intp, casting="safe"): + numpy.put(a, ind, vals, mode=mode) + dpnp.put(ia, iind, vals, mode=mode) + assert_array_equal(ia, a) + + b.put(ind, vals, mode=mode) + ib.put(iind, vals, mode=mode) + assert_array_equal(ib, b) + else: + assert_raises(TypeError, numpy.put, a, ind, vals, mode=mode) + assert_raises(TypeError, dpnp.put, ia, iind, vals, mode=mode) + + assert_raises(TypeError, b.put, ind, vals, mode=mode) + assert_raises(TypeError, ib.put, iind, vals, mode=mode) + + @pytest.mark.parametrize("a_dt", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize( + "indices", + [ + [0, 7], + [3, 4], + [-9, 8], + ], + ids=[ + "[0, 7]", + "[3, 4]", + "[-9, 8]", + ], + ) + @pytest.mark.parametrize("ind_dt", get_integer_dtypes()) + @pytest.mark.parametrize("vals", [[10, 20]], ids=["[10, 20]"]) + @pytest.mark.parametrize("mode", ["clip", "wrap"]) + def test_input_2d(self, a_dt, indices, ind_dt, vals, mode): + a = numpy.array([[-1, 0, 1], [-2, -3, -4], [2, 3, 4]], dtype=a_dt) + ia = dpnp.array(a) + + ind = numpy.array(indices, dtype=ind_dt) + iind = dpnp.array(ind) + + numpy.put(a, ind, vals, mode=mode) + dpnp.put(ia, iind, vals, mode=mode) + assert_array_equal(ia, a) + + def test_indices_2d(self): + a = numpy.arange(5) + ia = dpnp.array(a) + ind = numpy.array([[3, 0, 2, 1]]) + iind = dpnp.array(ind) + + numpy.put(a, ind, 10) + dpnp.put(ia, iind, 10) + assert_array_equal(ia, a) + + def test_non_contiguous(self): + # force non C-contiguous array + a = numpy.arange(6).reshape(2, 3).T + ia = dpnp.arange(6).reshape(2, 3).T + + a.put([0, 2], [44, 55]) + ia.put([0, 2], [44, 55]) + assert_equal(ia, a) + + @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize("mode", ["clip", "wrap"]) + def test_empty(self, dtype, mode): + a = numpy.zeros(1000, dtype=dtype) + ia = dpnp.array(a) + + numpy.put(a, [1, 2, 3], [], mode=mode) + dpnp.put(ia, [1, 2, 3], [], mode=mode) + assert_array_equal(ia, a) + + # TODO: enable test for numpy also since 2.0 + @pytest.mark.parametrize("mode", ["clip", "wrap"]) + def test_empty_input(self, mode): + empty = dpnp.asarray(list()) + with pytest.raises(IndexError): + empty.put(1, 1, mode=mode) + + @pytest.mark.parametrize( + "shape", + [ + (3,), + (4,), + ], + ids=[ + "(3,)", + "(4,)", + ], + ) + @pytest.mark.parametrize("mode", ["clip", "wrap"]) + def test_invalid_shape(self, shape, mode): + a = dpnp.arange(7) + ind = dpnp.array([2]) + vals = dpnp.ones(shape, dtype=a.dtype) + # vals must be broadcastable to the shape of ind` + with pytest.raises(ValueError): + dpnp.put(a, ind, vals, mode=mode) + + @pytest.mark.parametrize("xp", [dpnp, numpy]) + @pytest.mark.parametrize( + "axis", + [ + 1.0, + (0,), + [0, 1], + ], + ids=[ + "1.0", + "(0,)", + "[0, 1]", + ], + ) + def test_invalid_axis(self, xp, axis): + a = xp.arange(6).reshape(2, 3) + ind = xp.array([1]) + with pytest.raises(TypeError): + a.put(ind, [1], axis=axis) + + @pytest.mark.parametrize("xp", [dpnp, numpy]) + def test_unsupported_input_array_type(self, xp): + with pytest.raises(TypeError): + xp.put([1, 2, 3], [0, 2], 5) + + @pytest.mark.parametrize("xp", [dpnp, numpy]) + def test_non_writeable_input_array(self, xp): + a = xp.zeros(6) + a.flags["W"] = False + with pytest.raises(ValueError): + a.put([1, 3, 5], [1, 3, 5]) + + class TestPutAlongAxis: @pytest.mark.parametrize( "arr_dt", get_all_dtypes(no_bool=True, no_none=True) @@ -576,118 +731,6 @@ def test_place3(arr, mask, vals): assert_array_equal(a, ia) -@pytest.mark.parametrize("array_dtype", get_all_dtypes()) -@pytest.mark.parametrize( - "indices_dtype", [dpnp.int32, dpnp.int64], ids=["int32", "int64"] -) -@pytest.mark.parametrize( - "indices", [[-2, 2], [-5, 4]], ids=["[-2, 2]", "[-5, 4]"] -) -@pytest.mark.parametrize( - "vals", - [0, [1, 2], (2, 2), dpnp.array([1, 2])], - ids=["0", "[1, 2]", "(2, 2)", "dpnp.array([1,2])"], -) -@pytest.mark.parametrize("mode", ["clip", "wrap"], ids=["clip", "wrap"]) -def test_put_1d(indices, vals, array_dtype, indices_dtype, mode): - a = numpy.array([-2, -1, 0, 1, 2], dtype=array_dtype) - b = numpy.copy(a) - ia = dpnp.array(a) - ib = dpnp.array(b) - ind = numpy.array(indices, dtype=indices_dtype) - iind = dpnp.array(ind) - - numpy.put(a, ind, vals, mode=mode) - dpnp.put(ia, iind, vals, mode=mode) - assert_array_equal(a, ia) - - b.put(ind, vals, mode=mode) - ib.put(iind, vals, mode=mode) - assert_array_equal(b, ib) - - -@pytest.mark.parametrize("array_dtype", get_all_dtypes()) -@pytest.mark.parametrize( - "indices_dtype", [dpnp.int32, dpnp.int64], ids=["int32", "int64"] -) -@pytest.mark.parametrize("vals", [[10, 20]], ids=["[10, 20]"]) -@pytest.mark.parametrize( - "indices", - [ - [0, 7], - [3, 4], - [-9, 8], - ], - ids=[ - "[0, 7]", - "[3, 4]", - "[-9, 8]", - ], -) -@pytest.mark.parametrize("mode", ["clip", "wrap"], ids=["clip", "wrap"]) -def test_put_2d(array_dtype, indices_dtype, indices, vals, mode): - a = numpy.array([[-1, 0, 1], [-2, -3, -4], [2, 3, 4]], dtype=array_dtype) - ia = dpnp.array(a) - ind = numpy.array(indices, dtype=indices_dtype) - iind = dpnp.array(ind) - numpy.put(a, ind, vals, mode=mode) - dpnp.put(ia, iind, vals, mode=mode) - assert_array_equal(a, ia) - - -@pytest.mark.usefixtures("allow_fall_back_on_numpy") -def test_put_2d_ind(): - a = numpy.arange(5) - ia = dpnp.array(a) - ind = numpy.array([[3, 0, 2, 1]]) - iind = dpnp.array(ind) - numpy.put(a, ind, 10) - dpnp.put(ia, iind, 10) - assert_array_equal(a, ia) - - -@pytest.mark.parametrize( - "shape", - [ - (3,), - (4,), - ], - ids=[ - "(3,)", - "(4,)", - ], -) -@pytest.mark.parametrize("mode", ["clip", "wrap"]) -def test_put_invalid_shape(shape, mode): - a = dpnp.arange(7) - ind = dpnp.array([2]) - vals = dpnp.ones(shape, dtype=a.dtype) - # vals must be broadcastable to the shape of ind` - with pytest.raises(ValueError): - dpnp.put(a, ind, vals, mode=mode) - - -@pytest.mark.parametrize( - "axis", - [ - 1.0, - (0,), - [0, 1], - ], - ids=[ - "1.0", - "(0,)", - "[0, 1]", - ], -) -def test_put_invalid_axis(axis): - a = dpnp.arange(6).reshape(2, 3) - ind = dpnp.array([1]) - vals = [1] - with pytest.raises(TypeError): - dpnp.put(a, ind, vals, axis=axis) - - @pytest.mark.parametrize("vals", [[100, 200]], ids=["[100, 200]"]) @pytest.mark.parametrize( "mask",