diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 6839100fff63..8023040c08a8 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -49,6 +49,9 @@ import dpnp from .dpnp_array import dpnp_array + +# pylint: disable=no-name-in-module +from .dpnp_utils import get_usm_allocations from .dpnp_utils.dpnp_utils_pad import dpnp_pad __all__ = [ @@ -66,6 +69,7 @@ "concat", "concatenate", "copyto", + "delete", "dsplit", "dstack", "expand_dims", @@ -115,6 +119,135 @@ def _check_stack_arrays(arrays): ) +def _delete_with_slice(a, obj, axis): + """Utility function for ``dpnp.delete`` when obj is slice.""" + + a, a_ndim, order, axis, slobj, n, a_shape = _calc_parameters(a, axis) + start, stop, step = obj.indices(n) + xr = range(start, stop, step) + num_del = len(xr) + + if num_del <= 0: + return a.copy(order=order) + + # Invert if step is negative: + if step < 0: + step = -step + start = xr[-1] + stop = xr[0] + 1 + + a_shape[axis] -= num_del + new = dpnp.empty( + a_shape, + dtype=a.dtype, + order=order, + sycl_queue=a.sycl_queue, + usm_type=a.usm_type, + ) + # copy initial chunk + if start == 0: + pass + else: + slobj[axis] = slice(None, start) + new[tuple(slobj)] = a[tuple(slobj)] + # copy end chunk + if stop == n: + pass + else: + slobj[axis] = slice(stop - num_del, None) + slobj2 = [slice(None)] * a_ndim + slobj2[axis] = slice(stop, None) + new[tuple(slobj)] = a[tuple(slobj2)] + # copy middle pieces + if step == 1: + pass + else: # use array indexing. + keep = dpnp.ones( + stop - start, + dtype=dpnp.bool, + sycl_queue=a.sycl_queue, + usm_type=a.usm_type, + ) + keep[: stop - start : step] = False + slobj[axis] = slice(start, stop - num_del) + slobj2 = [slice(None)] * a_ndim + slobj2[axis] = slice(start, stop) + a = a[tuple(slobj2)] + slobj2[axis] = keep + new[tuple(slobj)] = a[tuple(slobj2)] + + return new + + +def _delete_without_slice(a, obj, axis, single_value, exec_q, usm_type): + """Utility function for ``dpnp.delete`` when obj is int or array of int.""" + + a, a_ndim, order, axis, slobj, n, a_shape = _calc_parameters(a, axis) + if single_value: + # optimization for a single value + if obj < -n or obj >= n: + raise IndexError( + f"index {obj} is out of bounds for axis {axis} with " + f"size {n}" + ) + if obj < 0: + obj += n + a_shape[axis] -= 1 + new = dpnp.empty( + a_shape, + dtype=a.dtype, + order=order, + sycl_queue=exec_q, + usm_type=usm_type, + ) + slobj[axis] = slice(None, obj) + new[tuple(slobj)] = a[tuple(slobj)] + slobj[axis] = slice(obj, None) + slobj2 = [slice(None)] * a_ndim + slobj2[axis] = slice(obj + 1, None) + new[tuple(slobj)] = a[tuple(slobj2)] + else: + if obj.dtype == dpnp.bool: + if obj.shape != (n,): + raise ValueError( + "boolean array argument `obj` to delete must be " + f"one-dimensional and match the axis length of {n}" + ) + + # optimization, the other branch is slower + keep = ~obj + else: + keep = dpnp.ones( + n, dtype=dpnp.bool, sycl_queue=exec_q, usm_type=usm_type + ) + keep[obj,] = False + + slobj[axis] = keep + new = a[tuple(slobj)] + + return new + + +def _calc_parameters(a, axis): + """Utility function for ``dpnp.delete`` and ``dpnp.insert``.""" + + a_ndim = a.ndim + order = "F" if a.flags.fnc else "C" + if axis is None: + if a_ndim != 1: + a = dpnp.ravel(a) + a_ndim = 1 + axis = 0 + else: + axis = normalize_axis_index(axis, a_ndim) + + slobj = [slice(None)] * a_ndim + n = a.shape[axis] + a_shape = list(a.shape) + + return a, a_ndim, order, axis, slobj, n, a_shape + + def _unique_1d( ar, return_index=False, @@ -1206,6 +1339,109 @@ def copyto(dst, src, casting="same_kind", where=True): dst_usm[mask_usm] = src_usm[mask_usm] +def delete(arr, obj, axis=None): + """ + Return a new array with sub-arrays along an axis deleted. For a one + dimensional array, this returns those entries not returned by + ``arr[obj]``. + + For full documentation refer to :obj:`numpy.delete`. + + Parameters + ---------- + arr : {dpnp.ndarray, usm_ndarray} + Input array. + obj : {slice, int, array-like of ints or boolean} + Indicate indices of sub-arrays to remove along the specified axis. + Boolean indices are treated as a mask of elements to remove. + axis : {None, int}, optional + The axis along which to delete the subarray defined by `obj`. + If `axis` is ``None``, `obj` is applied to the flattened array. + Default: ``None``. + + Returns + ------- + out : dpnp.ndarray + A copy of `arr` with the elements specified by `obj` removed. Note + that `delete` does not occur in-place. If `axis` is ``None``, `out` is + a flattened array. + + See Also + -------- + :obj:`dpnp.insert` : Insert elements into an array. + :obj:`dpnp.append` : Append elements at the end of an array. + + Notes + ----- + Often it is preferable to use a boolean mask. For example: + + >>> import dpnp as np + >>> arr = np.arange(12) + 1 + >>> mask = np.ones(len(arr), dtype=np.bool) + >>> mask[0] = mask[2] = mask[4] = False + >>> result = arr[mask,...] + + is equivalent to ``np.delete(arr, [0, 2, 4], axis=0)``, but allows further + use of `mask`. + + Examples + -------- + >>> import dpnp as np + >>> arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) + >>> arr + array([[ 1, 2, 3, 4], + [ 5, 6, 7, 8], + [ 9, 10, 11, 12]]) + >>> np.delete(arr, 1, 0) + array([[ 1, 2, 3, 4], + [ 9, 10, 11, 12]]) + + >>> np.delete(arr, slice(None, None, 2), 1) + array([[ 2, 4], + [ 6, 8], + [10, 12]]) + >>> np.delete(arr, [1, 3, 5], None) + array([ 1, 3, 5, 7, 8, 9, 10, 11, 12]) + + """ + + dpnp.check_supported_arrays_type(arr) + + if isinstance(obj, slice): + return _delete_with_slice(arr, obj, axis) + + if dpnp.is_supported_array_type(obj): + usm_type, exec_q = get_usm_allocations([arr, obj]) + else: + usm_type, exec_q = arr.usm_type, arr.sycl_queue + + if isinstance(obj, (int, dpnp.integer)) and not isinstance(obj, bool): + single_value = True + indices = obj + else: + single_value = False + is_array = isinstance(obj, (dpnp_array, numpy.ndarray, dpt.usm_ndarray)) + indices = dpnp.asarray(obj, sycl_queue=exec_q, usm_type=usm_type) + # if `obj` is originally an empty list, after converting it into + # an array, it will have float dtype, so we need to change its dtype + # to integer. However, if `obj` is originally an empty array with + # float dtype, it is a mistake by user and it will raise an error later + if indices.size == 0 and not is_array: + indices = indices.astype(dpnp.intp) + elif indices.size == 1 and indices.dtype.kind in "ui": + # For a size 1 integer array we can use the single-value path + # (most dtypes, except boolean, should just fail later). + if isinstance(obj, (dpnp_array, dpt.usm_ndarray)): + indices = indices.item() + else: + indices = numpy.asarray(obj).item() + single_value = True + + return _delete_without_slice( + arr, indices, axis, single_value, exec_q, usm_type + ) + + def dsplit(ary, indices_or_sections): """ Split array into multiple sub-arrays along the 3rd axis (depth). diff --git a/tests/test_manipulation.py b/tests/test_manipulation.py index 24c0d12f4143..c137ec6a22a9 100644 --- a/tests/test_manipulation.py +++ b/tests/test_manipulation.py @@ -332,6 +332,147 @@ def test_no_copy(self): assert_array_equal(b, a) +class TestDelete: + @pytest.mark.parametrize( + "obj", [slice(0, 4, 2), 3, [2, 3]], ids=["slice", "int", "list"] + ) + @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) + def test_dtype(self, dt, obj): + a = numpy.array([0, 1, 2, 3, 4, 5], dtype=dt) + a_dp = dpnp.array(a) + + expected = numpy.delete(a, obj) + result = dpnp.delete(a_dp, obj) + assert result.dtype == dt + assert_array_equal(result, expected) + + @pytest.mark.parametrize("start", [-6, -2, 0, 1, 2, 4, 5]) + @pytest.mark.parametrize("stop", [-6, -2, 0, 1, 2, 4, 5]) + @pytest.mark.parametrize("step", [-3, -1, 1, 3]) + def test_slice_1D(self, start, stop, step): + indices = slice(start, stop, step) + # 1D array + a = numpy.arange(5) + a_dp = dpnp.array(a) + expected = numpy.delete(a, indices) + result = dpnp.delete(a_dp, indices) + assert_array_equal(result, expected) + + # N-D array + a = numpy.arange(10).reshape(1, 5, 2) + a_dp = dpnp.array(a) + for axis in [None, 1, -1]: + expected = numpy.delete(a, indices, axis=axis) + result = dpnp.delete(a_dp, indices, axis=axis) + assert_array_equal(result, expected) + + @pytest.mark.parametrize( + "indices", [0, -4, [], [0, -1, 2, 2], [True, False, False, True, False]] + ) + def test_indices_1D(self, indices): + # 1D array + a = numpy.arange(5) + a_dp = dpnp.array(a) + expected = numpy.delete(a, indices) + result = dpnp.delete(a_dp, indices) + assert_array_equal(result, expected) + + # N-D array + a = numpy.arange(10).reshape(1, 5, 2) + a_dp = dpnp.array(a) + expected = numpy.delete(a, indices, axis=1) + result = dpnp.delete(a_dp, indices, axis=1) + assert_array_equal(result, expected) + + def test_obj_ndarray(self): + # 1D array + a = numpy.arange(5) + ind = numpy.array([[0, 1], [2, 1]]) + a_dp = dpnp.array(a) + ind_dp = dpnp.array(ind) + + expected = numpy.delete(a, ind) + # both numpy.ndarray and dpnp.ndarray are supported for obj in dpnp + for indices in [ind, ind_dp]: + result = dpnp.delete(a_dp, indices) + assert_array_equal(result, expected) + + # N-D array + b = numpy.arange(10).reshape(1, 5, 2) + b_dp = dpnp.array(b) + expected = numpy.delete(b, ind, axis=1) + for indices in [ind, ind_dp]: + result = dpnp.delete(b_dp, indices, axis=1) + assert_array_equal(result, expected) + + def test_error(self): + a = dpnp.arange(5) + # out of bounds index + with pytest.raises(IndexError): + dpnp.delete(a, [100]) + with pytest.raises(IndexError): + dpnp.delete(a, [-100]) + + # boolean array argument obj must be one dimensional + with pytest.raises(ValueError): + dpnp.delete(a, True) + + # not enough items + with pytest.raises(ValueError): + dpnp.delete(a, [False] * 4) + + # 0-D array + a = dpnp.array(1) + with pytest.raises(AxisError): + dpnp.delete(a, [], axis=0) + with pytest.raises(TypeError): + dpnp.delete(a, [], axis="nonsense") + + # index float + a = dpnp.array([1, 2, 3]) + with pytest.raises(IndexError): + dpnp.delete(a, dpnp.array([1.0, 2.0])) + with pytest.raises(IndexError): + dpnp.delete(a, dpnp.array([], dtype=dpnp.float32)) + + @pytest.mark.parametrize("order", ["C", "F"]) + def test_order(self, order): + a = numpy.arange(10).reshape(2, 5, order=order) + a_dp = dpnp.array(a) + + expected = numpy.delete(a, slice(3, None), axis=1) + result = dpnp.delete(a_dp, slice(3, None), axis=1) + + assert_equal(result.flags.c_contiguous, expected.flags.c_contiguous) + assert_equal(result.flags.f_contiguous, expected.flags.f_contiguous) + + @pytest.mark.parametrize("indexer", [1, dpnp.array([1]), [1]]) + def test_single_item_array(self, indexer): + a = numpy.arange(5) + a_dp = dpnp.array(a) + expected = numpy.delete(a, 1) + result = dpnp.delete(a_dp, indexer) + assert_equal(result, expected) + + b = numpy.arange(10).reshape(1, 5, 2) + b_dp = dpnp.array(b) + expected = numpy.delete(b, 1, axis=1) + result = dpnp.delete(b_dp, indexer, axis=1) + assert_equal(result, expected) + + @pytest.mark.parametrize("flag", [True, False]) + def test_boolean_obj(self, flag): + expected = numpy.delete(numpy.ones(1), numpy.array([flag])) + result = dpnp.delete(dpnp.ones(1), dpnp.array([flag])) + assert_array_equal(result, expected) + + expected = numpy.delete( + numpy.ones((3, 1)), numpy.array([flag]), axis=-1 + ) + result = dpnp.delete(dpnp.ones((3, 1)), dpnp.array([flag]), axis=-1) + assert_array_equal(result, expected) + + class TestDsplit: @pytest.mark.parametrize("xp", [numpy, dpnp]) def test_error(self, xp): diff --git a/tests/test_sycl_queue.py b/tests/test_sycl_queue.py index 2dfdd3c1dd1a..277c1b6ee10e 100644 --- a/tests/test_sycl_queue.py +++ b/tests/test_sycl_queue.py @@ -2048,6 +2048,36 @@ def test_concat_stack(func, data1, data2, device): assert_sycl_queue_equal(result.sycl_queue, x2.sycl_queue) +class TestDelete: + @pytest.mark.parametrize( + "obj", + [slice(None, None, 2), 3, [2, 3]], + ids=["slice", "scalar", "list"], + ) + @pytest.mark.parametrize( + "device", + valid_devices, + ids=[device.filter_string for device in valid_devices], + ) + def test_delete(self, obj, device): + x = dpnp.arange(5, device=device) + result = dpnp.delete(x, obj) + assert_sycl_queue_equal(result.sycl_queue, x.sycl_queue) + + @pytest.mark.parametrize( + "device", + valid_devices, + ids=[device.filter_string for device in valid_devices], + ) + def test_obj_ndarray(self, device): + x = dpnp.arange(5, device=device) + y = dpnp.array([1, 4], device=device) + result = dpnp.delete(x, y) + + assert_sycl_queue_equal(result.sycl_queue, x.sycl_queue) + assert_sycl_queue_equal(result.sycl_queue, y.sycl_queue) + + @pytest.mark.parametrize( "func,data1", [ diff --git a/tests/test_usm_type.py b/tests/test_usm_type.py index 33d0396541b3..2f1b2ca7bd65 100644 --- a/tests/test_usm_type.py +++ b/tests/test_usm_type.py @@ -831,6 +831,38 @@ def test_cond(usm_type, p): assert result.usm_type == usm_type +class TestDelete: + @pytest.mark.parametrize( + "obj", + [slice(None, None, 2), 3, [2, 3]], + ids=["slice", "scalar", "list"], + ) + @pytest.mark.parametrize( + "usm_type", list_of_usm_types, ids=list_of_usm_types + ) + def test_delete(self, obj, usm_type): + x = dp.arange(5, usm_type=usm_type) + result = dp.delete(x, obj) + + assert x.usm_type == usm_type + assert result.usm_type == usm_type + + @pytest.mark.parametrize( + "usm_type_x", list_of_usm_types, ids=list_of_usm_types + ) + @pytest.mark.parametrize( + "usm_type_y", list_of_usm_types, ids=list_of_usm_types + ) + def test_obj_ndarray(self, usm_type_x, usm_type_y): + x = dp.arange(5, usm_type=usm_type_x) + y = dp.array([1, 4], usm_type=usm_type_y) + z = dp.delete(x, y) + + assert x.usm_type == usm_type_x + assert y.usm_type == usm_type_y + assert z.usm_type == du.get_coerced_usm_type([usm_type_x, usm_type_y]) + + @pytest.mark.parametrize("usm_type", list_of_usm_types, ids=list_of_usm_types) def test_multi_dot(usm_type): numpy_array_list = [] diff --git a/tests/third_party/cupy/manipulation_tests/test_add_remove.py b/tests/third_party/cupy/manipulation_tests/test_add_remove.py index 264e7208cacd..c5cf365b3ad5 100644 --- a/tests/third_party/cupy/manipulation_tests/test_add_remove.py +++ b/tests/third_party/cupy/manipulation_tests/test_add_remove.py @@ -12,7 +12,6 @@ ) -@pytest.mark.skip("delete() is not implemented yet") class TestDelete(unittest.TestCase): @testing.numpy_cupy_array_equal() def test_delete_with_no_axis(self, xp): @@ -54,8 +53,8 @@ def test_delete_with_indices_as_slice(self, xp): def test_delete_with_indices_as_int(self, xp): arr = xp.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) indices = 5 - if cupy.cuda.runtime.is_hip: - pytest.xfail("HIP may have a bug") + # if cupy.cuda.runtime.is_hip: + # pytest.xfail("HIP may have a bug") return xp.delete(arr, indices)