From 4e36e7300992758c476f98b5839ceb1bb12632ed Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 2 Oct 2026 15:08:53 +0200 Subject: [PATCH 1/2] Document and make explicit `copy=None` support in `dpnp.nan_to_num` Align the `copy` keyword of `dpnp.nan_to_num` with NumPy by documenting and explicitly handling `copy=None` ("copy only if needed"), which for supported array inputs replaces values in-place. Clarify the docstring to describe the actual precondition (a writable array), and add tests covering `copy=None` for in-place replacement and the read-only error path. --- CHANGELOG.md | 1 + dpnp/dpnp_iface_mathematical.py | 17 +++++++++-------- dpnp/tests/test_mathematical.py | 17 ++++++++++++++--- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b27b00fdc4..7222877dbff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ This release is compatible with NumPy 2.5. * Avoided a copy of `dpnp.einsum` result into C-order by building the product in the requested layout directly [#3069](https://github.com/IntelPython/dpnp/pull/3069) * Updated the implementation of `dpnp.putmask` by adding dedicated contiguous and strided SYCL kernels [#3014](https://github.com/IntelPython/dpnp/pull/3014) * Changed `dpnp.sort`, `dpnp.argsort`, and their `dpnp.ndarray`/`dpnp.tensor` counterparts to place `NaN` values last instead of first when sorting in descending order [#3066](https://github.com/IntelPython/dpnp/pull/3066) +* Documented and made explicit support for `copy=None` ("copy only if needed") in `dpnp.nan_to_num` [#3083](https://github.com/IntelPython/dpnp/pull/3083) ### Deprecated diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index 7b7ffba9dbfa..6a08300e0850 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -3605,10 +3605,10 @@ def nan_to_num(x, copy=True, nan=0.0, posinf=None, neginf=None): ---------- x : {dpnp.ndarray, usm_ndarray} Input data. - copy : bool, optional + copy : {None, 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. + in-place (``False`` or ``None``). In-place operation requires `x` to + be writable; a ``ValueError`` is raised otherwise. Default: ``True``. nan : {scalar, array_like}, optional @@ -3635,8 +3635,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`` or + ``None``, this may be `x` itself. See Also -------- @@ -3713,10 +3713,11 @@ def _check_nan_inf(val, val_dt): if copy: out = dpnp.empty_like(x) - else: - if not x.flags.writable: - raise ValueError("copy is required for read-only array `x`") + elif x.flags.writable: + # copy=False or None (copy-if-needed): replace values in-place out = x + else: + raise ValueError("copy is required for read-only array `x`") # handle a special case when nan and infs are all scalars if all(dpnp.isscalar(el) for el in (nan, max_f, min_f)): diff --git a/dpnp/tests/test_mathematical.py b/dpnp/tests/test_mathematical.py index ff74cfbfdb38..e3c218b08dbc 100644 --- a/dpnp/tests/test_mathematical.py +++ b/dpnp/tests/test_mathematical.py @@ -1491,13 +1491,24 @@ def test_numpy_input_array(self): dpnp.nan_to_num(a) @pytest.mark.parametrize("xp", [dpnp, numpy]) - def test_error_readonly(self, xp): + @pytest.mark.parametrize("copy", [False, None]) + def test_error_readonly(self, xp, copy): a = xp.array([0, 1, xp.nan, xp.inf, -xp.inf]) a.flags["W"] = False with pytest.raises(ValueError, match="read-only"): - xp.nan_to_num(a, copy=False) + xp.nan_to_num(a, copy=copy) - @pytest.mark.parametrize("copy", [True, False]) + @pytest.mark.parametrize("copy", [False, None]) + def test_copy_in_place(self, copy): + a = numpy.array([0, 1, numpy.nan, numpy.inf, -numpy.inf]) + ia = dpnp.array(a) + + result = dpnp.nan_to_num(ia, copy=copy) + expected = numpy.nan_to_num(a, copy=copy) + assert result is ia + assert_allclose(result, expected) + + @pytest.mark.parametrize("copy", [True, False, None]) @pytest.mark.parametrize("dt", get_all_dtypes(no_bool=True, no_none=True)) def test_strided(self, copy, dt): n = 10 From d3cf74640d1e9ee8820ddf37562a8cb2c1573615 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 2 Oct 2026 16:01:50 +0200 Subject: [PATCH 2/2] Make `nan_to_num` copy test device-agnostic for non-fp64 devices Parametrize `test_copy_in_place` over the device-adaptive float/complex dtype list and build the NumPy input with that dtype, so the comparison stays dtype-consistent on devices without fp64 support (where a float64 input is downcast to float32). --- dpnp/tests/test_mathematical.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dpnp/tests/test_mathematical.py b/dpnp/tests/test_mathematical.py index e3c218b08dbc..b25cb2d84ce1 100644 --- a/dpnp/tests/test_mathematical.py +++ b/dpnp/tests/test_mathematical.py @@ -1499,8 +1499,9 @@ def test_error_readonly(self, xp, copy): xp.nan_to_num(a, copy=copy) @pytest.mark.parametrize("copy", [False, None]) - def test_copy_in_place(self, copy): - a = numpy.array([0, 1, numpy.nan, numpy.inf, -numpy.inf]) + @pytest.mark.parametrize("dt", get_float_complex_dtypes()) + def test_copy_in_place(self, copy, dt): + a = numpy.array([0, 1, numpy.nan, numpy.inf, -numpy.inf], dtype=dt) ia = dpnp.array(a) result = dpnp.nan_to_num(ia, copy=copy)