From e353d7dc14fa67e377e9744699e675bce2e98486 Mon Sep 17 00:00:00 2001 From: Anton <100830759+antonwolfy@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:10:52 +0200 Subject: [PATCH] Update `dpnp.place` implementation to get rid of limitations for input arguments (#1912) * Remove limitations from dpnp.take implementation * Add more test to cover specail cases and increase code coverage * Applied pre-commit hook * Corrected test_over_index * Update docsctrings with resolving typos * Use dpnp.reshape() to change shape and create dpnp array from usm_ndarray result * Remove limitations from dpnp.place implementation * Update relating tests * Roll back changed in dpnp.vander * Remove data sync at the end of function * Remove data sync from dpnp.get_result_array() * Fix typo in docstring * Corrected a link to dpnp.copyto() in description --- dpnp/dpnp_iface.py | 50 +++++ dpnp/dpnp_iface_histograms.py | 8 +- dpnp/dpnp_iface_indexing.py | 90 ++++++--- tests/test_indexing.py | 174 ++++-------------- .../cupy/indexing_tests/test_insert.py | 3 +- 5 files changed, 158 insertions(+), 167 deletions(-) diff --git a/dpnp/dpnp_iface.py b/dpnp/dpnp_iface.py index 3402f7d23a8..518e81dccfc 100644 --- a/dpnp/dpnp_iface.py +++ b/dpnp/dpnp_iface.py @@ -57,6 +57,7 @@ "array_equal", "asnumpy", "astype", + "as_usm_ndarray", "check_limitations", "check_supported_arrays_type", "convert_single_elem_array_to_scalar", @@ -247,6 +248,55 @@ def astype(x1, dtype, order="K", casting="unsafe", copy=True, device=None): return dpnp_array._create_from_usm_ndarray(array_obj) +def as_usm_ndarray(a, dtype=None, device=None, usm_type=None, sycl_queue=None): + """ + Return :class:`dpctl.tensor.usm_ndarray` from input object `a`. + + Parameters + ---------- + a : {array_like, scalar} + Input array or scalar. + dtype : {None, dtype}, optional + The desired dtype for the result array if new array is creating. If not + given, a default dtype will be used that can represent the values (by + considering Promotion Type Rule and device capabilities when necessary). + Default: ``None``. + device : {None, string, SyclDevice, SyclQueue}, optional + An array API concept of device where the result array is created if + required. + The `device` can be ``None`` (the default), an OneAPI filter selector + string, an instance of :class:`dpctl.SyclDevice` corresponding to + a non-partitioned SYCL device, an instance of :class:`dpctl.SyclQueue`, + or a `Device` object returned by + :obj:`dpnp.dpnp_array.dpnp_array.device` property. + Default: ``None``. + usm_type : {None, "device", "shared", "host"}, optional + The type of SYCL USM allocation for the result array if new array + is created. + Default: ``None``. + sycl_queue : {None, SyclQueue}, optional + A SYCL queue to use for result array allocation if required. + Default: ``None``. + + Returns + ------- + out : usm_ndarray + A dpctl USM ndarray from input array or scalar `a`. + If `a` is instance of :class:`dpnp.ndarray` + or :class:`dpctl.tensor.usm_ndarray`, no array allocation will be done + and `dtype`, `device`, `usm_type`, `sycl_queue` keywords + will be ignored. + + """ + + if is_supported_array_type(a): + return get_usm_ndarray(a) + + return dpt.asarray( + a, dtype=dtype, device=device, usm_type=usm_type, sycl_queue=sycl_queue + ) + + def check_limitations( order=None, subok=False, like=None, initial=None, where=True ): diff --git a/dpnp/dpnp_iface_histograms.py b/dpnp/dpnp_iface_histograms.py index 24c8b6aaf78..3b211c30ef0 100644 --- a/dpnp/dpnp_iface_histograms.py +++ b/dpnp/dpnp_iface_histograms.py @@ -164,11 +164,9 @@ def _get_bin_edges(a, bins, range, usm_type): "a and bins must be allocated on the same SYCL queue" ) - bin_edges = dpnp.get_usm_ndarray(bins) - else: - bin_edges = dpt.asarray( - bins, sycl_queue=sycl_queue, usm_type=usm_type - ) + bin_edges = dpnp.as_usm_ndarray( + bins, usm_type=usm_type, sycl_queue=sycl_queue + ) if dpnp.any(bin_edges[:-1] > bin_edges[1:]): raise ValueError( diff --git a/dpnp/dpnp_iface_indexing.py b/dpnp/dpnp_iface_indexing.py index e0afa9427d4..ca2df4d4605 100644 --- a/dpnp/dpnp_iface_indexing.py +++ b/dpnp/dpnp_iface_indexing.py @@ -551,12 +551,12 @@ def extract(condition, a): """ usm_a = dpnp.get_usm_ndarray(a) - if not dpnp.is_supported_array_type(condition): - usm_cond = dpt.asarray( - condition, usm_type=a.usm_type, sycl_queue=a.sycl_queue - ) - else: - usm_cond = dpnp.get_usm_ndarray(condition) + usm_cond = dpnp.as_usm_ndarray( + condition, + dtype=dpnp.bool, + usm_type=usm_a.usm_type, + sycl_queue=usm_a.sycl_queue, + ) if usm_cond.size != usm_a.size: usm_a = dpt.reshape(usm_a, -1) @@ -1011,30 +1011,74 @@ def nonzero(a): ) -def place(x, mask, vals, /): +def place(a, mask, vals): """ Change elements of an array based on conditional and input values. + Similar to ``dpnp.copyto(a, vals, where=mask)``, the difference is that + :obj:`dpnp.place` uses the first N elements of `vals`, where N is + the number of ``True`` values in `mask`, while :obj:`dpnp.copyto` uses + the elements where `mask` is ``True``. + + Note that :obj:`dpnp.extract` does the exact opposite of :obj:`dpnp.place`. + For full documentation refer to :obj:`numpy.place`. - Limitations - ----------- - Parameters `x`, `mask` and `vals` are supported either as - :class:`dpnp.ndarray` or :class:`dpctl.tensor.usm_ndarray`. - Otherwise the function will be executed sequentially on CPU. + Parameters + ---------- + a : {dpnp.ndarray, usm_ndarray} + Array to put data into. + mask : {array_like, scalar} + Boolean mask array. Must have the same size as `a`. + vals : {array_like, scalar} + Values to put into `a`. Only the first N elements are used, where N is + the number of ``True`` values in `mask`. If `vals` is smaller than N, + it will be repeated, and if elements of `a` are to be masked, this + sequence must be non-empty. + + See Also + -------- + :obj:`dpnp.copyto` : Copies values from one array to another. + :obj:`dpnp.put` : Replaces specified elements of an array with given values. + :obj:`dpnp.take` : Take elements from an array along an axis. + :obj:`dpnp.extract` : Return the elements of an array that satisfy some + condition. + + Examples + -------- + >>> import dpnp as np + >>> a = np.arange(6).reshape(2, 3) + >>> np.place(a, a > 2, [44, 55]) + >>> a + array([[ 0, 1, 2], + [44, 55, 44]]) + """ - if ( - dpnp.is_supported_array_type(x) - and dpnp.is_supported_array_type(mask) - and dpnp.is_supported_array_type(vals) - ): - dpt_array = x.get_array() if isinstance(x, dpnp_array) else x - dpt_mask = mask.get_array() if isinstance(mask, dpnp_array) else mask - dpt_vals = vals.get_array() if isinstance(vals, dpnp_array) else vals - return dpt.place(dpt_array, dpt_mask, dpt_vals) - - return call_origin(numpy.place, x, mask, vals, dpnp_inplace=True) + usm_a = dpnp.get_usm_ndarray(a) + usm_mask = dpnp.as_usm_ndarray( + mask, + dtype=dpnp.bool, + usm_type=usm_a.usm_type, + sycl_queue=usm_a.sycl_queue, + ) + usm_vals = dpnp.as_usm_ndarray( + vals, + dtype=usm_a.dtype, + usm_type=usm_a.usm_type, + sycl_queue=usm_a.sycl_queue, + ) + + if usm_vals.ndim != 1: + # dpt.place supports only 1-D array of values + usm_vals = dpt.reshape(usm_vals, -1) + + if usm_vals.dtype != usm_a.dtype: + # dpt.place casts values to a.dtype with "unsafe" rule, + # while numpy.place does that with "safe" casting rule + usm_vals = dpt.astype(usm_vals, usm_a.dtype, casting="safe", copy=False) + + dpt.place(usm_a, usm_mask, usm_vals) def put(a, ind, v, /, *, axis=None, mode="wrap"): diff --git a/tests/test_indexing.py b/tests/test_indexing.py index c8e6e37d0da..d3df20a7e49 100644 --- a/tests/test_indexing.py +++ b/tests/test_indexing.py @@ -95,6 +95,15 @@ def test_diagonal_errors(self): class TestExtins: + @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) + def test_extract(self, dt): + a = numpy.array([1, 3, 2, 1, 2, 3, 3], dtype=dt) + ia = dpnp.array(a) + + result = dpnp.extract(ia > 1, ia) + expected = numpy.extract(a > 1, a) + assert_array_equal(result, expected) + @pytest.mark.parametrize("a_dt", get_all_dtypes(no_none=True)) @pytest.mark.parametrize("cond_dt", get_all_dtypes(no_none=True)) def test_extract_diff_dtypes(self, a_dt, cond_dt): @@ -106,15 +115,6 @@ def test_extract_diff_dtypes(self, a_dt, cond_dt): expected = numpy.extract(cond, a) assert_array_equal(result, expected) - @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) - def test_extract(self, dt): - a = numpy.array([1, 3, 2, 1, 2, 3, 3], dtype=dt) - ia = dpnp.array(a) - - result = dpnp.extract(ia > 1, ia) - expected = numpy.extract(a > 1, a) - assert_array_equal(result, expected) - @pytest.mark.parametrize("a_dt", get_all_dtypes(no_none=True)) def test_extract_list_cond(self, a_dt): a = numpy.array([-2, -1, 0, 1, 2, 3], dtype=a_dt) @@ -125,7 +125,6 @@ def test_extract_list_cond(self, a_dt): expected = numpy.extract(cond, a) assert_array_equal(result, expected) - @pytest.mark.usefixtures("allow_fall_back_on_numpy") @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) def test_place(self, dt): a = numpy.array([1, 4, 3, 2, 5, 8, 7], dtype=dt) @@ -135,7 +134,34 @@ def test_place(self, dt): numpy.place(a, [0, 1, 0, 1, 0, 1, 0], [2, 4, 6]) assert_array_equal(ia, a) - @pytest.mark.usefixtures("allow_fall_back_on_numpy") + @pytest.mark.parametrize("a_dt", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize("mask_dt", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize("vals_dt", get_all_dtypes(no_none=True)) + def test_place_diff_dtypes(self, a_dt, mask_dt, vals_dt): + a = numpy.array( + [[[1, 2], [3, 4]], [[1, 2], [2, 1]], [[1, 3], [3, 1]]], dtype=a_dt + ) + mask = numpy.array( + [ + [[True, False], [False, True]], + [[False, True], [True, False]], + [[False, False], [True, True]], + ], + dtype=mask_dt, + ) + vals = numpy.array( + [100, 200, 300, 400, 500, 600, 800, 900], dtype=vals_dt + ) + ia, imask, ivals = dpnp.array(a), dpnp.array(mask), dpnp.array(vals) + + if numpy.can_cast(vals_dt, a_dt, casting="safe"): + dpnp.place(ia, imask, ivals) + numpy.place(a, mask, vals) + assert_array_equal(ia, a) + else: + assert_raises(TypeError, dpnp.place, ia, imask, ivals) + assert_raises(TypeError, numpy.place, a, mask, vals) + def test_place_broadcast_vals(self): a = numpy.array([1, 4, 3, 2, 5, 8, 7]) ia = dpnp.array(a) @@ -144,7 +170,6 @@ def test_place_broadcast_vals(self): numpy.place(a, [1, 0, 1, 0, 1, 0, 1], [8, 9]) assert_array_equal(ia, a) - @pytest.mark.usefixtures("allow_fall_back_on_numpy") def test_place_empty_vals(self): a = numpy.array([1, 4, 3, 2, 5, 8, 7]) mask = numpy.zeros(7) @@ -155,7 +180,6 @@ def test_place_empty_vals(self): numpy.place(a, mask, vals) assert_array_equal(ia, a) - @pytest.mark.usefixtures("allow_fall_back_on_numpy") @pytest.mark.parametrize("xp", [numpy, dpnp]) def test_place_insert_from_empty_vals(self, xp): a = xp.array([1, 4, 3, 2, 5, 8, 7]) @@ -165,12 +189,10 @@ def test_place_insert_from_empty_vals(self, xp): lambda: xp.place(a, [0, 0, 0, 0, 0, 1, 0], []), ) - @pytest.mark.usefixtures("allow_fall_back_on_numpy") @pytest.mark.parametrize("xp", [numpy, dpnp]) def test_place_wrong_array_type(self, xp): assert_raises(TypeError, xp.place, [1, 2, 3], [True, False], [0, 1]) - @pytest.mark.usefixtures("allow_fall_back_on_numpy") @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) def test_both(self, dt): a = numpy.random.rand(10).astype(dt) @@ -781,128 +803,6 @@ def test_indices(dimension, dtype, sparse): assert_array_equal(Xnp, X) -@pytest.mark.parametrize( - "vals", [[100, 200], (100, 200)], ids=["[100, 200]", "(100, 200)"] -) -@pytest.mark.parametrize( - "mask", - [ - [[True, False], [False, True]], - [[False, True], [True, False]], - [[False, False], [True, True]], - ], - ids=[ - "[[True, False], [False, True]]", - "[[False, True], [True, False]]", - "[[False, False], [True, True]]", - ], -) -@pytest.mark.parametrize( - "arr", - [[[0, 0], [0, 0]], [[1, 2], [1, 2]], [[1, 2], [3, 4]]], - ids=["[[0, 0], [0, 0]]", "[[1, 2], [1, 2]]", "[[1, 2], [3, 4]]"], -) -def test_place1(arr, mask, vals): - a = numpy.array(arr) - ia = dpnp.array(a) - m = numpy.array(mask) - im = dpnp.array(m) - iv = dpnp.array(vals) - numpy.place(a, m, vals) - dpnp.place(ia, im, iv) - assert_array_equal(a, ia) - - -@pytest.mark.parametrize( - "vals", - [ - [100, 200], - [100, 200, 300, 400, 500, 600], - [100, 200, 300, 400, 500, 600, 800, 900], - ], - ids=[ - "[100, 200]", - "[100, 200, 300, 400, 500, 600]", - "[100, 200, 300, 400, 500, 600, 800, 900]", - ], -) -@pytest.mark.parametrize( - "mask", - [ - [ - [[True, False], [False, True]], - [[False, True], [True, False]], - [[False, False], [True, True]], - ] - ], - ids=[ - "[[[True, False], [False, True]], [[False, True], [True, False]], [[False, False], [True, True]]]" - ], -) -@pytest.mark.parametrize( - "arr", - [[[[1, 2], [3, 4]], [[1, 2], [2, 1]], [[1, 3], [3, 1]]]], - ids=["[[[1, 2], [3, 4]], [[1, 2], [2, 1]], [[1, 3], [3, 1]]]"], -) -def test_place2(arr, mask, vals): - a = numpy.array(arr) - ia = dpnp.array(a) - m = numpy.array(mask) - im = dpnp.array(m) - iv = dpnp.array(vals) - numpy.place(a, m, vals) - dpnp.place(ia, im, iv) - assert_array_equal(a, ia) - - -@pytest.mark.parametrize( - "vals", - [ - [100, 200], - [100, 200, 300, 400, 500, 600], - [100, 200, 300, 400, 500, 600, 800, 900], - ], - ids=[ - "[100, 200]", - "[100, 200, 300, 400, 500, 600]", - "[100, 200, 300, 400, 500, 600, 800, 900]", - ], -) -@pytest.mark.parametrize( - "mask", - [ - [ - [[[False, False], [True, True]], [[True, True], [True, True]]], - [[[False, False], [True, True]], [[False, False], [False, False]]], - ] - ], - ids=[ - "[[[[False, False], [True, True]], [[True, True], [True, True]]], [[[False, False], [True, True]], [[False, False], [False, False]]]]" - ], -) -@pytest.mark.parametrize( - "arr", - [ - [ - [[[1, 2], [3, 4]], [[1, 2], [2, 1]]], - [[[1, 3], [3, 1]], [[0, 1], [1, 3]]], - ] - ], - ids=[ - "[[[[1, 2], [3, 4]], [[1, 2], [2, 1]]], [[[1, 3], [3, 1]], [[0, 1], [1, 3]]]]" - ], -) -def test_place3(arr, mask, vals): - a = numpy.array(arr) - ia = dpnp.array(a) - m = numpy.array(mask) - im = dpnp.array(m) - iv = dpnp.array(vals) - numpy.place(a, m, vals) - dpnp.place(ia, im, iv) - assert_array_equal(a, ia) - - @pytest.mark.parametrize("vals", [[100, 200]], ids=["[100, 200]"]) @pytest.mark.parametrize( "mask", diff --git a/tests/third_party/cupy/indexing_tests/test_insert.py b/tests/third_party/cupy/indexing_tests/test_insert.py index 538039bd43c..1b355f8ad08 100644 --- a/tests/third_party/cupy/indexing_tests/test_insert.py +++ b/tests/third_party/cupy/indexing_tests/test_insert.py @@ -43,11 +43,10 @@ class TestPlaceRaises(unittest.TestCase): # https://github.com/numpy/numpy/pull/5821 @testing.with_requires("numpy>=1.10") @testing.for_all_dtypes() - @pytest.mark.usefixtures("allow_fall_back_on_numpy") def test_place_empty_value_error(self, dtype): for xp in (numpy, cupy): a = testing.shaped_arange(self.shape, xp, dtype) - mask = testing.shaped_arange(self.shape, xp, numpy.int_) % 2 == 0 + mask = testing.shaped_arange(self.shape, xp, int) % 2 == 0 vals = testing.shaped_random((0,), xp, dtype) with pytest.raises(ValueError): xp.place(a, mask, vals)