From eea6d1ecb193b7df8d71fc48acb71d7c0a039bc5 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Fri, 2 Aug 2024 21:01:25 +0200 Subject: [PATCH 01/11] Implement dpnp.nan_to_num() --- dpnp/dpnp_iface_mathematical.py | 121 ++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index 5a6e66ce2f7..ed477358e98 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -110,6 +110,7 @@ "mod", "modf", "multiply", + "nan_to_num", "negative", "nextafter", "positive", @@ -155,6 +156,13 @@ def _append_to_diff_array(a, axis, combined, values): combined.append(values) +def _get_max_min(dtype): + """Get the maximum and minimum representable values for an inexact dtype.""" + + f = dpnp.finfo(dtype) + return f.max, f.min + + def _get_reduction_res_dt(a, dtype, _out): """Get a data type used by dpctl for result array in reduction function.""" @@ -2304,6 +2312,119 @@ def modf(x1, **kwargs): ) +def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): + """ + Replace NaN with zero and infinity with large finite numbers (default + behaviour) or with the numbers defined by the user using the `nan`, + `posinf` and/or `neginf` keywords. + + If `x` is inexact, NaN is replaced by zero or by the user defined value in + `nan` keyword, infinity is replaced by the largest finite floating point + values representable by ``x.dtype`` or by the user defined value in + `posinf` keyword and -infinity is replaced by the most negative finite + floating point values representable by ``x.dtype`` or by the user defined + value in `neginf` keyword. + + For complex dtypes, the above is applied to each of the real and + imaginary components of `x` separately. + + If `x` is not inexact, then no replacements are made. + + For full documentation refer to :obj:`numpy.nan_to_num`. + + Parameters + ---------- + x : {dpnp.ndarray, usm_ndarray} + Input data. + copy : bool, optional + Whether to create a copy of `x` (True) or to replace values + in-place (False). The in-place operation only occurs if + casting to an array does not require a copy. + Default: ``True``. + nan : {int, float}, optional + Value to be used to fill NaN values. + Default: ``0.0``. + posinf : {int, float, None}, optional + Value to be used to fill positive infinity values. If no value is + passed then positive infinity values will be replaced with a very + large number. + Default: ``None``. + neginf : {int, float, None} optional + Value to be used to fill negative infinity values. If no value is + passed then negative infinity values will be replaced with a very + small (or negative) number. + Default: ``None``. + + Returns + ------- + out : dpnp.ndarray + `x`, with the non-finite values replaced. If `copy` is False, this may + be `x` itself. + + See Also + -------- + :obj:`dpnp.isinf` : Shows which elements are positive or negative infinity. + :obj:`dpnp.isneginf` : Shows which elements are negative infinity. + :obj:`dpnp.isposinf` : Shows which elements are positive infinity. + :obj:`dpnp.isnan` : Shows which elements are Not a Number (NaN). + :obj:`dpnp.isfinite` : Shows which elements are finite + (not NaN, not infinity) + + Examples + -------- + >>> import dpnp as np + >>> np.nan_to_num(np.array(np.inf)) + array(1.79769313e+308) + >>> np.nan_to_num(np.array(-np.inf)) + array(-1.79769313e+308) + >>> np.nan_to_num(np.array(np.nan)) + array(0.) + >>> x = np.array([np.inf, -np.inf, np.nan, -128, 128]) + >>> np.nan_to_num(x) + array([ 1.79769313e+308, -1.79769313e+308, 0.00000000e+000, + -1.28000000e+002, 1.28000000e+002]) + >>> np.nan_to_num(x, nan=-9999, posinf=33333333, neginf=33333333) + array([ 3.3333333e+07, 3.3333333e+07, -9.9990000e+03, -1.2800000e+02, + 1.2800000e+02]) + >>> y = np.array([complex(np.inf, np.nan), np.nan, complex(np.nan, np.inf)]) + >>> np.nan_to_num(y) + array([1.79769313e+308 +0.00000000e+000j, # may vary + 0.00000000e+000 +0.00000000e+000j, + 0.00000000e+000 +1.79769313e+308j]) + >>> np.nan_to_num(y, nan=111111, posinf=222222) + array([222222.+111111.j, 111111. +0.j, 111111.+222222.j]) + + """ + + dpnp.check_supported_arrays_type(x) + + x = dpnp.array(x, copy=copy) + x_type = x.dtype.type + + if not issubclass(x_type, dpnp.inexact): + return x + + parts = ( + (x.real, x.imag) if issubclass(x_type, dpnp.complexfloating) else (x,) + ) + max_f, min_f = _get_max_min(x.real.dtype) + if posinf is not None: + max_f = posinf + if neginf is not None: + min_f = neginf + + for part in parts: + nan_mask = dpnp.isnan(part) + posinf_mask = dpnp.isposinf(part) + neginf_mask = dpnp.isneginf(part) + + part = dpnp.where(nan_mask, nan, part, out=part) + part = dpnp.where(posinf_mask, max_f, part, out=part) + part = dpnp.where(neginf_mask, min_f, part, out=part) + + return x + + _NEGATIVE_DOCSTRING = """ Computes the numerical negative for each element `x_i` of input array `x`. From 9e274fd7279f10769244f6f76789c2120181ca9f Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Fri, 2 Aug 2024 21:02:33 +0200 Subject: [PATCH 02/11] Update cupy tests for nan_to_num() --- tests/skipped_tests.tbl | 16 ---------------- tests/skipped_tests_gpu.tbl | 16 ---------------- tests/third_party/cupy/math_tests/test_misc.py | 13 ++++++++----- 3 files changed, 8 insertions(+), 37 deletions(-) diff --git a/tests/skipped_tests.tbl b/tests/skipped_tests.tbl index 836eb3e11a4..4f375fe7650 100644 --- a/tests/skipped_tests.tbl +++ b/tests/skipped_tests.tbl @@ -207,22 +207,6 @@ tests/third_party/cupy/manipulation_tests/test_dims.py::TestInvalidBroadcast_par tests/third_party/cupy/manipulation_tests/test_dims.py::TestInvalidBroadcast_param_2_{shapes=[(3, 2), (3, 4)]}::test_invalid_broadcast tests/third_party/cupy/manipulation_tests/test_dims.py::TestInvalidBroadcast_param_3_{shapes=[(0,), (2,)]}::test_invalid_broadcast -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_negative -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_for_old_numpy -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_negative_for_old_numpy -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_inf -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_nan -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_inf_nan -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_nan_arg -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_inf_arg -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_broadcast[nan] -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_broadcast[posinf] -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_broadcast[neginf] - -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_scalar_nan -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_copy -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_inplace tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_real_if_close_real_dtypes tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_real_if_close_with_tol_real_dtypes tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_real_if_close_true diff --git a/tests/skipped_tests_gpu.tbl b/tests/skipped_tests_gpu.tbl index 8a5f1d7aba6..c694dce7978 100644 --- a/tests/skipped_tests_gpu.tbl +++ b/tests/skipped_tests_gpu.tbl @@ -261,22 +261,6 @@ tests/third_party/cupy/manipulation_tests/test_dims.py::TestInvalidBroadcast_par tests/third_party/cupy/manipulation_tests/test_dims.py::TestInvalidBroadcast_param_2_{shapes=[(3, 2), (3, 4)]}::test_invalid_broadcast tests/third_party/cupy/manipulation_tests/test_dims.py::TestInvalidBroadcast_param_3_{shapes=[(0,), (2,)]}::test_invalid_broadcast -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_negative -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_for_old_numpy -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_negative_for_old_numpy -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_inf -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_nan -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_inf_nan -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_nan_arg -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_inf_arg -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_broadcast[nan] -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_broadcast[posinf] -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_broadcast[neginf] - -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_scalar_nan -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_copy -tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_nan_to_num_inplace tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_real_if_close_real_dtypes tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_real_if_close_with_tol_real_dtypes tests/third_party/cupy/math_tests/test_misc.py::TestMisc::test_real_if_close_true diff --git a/tests/third_party/cupy/math_tests/test_misc.py b/tests/third_party/cupy/math_tests/test_misc.py index 62717803aca..c3b3fd2b8b9 100644 --- a/tests/third_party/cupy/math_tests/test_misc.py +++ b/tests/third_party/cupy/math_tests/test_misc.py @@ -260,14 +260,14 @@ def test_nan_to_num_inf_arg(self): @testing.numpy_cupy_array_equal() def test_nan_to_num_copy(self, xp): - x = xp.asarray([0, 1, xp.nan, 4], dtype=xp.float64) + x = xp.asarray([0, 1, xp.nan, 4], dtype=cupy.default_float_type()) y = xp.nan_to_num(x, copy=True) assert x is not y return y @testing.numpy_cupy_array_equal() def test_nan_to_num_inplace(self, xp): - x = xp.asarray([0, 1, xp.nan, 4], dtype=xp.float64) + x = xp.asarray([0, 1, xp.nan, 4], dtype=cupy.default_float_type()) y = xp.nan_to_num(x, copy=False) assert x is y return y @@ -275,12 +275,15 @@ def test_nan_to_num_inplace(self, xp): @pytest.mark.parametrize("kwarg", ["nan", "posinf", "neginf"]) def test_nan_to_num_broadcast(self, kwarg): for xp in (numpy, cupy): - x = xp.asarray([0, 1, xp.nan, 4], dtype=xp.float64) - y = xp.zeros((2, 4), dtype=xp.float64) + x = xp.asarray([0, 1, xp.nan, 4], dtype=cupy.default_float_type()) + y = xp.zeros((2, 4), dtype=cupy.default_float_type()) with pytest.raises(ValueError): xp.nan_to_num(x, **{kwarg: y}) + # dpnp.nan_to_num() doesn`t support a scalar as an input + # convert 0.0 to 0-ndim array with pytest.raises(ValueError): - xp.nan_to_num(0.0, **{kwarg: y}) + x_ndim_0 = xp.array(0.0) + xp.nan_to_num(x_ndim_0, **{kwarg: y}) @testing.for_all_dtypes(no_bool=True, no_complex=True) @testing.numpy_cupy_array_equal() From 68808cd45fb04dcb66e4d1f92dad1d615cddd945 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Fri, 2 Aug 2024 22:21:18 +0200 Subject: [PATCH 03/11] Add dpnp tests --- tests/test_mathematical.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_mathematical.py b/tests/test_mathematical.py index 7d2a2fd907e..6749fe59dc9 100644 --- a/tests/test_mathematical.py +++ b/tests/test_mathematical.py @@ -1370,6 +1370,40 @@ def test_power_scalar(shape, dtype): assert_allclose(result, expected, rtol=1e-6) +class TestNanToNum: + @pytest.mark.parametrize("dtype", get_all_dtypes()) + @pytest.mark.parametrize("shape", [(3,), (2, 3), (3, 2, 2)]) + def test_nan_to_num(self, dtype, shape): + a = numpy.random.randn(*shape).astype(dtype) + if not dpnp.issubdtype(dtype, dpnp.integer): + a.flat[1] = numpy.nan + a_dp = dpnp.array(a) + + result = dpnp.nan_to_num(a_dp) + expected = numpy.nan_to_num(a) + assert_allclose(result, expected) + + @pytest.mark.parametrize( + "data", [[], [numpy.nan], [numpy.inf], [-numpy.inf]] + ) + @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) + def test_empty_and_single_value_arrays(self, data, dtype): + a = numpy.array(data, dtype) + ia = dpnp.array(a) + + result = dpnp.nan_to_num(ia) + expected = numpy.nan_to_num(a) + assert_allclose(result, expected) + + def test_boolean_array(self): + a = numpy.array([True, False, numpy.nan], dtype=bool) + ia = dpnp.array(a) + + result = dpnp.nan_to_num(ia) + expected = numpy.nan_to_num(a) + assert_allclose(result, expected) + + @pytest.mark.parametrize( "data", [[[1.0, -1.0], [0.1, -0.1]], [-2, -1, 0, 1, 2]], From fba7f3cc95e8befb3ddd239136782ea0b47667da Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 5 Aug 2024 10:19:05 +0200 Subject: [PATCH 04/11] Skip test_nan_to_num_scalar_nan --- tests/third_party/cupy/math_tests/test_misc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/third_party/cupy/math_tests/test_misc.py b/tests/third_party/cupy/math_tests/test_misc.py index c3b3fd2b8b9..ed897d6b193 100644 --- a/tests/third_party/cupy/math_tests/test_misc.py +++ b/tests/third_party/cupy/math_tests/test_misc.py @@ -245,6 +245,7 @@ def test_nan_to_num_inf(self): def test_nan_to_num_nan(self): self.check_unary_nan("nan_to_num") + @pytest.mark.skip(reason="Scalar input is not supported") @testing.numpy_cupy_allclose(atol=1e-5) def test_nan_to_num_scalar_nan(self, xp): return xp.nan_to_num(xp.nan) From 5b024ff9519ddb7b79d5af16c94926bf23d4ba8f Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 5 Aug 2024 15:22:28 +0200 Subject: [PATCH 05/11] Applied review comments --- dpnp/dpnp_iface_mathematical.py | 19 +++++---- tests/test_mathematical.py | 68 ++++++++++++++++----------------- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index c821af6af46..fc204480491 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -2264,12 +2264,12 @@ def modf(x1, **kwargs): def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): """ - Replace NaN with zero and infinity with large finite numbers (default + Replace ``NaN`` with zero and infinity with large finite numbers (default behaviour) or with the numbers defined by the user using the `nan`, `posinf` and/or `neginf` keywords. - If `x` is inexact, NaN is replaced by zero or by the user defined value in - `nan` keyword, infinity is replaced by the largest finite floating point + If `x` is inexact, ``NaN`` is replaced by zero or by the user defined value + in `nan` keyword, infinity is replaced by the largest finite floating point values representable by ``x.dtype`` or by the user defined value in `posinf` keyword and -infinity is replaced by the most negative finite floating point values representable by ``x.dtype`` or by the user defined @@ -2287,12 +2287,11 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): x : {dpnp.ndarray, usm_ndarray} Input data. copy : bool, optional - Whether to create a copy of `x` (True) or to replace values - in-place (False). The in-place operation only occurs if - casting to an array does not require a copy. - Default: ``True``. + Whether to create a copy of `x` (``True``) or to replace values + in-place (``False``). The in-place operation only occurs if casting to + an array does not require a copy. nan : {int, float}, optional - Value to be used to fill NaN values. + Value to be used to fill ``NaN`` values. Default: ``0.0``. posinf : {int, float, None}, optional Value to be used to fill positive infinity values. If no value is @@ -2308,8 +2307,8 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): Returns ------- out : dpnp.ndarray - `x`, with the non-finite values replaced. If `copy` is False, this may - be `x` itself. + `x`, with the non-finite values replaced. If `copy` is ``False``, this + may be `x` itself. See Also -------- diff --git a/tests/test_mathematical.py b/tests/test_mathematical.py index e23d9bab221..fa7041bcc05 100644 --- a/tests/test_mathematical.py +++ b/tests/test_mathematical.py @@ -1116,6 +1116,40 @@ def test_subtract(self, dtype, lhs, rhs): self._test_mathematical("subtract", dtype, lhs, rhs, check_type=False) +class TestNanToNum: + @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize("shape", [(3,), (2, 3), (3, 2, 2)]) + def test_nan_to_num(self, dtype, shape): + a = numpy.random.randn(*shape).astype(dtype) + if not dpnp.issubdtype(dtype, dpnp.integer): + a.flat[1] = numpy.nan + a_dp = dpnp.array(a) + + result = dpnp.nan_to_num(a_dp) + expected = numpy.nan_to_num(a) + assert_allclose(result, expected) + + @pytest.mark.parametrize( + "data", [[], [numpy.nan], [numpy.inf], [-numpy.inf]] + ) + @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) + def test_empty_and_single_value_arrays(self, data, dtype): + a = numpy.array(data, dtype) + ia = dpnp.array(a) + + result = dpnp.nan_to_num(ia) + expected = numpy.nan_to_num(a) + assert_allclose(result, expected) + + def test_boolean_array(self): + a = numpy.array([True, False, numpy.nan], dtype=bool) + ia = dpnp.array(a) + + result = dpnp.nan_to_num(ia) + expected = numpy.nan_to_num(a) + assert_allclose(result, expected) + + class TestNextafter: @pytest.mark.parametrize("dt", get_float_dtypes()) @pytest.mark.parametrize( @@ -1445,40 +1479,6 @@ def test_power_scalar(shape, dtype): assert_allclose(result, expected, rtol=1e-6) -class TestNanToNum: - @pytest.mark.parametrize("dtype", get_all_dtypes()) - @pytest.mark.parametrize("shape", [(3,), (2, 3), (3, 2, 2)]) - def test_nan_to_num(self, dtype, shape): - a = numpy.random.randn(*shape).astype(dtype) - if not dpnp.issubdtype(dtype, dpnp.integer): - a.flat[1] = numpy.nan - a_dp = dpnp.array(a) - - result = dpnp.nan_to_num(a_dp) - expected = numpy.nan_to_num(a) - assert_allclose(result, expected) - - @pytest.mark.parametrize( - "data", [[], [numpy.nan], [numpy.inf], [-numpy.inf]] - ) - @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) - def test_empty_and_single_value_arrays(self, data, dtype): - a = numpy.array(data, dtype) - ia = dpnp.array(a) - - result = dpnp.nan_to_num(ia) - expected = numpy.nan_to_num(a) - assert_allclose(result, expected) - - def test_boolean_array(self): - a = numpy.array([True, False, numpy.nan], dtype=bool) - ia = dpnp.array(a) - - result = dpnp.nan_to_num(ia) - expected = numpy.nan_to_num(a) - assert_allclose(result, expected) - - @pytest.mark.parametrize( "data", [[[1.0, -1.0], [0.1, -0.1]], [-2, -1, 0, 1, 2]], From b1a40f746a2466459445cb432e3c117cd96c51b8 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 5 Aug 2024 15:39:33 +0200 Subject: [PATCH 06/11] Add more tests for nan_to_num() --- tests/test_sycl_queue.py | 17 +++++++++++++++++ tests/test_usm_type.py | 13 +++++++++++++ 2 files changed, 30 insertions(+) diff --git a/tests/test_sycl_queue.py b/tests/test_sycl_queue.py index b35c18d50d2..98d129689ff 100644 --- a/tests/test_sycl_queue.py +++ b/tests/test_sycl_queue.py @@ -2329,3 +2329,20 @@ def test_astype(device_x, device_y): sycl_queue = dpctl.SyclQueue(device_y) y = dpnp.astype(x, dtype="f4", device=sycl_queue) assert_sycl_queue_equal(y.sycl_queue, sycl_queue) + + +@pytest.mark.parametrize("copy", [True, False], ids=["True", "False"]) +@pytest.mark.parametrize( + "device", + valid_devices, + ids=[device.filter_string for device in valid_devices], +) +def test_nan_to_num(copy, device): + a = dpnp.array([-dpnp.nan, -1, 0, 1, dpnp.nan], device=device) + result = dpnp.nan_to_num(a, copy=copy) + + assert_sycl_queue_equal(result.sycl_queue, a.sycl_queue) + if copy: + assert result is not a + else: + assert result is a diff --git a/tests/test_usm_type.py b/tests/test_usm_type.py index ace6fb35e98..803060ce427 100644 --- a/tests/test_usm_type.py +++ b/tests/test_usm_type.py @@ -1354,3 +1354,16 @@ def test_histogram_bin_edges(usm_type_v, usm_type_w): assert v.usm_type == usm_type_v assert w.usm_type == usm_type_w assert edges.usm_type == du.get_coerced_usm_type([usm_type_v, usm_type_w]) + + +@pytest.mark.parametrize("copy", [True, False], ids=["True", "False"]) +@pytest.mark.parametrize("usm_type_a", list_of_usm_types, ids=list_of_usm_types) +def test_nan_to_num(copy, usm_type_a): + a = dp.array([-dp.nan, -1, 0, 1, dp.nan], usm_type=usm_type_a) + result = dp.nan_to_num(a, copy=copy) + + assert result.usm_type == usm_type_a + if copy: + assert result is not a + else: + assert result is a From c67d2186bc55e5cf79cc56e830c449c3e15e317e Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Tue, 6 Aug 2024 10:26:48 +0200 Subject: [PATCH 07/11] Improve perfomance using out empty_like array --- dpnp/dpnp_iface_mathematical.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index fc204480491..ee86ea1e2b2 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -2347,7 +2347,7 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): dpnp.check_supported_arrays_type(x) - x = dpnp.array(x, copy=copy) + out = dpnp.empty_like(x) if copy else x x_type = x.dtype.type if not issubclass(x_type, dpnp.inexact): @@ -2356,22 +2356,27 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): parts = ( (x.real, x.imag) if issubclass(x_type, dpnp.complexfloating) else (x,) ) + parts_out = ( + (out.real, out.imag) + if issubclass(x_type, dpnp.complexfloating) + else (out,) + ) max_f, min_f = _get_max_min(x.real.dtype) if posinf is not None: max_f = posinf if neginf is not None: min_f = neginf - for part in parts: + for part, part_out in zip(parts, parts_out): nan_mask = dpnp.isnan(part) posinf_mask = dpnp.isposinf(part) neginf_mask = dpnp.isneginf(part) - part = dpnp.where(nan_mask, nan, part, out=part) - part = dpnp.where(posinf_mask, max_f, part, out=part) - part = dpnp.where(neginf_mask, min_f, part, out=part) + part = dpnp.where(nan_mask, nan, part, out=part_out) + part = dpnp.where(posinf_mask, max_f, part, out=part_out) + part = dpnp.where(neginf_mask, min_f, part, out=part_out) - return x + return out _NEGATIVE_DOCSTRING = """ From 9d2d176fac7c82ac8778e5e50baa771a3cb84b6f Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Tue, 6 Aug 2024 13:30:25 +0200 Subject: [PATCH 08/11] Add checks for nan, posinf, neginf args --- dpnp/dpnp_iface_mathematical.py | 11 +++++++++++ tests/test_mathematical.py | 19 +++++++++++++++++++ .../third_party/cupy/math_tests/test_misc.py | 5 +++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index ee86ea1e2b2..f2e30a192aa 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -2347,6 +2347,9 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): dpnp.check_supported_arrays_type(x) + if not dpnp.isscalar(nan): + raise TypeError(f"nan must be a scalar, but got {type(nan)}") + out = dpnp.empty_like(x) if copy else x x_type = x.dtype.type @@ -2363,8 +2366,16 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): ) max_f, min_f = _get_max_min(x.real.dtype) if posinf is not None: + if not dpnp.isscalar(posinf): + raise TypeError( + f"posinf must be a scalar or None, but got {type(posinf)}" + ) max_f = posinf if neginf is not None: + if not dpnp.isscalar(neginf): + raise TypeError( + f"neginf must be a scalar or None, but got {type(neginf)}" + ) min_f = neginf for part, part_out in zip(parts, parts_out): diff --git a/tests/test_mathematical.py b/tests/test_mathematical.py index fa7041bcc05..01ca67e4680 100644 --- a/tests/test_mathematical.py +++ b/tests/test_mathematical.py @@ -1149,6 +1149,25 @@ def test_boolean_array(self): expected = numpy.nan_to_num(a) assert_allclose(result, expected) + def test_errors(self): + ia = dpnp.array([0, 1, dpnp.nan, dpnp.inf, -dpnp.inf]) + + # unsupported type `a` + a_np = dpnp.asnumpy(ia) + assert_raises(TypeError, dpnp.nan_to_num, a_np) + + # unsupported type `nan` + i_nan = dpnp.array(1) + assert_raises(TypeError, dpnp.nan_to_num, ia, nan=i_nan) + + # unsupported type `posinf` + i_posinf = dpnp.array(1) + assert_raises(TypeError, dpnp.nan_to_num, ia, posinf=i_posinf) + + # unsupported type `neginf` + i_neginf = dpnp.array(1) + assert_raises(TypeError, dpnp.nan_to_num, ia, neginf=i_neginf) + class TestNextafter: @pytest.mark.parametrize("dt", get_float_dtypes()) diff --git a/tests/third_party/cupy/math_tests/test_misc.py b/tests/third_party/cupy/math_tests/test_misc.py index ed897d6b193..04ca2f72105 100644 --- a/tests/third_party/cupy/math_tests/test_misc.py +++ b/tests/third_party/cupy/math_tests/test_misc.py @@ -273,16 +273,17 @@ def test_nan_to_num_inplace(self, xp): assert x is y return y + @pytest.mark.skip(reason="nan, posinf, neginf as array are not supported") @pytest.mark.parametrize("kwarg", ["nan", "posinf", "neginf"]) def test_nan_to_num_broadcast(self, kwarg): for xp in (numpy, cupy): x = xp.asarray([0, 1, xp.nan, 4], dtype=cupy.default_float_type()) y = xp.zeros((2, 4), dtype=cupy.default_float_type()) - with pytest.raises(ValueError): + with pytest.raises(TypeError): xp.nan_to_num(x, **{kwarg: y}) # dpnp.nan_to_num() doesn`t support a scalar as an input # convert 0.0 to 0-ndim array - with pytest.raises(ValueError): + with pytest.raises(TypeError): x_ndim_0 = xp.array(0.0) xp.nan_to_num(x_ndim_0, **{kwarg: y}) From 3d6b82578dc83ea8331182eb81ef279fbe99d133 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Tue, 6 Aug 2024 17:04:08 +0200 Subject: [PATCH 09/11] Add type check for nan, posinf and neginf --- dpnp/dpnp_iface_mathematical.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index f2e30a192aa..06bdd9b4ef4 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -2347,8 +2347,10 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): dpnp.check_supported_arrays_type(x) - if not dpnp.isscalar(nan): - raise TypeError(f"nan must be a scalar, but got {type(nan)}") + if isinstance(nan, bool) or not isinstance(nan, (int, float)): + raise TypeError( + f"nan must be a scalar of an integer or float, but got {type(nan)}" + ) out = dpnp.empty_like(x) if copy else x x_type = x.dtype.type @@ -2366,15 +2368,17 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): ) max_f, min_f = _get_max_min(x.real.dtype) if posinf is not None: - if not dpnp.isscalar(posinf): + if isinstance(posinf, bool) or not isinstance(posinf, (int, float)): raise TypeError( - f"posinf must be a scalar or None, but got {type(posinf)}" + "posinf must be a scalar of an integer or float, or None, " + f"but got {type(posinf)}" ) max_f = posinf if neginf is not None: - if not dpnp.isscalar(neginf): + if isinstance(neginf, bool) or not isinstance(neginf, (int, float)): raise TypeError( - f"neginf must be a scalar or None, but got {type(neginf)}" + "neginf must be a scalar of an integer or float, or None, " + f"but got {type(neginf)}" ) min_f = neginf From 22d6970f7c9ef13cc2bb84ad51d176951e90b327 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Tue, 6 Aug 2024 19:58:54 +0200 Subject: [PATCH 10/11] Update tests --- tests/test_mathematical.py | 7 +++++++ tests/test_sycl_queue.py | 5 +---- tests/test_usm_type.py | 5 +---- tests/third_party/cupy/math_tests/test_misc.py | 5 +---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/test_mathematical.py b/tests/test_mathematical.py index 01ca67e4680..c4bbb30a72f 100644 --- a/tests/test_mathematical.py +++ b/tests/test_mathematical.py @@ -1168,6 +1168,13 @@ def test_errors(self): i_neginf = dpnp.array(1) assert_raises(TypeError, dpnp.nan_to_num, ia, neginf=i_neginf) + @pytest.mark.parametrize("kwarg", ["nan", "posinf", "neginf"]) + @pytest.mark.parametrize("value", [True, 1 - 0j, [1, 2]]) + def test_errors_diff_types(self, kwarg, value): + ia = dpnp.array([0, 1, dpnp.nan, dpnp.inf, -dpnp.inf]) + with pytest.raises(TypeError): + dpnp.nan_to_num(ia, **{kwarg: value}) + class TestNextafter: @pytest.mark.parametrize("dt", get_float_dtypes()) diff --git a/tests/test_sycl_queue.py b/tests/test_sycl_queue.py index 62ea150678f..c21b6817f22 100644 --- a/tests/test_sycl_queue.py +++ b/tests/test_sycl_queue.py @@ -2342,7 +2342,4 @@ def test_nan_to_num(copy, device): result = dpnp.nan_to_num(a, copy=copy) assert_sycl_queue_equal(result.sycl_queue, a.sycl_queue) - if copy: - assert result is not a - else: - assert result is a + assert copy == (result is not a) diff --git a/tests/test_usm_type.py b/tests/test_usm_type.py index 803060ce427..bda4896e278 100644 --- a/tests/test_usm_type.py +++ b/tests/test_usm_type.py @@ -1363,7 +1363,4 @@ def test_nan_to_num(copy, usm_type_a): result = dp.nan_to_num(a, copy=copy) assert result.usm_type == usm_type_a - if copy: - assert result is not a - else: - assert result is a + assert copy == (result is not a) diff --git a/tests/third_party/cupy/math_tests/test_misc.py b/tests/third_party/cupy/math_tests/test_misc.py index 04ca2f72105..e3251d84125 100644 --- a/tests/third_party/cupy/math_tests/test_misc.py +++ b/tests/third_party/cupy/math_tests/test_misc.py @@ -281,11 +281,8 @@ def test_nan_to_num_broadcast(self, kwarg): y = xp.zeros((2, 4), dtype=cupy.default_float_type()) with pytest.raises(TypeError): xp.nan_to_num(x, **{kwarg: y}) - # dpnp.nan_to_num() doesn`t support a scalar as an input - # convert 0.0 to 0-ndim array with pytest.raises(TypeError): - x_ndim_0 = xp.array(0.0) - xp.nan_to_num(x_ndim_0, **{kwarg: y}) + xp.nan_to_num(0.0, **{kwarg: y}) @testing.for_all_dtypes(no_bool=True, no_complex=True) @testing.numpy_cupy_array_equal() From b20ce16ac309d31e279849705e943c8d724d0bfd Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 7 Aug 2024 13:14:16 +0200 Subject: [PATCH 11/11] Add support boolean type --- dpnp/dpnp_iface_mathematical.py | 25 ++++++++++++++----------- tests/test_mathematical.py | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index 06bdd9b4ef4..07285b785df 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -2290,15 +2290,15 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): Whether to create a copy of `x` (``True``) or to replace values in-place (``False``). The in-place operation only occurs if casting to an array does not require a copy. - nan : {int, float}, optional + nan : {int, float, bool}, optional Value to be used to fill ``NaN`` values. Default: ``0.0``. - posinf : {int, float, None}, optional + posinf : {int, float, bool, None}, optional Value to be used to fill positive infinity values. If no value is passed then positive infinity values will be replaced with a very large number. Default: ``None``. - neginf : {int, float, None} optional + neginf : {int, float, bool, None} optional Value to be used to fill negative infinity values. If no value is passed then negative infinity values will be replaced with a very small (or negative) number. @@ -2347,9 +2347,12 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): dpnp.check_supported_arrays_type(x) - if isinstance(nan, bool) or not isinstance(nan, (int, float)): + # Python boolean is a subtype of an integer + # so additional check for bool is not needed. + if not isinstance(nan, (int, float)): raise TypeError( - f"nan must be a scalar of an integer or float, but got {type(nan)}" + "nan must be a scalar of an integer, float, bool, " + f"but got {type(nan)}" ) out = dpnp.empty_like(x) if copy else x @@ -2368,17 +2371,17 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): ) max_f, min_f = _get_max_min(x.real.dtype) if posinf is not None: - if isinstance(posinf, bool) or not isinstance(posinf, (int, float)): + if not isinstance(posinf, (int, float)): raise TypeError( - "posinf must be a scalar of an integer or float, or None, " - f"but got {type(posinf)}" + "posinf must be a scalar of an integer, float, bool, " + f"or be None, but got {type(posinf)}" ) max_f = posinf if neginf is not None: - if isinstance(neginf, bool) or not isinstance(neginf, (int, float)): + if not isinstance(neginf, (int, float)): raise TypeError( - "neginf must be a scalar of an integer or float, or None, " - f"but got {type(neginf)}" + "neginf must be a scalar of an integer, float, bool, " + f"or be None, but got {type(neginf)}" ) min_f = neginf diff --git a/tests/test_mathematical.py b/tests/test_mathematical.py index c4bbb30a72f..6ae071ab394 100644 --- a/tests/test_mathematical.py +++ b/tests/test_mathematical.py @@ -1169,7 +1169,7 @@ def test_errors(self): assert_raises(TypeError, dpnp.nan_to_num, ia, neginf=i_neginf) @pytest.mark.parametrize("kwarg", ["nan", "posinf", "neginf"]) - @pytest.mark.parametrize("value", [True, 1 - 0j, [1, 2]]) + @pytest.mark.parametrize("value", [1 - 0j, [1, 2], (1,)]) def test_errors_diff_types(self, kwarg, value): ia = dpnp.array([0, 1, dpnp.nan, dpnp.inf, -dpnp.inf]) with pytest.raises(TypeError):