From b83db1968cba98f49e6365b169b7aa4f01119ebd Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Tue, 25 Jun 2024 10:57:11 -0600 Subject: [PATCH 01/14] Add a reference implementation for signbit --- array_api_tests/test_operators_and_elementwise_functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/array_api_tests/test_operators_and_elementwise_functions.py b/array_api_tests/test_operators_and_elementwise_functions.py index 4c019a2d..ab0afe6f 100644 --- a/array_api_tests/test_operators_and_elementwise_functions.py +++ b/array_api_tests/test_operators_and_elementwise_functions.py @@ -1535,7 +1535,8 @@ def test_signbit(x): out = xp.signbit(x) ph.assert_dtype("signbit", in_dtype=x.dtype, out_dtype=out.dtype, expected=xp.bool) ph.assert_shape("signbit", out_shape=out.shape, expected=x.shape) - # TODO: values testing + refimpl = lambda x: math.copysign(1.0, x) < 0 + unary_assert_against_refimpl("round", x, out, refimpl, strict_check=True) @given(hh.arrays(dtype=hh.numeric_dtypes, shape=hh.shapes(), elements=finite_kw)) From 3e39446adff43ea8978b40b37c2c1061f1694ea7 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Tue, 25 Jun 2024 10:58:57 -0600 Subject: [PATCH 02/14] Remove completed TODO comment --- array_api_tests/test_operators_and_elementwise_functions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/array_api_tests/test_operators_and_elementwise_functions.py b/array_api_tests/test_operators_and_elementwise_functions.py index ab0afe6f..e7fe2f8a 100644 --- a/array_api_tests/test_operators_and_elementwise_functions.py +++ b/array_api_tests/test_operators_and_elementwise_functions.py @@ -929,8 +929,6 @@ def test_ceil(x): @pytest.mark.min_version("2023.12") @given(x=hh.arrays(dtype=hh.real_dtypes, shape=hh.shapes()), data=st.data()) def test_clip(x, data): - # TODO: test min/max kwargs, adjust values testing accordingly - # Ensure that if both min and max are arrays that all three of x, min, max # are broadcast compatible. shape1, shape2 = data.draw(hh.mutually_broadcastable_shapes(2, From f59e61a551e39c111dc8fb522381e40720d153b3 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Tue, 25 Jun 2024 10:59:38 -0600 Subject: [PATCH 03/14] Add a reference implementation for copysign --- array_api_tests/test_operators_and_elementwise_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/array_api_tests/test_operators_and_elementwise_functions.py b/array_api_tests/test_operators_and_elementwise_functions.py index e7fe2f8a..6808cf7b 100644 --- a/array_api_tests/test_operators_and_elementwise_functions.py +++ b/array_api_tests/test_operators_and_elementwise_functions.py @@ -1060,7 +1060,7 @@ def test_copysign(x1, x2): out = xp.copysign(x1, x2) ph.assert_dtype("copysign", in_dtype=[x1.dtype, x2.dtype], out_dtype=out.dtype) ph.assert_result_shape("copysign", in_shapes=[x1.shape, x2.shape], out_shape=out.shape) - # TODO: values testing + binary_assert_against_refimpl("copysign", x1, x2, out, math.copysign) @given(hh.arrays(dtype=hh.all_floating_dtypes(), shape=hh.shapes())) From 63a64ab1a104e0f05267df3173ec09bea5f2670a Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 26 Jun 2024 12:20:12 -0600 Subject: [PATCH 04/14] Use American spelling --- array_api_tests/shape_helpers.py | 4 ++-- array_api_tests/test_array_object.py | 10 +++++----- array_api_tests/test_fft.py | 6 +++--- array_api_tests/test_linalg.py | 2 +- array_api_tests/test_manipulation_functions.py | 6 +++--- array_api_tests/test_searching_functions.py | 4 ++-- array_api_tests/test_sorting_functions.py | 4 ++-- array_api_tests/test_statistical_functions.py | 16 ++++++++-------- array_api_tests/test_utility_functions.py | 4 ++-- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/array_api_tests/shape_helpers.py b/array_api_tests/shape_helpers.py index 6a0bdfde..52c6f3fc 100644 --- a/array_api_tests/shape_helpers.py +++ b/array_api_tests/shape_helpers.py @@ -8,7 +8,7 @@ __all__ = [ "broadcast_shapes", - "normalise_axis", + "normalize_axis", "ndindex", "axis_ndindex", "axes_ndindex", @@ -65,7 +65,7 @@ def broadcast_shapes(*shapes: Shape): return result -def normalise_axis( +def normalize_axis( axis: Optional[Union[int, Sequence[int]]], ndim: int ) -> Tuple[int, ...]: if axis is None: diff --git a/array_api_tests/test_array_object.py b/array_api_tests/test_array_object.py index 3ea7c40b..3e120e7e 100644 --- a/array_api_tests/test_array_object.py +++ b/array_api_tests/test_array_object.py @@ -26,9 +26,9 @@ def scalar_objects( ) -def normalise_key(key: Index, shape: Shape) -> Tuple[Union[int, slice], ...]: +def normalize_key(key: Index, shape: Shape) -> Tuple[Union[int, slice], ...]: """ - Normalise an indexing key. + Normalize an indexing key. * If a non-tuple index, wrap as a tuple. * Represent ellipsis as equivalent slices. @@ -48,7 +48,7 @@ def get_indexed_axes_and_out_shape( key: Tuple[Union[int, slice, None], ...], shape: Shape ) -> Tuple[Tuple[Sequence[int], ...], Shape]: """ - From the (normalised) key and input shape, calculates: + From the (normalized) key and input shape, calculates: * indexed_axes: For each dimension, the axes which the key indexes. * out_shape: The resulting shape of indexing an array (of the input shape) @@ -88,7 +88,7 @@ def test_getitem(shape, dtype, data): out = x[key] ph.assert_dtype("__getitem__", in_dtype=x.dtype, out_dtype=out.dtype) - _key = normalise_key(key, shape) + _key = normalize_key(key, shape) axes_indices, expected_shape = get_indexed_axes_and_out_shape(_key, shape) ph.assert_shape("__getitem__", out_shape=out.shape, expected=expected_shape) out_zero_sided = any(side == 0 for side in expected_shape) @@ -119,7 +119,7 @@ def test_setitem(shape, dtypes, data): x = xp.asarray(obj, dtype=dtypes.result_dtype) note(f"{x=}") key = data.draw(xps.indices(shape=shape), label="key") - _key = normalise_key(key, shape) + _key = normalize_key(key, shape) axes_indices, out_shape = get_indexed_axes_and_out_shape(_key, shape) value_strat = hh.arrays(dtype=dtypes.result_dtype, shape=out_shape) if out_shape == (): diff --git a/array_api_tests/test_fft.py b/array_api_tests/test_fft.py index ff71433d..582ff5ee 100644 --- a/array_api_tests/test_fft.py +++ b/array_api_tests/test_fft.py @@ -94,7 +94,7 @@ def assert_s_axes_shape( axes: Optional[List[int]], out: Array, ): - _axes = sh.normalise_axis(axes, x.ndim) + _axes = sh.normalize_axis(axes, x.ndim) _s = x.shape if s is None else s expected = [] for i in range(x.ndim): @@ -193,7 +193,7 @@ def test_rfftn(x, data): ph.assert_float_to_complex_dtype("rfftn", in_dtype=x.dtype, out_dtype=out.dtype) - _axes = sh.normalise_axis(axes, x.ndim) + _axes = sh.normalize_axis(axes, x.ndim) _s = x.shape if s is None else s expected = [] for i in range(x.ndim): @@ -225,7 +225,7 @@ def test_irfftn(x, data): ) # TODO: assert shape correctly - # _axes = sh.normalise_axis(axes, x.ndim) + # _axes = sh.normalize_axis(axes, x.ndim) # _s = x.shape if s is None else s # expected = [] # for i in range(x.ndim): diff --git a/array_api_tests/test_linalg.py b/array_api_tests/test_linalg.py index 4e394f0a..40fe035d 100644 --- a/array_api_tests/test_linalg.py +++ b/array_api_tests/test_linalg.py @@ -980,7 +980,7 @@ def test_vector_norm(x, data): # TODO: Check that the ord values give the correct norms. # ord = kw.get('ord', 2) - _axes = sh.normalise_axis(axis, x.ndim) + _axes = sh.normalize_axis(axis, x.ndim) ph.assert_keepdimable_shape('linalg.vector_norm', out_shape=res.shape, in_shape=x.shape, axes=_axes, diff --git a/array_api_tests/test_manipulation_functions.py b/array_api_tests/test_manipulation_functions.py index a77a0b6a..ce7a8cf7 100644 --- a/array_api_tests/test_manipulation_functions.py +++ b/array_api_tests/test_manipulation_functions.py @@ -190,7 +190,7 @@ def test_squeeze(x, data): ) axes = (axis,) if isinstance(axis, int) else axis - axes = sh.normalise_axis(axes, x.ndim) + axes = sh.normalize_axis(axes, x.ndim) squeezable_axes = [i for i, side in enumerate(x.shape) if side == 1] if any(i not in squeezable_axes for i in axes): @@ -230,7 +230,7 @@ def test_flip(x, data): ph.assert_dtype("flip", in_dtype=x.dtype, out_dtype=out.dtype) - _axes = sh.normalise_axis(kw.get("axis", None), x.ndim) + _axes = sh.normalize_axis(kw.get("axis", None), x.ndim) for indices in sh.axes_ndindex(x.shape, _axes): reverse_indices = indices[::-1] assert_array_ndindex("flip", x, x_indices=indices, out=out, @@ -360,7 +360,7 @@ def test_roll(x, data): assert_array_ndindex("roll", x, x_indices=indices, out=out, out_indices=shifted_indices, kw=kw) else: shifts = (shift,) if isinstance(shift, int) else shift - axes = sh.normalise_axis(kw["axis"], x.ndim) + axes = sh.normalize_axis(kw["axis"], x.ndim) shifted_indices = roll_ndindex(x.shape, shifts, axes) assert_array_ndindex("roll", x, x_indices=sh.ndindex(x.shape), out=out, out_indices=shifted_indices, kw=kw) diff --git a/array_api_tests/test_searching_functions.py b/array_api_tests/test_searching_functions.py index c479d2f9..579cbbc2 100644 --- a/array_api_tests/test_searching_functions.py +++ b/array_api_tests/test_searching_functions.py @@ -36,7 +36,7 @@ def test_argmax(x, data): out = xp.argmax(x, **kw) ph.assert_default_index("argmax", out.dtype) - axes = sh.normalise_axis(kw.get("axis", None), x.ndim) + axes = sh.normalize_axis(kw.get("axis", None), x.ndim) ph.assert_keepdimable_shape( "argmax", in_shape=x.shape, out_shape=out.shape, axes=axes, keepdims=keepdims, kw=kw ) @@ -73,7 +73,7 @@ def test_argmin(x, data): out = xp.argmin(x, **kw) ph.assert_default_index("argmin", out.dtype) - axes = sh.normalise_axis(kw.get("axis", None), x.ndim) + axes = sh.normalize_axis(kw.get("axis", None), x.ndim) ph.assert_keepdimable_shape( "argmin", in_shape=x.shape, out_shape=out.shape, axes=axes, keepdims=keepdims, kw=kw ) diff --git a/array_api_tests/test_sorting_functions.py b/array_api_tests/test_sorting_functions.py index 8501045c..3d25798c 100644 --- a/array_api_tests/test_sorting_functions.py +++ b/array_api_tests/test_sorting_functions.py @@ -56,7 +56,7 @@ def test_argsort(x, data): ph.assert_default_index("argsort", out.dtype) ph.assert_shape("argsort", out_shape=out.shape, expected=x.shape, kw=kw) axis = kw.get("axis", -1) - axes = sh.normalise_axis(axis, x.ndim) + axes = sh.normalize_axis(axis, x.ndim) scalar_type = dh.get_scalar_type(x.dtype) for indices in sh.axes_ndindex(x.shape, axes): elements = [scalar_type(x[idx]) for idx in indices] @@ -117,7 +117,7 @@ def test_sort(x, data): ph.assert_dtype("sort", out_dtype=out.dtype, in_dtype=x.dtype) ph.assert_shape("sort", out_shape=out.shape, expected=x.shape, kw=kw) axis = kw.get("axis", -1) - axes = sh.normalise_axis(axis, x.ndim) + axes = sh.normalize_axis(axis, x.ndim) scalar_type = dh.get_scalar_type(x.dtype) for indices in sh.axes_ndindex(x.shape, axes): elements = [scalar_type(x[idx]) for idx in indices] diff --git a/array_api_tests/test_statistical_functions.py b/array_api_tests/test_statistical_functions.py index 778cdea1..d533e51b 100644 --- a/array_api_tests/test_statistical_functions.py +++ b/array_api_tests/test_statistical_functions.py @@ -28,7 +28,7 @@ def test_cumulative_sum(x, data): if x.ndim == 1: axes = axes | st.none() axis = data.draw(axes, label='axis') - _axis, = sh.normalise_axis(axis, x.ndim) + _axis, = sh.normalize_axis(axis, x.ndim) dtype = data.draw(kwarg_dtypes(x.dtype)) include_initial = data.draw(st.booleans(), label="include_initial") @@ -108,7 +108,7 @@ def test_max(x, data): out = xp.max(x, **kw) ph.assert_dtype("max", in_dtype=x.dtype, out_dtype=out.dtype) - _axes = sh.normalise_axis(kw.get("axis", None), x.ndim) + _axes = sh.normalize_axis(kw.get("axis", None), x.ndim) ph.assert_keepdimable_shape( "max", in_shape=x.shape, out_shape=out.shape, axes=_axes, keepdims=keepdims, kw=kw ) @@ -138,7 +138,7 @@ def test_mean(x, data): out = xp.mean(x, **kw) ph.assert_dtype("mean", in_dtype=x.dtype, out_dtype=out.dtype) - _axes = sh.normalise_axis(kw.get("axis", None), x.ndim) + _axes = sh.normalize_axis(kw.get("axis", None), x.ndim) ph.assert_keepdimable_shape( "mean", in_shape=x.shape, out_shape=out.shape, axes=_axes, keepdims=keepdims, kw=kw ) @@ -161,7 +161,7 @@ def test_min(x, data): out = xp.min(x, **kw) ph.assert_dtype("min", in_dtype=x.dtype, out_dtype=out.dtype) - _axes = sh.normalise_axis(kw.get("axis", None), x.ndim) + _axes = sh.normalize_axis(kw.get("axis", None), x.ndim) ph.assert_keepdimable_shape( "min", in_shape=x.shape, out_shape=out.shape, axes=_axes, keepdims=keepdims, kw=kw ) @@ -209,7 +209,7 @@ def test_prod(x, data): assert dh.is_int_dtype(out.dtype) # sanity check else: ph.assert_dtype("prod", in_dtype=x.dtype, out_dtype=out.dtype, expected=expected_dtype) - _axes = sh.normalise_axis(kw.get("axis", None), x.ndim) + _axes = sh.normalize_axis(kw.get("axis", None), x.ndim) ph.assert_keepdimable_shape( "prod", in_shape=x.shape, out_shape=out.shape, axes=_axes, keepdims=keepdims, kw=kw ) @@ -239,7 +239,7 @@ def test_prod(x, data): ) def test_std(x, data): axis = data.draw(hh.axes(x.ndim), label="axis") - _axes = sh.normalise_axis(axis, x.ndim) + _axes = sh.normalize_axis(axis, x.ndim) N = sum(side for axis, side in enumerate(x.shape) if axis not in _axes) correction = data.draw( st.floats(0.0, N, allow_infinity=False, allow_nan=False) | st.integers(0, N), @@ -298,7 +298,7 @@ def test_sum(x, data): assert dh.is_int_dtype(out.dtype) # sanity check else: ph.assert_dtype("sum", in_dtype=x.dtype, out_dtype=out.dtype, expected=expected_dtype) - _axes = sh.normalise_axis(kw.get("axis", None), x.ndim) + _axes = sh.normalize_axis(kw.get("axis", None), x.ndim) ph.assert_keepdimable_shape( "sum", in_shape=x.shape, out_shape=out.shape, axes=_axes, keepdims=keepdims, kw=kw ) @@ -329,7 +329,7 @@ def test_sum(x, data): ) def test_var(x, data): axis = data.draw(hh.axes(x.ndim), label="axis") - _axes = sh.normalise_axis(axis, x.ndim) + _axes = sh.normalize_axis(axis, x.ndim) N = sum(side for axis, side in enumerate(x.shape) if axis not in _axes) correction = data.draw( st.floats(0.0, N, allow_infinity=False, allow_nan=False) | st.integers(0, N), diff --git a/array_api_tests/test_utility_functions.py b/array_api_tests/test_utility_functions.py index 2338d1d2..b5076d9b 100644 --- a/array_api_tests/test_utility_functions.py +++ b/array_api_tests/test_utility_functions.py @@ -21,7 +21,7 @@ def test_all(x, data): out = xp.all(x, **kw) ph.assert_dtype("all", in_dtype=x.dtype, out_dtype=out.dtype, expected=xp.bool) - _axes = sh.normalise_axis(kw.get("axis", None), x.ndim) + _axes = sh.normalize_axis(kw.get("axis", None), x.ndim) ph.assert_keepdimable_shape( "all", in_shape=x.shape, out_shape=out.shape, axes=_axes, keepdims=keepdims, kw=kw ) @@ -49,7 +49,7 @@ def test_any(x, data): out = xp.any(x, **kw) ph.assert_dtype("any", in_dtype=x.dtype, out_dtype=out.dtype, expected=xp.bool) - _axes = sh.normalise_axis(kw.get("axis", None), x.ndim) + _axes = sh.normalize_axis(kw.get("axis", None), x.ndim) ph.assert_keepdimable_shape( "any", in_shape=x.shape, out_shape=out.shape, axes=_axes, keepdims=keepdims, kw=kw, ) From 43e0148ce41a5ba971cf15a7ff1930f23a5a9b57 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 26 Jun 2024 14:38:57 -0600 Subject: [PATCH 05/14] Add shape testing for moveaxis --- array_api_tests/test_manipulation_functions.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/array_api_tests/test_manipulation_functions.py b/array_api_tests/test_manipulation_functions.py index ce7a8cf7..273c4529 100644 --- a/array_api_tests/test_manipulation_functions.py +++ b/array_api_tests/test_manipulation_functions.py @@ -172,7 +172,21 @@ def test_moveaxis(x, data): out = xp.moveaxis(x, source, destination) ph.assert_dtype("moveaxis", in_dtype=x.dtype, out_dtype=out.dtype) - # TODO: shape and values testing + + # Shape testing + _source = sh.normalize_axis(source, x.ndim) + _destination = sh.normalize_axis(destination, x.ndim) + + new_axes = [n for n in range(x.ndim) if n not in _source] + + for dest, src in sorted(zip(_destination, _source)): + new_axes.insert(dest, src) + + expected_shape = tuple(x.shape[i] for i in new_axes) + + ph.assert_result_shape("moveaxis", in_shapes=[x.shape], + out_shape=out.shape, expected=expected_shape, + kw={"source": source, "destination": destination}) @pytest.mark.unvectorized @given( From c9bdcf1fda7ebe988255b1e54ddb3d27c2c0eec9 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 26 Jun 2024 14:45:26 -0600 Subject: [PATCH 06/14] Add values testing for moveaxis --- array_api_tests/test_manipulation_functions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/array_api_tests/test_manipulation_functions.py b/array_api_tests/test_manipulation_functions.py index 273c4529..28b54802 100644 --- a/array_api_tests/test_manipulation_functions.py +++ b/array_api_tests/test_manipulation_functions.py @@ -173,7 +173,7 @@ def test_moveaxis(x, data): ph.assert_dtype("moveaxis", in_dtype=x.dtype, out_dtype=out.dtype) - # Shape testing + _source = sh.normalize_axis(source, x.ndim) _destination = sh.normalize_axis(destination, x.ndim) @@ -188,6 +188,12 @@ def test_moveaxis(x, data): out_shape=out.shape, expected=expected_shape, kw={"source": source, "destination": destination}) + indices = list(sh.ndindex(x.shape)) + permuted_indices = [tuple(idx[axis] for axis in new_axes) for idx in indices] + assert_array_ndindex( + "moveaxis", x, x_indices=sh.ndindex(x.shape), out=out, out_indices=permuted_indices + ) + @pytest.mark.unvectorized @given( x=hh.arrays( From 94659590b8dbe1ace3942d892ac0d90e56f0e905 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 3 Jul 2024 13:04:58 -0600 Subject: [PATCH 07/14] Add __contains__ to MinMax --- array_api_tests/dtype_helpers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/array_api_tests/dtype_helpers.py b/array_api_tests/dtype_helpers.py index 8acf11fa..6ff8d49c 100644 --- a/array_api_tests/dtype_helpers.py +++ b/array_api_tests/dtype_helpers.py @@ -209,6 +209,9 @@ class MinMax(NamedTuple): min: Union[int, float] max: Union[int, float] + def __contains__(self, other): + assert isinstance(other, (int, float)) + return self.min <= other <= self.max dtype_ranges = _make_dtype_mapping_from_names( { From 3b773db44ac9e367aaccf2aecc2fa971ef8b7c1b Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 3 Jul 2024 13:05:08 -0600 Subject: [PATCH 08/14] Combine different clip test cases together and omit out-of-bounds cases --- ...est_operators_and_elementwise_functions.py | 123 ++++++++---------- 1 file changed, 54 insertions(+), 69 deletions(-) diff --git a/array_api_tests/test_operators_and_elementwise_functions.py b/array_api_tests/test_operators_and_elementwise_functions.py index 6808cf7b..851cd7a0 100644 --- a/array_api_tests/test_operators_and_elementwise_functions.py +++ b/array_api_tests/test_operators_and_elementwise_functions.py @@ -970,80 +970,65 @@ def test_clip(x, data): expected_shape = sh.broadcast_shapes(*shapes) ph.assert_shape("clip", out_shape=out.shape, expected=expected_shape) - if min is max is None: - ph.assert_array_elements("clip", out=out, expected=x) - elif max is None: - # If one operand is nan, the result is nan. See - # https://github.com/data-apis/array-api/pull/813. - def refimpl(_x, _min): - if math.isnan(_x) or math.isnan(_min): - return math.nan + # This is based on right_scalar_assert_against_refimpl and + # binary_assert_against_refimpl. clip() is currently the only ternary + # elementwise function and the only function that supports arrays and + # scalars. However, where() (in test_searching_functions) is similar + # and if scalar support is added to it, we may want to factor out and + # reuse this logic. + + def refimpl(_x, _min, _max): + # Skip cases where _min and _max are integers whose values do not + # fit in the dtype of _x, since this behavior is unspecified. + if dh.is_int_dtype(x.dtype): + if _min is not None and _min not in dh.dtype_ranges[x.dtype]: + return None + if _max is not None and _max not in dh.dtype_ranges[x.dtype]: + return None + + if (math.isnan(_x) + or (_min is not None and math.isnan(_min)) + or (_max is not None and math.isnan(_max))): + return math.nan + if _min is _max is None: + return _x + if _max is None: return builtins.max(_x, _min) - if dh.is_scalar(min): - right_scalar_assert_against_refimpl( - "clip", x, min, out, refimpl, - left_sym="x", - expr_template="clip({}, min={})", - ) - else: - binary_assert_against_refimpl( - "clip", x, min, out, refimpl, - left_sym="x", right_sym="min", - expr_template="clip({}, min={})", - ) - elif min is None: - def refimpl(_x, _max): - if math.isnan(_x) or math.isnan(_max): - return math.nan + if _min is None: return builtins.min(_x, _max) - if dh.is_scalar(max): - right_scalar_assert_against_refimpl( - "clip", x, max, out, refimpl, - left_sym="x", - expr_template="clip({}, max={})", + return builtins.min(builtins.max(_x, _min), _max) + + stype = dh.get_scalar_type(x.dtype) + min_shape = () if min is None or dh.is_scalar(min) else min.shape + max_shape = () if max is None or dh.is_scalar(max) else max.shape + + for x_idx, min_idx, max_idx, o_idx in sh.iter_indices( + x.shape, min_shape, max_shape, out.shape): + x_val = stype(x[x_idx]) + if min is None or dh.is_scalar(min): + min_val = min + else: + min_val = stype(min[min_idx]) + if max is None or dh.is_scalar(max): + max_val = max + else: + max_val = stype(max[max_idx]) + expected = refimpl(x_val, min_val, max_val) + if expected is None: + continue + out_val = stype(out[o_idx]) + if math.isnan(expected): + assert math.isnan(out_val), ( + f"out[{o_idx}]={out[o_idx]} but should be nan [clip()]\n" + f"x[{x_idx}]={x_val}, min[{min_idx}]={min_val}, max[{max_idx}]={max_val}" ) else: - binary_assert_against_refimpl( - "clip", x, max, out, refimpl, - left_sym="x", right_sym="max", - expr_template="clip({}, max={})", + assert out_val == expected, ( + f"out[{o_idx}]={out[o_idx]} but should be {expected} [clip()]\n" + f"x[{x_idx}]={x_val}, min[{min_idx}]={min_val}, max[{max_idx}]={max_val}" ) - else: - def refimpl(_x, _min, _max): - if math.isnan(_x) or math.isnan(_min) or math.isnan(_max): - return math.nan - return builtins.min(builtins.max(_x, _min), _max) - - # This is based on right_scalar_assert_against_refimpl and - # binary_assert_against_refimpl. clip() is currently the only ternary - # elementwise function and the only function that supports arrays and - # scalars. However, where() (in test_searching_functions) is similar - # and if scalar support is added to it, we may want to factor out and - # reuse this logic. - - stype = dh.get_scalar_type(x.dtype) - min_shape = () if dh.is_scalar(min) else min.shape - max_shape = () if dh.is_scalar(max) else max.shape - - for x_idx, min_idx, max_idx, o_idx in sh.iter_indices( - x.shape, min_shape, max_shape, out.shape): - x_val = stype(x[x_idx]) - min_val = min if dh.is_scalar(min) else min[min_idx] - min_val = stype(min_val) - max_val = max if dh.is_scalar(max) else max[max_idx] - max_val = stype(max_val) - expected = refimpl(x_val, min_val, max_val) - out_val = stype(out[o_idx]) - if math.isnan(expected): - assert math.isnan(out_val), ( - f"out[{o_idx}]={out[o_idx]} but should be nan [clip()]\n" - f"x[{x_idx}]={x_val}, min[{min_idx}]={min_val}, max[{max_idx}]={max_val}" - ) - else: - assert out_val == expected, ( - f"out[{o_idx}]={out[o_idx]} but should be {expected} [clip()]\n" - f"x[{x_idx}]={x_val}, min[{min_idx}]={min_val}, max[{max_idx}]={max_val}" -) + + if api_version >= "2022.12": @given(hh.arrays(dtype=hh.complex_dtypes, shape=hh.shapes())) From 704f456455365aba27fac2633dfc2b38d74efb4c Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Tue, 9 Jul 2024 15:07:17 -0600 Subject: [PATCH 09/14] Ignore unspecified rounding in downcasting in clip() --- ...est_operators_and_elementwise_functions.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/array_api_tests/test_operators_and_elementwise_functions.py b/array_api_tests/test_operators_and_elementwise_functions.py index 851cd7a0..25aab972 100644 --- a/array_api_tests/test_operators_and_elementwise_functions.py +++ b/array_api_tests/test_operators_and_elementwise_functions.py @@ -986,6 +986,27 @@ def refimpl(_x, _min, _max): if _max is not None and _max not in dh.dtype_ranges[x.dtype]: return None + # If min or max are float64 and x is float32, they will need to be + # downcast to float32. This could result in a round in the wrong + # direction meaning the resulting clipped value might not actually be + # between min and max. This behavior is unspecified, so skip any cases + # where x is within the rounding error of downcasting min or max. + if x.dtype == xp.float32: + if min is not None and not dh.is_scalar(min) and min.dtype == xp.float64 and math.isfinite(_min): + _min_float32 = float(xp.asarray(_min, dtype=xp.float32)) + if math.isinf(_min_float32): + return None + tol = abs(_min - _min_float32) + if math.isclose(_min, _min_float32, abs_tol=tol): + return None + if max is not None and not dh.is_scalar(max) and max.dtype == xp.float64 and math.isfinite(_max): + _max_float32 = float(xp.asarray(_max, dtype=xp.float32)) + if math.isinf(_max_float32): + return None + tol = abs(_max - _max_float32) + if math.isclose(_max, _max_float32, abs_tol=tol): + return None + if (math.isnan(_x) or (_min is not None and math.isnan(_min)) or (_max is not None and math.isnan(_max))): From fbec5fd73e830ed68b6ab7b0498a82ec716a0b3a Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Tue, 16 Jul 2024 14:07:06 -0600 Subject: [PATCH 10/14] Add a less() helper to allow comparing uint64 and signed int arrays --- array_api_tests/array_helpers.py | 12 +++++++++++- meta_tests/test_array_helpers.py | 14 +++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/array_api_tests/array_helpers.py b/array_api_tests/array_helpers.py index c11d5732..a74dab24 100644 --- a/array_api_tests/array_helpers.py +++ b/array_api_tests/array_helpers.py @@ -1,5 +1,5 @@ from ._array_module import (isnan, all, any, equal, not_equal, logical_and, - logical_or, isfinite, greater, less, less_equal, + logical_or, isfinite, greater, less_equal, zeros, ones, full, bool, int8, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64, nan, inf, pi, remainder, divide, isinf, @@ -164,6 +164,16 @@ def notequal(x, y): return not_equal(x, y) +def less(x, y): + """ + Same as less(x, y) except it allows comparing uint64 with signed int dtypes + """ + if x.dtype == uint64 and dh.dtype_signed[y.dtype]: + return xp.where(y < 0, xp.asarray(False), xp.less(x, xp.astype(y, uint64))) + if y.dtype == uint64 and dh.dtype_signed[x.dtype]: + return xp.where(x < 0, xp.asarray(True), xp.less(xp.astype(x, uint64), y)) + return xp.less(x, y) + def assert_exactly_equal(x, y, msg_extra=None): """ Test that the arrays x and y are exactly equal. diff --git a/meta_tests/test_array_helpers.py b/meta_tests/test_array_helpers.py index 99d83f40..6c87eb3b 100644 --- a/meta_tests/test_array_helpers.py +++ b/meta_tests/test_array_helpers.py @@ -1,5 +1,10 @@ +from hypothesis import given + from array_api_tests import _array_module as xp -from array_api_tests .array_helpers import exactly_equal, notequal +from array_api_tests.hypothesis_helpers import two_mutual_arrays +from array_api_tests.dtype_helpers import int_dtypes +from array_api_tests.shape_helpers import iter_indices, broadcast_shapes +from array_api_tests .array_helpers import exactly_equal, notequal, less # TODO: These meta-tests currently only work with NumPy @@ -17,3 +22,10 @@ def test_notequal(): res = xp.asarray([False, True, False, False, False, True, False, True]) assert xp.all(xp.equal(notequal(a, b), res)) + +@given(*two_mutual_arrays(dtypes=int_dtypes)) +def test_less(x, y): + res = less(x, y) + + for i, j, k in iter_indices(x.shape, y.shape, broadcast_shapes(x.shape, y.shape)): + assert res[k] == (int(x[i]) < int(y[j])) From c0ec0bb17d25d75c78541ee766aea26d8136490a Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Tue, 16 Jul 2024 14:07:22 -0600 Subject: [PATCH 11/14] Use the less helper in test_clip --- array_api_tests/test_operators_and_elementwise_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/array_api_tests/test_operators_and_elementwise_functions.py b/array_api_tests/test_operators_and_elementwise_functions.py index 25aab972..4c8333c9 100644 --- a/array_api_tests/test_operators_and_elementwise_functions.py +++ b/array_api_tests/test_operators_and_elementwise_functions.py @@ -949,7 +949,7 @@ def test_clip(x, data): ), label="max") # min > max is undefined (but allow nans) - assume(min is None or max is None or not xp.any(xp.asarray(min) > xp.asarray(max))) + assume(min is None or max is None or not xp.any(ah.less(xp.asarray(max), xp.asarray(min)))) kw = data.draw( hh.specified_kwargs( From 8e35f2e47f9f5f827f25325ac259e94a78d43e05 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Thu, 18 Jul 2024 15:14:33 -0600 Subject: [PATCH 12/14] Fix the less() meta-test to actually test all dtype combinations --- meta_tests/test_array_helpers.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/meta_tests/test_array_helpers.py b/meta_tests/test_array_helpers.py index 6c87eb3b..c46df50d 100644 --- a/meta_tests/test_array_helpers.py +++ b/meta_tests/test_array_helpers.py @@ -1,8 +1,9 @@ from hypothesis import given +from hypothesis import strategies as st from array_api_tests import _array_module as xp -from array_api_tests.hypothesis_helpers import two_mutual_arrays -from array_api_tests.dtype_helpers import int_dtypes +from array_api_tests.hypothesis_helpers import (int_dtypes, arrays, + two_mutually_broadcastable_shapes) from array_api_tests.shape_helpers import iter_indices, broadcast_shapes from array_api_tests .array_helpers import exactly_equal, notequal, less @@ -23,8 +24,11 @@ def test_notequal(): assert xp.all(xp.equal(notequal(a, b), res)) -@given(*two_mutual_arrays(dtypes=int_dtypes)) -def test_less(x, y): +@given(two_mutually_broadcastable_shapes, int_dtypes, int_dtypes, st.data()) +def test_less(shapes, dtype1, dtype2, data): + x = data.draw(arrays(shape=shapes[0], dtype=dtype1)) + y = data.draw(arrays(shape=shapes[1], dtype=dtype2)) + res = less(x, y) for i, j, k in iter_indices(x.shape, y.shape, broadcast_shapes(x.shape, y.shape)): From f2d071cc32eab5a5cc0a0987fa84e94ed7d56af1 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Thu, 18 Jul 2024 15:18:58 -0600 Subject: [PATCH 13/14] Fix int_dtypes strategy to include unsigned int dtypes --- array_api_tests/hypothesis_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/array_api_tests/hypothesis_helpers.py b/array_api_tests/hypothesis_helpers.py index 08eab19b..cc70c13d 100644 --- a/array_api_tests/hypothesis_helpers.py +++ b/array_api_tests/hypothesis_helpers.py @@ -177,7 +177,7 @@ def oneway_broadcastable_shapes(draw) -> OnewayBroadcastableShapes: # Use these instead of xps.scalar_dtypes, etc. because it skips dtypes from # ARRAY_API_TESTS_SKIP_DTYPES all_dtypes = sampled_from(_sorted_dtypes) -int_dtypes = sampled_from(dh.int_dtypes) +int_dtypes = sampled_from(dh.all_int_dtypes) uint_dtypes = sampled_from(dh.uint_dtypes) real_dtypes = sampled_from(dh.real_dtypes) # Warning: The hypothesis "floating_dtypes" is what we call From 7a1ad70e639f545598de51ff28ed42ce20c5b95c Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Mon, 22 Jul 2024 16:18:08 -0600 Subject: [PATCH 14/14] Run the array-api-strict tests on CI with 2023.12 --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1a91300..a58f1841 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,7 @@ jobs: - name: Run the test suite env: ARRAY_API_TESTS_MODULE: array_api_strict + ARRAY_API_STRICT_API_VERSION: 2023.12 run: | pytest -v -rxXfE --skips-file array-api-strict-skips.txt array_api_tests/ # We also have internal tests that isn't really necessary for adopters