From d72d63a62614c239d5f0e5ac7f15c8aedb19f7d5 Mon Sep 17 00:00:00 2001 From: vlad-perevezentsev Date: Fri, 27 Sep 2024 22:34:40 +0200 Subject: [PATCH] Backport gh-2048 & gh-2074 (#2076) * Update `dpnp.clip()` with Numpy 2.0 (#2048) * Update dpnp.clip to align with numpy 2.0 * Update cupy tests * Compliance of dpnp.clip() with Array API * Update CHANGELOG.md * Update cupy tests for dpnp.linalg.solve() (#2074) * Update cupy tests for dpnp.linalg.solve() * Update CHANGELOG.md --- CHANGELOG.md | 2 ++ dpnp/dpnp_iface_mathematical.py | 25 ++++++++--------- .../cupy/linalg_tests/test_solve.py | 16 ++++++++--- .../third_party/cupy/math_tests/test_misc.py | 27 ++++++++++++++++--- 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1115e80b324..0e0fe85f570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,8 @@ In addition, this release completes implementation of `dpnp.fft` module and adds * Use `dpctl::tensor::alloc_utils::sycl_free_noexcept` instead of `sycl::free` in `host_task` tasks associated with life-time management of temporary USM allocations [#2058](https://github.com/IntelPython/dpnp/pull/2058) * Improved implementation of `dpnp.kron` to avoid unnecessary copy for non-contiguous arrays [#2059](https://github.com/IntelPython/dpnp/pull/2059) * Updated the test suit for `dpnp.fft` module [#2071](https://github.com/IntelPython/dpnp/pull/2071) +* Reworked `dpnp.clip` implementation to align with Python Array API 2023.12 specification [#2048](https://github.com/IntelPython/dpnp/pull/2048) +* Skipped outdated tests for `dpnp.linalg.solve` due to compatibility issues with NumPy 2.0 [#2074](https://github.com/IntelPython/dpnp/pull/2074) ### Fixed diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index 443ad7c43da..050f229e8a7 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -627,7 +627,7 @@ def around(x, /, decimals=0, out=None): ) -def clip(a, a_min, a_max, *, out=None, order="K", **kwargs): +def clip(a, /, min=None, max=None, *, out=None, order="K", **kwargs): """ Clip (limit) the values in an array. @@ -637,23 +637,27 @@ def clip(a, a_min, a_max, *, out=None, order="K", **kwargs): ---------- a : {dpnp.ndarray, usm_ndarray} Array containing elements to clip. - a_min, a_max : {dpnp.ndarray, usm_ndarray, None} + min, max : {dpnp.ndarray, usm_ndarray, None} Minimum and maximum value. If ``None``, clipping is not performed on - the corresponding edge. Only one of `a_min` and `a_max` may be - ``None``. Both are broadcast against `a`. + the corresponding edge. If both `min` and `max` are ``None``, + the elements of the returned array stay the same. + Both are broadcast against `a`. + Default : ``None``. out : {None, dpnp.ndarray, usm_ndarray}, optional The results will be placed in this array. It may be the input array for in-place clipping. `out` must be of the right shape to hold the output. Its type is preserved. + Default : ``None``. order : {"C", "F", "A", "K", None}, optional - Memory layout of the newly output array, if parameter `out` is `None`. + Memory layout of the newly output array, if parameter `out` is ``None``. If `order` is ``None``, the default value ``"K"`` will be used. + Default: ``"K"``. Returns ------- out : dpnp.ndarray - An array with the elements of `a`, but where values < `a_min` are - replaced with `a_min`, and those > `a_max` with `a_max`. + An array with the elements of `a`, but where values < `min` are + replaced with `min`, and those > `max` with `max`. Limitations ----------- @@ -687,15 +691,12 @@ def clip(a, a_min, a_max, *, out=None, order="K", **kwargs): if kwargs: raise NotImplementedError(f"kwargs={kwargs} is currently not supported") - if a_min is None and a_max is None: - raise ValueError("One of max or min must be given") - if order is None: order = "K" usm_arr = dpnp.get_usm_ndarray(a) - usm_min = None if a_min is None else dpnp.get_usm_ndarray_or_scalar(a_min) - usm_max = None if a_max is None else dpnp.get_usm_ndarray_or_scalar(a_max) + usm_min = None if min is None else dpnp.get_usm_ndarray_or_scalar(min) + usm_max = None if max is None else dpnp.get_usm_ndarray_or_scalar(max) usm_out = None if out is None else dpnp.get_usm_ndarray(out) usm_res = dpt.clip(usm_arr, usm_min, usm_max, out=usm_out, order=order) diff --git a/tests/third_party/cupy/linalg_tests/test_solve.py b/tests/third_party/cupy/linalg_tests/test_solve.py index dd15e8303af..d7204f7d4c1 100644 --- a/tests/third_party/cupy/linalg_tests/test_solve.py +++ b/tests/third_party/cupy/linalg_tests/test_solve.py @@ -50,14 +50,20 @@ def check_x(self, a_shape, b_shape, xp, dtype): def test_solve(self): self.check_x((4, 4), (4,)) self.check_x((5, 5), (5, 2)) - self.check_x((2, 4, 4), (2, 4)) self.check_x((2, 5, 5), (2, 5, 2)) - self.check_x((2, 3, 2, 2), (2, 3, 2)) self.check_x((2, 3, 3, 3), (2, 3, 3, 2)) self.check_x((0, 0), (0,)) self.check_x((0, 0), (0, 2)) - self.check_x((0, 2, 2), (0, 2)) self.check_x((0, 2, 2), (0, 2, 3)) + # In numpy 2.0 the broadcast ambiguity has been removed and now + # b is treaded as a single vector if and only if it is 1-dimensional; + # for other cases this signature must be followed + # (..., m, m), (..., m, n) -> (..., m, n) + # https://github.com/numpy/numpy/pull/25914 + if numpy.lib.NumpyVersion(numpy.__version__) < "2.0.0": + self.check_x((2, 4, 4), (2, 4)) + self.check_x((2, 3, 2, 2), (2, 3, 2)) + self.check_x((0, 2, 2), (0, 2)) def check_shape(self, a_shape, b_shape, error_types): for xp, error_type in error_types.items(): @@ -90,7 +96,9 @@ def test_invalid_shape(self): self.check_shape((3, 3), (2,), value_errors) self.check_shape((3, 3), (2, 2), value_errors) self.check_shape((3, 3, 4), (3,), linalg_errors) - self.check_shape((2, 3, 3), (3,), value_errors) + # Since numpy >= 2.0, this case does not raise an error + if numpy.lib.NumpyVersion(numpy.__version__) < "2.0.0": + self.check_shape((2, 3, 3), (3,), value_errors) self.check_shape((3, 3), (0,), value_errors) self.check_shape((0, 3, 4), (3,), linalg_errors) diff --git a/tests/third_party/cupy/math_tests/test_misc.py b/tests/third_party/cupy/math_tests/test_misc.py index 086f7da1728..05254f71287 100644 --- a/tests/third_party/cupy/math_tests/test_misc.py +++ b/tests/third_party/cupy/math_tests/test_misc.py @@ -130,8 +130,17 @@ def test_clip_max_none(self, xp, dtype): def test_clip_min_max_none(self, dtype): for xp in (numpy, cupy): a = testing.shaped_arange((2, 3, 4), xp, dtype) - with pytest.raises(ValueError): - a.clip(None, None) + # According to Python Array API, clip() should return an array + # with the same elements in `a` if `min` and `max` are `None`. + # Numpy < 2.1 is not compatible with this and raises a ValueError + if ( + xp is numpy + and numpy.lib.NumpyVersion(numpy.__version__) < "2.1.0" + ): + with pytest.raises(ValueError): + a.clip(None, None) + else: + return a.clip(None, None) @testing.for_all_dtypes(no_complex=True) @testing.numpy_cupy_array_equal() @@ -155,8 +164,18 @@ def test_external_clip3(self, xp, dtype): def test_external_clip4(self, dtype): for xp in (numpy, cupy): a = testing.shaped_arange((2, 3, 4), xp, dtype) - with pytest.raises(TypeError): - xp.clip(a, 3) + # Starting with numpy 2.1.0, it's possible to pass only one argument + # (min or max) as a keyword argument according to Python Array API. + # In older versions of numpy, both arguments must be positional; + # passing only one raises a TypeError. + if ( + xp is numpy + and numpy.lib.NumpyVersion(numpy.__version__) < "2.1.0" + ): + with pytest.raises(TypeError): + xp.clip(a, 3) + else: + return xp.clip(a, min=3) @testing.for_all_dtypes(no_complex=True) @testing.numpy_cupy_array_equal()