From d59344c40ccefdea070188de802f4310710c63e9 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Thu, 27 Feb 2025 17:08:11 +0000 Subject: [PATCH 1/7] BUG: `clip()` should not have `out=` parameter --- array_api_compat/common/_aliases.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/array_api_compat/common/_aliases.py b/array_api_compat/common/_aliases.py index 98b8e425..4bdc1ef9 100644 --- a/array_api_compat/common/_aliases.py +++ b/array_api_compat/common/_aliases.py @@ -331,8 +331,6 @@ def clip( max: Optional[Union[int, float, ndarray]] = None, *, xp, - # TODO: np.clip has other ufunc kwargs - out: Optional[ndarray] = None, ) -> ndarray: def _isscalar(a): return isinstance(a, (int, float, type(None))) @@ -368,9 +366,9 @@ def _isscalar(a): if type(max) is int and max >= wrapped_xp.iinfo(x.dtype).max: max = None - if out is None: - out = wrapped_xp.asarray(xp.broadcast_to(x, result_shape), - copy=True, device=device(x)) + out = wrapped_xp.asarray(xp.broadcast_to(x, result_shape), + copy=True, device=device(x)) + if min is not None: if is_torch_array(x) and x.dtype == xp.float64 and _isscalar(min): # Avoid loss of precision due to torch defaulting to float32 From b940c19fa780ade143da4b6f0b602fb614dcaa91 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Tue, 4 Mar 2025 17:49:40 +0000 Subject: [PATCH 2/7] Revert "BUG: `clip()` should not have `out=` parameter" This reverts commit d59344c40ccefdea070188de802f4310710c63e9. --- array_api_compat/common/_aliases.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/array_api_compat/common/_aliases.py b/array_api_compat/common/_aliases.py index 4bdc1ef9..98b8e425 100644 --- a/array_api_compat/common/_aliases.py +++ b/array_api_compat/common/_aliases.py @@ -331,6 +331,8 @@ def clip( max: Optional[Union[int, float, ndarray]] = None, *, xp, + # TODO: np.clip has other ufunc kwargs + out: Optional[ndarray] = None, ) -> ndarray: def _isscalar(a): return isinstance(a, (int, float, type(None))) @@ -366,9 +368,9 @@ def _isscalar(a): if type(max) is int and max >= wrapped_xp.iinfo(x.dtype).max: max = None - out = wrapped_xp.asarray(xp.broadcast_to(x, result_shape), - copy=True, device=device(x)) - + if out is None: + out = wrapped_xp.asarray(xp.broadcast_to(x, result_shape), + copy=True, device=device(x)) if min is not None: if is_torch_array(x) and x.dtype == xp.float64 and _isscalar(min): # Avoid loss of precision due to torch defaulting to float32 From df226fe5140843172e4af24fa778ec92a72b2ae0 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Tue, 4 Mar 2025 18:25:21 +0000 Subject: [PATCH 3/7] Fix and test out= --- array_api_compat/common/_aliases.py | 23 ++++++++++++----------- tests/test_common.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/array_api_compat/common/_aliases.py b/array_api_compat/common/_aliases.py index 98b8e425..628ca435 100644 --- a/array_api_compat/common/_aliases.py +++ b/array_api_compat/common/_aliases.py @@ -368,23 +368,24 @@ def _isscalar(a): if type(max) is int and max >= wrapped_xp.iinfo(x.dtype).max: max = None + dev = device(x) if out is None: - out = wrapped_xp.asarray(xp.broadcast_to(x, result_shape), - copy=True, device=device(x)) + out = wrapped_xp.empty(result_shape, dtype=x.dtype, device=dev) + out[()] = x + if min is not None: - if is_torch_array(x) and x.dtype == xp.float64 and _isscalar(min): - # Avoid loss of precision due to torch defaulting to float32 - min = wrapped_xp.asarray(min, dtype=xp.float64) - a = xp.broadcast_to(wrapped_xp.asarray(min, device=device(x)), result_shape) + a = wrapped_xp.asarray(min, dtype=x.dtype, device=dev) + a = xp.broadcast_to(a, result_shape) ia = (out < a) | xp.isnan(a) # torch requires an explicit cast here - out[ia] = wrapped_xp.astype(a[ia], out.dtype) + out[ia] = a[ia] + if max is not None: - if is_torch_array(x) and x.dtype == xp.float64 and _isscalar(max): - max = wrapped_xp.asarray(max, dtype=xp.float64) - b = xp.broadcast_to(wrapped_xp.asarray(max, device=device(x)), result_shape) + b = wrapped_xp.asarray(max, dtype=x.dtype, device=dev) + b = xp.broadcast_to(b, result_shape) ib = (out > b) | xp.isnan(b) - out[ib] = wrapped_xp.astype(b[ib], out.dtype) + out[ib] = b[ib] + # Return a scalar for 0-D return out[()] diff --git a/tests/test_common.py b/tests/test_common.py index 32876e69..f86e0936 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -367,3 +367,18 @@ def test_asarray_copy(library): assert all(b[0] == 1.0) else: assert all(b[0] == 0.0) + + +@pytest.mark.parametrize("library", ["numpy", "cupy", "torch"]) +def test_clip_out(library): + """Test non-standard out= parameter for clip() + + (see "Avoid Restricting Behavior that is Outside the Scope of the Standard" + in https://data-apis.org/array-api-compat/dev/special-considerations.html) + """ + xp = import_(library, wrapper=True) + x = xp.asarray([10, 20, 30]) + out = xp.zeros_like(x) + xp.clip(x, 15, 25, out=out) + expect = xp.asarray([15, 20, 25]) + assert xp.all(out == expect) From ed83b80deb12711b15d27a98b4b2f3ebcfcc01f8 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Tue, 4 Mar 2025 18:27:08 +0000 Subject: [PATCH 4/7] lint --- array_api_compat/common/_aliases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/array_api_compat/common/_aliases.py b/array_api_compat/common/_aliases.py index 628ca435..cfffbb54 100644 --- a/array_api_compat/common/_aliases.py +++ b/array_api_compat/common/_aliases.py @@ -12,7 +12,7 @@ from typing import NamedTuple import inspect -from ._helpers import array_namespace, _check_device, device, is_torch_array, is_cupy_namespace +from ._helpers import array_namespace, _check_device, device, is_cupy_namespace # These functions are modified from the NumPy versions. From fa240215fd4cadba3b2d32baddaf1140c62294e9 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Tue, 4 Mar 2025 18:37:28 +0000 Subject: [PATCH 5/7] dead comment --- array_api_compat/common/_aliases.py | 1 - 1 file changed, 1 deletion(-) diff --git a/array_api_compat/common/_aliases.py b/array_api_compat/common/_aliases.py index cfffbb54..d7e8ef2d 100644 --- a/array_api_compat/common/_aliases.py +++ b/array_api_compat/common/_aliases.py @@ -377,7 +377,6 @@ def _isscalar(a): a = wrapped_xp.asarray(min, dtype=x.dtype, device=dev) a = xp.broadcast_to(a, result_shape) ia = (out < a) | xp.isnan(a) - # torch requires an explicit cast here out[ia] = a[ia] if max is not None: From 94be1c89843ee2a7e31ef8470892b0c16e9e96e4 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Tue, 4 Mar 2025 18:50:56 +0000 Subject: [PATCH 6/7] Speed up --- array_api_compat/common/_aliases.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/array_api_compat/common/_aliases.py b/array_api_compat/common/_aliases.py index d7e8ef2d..be4ea506 100644 --- a/array_api_compat/common/_aliases.py +++ b/array_api_compat/common/_aliases.py @@ -376,13 +376,13 @@ def _isscalar(a): if min is not None: a = wrapped_xp.asarray(min, dtype=x.dtype, device=dev) a = xp.broadcast_to(a, result_shape) - ia = (out < a) | xp.isnan(a) + ia = ~(out >= a) # Propagate NaNs out[ia] = a[ia] if max is not None: b = wrapped_xp.asarray(max, dtype=x.dtype, device=dev) b = xp.broadcast_to(b, result_shape) - ib = (out > b) | xp.isnan(b) + ib = ~(out <= b) # Propagate NaNs out[ib] = b[ib] # Return a scalar for 0-D From 030f670a51f0dc34d746274956c4eb99434e616a Mon Sep 17 00:00:00 2001 From: crusaderky Date: Tue, 4 Mar 2025 18:54:24 +0000 Subject: [PATCH 7/7] Revert "Speed up" This reverts commit 94be1c89843ee2a7e31ef8470892b0c16e9e96e4. --- array_api_compat/common/_aliases.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/array_api_compat/common/_aliases.py b/array_api_compat/common/_aliases.py index be4ea506..d7e8ef2d 100644 --- a/array_api_compat/common/_aliases.py +++ b/array_api_compat/common/_aliases.py @@ -376,13 +376,13 @@ def _isscalar(a): if min is not None: a = wrapped_xp.asarray(min, dtype=x.dtype, device=dev) a = xp.broadcast_to(a, result_shape) - ia = ~(out >= a) # Propagate NaNs + ia = (out < a) | xp.isnan(a) out[ia] = a[ia] if max is not None: b = wrapped_xp.asarray(max, dtype=x.dtype, device=dev) b = xp.broadcast_to(b, result_shape) - ib = ~(out <= b) # Propagate NaNs + ib = (out > b) | xp.isnan(b) out[ib] = b[ib] # Return a scalar for 0-D