Skip to content

Commit 5a368d9

Browse files
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
1 parent d2ef11a commit 5a368d9

File tree

3 files changed

+37
-16
lines changed

3 files changed

+37
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ In addition, this release completes implementation of `dpnp.fft` module and adds
115115
* 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)
116116
* Improved implementation of `dpnp.kron` to avoid unnecessary copy for non-contiguous arrays [#2059](https://github.com/IntelPython/dpnp/pull/2059)
117117
* Updated the test suit for `dpnp.fft` module [#2071](https://github.com/IntelPython/dpnp/pull/2071)
118+
* Reworked `dpnp.clip` implementation to align with Python Array API 2023.12 specification [#2048](https://github.com/IntelPython/dpnp/pull/2048)
118119

119120
### Fixed
120121

dpnp/dpnp_iface_mathematical.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ def around(x, /, decimals=0, out=None):
627627
)
628628

629629

630-
def clip(a, a_min, a_max, *, out=None, order="K", **kwargs):
630+
def clip(a, /, min=None, max=None, *, out=None, order="K", **kwargs):
631631
"""
632632
Clip (limit) the values in an array.
633633
@@ -637,23 +637,27 @@ def clip(a, a_min, a_max, *, out=None, order="K", **kwargs):
637637
----------
638638
a : {dpnp.ndarray, usm_ndarray}
639639
Array containing elements to clip.
640-
a_min, a_max : {dpnp.ndarray, usm_ndarray, None}
640+
min, max : {dpnp.ndarray, usm_ndarray, None}
641641
Minimum and maximum value. If ``None``, clipping is not performed on
642-
the corresponding edge. Only one of `a_min` and `a_max` may be
643-
``None``. Both are broadcast against `a`.
642+
the corresponding edge. If both `min` and `max` are ``None``,
643+
the elements of the returned array stay the same.
644+
Both are broadcast against `a`.
645+
Default : ``None``.
644646
out : {None, dpnp.ndarray, usm_ndarray}, optional
645647
The results will be placed in this array. It may be the input array
646648
for in-place clipping. `out` must be of the right shape to hold the
647649
output. Its type is preserved.
650+
Default : ``None``.
648651
order : {"C", "F", "A", "K", None}, optional
649-
Memory layout of the newly output array, if parameter `out` is `None`.
652+
Memory layout of the newly output array, if parameter `out` is ``None``.
650653
If `order` is ``None``, the default value ``"K"`` will be used.
654+
Default: ``"K"``.
651655
652656
Returns
653657
-------
654658
out : dpnp.ndarray
655-
An array with the elements of `a`, but where values < `a_min` are
656-
replaced with `a_min`, and those > `a_max` with `a_max`.
659+
An array with the elements of `a`, but where values < `min` are
660+
replaced with `min`, and those > `max` with `max`.
657661
658662
Limitations
659663
-----------
@@ -687,15 +691,12 @@ def clip(a, a_min, a_max, *, out=None, order="K", **kwargs):
687691
if kwargs:
688692
raise NotImplementedError(f"kwargs={kwargs} is currently not supported")
689693

690-
if a_min is None and a_max is None:
691-
raise ValueError("One of max or min must be given")
692-
693694
if order is None:
694695
order = "K"
695696

696697
usm_arr = dpnp.get_usm_ndarray(a)
697-
usm_min = None if a_min is None else dpnp.get_usm_ndarray_or_scalar(a_min)
698-
usm_max = None if a_max is None else dpnp.get_usm_ndarray_or_scalar(a_max)
698+
usm_min = None if min is None else dpnp.get_usm_ndarray_or_scalar(min)
699+
usm_max = None if max is None else dpnp.get_usm_ndarray_or_scalar(max)
699700

700701
usm_out = None if out is None else dpnp.get_usm_ndarray(out)
701702
usm_res = dpt.clip(usm_arr, usm_min, usm_max, out=usm_out, order=order)

tests/third_party/cupy/math_tests/test_misc.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,17 @@ def test_clip_max_none(self, xp, dtype):
130130
def test_clip_min_max_none(self, dtype):
131131
for xp in (numpy, cupy):
132132
a = testing.shaped_arange((2, 3, 4), xp, dtype)
133-
with pytest.raises(ValueError):
134-
a.clip(None, None)
133+
# According to Python Array API, clip() should return an array
134+
# with the same elements in `a` if `min` and `max` are `None`.
135+
# Numpy < 2.1 is not compatible with this and raises a ValueError
136+
if (
137+
xp is numpy
138+
and numpy.lib.NumpyVersion(numpy.__version__) < "2.1.0"
139+
):
140+
with pytest.raises(ValueError):
141+
a.clip(None, None)
142+
else:
143+
return a.clip(None, None)
135144

136145
@testing.for_all_dtypes(no_complex=True)
137146
@testing.numpy_cupy_array_equal()
@@ -155,8 +164,18 @@ def test_external_clip3(self, xp, dtype):
155164
def test_external_clip4(self, dtype):
156165
for xp in (numpy, cupy):
157166
a = testing.shaped_arange((2, 3, 4), xp, dtype)
158-
with pytest.raises(TypeError):
159-
xp.clip(a, 3)
167+
# Starting with numpy 2.1.0, it's possible to pass only one argument
168+
# (min or max) as a keyword argument according to Python Array API.
169+
# In older versions of numpy, both arguments must be positional;
170+
# passing only one raises a TypeError.
171+
if (
172+
xp is numpy
173+
and numpy.lib.NumpyVersion(numpy.__version__) < "2.1.0"
174+
):
175+
with pytest.raises(TypeError):
176+
xp.clip(a, 3)
177+
else:
178+
return xp.clip(a, min=3)
160179

161180
@testing.for_all_dtypes(no_complex=True)
162181
@testing.numpy_cupy_array_equal()

0 commit comments

Comments
 (0)