diff --git a/array_api_tests/algos.py b/array_api_tests/algos.py new file mode 100644 index 00000000..8aa77b3f --- /dev/null +++ b/array_api_tests/algos.py @@ -0,0 +1,53 @@ +__all__ = ["broadcast_shapes"] + + +from .typing import Shape + + +# We use a custom exception to differentiate from potential bugs +class BroadcastError(ValueError): + pass + + +def _broadcast_shapes(shape1: Shape, shape2: Shape) -> Shape: + """Broadcasts `shape1` and `shape2`""" + N1 = len(shape1) + N2 = len(shape2) + N = max(N1, N2) + shape = [None for _ in range(N)] + i = N - 1 + while i >= 0: + n1 = N1 - N + i + if N1 - N + i >= 0: + d1 = shape1[n1] + else: + d1 = 1 + n2 = N2 - N + i + if N2 - N + i >= 0: + d2 = shape2[n2] + else: + d2 = 1 + + if d1 == 1: + shape[i] = d2 + elif d2 == 1: + shape[i] = d1 + elif d1 == d2: + shape[i] = d1 + else: + raise BroadcastError + + i = i - 1 + + return tuple(shape) + + +def broadcast_shapes(*shapes: Shape): + if len(shapes) == 0: + raise ValueError("shapes=[] must be non-empty") + elif len(shapes) == 1: + return shapes[0] + result = _broadcast_shapes(shapes[0], shapes[1]) + for i in range(2, len(shapes)): + result = _broadcast_shapes(result, shapes[i]) + return result diff --git a/array_api_tests/dtype_helpers.py b/array_api_tests/dtype_helpers.py index 28844c87..65b4090a 100644 --- a/array_api_tests/dtype_helpers.py +++ b/array_api_tests/dtype_helpers.py @@ -19,6 +19,7 @@ 'dtype_to_scalars', 'is_int_dtype', 'is_float_dtype', + 'get_scalar_type', 'dtype_ranges', 'default_int', 'default_float', @@ -30,6 +31,7 @@ 'binary_op_to_symbol', 'unary_op_to_symbol', 'inplace_op_to_symbol', + 'op_to_func', 'fmt_types', ] @@ -74,6 +76,15 @@ def is_float_dtype(dtype): return dtype in float_dtypes +def get_scalar_type(dtype: DataType) -> ScalarType: + if is_int_dtype(dtype): + return int + elif is_float_dtype(dtype): + return float + else: + return bool + + class MinMax(NamedTuple): min: int max: int @@ -332,7 +343,7 @@ def result_type(*dtypes: DataType): } -_op_to_func = { +op_to_func = { '__abs__': 'abs', '__add__': 'add', '__and__': 'bitwise_and', @@ -341,7 +352,6 @@ def result_type(*dtypes: DataType): '__ge__': 'greater_equal', '__gt__': 'greater', '__le__': 'less_equal', - '__lshift__': 'bitwise_left_shift', '__lt__': 'less', # '__matmul__': 'matmul', # TODO: support matmul '__mod__': 'remainder', @@ -349,6 +359,7 @@ def result_type(*dtypes: DataType): '__ne__': 'not_equal', '__or__': 'bitwise_or', '__pow__': 'pow', + '__lshift__': 'bitwise_left_shift', '__rshift__': 'bitwise_right_shift', '__sub__': 'subtract', '__truediv__': 'divide', @@ -359,7 +370,7 @@ def result_type(*dtypes: DataType): } -for op, elwise_func in _op_to_func.items(): +for op, elwise_func in op_to_func.items(): func_in_dtypes[op] = func_in_dtypes[elwise_func] func_returns_bool[op] = func_returns_bool[elwise_func] diff --git a/array_api_tests/hypothesis_helpers.py b/array_api_tests/hypothesis_helpers.py index c2a21733..a14f3d51 100644 --- a/array_api_tests/hypothesis_helpers.py +++ b/array_api_tests/hypothesis_helpers.py @@ -18,7 +18,8 @@ from .array_helpers import ndindex from .function_stubs import elementwise_functions from .pytest_helpers import nargs -from .typing import DataType, Shape, Array +from .typing import Array, DataType, Shape +from .algos import broadcast_shapes # Set this to True to not fail tests just because a dtype isn't implemented. # If no compatible dtype is implemented for a given test, the test will fail @@ -218,7 +219,6 @@ def two_broadcastable_shapes(draw): This will produce two shapes (shape1, shape2) such that shape2 can be broadcast to shape1. """ - from .test_broadcasting import broadcast_shapes shape1, shape2 = draw(two_mutually_broadcastable_shapes) assume(broadcast_shapes(shape1, shape2) == shape1) return (shape1, shape2) diff --git a/array_api_tests/meta/test_broadcasting.py b/array_api_tests/meta/test_broadcasting.py new file mode 100644 index 00000000..e347e525 --- /dev/null +++ b/array_api_tests/meta/test_broadcasting.py @@ -0,0 +1,35 @@ +""" +https://github.com/data-apis/array-api/blob/master/spec/API_specification/broadcasting.md +""" + +import pytest + +from ..algos import BroadcastError, _broadcast_shapes + + +@pytest.mark.parametrize( + "shape1, shape2, expected", + [ + [(8, 1, 6, 1), (7, 1, 5), (8, 7, 6, 5)], + [(5, 4), (1,), (5, 4)], + [(5, 4), (4,), (5, 4)], + [(15, 3, 5), (15, 1, 5), (15, 3, 5)], + [(15, 3, 5), (3, 5), (15, 3, 5)], + [(15, 3, 5), (3, 1), (15, 3, 5)], + ], +) +def test_broadcast_shapes(shape1, shape2, expected): + assert _broadcast_shapes(shape1, shape2) == expected + + +@pytest.mark.parametrize( + "shape1, shape2", + [ + [(3,), (4,)], # dimension does not match + [(2, 1), (8, 4, 3)], # second dimension does not match + [(15, 3, 5), (15, 3)], # singleton dimensions can only be prepended + ], +) +def test_broadcast_shapes_fails_on_bad_shapes(shape1, shape2): + with pytest.raises(BroadcastError): + _broadcast_shapes(shape1, shape2) diff --git a/array_api_tests/meta/test_hypothesis_helpers.py b/array_api_tests/meta/test_hypothesis_helpers.py index c93fe339..652644c1 100644 --- a/array_api_tests/meta/test_hypothesis_helpers.py +++ b/array_api_tests/meta/test_hypothesis_helpers.py @@ -1,15 +1,16 @@ from math import prod import pytest -from hypothesis import given, strategies as st, settings +from hypothesis import given, settings +from hypothesis import strategies as st from .. import _array_module as xp -from .. import xps -from .._array_module import _UndefinedStub from .. import array_helpers as ah from .. import dtype_helpers as dh from .. import hypothesis_helpers as hh -from ..test_broadcasting import broadcast_shapes +from .. import xps +from .._array_module import _UndefinedStub +from ..algos import broadcast_shapes UNDEFINED_DTYPES = any(isinstance(d, _UndefinedStub) for d in dh.all_dtypes) pytestmark = [pytest.mark.skipif(UNDEFINED_DTYPES, reason="undefined dtypes")] diff --git a/array_api_tests/pytest_helpers.py b/array_api_tests/pytest_helpers.py index a9573515..9424ba35 100644 --- a/array_api_tests/pytest_helpers.py +++ b/array_api_tests/pytest_helpers.py @@ -1,3 +1,4 @@ +from array_api_tests.algos import broadcast_shapes import math from inspect import getfullargspec from typing import Any, Dict, Optional, Tuple, Union @@ -17,6 +18,7 @@ "assert_default_float", "assert_default_int", "assert_shape", + "assert_result_shape", "assert_fill", ] @@ -69,7 +71,7 @@ def assert_dtype( out_dtype: DataType, expected: Optional[DataType] = None, *, - out_name: str = "out.dtype", + repr_name: str = "out.dtype", ): f_in_dtypes = dh.fmt_types(in_dtypes) f_out_dtype = dh.dtype_to_name[out_dtype] @@ -77,7 +79,7 @@ def assert_dtype( expected = dh.result_type(*in_dtypes) f_expected = dh.dtype_to_name[expected] msg = ( - f"{out_name}={f_out_dtype}, but should be {f_expected} " + f"{repr_name}={f_out_dtype}, but should be {f_expected} " f"[{func_name}({f_in_dtypes})]" ) assert out_dtype == expected, msg @@ -114,14 +116,41 @@ def assert_default_int(func_name: str, dtype: DataType): def assert_shape( - func_name: str, out_shape: Union[int, Shape], expected: Union[int, Shape], /, **kw + func_name: str, + out_shape: Union[int, Shape], + expected: Union[int, Shape], + /, + repr_name="out.shape", + **kw, ): if isinstance(out_shape, int): out_shape = (out_shape,) if isinstance(expected, int): expected = (expected,) msg = ( - f"out.shape={out_shape}, but should be {expected} [{func_name}({fmt_kw(kw)})]" + f"{repr_name}={out_shape}, but should be {expected} [{func_name}({fmt_kw(kw)})]" + ) + assert out_shape == expected, msg + + +def assert_result_shape( + func_name: str, + in_shapes: Tuple[Shape], + out_shape: Shape, + /, + expected: Optional[Shape] = None, + *, + repr_name="out.shape", + **kw, +): + if expected is None: + expected = broadcast_shapes(*in_shapes) + f_in_shapes = " . ".join(str(s) for s in in_shapes) + f_sig = f" {f_in_shapes} " + if kw: + f_sig += f", {fmt_kw(kw)}" + msg = ( + f"{repr_name}={out_shape}, but should be {expected} [{func_name}({f_sig})]" ) assert out_shape == expected, msg diff --git a/array_api_tests/test_broadcasting.py b/array_api_tests/test_broadcasting.py deleted file mode 100644 index d951d48f..00000000 --- a/array_api_tests/test_broadcasting.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -https://github.com/data-apis/array-api/blob/master/spec/API_specification/broadcasting.md -""" - -import pytest - -from hypothesis import given, assume -from hypothesis.strategies import data, sampled_from - -from .hypothesis_helpers import shapes, FILTER_UNDEFINED_DTYPES -from .pytest_helpers import raises, doesnt_raise, nargs - -from .dtype_helpers import func_in_dtypes -from .function_stubs import elementwise_functions -from . import _array_module -from ._array_module import ones, _UndefinedStub - -# The spec does not specify what exception is raised on broadcast errors. We -# use a custom exception to distinguish it from potential bugs in -# broadcast_shapes(). -class BroadcastError(Exception): - pass - -# The spec only specifies broadcasting for two shapes. -def broadcast_shapes(shape1, shape2): - """ - Broadcast shapes `shape1` and `shape2`. - - The code in this function should follow the pseudocode in the spec as - closely as possible. - """ - N1 = len(shape1) - N2 = len(shape2) - N = max(N1, N2) - shape = [None]*N - i = N - 1 - while i >= 0: - n1 = N1 - N + i - if N1 - N + i >= 0: - d1 = shape1[n1] - else: - d1 = 1 - n2 = N2 - N + i - if N2 - N + i >= 0: - d2 = shape2[n2] - else: - d2 = 1 - - if d1 == 1: - shape[i] = d2 - elif d2 == 1: - shape[i] = d1 - elif d1 == d2: - shape[i] = d1 - else: - raise BroadcastError - - i = i - 1 - - return tuple(shape) - -def test_broadcast_shapes_explicit_spec(): - """ - Explicit broadcast shapes examples from the spec - """ - shape1 = (8, 1, 6, 1) - shape2 = (7, 1, 5) - result = (8, 7, 6, 5) - assert broadcast_shapes(shape1, shape2) == result - - shape1 = (5, 4) - shape2 = (1,) - result = (5, 4) - assert broadcast_shapes(shape1, shape2) == result - - shape1 = (5, 4) - shape2 = (4,) - result = (5, 4) - assert broadcast_shapes(shape1, shape2) == result - - shape1 = (15, 3, 5) - shape2 = (15, 1, 5) - result = (15, 3, 5) - assert broadcast_shapes(shape1, shape2) == result - - shape1 = (15, 3, 5) - shape2 = (3, 5) - result = (15, 3, 5) - assert broadcast_shapes(shape1, shape2) == result - - shape1 = (15, 3, 5) - shape2 = (3, 1) - result = (15, 3, 5) - assert broadcast_shapes(shape1, shape2) == result - - shape1 = (3,) - shape2 = (4,) - raises(BroadcastError, lambda: broadcast_shapes(shape1, shape2)) # dimension does not match - - shape1 = (2, 1) - shape2 = (8, 4, 3) - raises(BroadcastError, lambda: broadcast_shapes(shape1, shape2)) # second dimension does not match - - shape1 = (15, 3, 5) - shape2 = (15, 3) - raises(BroadcastError, lambda: broadcast_shapes(shape1, shape2)) # singleton dimensions can only be prepended, not appended - -# TODO: Extend this to all functions (not just elementwise), and handle -# functions that take more than 2 args -@pytest.mark.parametrize('func_name', [i for i in - elementwise_functions.__all__ if - nargs(i) > 1]) -@given(shape1=shapes(), shape2=shapes(), data=data()) -def test_broadcasting_hypothesis(func_name, shape1, shape2, data): - # Internal consistency checks - assert nargs(func_name) == 2 - - dtype = data.draw(sampled_from(func_in_dtypes[func_name])) - if FILTER_UNDEFINED_DTYPES: - assume(not isinstance(dtype, _UndefinedStub)) - func = getattr(_array_module, func_name) - - if isinstance(func, _array_module._UndefinedStub): - func._raise() - - args = [ones(shape1, dtype=dtype), ones(shape2, dtype=dtype)] - try: - broadcast_shape = broadcast_shapes(shape1, shape2) - except BroadcastError: - raises(Exception, lambda: func(*args), - f"{func_name} should raise an exception from not being able to broadcast inputs with shapes {(shape1, shape2)}") - else: - result = doesnt_raise(lambda: func(*args), - f"{func_name} raised an unexpected exception from broadcastable inputs with shapes {(shape1, shape2)}") - assert result.shape == broadcast_shape, "broadcast shapes incorrect" diff --git a/array_api_tests/test_elementwise_functions.py b/array_api_tests/test_elementwise_functions.py index acfc8cfb..4d288ee4 100644 --- a/array_api_tests/test_elementwise_functions.py +++ b/array_api_tests/test_elementwise_functions.py @@ -10,8 +10,13 @@ """ import math +from enum import Enum, auto +from typing import Callable, List, Sequence, Union +import pytest from hypothesis import assume, given +from hypothesis import strategies as st +from hypothesis.control import reject from . import _array_module as xp from . import array_helpers as ah @@ -19,111 +24,287 @@ from . import hypothesis_helpers as hh from . import pytest_helpers as ph from . import xps -# We might as well use this implementation rather than requiring -# mod.broadcast_shapes(). See test_equal() and others. -from .test_broadcasting import broadcast_shapes - +from .algos import broadcast_shapes +from .typing import Array, DataType, Param, Scalar + +# When appropiate, this module tests operators alongside their respective +# elementwise methods. We do this by parametrizing a generalised test method +# with every relevant method and operator. +# +# Notable arguments in the parameter: +# - The function object, which for operator test cases is a wrapper that allows +# test logic to be generalised. +# - The argument strategies, which can be used to draw arguments for the test +# case. They may require additional filtering for certain test cases. +# - right_is_scalar (binary parameters), which denotes if the right argument is +# a scalar in a test case. This can be used to appropiately adjust draw +# filtering and test logic. + + +func_to_op = {v: k for k, v in dh.op_to_func.items()} +all_op_to_symbol = {**dh.binary_op_to_symbol, **dh.inplace_op_to_symbol} +finite_kw = {"allow_nan": False, "allow_infinity": False} + +unary_argnames = ("func_name", "func", "strat") +UnaryParam = Param[str, Callable[[Array], Array], st.SearchStrategy[Array]] + + +def make_unary_params( + elwise_func_name: str, dtypes: Sequence[DataType] +) -> List[UnaryParam]: + strat = xps.arrays(dtype=st.sampled_from(dtypes), shape=hh.shapes()) + func = getattr(xp, elwise_func_name) + op_name = func_to_op[elwise_func_name] + op = lambda x: getattr(x, op_name)() + return [ + pytest.param(elwise_func_name, func, strat, id=elwise_func_name), + pytest.param(op_name, op, strat, id=op_name), + ] + + +binary_argnames = ( + "func_name", + "func", + "left_sym", + "left_strat", + "right_sym", + "right_strat", + "right_is_scalar", + "res_name", +) +BinaryParam = Param[ + str, + Callable[[Array, Union[Scalar, Array]], Array], + str, + st.SearchStrategy[Array], + str, + st.SearchStrategy[Union[Scalar, Array]], + bool, +] + + +class FuncType(Enum): + FUNC = auto() + OP = auto() + IOP = auto() + + +def make_binary_params( + elwise_func_name: str, dtypes: Sequence[DataType] +) -> List[BinaryParam]: + dtypes_strat = st.sampled_from(dtypes) + + def make_param( + func_name: str, func_type: FuncType, right_is_scalar: bool + ) -> BinaryParam: + if right_is_scalar: + left_sym = "x" + right_sym = "s" + else: + left_sym = "x1" + right_sym = "x2" + + shared_dtypes = st.shared(dtypes_strat) + if right_is_scalar: + left_strat = xps.arrays(dtype=shared_dtypes, shape=hh.shapes()) + right_strat = shared_dtypes.flatmap( + lambda d: xps.from_dtype(d, **finite_kw) + ) + else: + if func_type is FuncType.IOP: + shared_shapes = st.shared(hh.shapes()) + left_strat = xps.arrays(dtype=shared_dtypes, shape=shared_shapes) + right_strat = xps.arrays(dtype=shared_dtypes, shape=shared_shapes) + else: + left_strat, right_strat = hh.two_mutual_arrays(dtypes) + + if func_type is FuncType.FUNC: + func = getattr(xp, func_name) + else: + op_sym = all_op_to_symbol[func_name] + expr = f"{left_sym} {op_sym} {right_sym}" + if func_type is FuncType.OP: + + def func(l: Array, r: Union[Scalar, Array]) -> Array: + locals_ = {} + locals_[left_sym] = l + locals_[right_sym] = r + return eval(expr, locals_) + + else: + + def func(l: Array, r: Union[Scalar, Array]) -> Array: + locals_ = {} + locals_[left_sym] = ah.asarray( + l, copy=True + ) # prevents left mutating + locals_[right_sym] = r + exec(expr, locals_) + return locals_[left_sym] + + func.__name__ = func_name # for repr + + if func_type is FuncType.IOP: + res_name = left_sym + else: + res_name = "out" + + return pytest.param( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + id=f"{func_name}({left_sym}, {right_sym})", + ) -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) -def test_abs(x): - if dh.is_int_dtype(x.dtype): - minval = dh.dtype_ranges[x.dtype][0] - if minval < 0: - # abs of the smallest representable negative integer is not defined - mask = xp.not_equal(x, ah.full(x.shape, minval, dtype=x.dtype)) - x = x[mask] - a = xp.abs(x) - ph.assert_shape("abs", a.shape, x.shape) - assert ah.all(ah.logical_not(ah.negative_mathematical_sign(a))), "abs(x) did not have positive sign" + op_name = func_to_op[elwise_func_name] + params = [ + make_param(elwise_func_name, FuncType.FUNC, False), + make_param(op_name, FuncType.OP, False), + make_param(op_name, FuncType.OP, True), + ] + iop_name = f"__i{op_name[2:]}" + if iop_name in dh.inplace_op_to_symbol.keys(): + params.append(make_param(iop_name, FuncType.IOP, False)) + params.append(make_param(iop_name, FuncType.IOP, True)) + + return params + + +@pytest.mark.parametrize(unary_argnames, make_unary_params("abs", dh.numeric_dtypes)) +@given(data=st.data()) +def test_abs(func_name, func, strat, data): + x = data.draw(strat, label="x") + if x.dtype in dh.int_dtypes: + # abs of the smallest representable negative integer is not defined + mask = xp.not_equal( + x, ah.full(x.shape, dh.dtype_ranges[x.dtype].min, dtype=x.dtype) + ) + x = x[mask] + out = func(x) + ph.assert_shape(func_name, out.shape, x.shape) + assert ah.all( + ah.logical_not(ah.negative_mathematical_sign(out)) + ), f"out elements not all positively signed [{func_name}()]\n{out=}" less_zero = ah.negative_mathematical_sign(x) negx = ah.negative(x) # abs(x) = -x for x < 0 - ah.assert_exactly_equal(a[less_zero], negx[less_zero]) + ah.assert_exactly_equal(out[less_zero], negx[less_zero]) # abs(x) = x for x >= 0 - ah.assert_exactly_equal(a[ah.logical_not(less_zero)], x[ah.logical_not(less_zero)]) + ah.assert_exactly_equal( + out[ah.logical_not(less_zero)], x[ah.logical_not(less_zero)] + ) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_acos(x): - a = xp.acos(x) - ph.assert_shape("acos", a.shape, x.shape) + res = xp.acos(x) + ph.assert_shape("acos", res.shape, x.shape) ONE = ah.one(x.shape, x.dtype) - # Here (and elsewhere), should technically be a.dtype, but this is the + # Here (and elsewhere), should technically be res.dtype, but this is the # same as x.dtype, as tested by the type_promotion tests. PI = ah.π(x.shape, x.dtype) ZERO = ah.zero(x.shape, x.dtype) domain = ah.inrange(x, -ONE, ONE) - codomain = ah.inrange(a, ZERO, PI) + codomain = ah.inrange(res, ZERO, PI) # acos maps [-1, 1] to [0, pi]. Values outside this domain are mapped to # nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_acosh(x): - a = xp.acosh(x) - ph.assert_shape("acosh", a.shape, x.shape) + res = xp.acosh(x) + ph.assert_shape("acosh", res.shape, x.shape) ONE = ah.one(x.shape, x.dtype) INFINITY = ah.infinity(x.shape, x.dtype) ZERO = ah.zero(x.shape, x.dtype) domain = ah.inrange(x, ONE, INFINITY) - codomain = ah.inrange(a, ZERO, INFINITY) + codomain = ah.inrange(res, ZERO, INFINITY) # acosh maps [-1, inf] to [0, inf]. Values outside this domain are mapped # to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(*hh.two_mutual_arrays(dh.numeric_dtypes)) -def test_add(x1, x2): - a = xp.add(x1, x2) - b = xp.add(x2, x1) - # add is commutative - ah.assert_exactly_equal(a, b) - # TODO: Test that add is actually addition +@pytest.mark.parametrize(binary_argnames, make_binary_params("add", dh.numeric_dtypes)) +@given(data=st.data()) +def test_add( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + try: + res = func(left, right) + except OverflowError: + reject() + + if not right_is_scalar: + # add is commutative + expected = func(right, left) + ah.assert_exactly_equal(res, expected) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_asin(x): - a = xp.asin(x) - ph.assert_shape("asin", a.shape, x.shape) + res = xp.asin(x) + ph.assert_shape("asin", res.shape, x.shape) ONE = ah.one(x.shape, x.dtype) PI = ah.π(x.shape, x.dtype) domain = ah.inrange(x, -ONE, ONE) - codomain = ah.inrange(a, -PI/2, PI/2) + codomain = ah.inrange(res, -PI / 2, PI / 2) # asin maps [-1, 1] to [-pi/2, pi/2]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_asinh(x): - a = xp.asinh(x) - ph.assert_shape("asinh", a.shape, x.shape) + res = xp.asinh(x) + ph.assert_shape("asinh", res.shape, x.shape) INFINITY = ah.infinity(x.shape, x.dtype) domain = ah.inrange(x, -INFINITY, INFINITY) - codomain = ah.inrange(a, -INFINITY, INFINITY) + codomain = ah.inrange(res, -INFINITY, INFINITY) # asinh maps [-inf, inf] to [-inf, inf]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_atan(x): - a = xp.atan(x) - ph.assert_shape("atan", a.shape, x.shape) + res = xp.atan(x) + ph.assert_shape("atan", res.shape, x.shape) INFINITY = ah.infinity(x.shape, x.dtype) PI = ah.π(x.shape, x.dtype) domain = ah.inrange(x, -INFINITY, INFINITY) - codomain = ah.inrange(a, -PI/2, PI/2) + codomain = ah.inrange(res, -PI / 2, PI / 2) # atan maps [-inf, inf] to [-pi/2, pi/2]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(*hh.two_mutual_arrays(dh.float_dtypes)) def test_atan2(x1, x2): - a = xp.atan2(x1, x2) + res = xp.atan2(x1, x2) INFINITY1 = ah.infinity(x1.shape, x1.dtype) INFINITY2 = ah.infinity(x2.shape, x2.dtype) - PI = ah.π(a.shape, a.dtype) + PI = ah.π(res.shape, res.dtype) domainx1 = ah.inrange(x1, -INFINITY1, INFINITY1) domainx2 = ah.inrange(x2, -INFINITY2, INFINITY2) - # codomain = ah.inrange(a, -PI, PI, 1e-5) - codomain = ah.inrange(a, -PI, PI) + # codomain = ah.inrange(res, -PI, PI, 1e-5) + codomain = ah.inrange(res, -PI, PI) # atan2 maps [-inf, inf] x [-inf, inf] to [-pi, pi]. Values outside # this domain are mapped to nan, which is already tested in the special # cases. @@ -138,204 +319,349 @@ def test_atan2(x1, x2): # This is equivalent to atan2(x1, x2) has the same sign as x1 when x2 is # finite. - posx1 = ah.positive_mathematical_sign(x1) - negx1 = ah.negative_mathematical_sign(x1) - posx2 = ah.positive_mathematical_sign(x2) - negx2 = ah.negative_mathematical_sign(x2) - posa = ah.positive_mathematical_sign(a) - nega = ah.negative_mathematical_sign(a) - ah.assert_exactly_equal(ah.logical_or(ah.logical_and(posx1, posx2), - ah.logical_and(posx1, negx2)), posa) - ah.assert_exactly_equal(ah.logical_or(ah.logical_and(negx1, posx2), - ah.logical_and(negx1, negx2)), nega) + pos_x1 = ah.positive_mathematical_sign(x1) + neg_x1 = ah.negative_mathematical_sign(x1) + pos_x2 = ah.positive_mathematical_sign(x2) + neg_x2 = ah.negative_mathematical_sign(x2) + pos_out = ah.positive_mathematical_sign(res) + neg_out = ah.negative_mathematical_sign(res) + ah.assert_exactly_equal( + ah.logical_or(ah.logical_and(pos_x1, pos_x2), ah.logical_and(pos_x1, neg_x2)), + pos_out, + ) + ah.assert_exactly_equal( + ah.logical_or(ah.logical_and(neg_x1, pos_x2), ah.logical_and(neg_x1, neg_x2)), + neg_out, + ) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_atanh(x): - a = xp.atanh(x) - ph.assert_shape("atanh", a.shape, x.shape) + res = xp.atanh(x) + ph.assert_shape("atanh", res.shape, x.shape) ONE = ah.one(x.shape, x.dtype) INFINITY = ah.infinity(x.shape, x.dtype) domain = ah.inrange(x, -ONE, ONE) - codomain = ah.inrange(a, -INFINITY, INFINITY) + codomain = ah.inrange(res, -INFINITY, INFINITY) # atanh maps [-1, 1] to [-inf, inf]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(*hh.two_mutual_arrays(dh.bool_and_all_int_dtypes)) -def test_bitwise_and(x1, x2): - out = xp.bitwise_and(x1, x2) - # TODO: generate indices without broadcasting arrays (see test_equal comment) - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("bitwise_and", out.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) - - # Compare against the Python & operator. - if out.dtype == xp.bool: - for idx in ah.ndindex(out.shape): - val1 = bool(_x1[idx]) - val2 = bool(_x2[idx]) - res = bool(out[idx]) - assert (val1 and val2) == res +@pytest.mark.parametrize( + binary_argnames, make_binary_params("bitwise_and", dh.bool_and_all_int_dtypes) +) +@given(data=st.data()) +def test_bitwise_and( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + res = func(left, right) + + if not right_is_scalar: + # TODO: generate indices without broadcasting arrays (see test_equal comment) + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape(func_name, res.shape, shape, repr_name=f"{res_name}.shape") + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + # Compare against the Python & operator. + if res.dtype == xp.bool: + for idx in ah.ndindex(res.shape): + s_left = bool(_left[idx]) + s_right = bool(_right[idx]) + s_res = bool(res[idx]) + assert (s_left and s_right) == s_res + else: + for idx in ah.ndindex(res.shape): + s_left = int(_left[idx]) + s_right = int(_right[idx]) + s_res = int(res[idx]) + s_and = ah.int_to_dtype( + s_left & s_right, + dh.dtype_nbits[res.dtype], + dh.dtype_signed[res.dtype], + ) + assert s_and == s_res + + +@pytest.mark.parametrize( + binary_argnames, make_binary_params("bitwise_left_shift", dh.all_int_dtypes) +) +@given(data=st.data()) +def test_bitwise_left_shift( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + if right_is_scalar: + assume(right >= 0) else: - for idx in ah.ndindex(out.shape): - val1 = int(_x1[idx]) - val2 = int(_x2[idx]) - res = int(out[idx]) - vals_and = val1 & val2 - vals_and = ah.int_to_dtype(vals_and, dh.dtype_nbits[out.dtype], dh.dtype_signed[out.dtype]) - assert vals_and == res - -@given(*hh.two_mutual_arrays(dh.all_int_dtypes)) -def test_bitwise_left_shift(x1, x2): - assume(not ah.any(ah.isnegative(x2))) - out = xp.bitwise_left_shift(x1, x2) - - # TODO: generate indices without broadcasting arrays (see test_equal comment) - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("bitwise_left_shift", out.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) - - # Compare against the Python << operator. - for idx in ah.ndindex(out.shape): - val1 = int(_x1[idx]) - val2 = int(_x2[idx]) - res = int(out[idx]) - # We avoid shifting very large ints - vals_shift = val1 << val2 if val2 < dh.dtype_nbits[out.dtype] else 0 - vals_shift = ah.int_to_dtype(vals_shift, dh.dtype_nbits[out.dtype], dh.dtype_signed[out.dtype]) - assert vals_shift == res - -@given(xps.arrays(dtype=hh.integer_or_boolean_dtypes, shape=hh.shapes())) -def test_bitwise_invert(x): - out = xp.bitwise_invert(x) - ph.assert_shape("bitwise_invert", out.shape, x.shape) + assume(not ah.any(ah.isnegative(right))) + + res = func(left, right) + + if not right_is_scalar: + # TODO: generate indices without broadcasting arrays (see test_equal comment) + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape(func_name, res.shape, shape, repr_name=f"{res_name}.shape") + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + # Compare against the Python << operator. + for idx in ah.ndindex(res.shape): + s_left = int(_left[idx]) + s_right = int(_right[idx]) + s_res = int(res[idx]) + s_shift = ah.int_to_dtype( + # We avoid shifting very large ints + s_left << s_right if s_right < dh.dtype_nbits[res.dtype] else 0, + dh.dtype_nbits[res.dtype], + dh.dtype_signed[res.dtype], + ) + assert s_shift == s_res + + +@pytest.mark.parametrize( + unary_argnames, make_unary_params("bitwise_invert", dh.bool_and_all_int_dtypes) +) +@given(data=st.data()) +def test_bitwise_invert(func_name, func, strat, data): + x = data.draw(strat, label="x") + + out = func(x) + + ph.assert_shape(func_name, out.shape, x.shape) # Compare against the Python ~ operator. if out.dtype == xp.bool: for idx in ah.ndindex(out.shape): - val = bool(x[idx]) - res = bool(out[idx]) - assert (not val) == res + s_x = bool(x[idx]) + s_out = bool(out[idx]) + assert (not s_x) == s_out else: for idx in ah.ndindex(out.shape): - val = int(x[idx]) - res = int(out[idx]) - val_invert = ~val - val_invert = ah.int_to_dtype(val_invert, dh.dtype_nbits[out.dtype], dh.dtype_signed[out.dtype]) - assert val_invert == res - -@given(*hh.two_mutual_arrays(dh.bool_and_all_int_dtypes)) -def test_bitwise_or(x1, x2): - out = xp.bitwise_or(x1, x2) - - # TODO: generate indices without broadcasting arrays (see test_equal comment) - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("bitwise_or", out.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) - - # Compare against the Python | operator. - if out.dtype == xp.bool: - for idx in ah.ndindex(out.shape): - val1 = bool(_x1[idx]) - val2 = bool(_x2[idx]) - res = bool(out[idx]) - assert (val1 or val2) == res + s_x = int(x[idx]) + s_out = int(out[idx]) + s_invert = ah.int_to_dtype( + ~s_x, dh.dtype_nbits[out.dtype], dh.dtype_signed[out.dtype] + ) + assert s_invert == s_out + + +@pytest.mark.parametrize( + binary_argnames, make_binary_params("bitwise_or", dh.bool_and_all_int_dtypes) +) +@given(data=st.data()) +def test_bitwise_or( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + res = func(left, right) + + if not right_is_scalar: + # TODO: generate indices without broadcasting arrays (see test_equal comment) + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape(func_name, res.shape, shape, repr_name=f"{res_name}.shape") + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + # Compare against the Python | operator. + if res.dtype == xp.bool: + for idx in ah.ndindex(res.shape): + s_left = bool(_left[idx]) + s_right = bool(_right[idx]) + s_res = bool(res[idx]) + assert (s_left or s_right) == s_res + else: + for idx in ah.ndindex(res.shape): + s_left = int(_left[idx]) + s_right = int(_right[idx]) + s_res = int(res[idx]) + s_or = ah.int_to_dtype( + s_left | s_right, + dh.dtype_nbits[res.dtype], + dh.dtype_signed[res.dtype], + ) + assert s_or == s_res + + +@pytest.mark.parametrize( + binary_argnames, make_binary_params("bitwise_right_shift", dh.all_int_dtypes) +) +@given(data=st.data()) +def test_bitwise_right_shift( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + if right_is_scalar: + assume(right >= 0) else: - for idx in ah.ndindex(out.shape): - val1 = int(_x1[idx]) - val2 = int(_x2[idx]) - res = int(out[idx]) - vals_or = val1 | val2 - vals_or = ah.int_to_dtype(vals_or, dh.dtype_nbits[out.dtype], dh.dtype_signed[out.dtype]) - assert vals_or == res - -@given(*hh.two_mutual_arrays(dh.all_int_dtypes)) -def test_bitwise_right_shift(x1, x2): - assume(not ah.any(ah.isnegative(x2))) - out = xp.bitwise_right_shift(x1, x2) - - # TODO: generate indices without broadcasting arrays (see test_equal comment) - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("bitwise_right_shift", out.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) + assume(not ah.any(ah.isnegative(right))) - # Compare against the Python >> operator. - for idx in ah.ndindex(out.shape): - val1 = int(_x1[idx]) - val2 = int(_x2[idx]) - res = int(out[idx]) - vals_shift = val1 >> val2 - vals_shift = ah.int_to_dtype(vals_shift, dh.dtype_nbits[out.dtype], dh.dtype_signed[out.dtype]) - assert vals_shift == res + res = func(left, right) -@given(*hh.two_mutual_arrays(dh.bool_and_all_int_dtypes)) -def test_bitwise_xor(x1, x2): - out = xp.bitwise_xor(x1, x2) - - # TODO: generate indices without broadcasting arrays (see test_equal comment) - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("bitwise_xor", out.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) + if not right_is_scalar: + # TODO: generate indices without broadcasting arrays (see test_equal comment) + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape( + "bitwise_right_shift", res.shape, shape, repr_name=f"{res_name}.shape" + ) + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + # Compare against the Python >> operator. + for idx in ah.ndindex(res.shape): + s_left = int(_left[idx]) + s_right = int(_right[idx]) + s_res = int(res[idx]) + s_shift = ah.int_to_dtype( + s_left >> s_right, dh.dtype_nbits[res.dtype], dh.dtype_signed[res.dtype] + ) + assert s_shift == s_res + + +@pytest.mark.parametrize( + binary_argnames, make_binary_params("bitwise_xor", dh.bool_and_all_int_dtypes) +) +@given(data=st.data()) +def test_bitwise_xor( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + res = func(left, right) + + if not right_is_scalar: + # TODO: generate indices without broadcasting arrays (see test_equal comment) + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape(func_name, res.shape, shape, repr_name=f"{res_name}.shape") + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + # Compare against the Python ^ operator. + if res.dtype == xp.bool: + for idx in ah.ndindex(res.shape): + s_left = bool(_left[idx]) + s_right = bool(_right[idx]) + s_res = bool(res[idx]) + assert (s_left ^ s_right) == s_res + else: + for idx in ah.ndindex(res.shape): + s_left = int(_left[idx]) + s_right = int(_right[idx]) + s_res = int(res[idx]) + s_xor = ah.int_to_dtype( + s_left ^ s_right, + dh.dtype_nbits[res.dtype], + dh.dtype_signed[res.dtype], + ) + assert s_xor == s_res - # Compare against the Python ^ operator. - if out.dtype == xp.bool: - for idx in ah.ndindex(out.shape): - val1 = bool(_x1[idx]) - val2 = bool(_x2[idx]) - res = bool(out[idx]) - assert (val1 ^ val2) == res - else: - for idx in ah.ndindex(out.shape): - val1 = int(_x1[idx]) - val2 = int(_x2[idx]) - res = int(out[idx]) - vals_xor = val1 ^ val2 - vals_xor = ah.int_to_dtype(vals_xor, dh.dtype_nbits[out.dtype], dh.dtype_signed[out.dtype]) - assert vals_xor == res @given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_ceil(x): # This test is almost identical to test_floor() - a = xp.ceil(x) - ph.assert_shape("ceil", a.shape, x.shape) + res = xp.ceil(x) + ph.assert_shape("ceil", res.shape, x.shape) finite = ah.isfinite(x) - ah.assert_integral(a[finite]) - assert ah.all(ah.less_equal(x[finite], a[finite])) - assert ah.all(ah.less_equal(a[finite] - x[finite], ah.one(x[finite].shape, x.dtype))) + ah.assert_integral(res[finite]) + assert ah.all(ah.less_equal(x[finite], res[finite])) + assert ah.all( + ah.less_equal(res[finite] - x[finite], ah.one(x[finite].shape, x.dtype)) + ) integers = ah.isintegral(x) - ah.assert_exactly_equal(a[integers], x[integers]) + ah.assert_exactly_equal(res[integers], x[integers]) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_cos(x): - a = xp.cos(x) - ph.assert_shape("cos", a.shape, x.shape) + res = xp.cos(x) + ph.assert_shape("cos", res.shape, x.shape) ONE = ah.one(x.shape, x.dtype) INFINITY = ah.infinity(x.shape, x.dtype) domain = ah.inrange(x, -INFINITY, INFINITY, open=True) - codomain = ah.inrange(a, -ONE, ONE) + codomain = ah.inrange(res, -ONE, ONE) # cos maps (-inf, inf) to [-1, 1]. Values outside this domain are mapped # to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_cosh(x): - a = xp.cosh(x) - ph.assert_shape("cosh", a.shape, x.shape) + res = xp.cosh(x) + ph.assert_shape("cosh", res.shape, x.shape) INFINITY = ah.infinity(x.shape, x.dtype) domain = ah.inrange(x, -INFINITY, INFINITY) - codomain = ah.inrange(a, -INFINITY, INFINITY) + codomain = ah.inrange(res, -INFINITY, INFINITY) # cosh maps [-inf, inf] to [-inf, inf]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(*hh.two_mutual_arrays(dh.float_dtypes)) -def test_divide(x1, x2): - xp.divide(x1, x2) + +@pytest.mark.parametrize(binary_argnames, make_binary_params("divide", dh.float_dtypes)) +@given(data=st.data()) +def test_divide( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + func(left, right) + # There isn't much we can test here. The spec doesn't require any behavior # beyond the special cases, and indeed, there aren't many mathematical # properties of division that strictly hold for floating-point numbers. We @@ -343,9 +669,24 @@ def test_divide(x1, x2): # have those sorts in general for this module. -@given(*hh.two_mutual_arrays()) -def test_equal(x1, x2): - a = ah.equal(x1, x2) +@pytest.mark.parametrize(binary_argnames, make_binary_params("equal", dh.all_dtypes)) +@given(data=st.data()) +def test_equal( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + out = func(left, right) + # NOTE: ah.assert_exactly_equal() itself uses ah.equal(), so we must be careful # not to use it here. Otherwise, the test would be circular and # meaningless. Instead, we implement this by iterating every element of @@ -354,319 +695,393 @@ def test_equal(x1, x2): # always return bool (greater(), greater_equal(), less(), less_equal(), # and not_equal()). - # First we broadcast the arrays so that they can be indexed uniformly. - # TODO: it should be possible to skip this step if we instead generate - # indices to x1 and x2 that correspond to the broadcasted shapes. This - # would avoid the dependence in this test on broadcast_to(). - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("equal", a.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) + if not right_is_scalar: + # First we broadcast the arrays so that they can be indexed uniformly. + # TODO: it should be possible to skip this step if we instead generate + # indices to x1 and x2 that correspond to the broadcasted shapes. This + # would avoid the dependence in this test on broadcast_to(). + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape(func_name, out.shape, shape) + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + # Second, manually promote the dtypes. This is important. If the internal + # type promotion in ah.equal() is wrong, it will not be directly visible in + # the output type, but it can lead to wrong answers. For example, + # ah.equal(array(1.0, dtype=xp.float32), array(1.00000001, dtype=xp.float64)) will + # be wrong if the float64 is downcast to float32. # be wrong if the + # xp.float64 is downcast to float32. See the comment on + # test_elementwise_function_two_arg_bool_type_promotion() in + # test_type_promotion.py. The type promotion for ah.equal() is not *really* + # tested in that file, because doing so requires doing the consistency + + # check we do here rather than just checking the res dtype. + promoted_dtype = dh.promotion_table[left.dtype, right.dtype] + _left = ah.asarray(_left, dtype=promoted_dtype) + _right = ah.asarray(_right, dtype=promoted_dtype) + + scalar_type = dh.get_scalar_type(promoted_dtype) + for idx in ah.ndindex(shape): + x1_idx = _left[idx] + x2_idx = _right[idx] + out_idx = out[idx] + assert out_idx.shape == x1_idx.shape == x2_idx.shape # sanity check + assert bool(out_idx) == (scalar_type(x1_idx) == scalar_type(x2_idx)) - # Second, manually promote the dtypes. This is important. If the internal - # type promotion in ah.equal() is wrong, it will not be directly visible in - # the output type, but it can lead to wrong answers. For example, - # ah.equal(array(1.0, dtype=xp.float32), array(1.00000001, dtype=xp.float64)) will - # be wrong if the float64 is downcast to float32. # be wrong if the - # xp.float64 is downcast to float32. See the comment on - # test_elementwise_function_two_arg_bool_type_promotion() in - # test_type_promotion.py. The type promotion for ah.equal() is not *really* - # tested in that file, because doing so requires doing the consistency - - # check we do here rather than just checking the result dtype. - promoted_dtype = dh.promotion_table[x1.dtype, x2.dtype] - _x1 = ah.asarray(_x1, dtype=promoted_dtype) - _x2 = ah.asarray(_x2, dtype=promoted_dtype) - - if dh.is_int_dtype(promoted_dtype): - scalar_func = int - elif dh.is_float_dtype(promoted_dtype): - scalar_func = float - else: - scalar_func = bool - for idx in ah.ndindex(shape): - aidx = a[idx] - x1idx = _x1[idx] - x2idx = _x2[idx] - # Sanity check - assert aidx.shape == x1idx.shape == x2idx.shape - assert bool(aidx) == (scalar_func(x1idx) == scalar_func(x2idx)) @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_exp(x): - a = xp.exp(x) - ph.assert_shape("exp", a.shape, x.shape) + res = xp.exp(x) + ph.assert_shape("exp", res.shape, x.shape) INFINITY = ah.infinity(x.shape, x.dtype) ZERO = ah.zero(x.shape, x.dtype) domain = ah.inrange(x, -INFINITY, INFINITY) - codomain = ah.inrange(a, ZERO, INFINITY) + codomain = ah.inrange(res, ZERO, INFINITY) # exp maps [-inf, inf] to [0, inf]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_expm1(x): - a = xp.expm1(x) - ph.assert_shape("expm1", a.shape, x.shape) + res = xp.expm1(x) + ph.assert_shape("expm1", res.shape, x.shape) INFINITY = ah.infinity(x.shape, x.dtype) NEGONE = -ah.one(x.shape, x.dtype) domain = ah.inrange(x, -INFINITY, INFINITY) - codomain = ah.inrange(a, NEGONE, INFINITY) + codomain = ah.inrange(res, NEGONE, INFINITY) # expm1 maps [-inf, inf] to [1, inf]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_floor(x): # This test is almost identical to test_ceil - a = xp.floor(x) - ph.assert_shape("floor", a.shape, x.shape) + res = xp.floor(x) + ph.assert_shape("floor", res.shape, x.shape) finite = ah.isfinite(x) - ah.assert_integral(a[finite]) - assert ah.all(ah.less_equal(a[finite], x[finite])) - assert ah.all(ah.less_equal(x[finite] - a[finite], ah.one(x[finite].shape, x.dtype))) + ah.assert_integral(res[finite]) + assert ah.all(ah.less_equal(res[finite], x[finite])) + assert ah.all( + ah.less_equal(x[finite] - res[finite], ah.one(x[finite].shape, x.dtype)) + ) integers = ah.isintegral(x) - ah.assert_exactly_equal(a[integers], x[integers]) - -@given(*hh.two_mutual_arrays(dh.numeric_dtypes)) -def test_floor_divide(x1, x2): - if dh.is_int_dtype(x1.dtype): - # The spec does not specify the behavior for division by 0 for integer - # dtypes. A library may choose to raise an exception in this case, so - # we avoid passing it in entirely. - assume(not ah.any(x1 == 0) and not ah.any(x2 == 0)) - div = xp.divide( - ah.asarray(x1, dtype=xp.float64), - ah.asarray(x2, dtype=xp.float64), - ) + ah.assert_exactly_equal(res[integers], x[integers]) + + +@pytest.mark.parametrize( + binary_argnames, make_binary_params("floor_divide", dh.numeric_dtypes) +) +@given(data=st.data()) +def test_floor_divide( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat.filter(lambda x: not ah.any(x == 0)), label=left_sym) + right = data.draw(right_strat, label=right_sym) + if right_is_scalar: + assume(right != 0) else: - div = xp.divide(x1, x2) - - out = xp.floor_divide(x1, x2) - - # TODO: The spec doesn't clearly specify the behavior of floor_divide on - # infinities. See https://github.com/data-apis/array-api/issues/199. - finite = ah.isfinite(div) - ah.assert_integral(out[finite]) + assume(not ah.any(right == 0)) + + res = func(left, right) + + if not right_is_scalar: + if dh.is_int_dtype(left.dtype): + # The spec does not specify the behavior for division by 0 for integer + # dtypes. A library may choose to raise an exception in this case, so + # we avoid passing it in entirely. + div = xp.divide( + ah.asarray(left, dtype=xp.float64), + ah.asarray(right, dtype=xp.float64), + ) + else: + div = xp.divide(left, right) + + # TODO: The spec doesn't clearly specify the behavior of floor_divide on + # infinities. See https://github.com/data-apis/array-api/issues/199. + finite = ah.isfinite(div) + ah.assert_integral(res[finite]) # TODO: Test the exact output for floor_divide. -@given(*hh.two_mutual_arrays(dh.numeric_dtypes)) -def test_greater(x1, x2): - a = xp.greater(x1, x2) - - # See the comments in test_equal() for a description of how this test - # works. - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("greater", a.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) - - promoted_dtype = dh.promotion_table[x1.dtype, x2.dtype] - _x1 = ah.asarray(_x1, dtype=promoted_dtype) - _x2 = ah.asarray(_x2, dtype=promoted_dtype) - if dh.is_int_dtype(promoted_dtype): - scalar_func = int - elif dh.is_float_dtype(promoted_dtype): - scalar_func = float - else: - scalar_func = bool - for idx in ah.ndindex(shape): - aidx = a[idx] - x1idx = _x1[idx] - x2idx = _x2[idx] - # Sanity check - assert aidx.shape == x1idx.shape == x2idx.shape - assert bool(aidx) == (scalar_func(x1idx) > scalar_func(x2idx)) - -@given(*hh.two_mutual_arrays(dh.numeric_dtypes)) -def test_greater_equal(x1, x2): - a = xp.greater_equal(x1, x2) - - # See the comments in test_equal() for a description of how this test - # works. - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("greater_equal", a.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) +@pytest.mark.parametrize( + binary_argnames, make_binary_params("greater", dh.numeric_dtypes) +) +@given(data=st.data()) +def test_greater( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + out = func(left, right) + + if not right_is_scalar: + # TODO: generate indices without broadcasting arrays (see test_equal comment) + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape(func_name, out.shape, shape) + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + promoted_dtype = dh.promotion_table[left.dtype, right.dtype] + _left = ah.asarray(_left, dtype=promoted_dtype) + _right = ah.asarray(_right, dtype=promoted_dtype) + + scalar_type = dh.get_scalar_type(promoted_dtype) + for idx in ah.ndindex(shape): + out_idx = out[idx] + x1_idx = _left[idx] + x2_idx = _right[idx] + assert out_idx.shape == x1_idx.shape == x2_idx.shape # sanity check + assert bool(out_idx) == (scalar_type(x1_idx) > scalar_type(x2_idx)) + + +@pytest.mark.parametrize( + binary_argnames, make_binary_params("greater_equal", dh.numeric_dtypes) +) +@given(data=st.data()) +def test_greater_equal( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + out = func(left, right) + + if not right_is_scalar: + # TODO: generate indices without broadcasting arrays (see test_equal comment) + + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape(func_name, out.shape, shape) + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + promoted_dtype = dh.promotion_table[left.dtype, right.dtype] + _left = ah.asarray(_left, dtype=promoted_dtype) + _right = ah.asarray(_right, dtype=promoted_dtype) + + scalar_type = dh.get_scalar_type(promoted_dtype) + for idx in ah.ndindex(shape): + out_idx = out[idx] + x1_idx = _left[idx] + x2_idx = _right[idx] + assert out_idx.shape == x1_idx.shape == x2_idx.shape # sanity check + assert bool(out_idx) == (scalar_type(x1_idx) >= scalar_type(x2_idx)) - promoted_dtype = dh.promotion_table[x1.dtype, x2.dtype] - _x1 = ah.asarray(_x1, dtype=promoted_dtype) - _x2 = ah.asarray(_x2, dtype=promoted_dtype) - - if dh.is_int_dtype(promoted_dtype): - scalar_func = int - elif dh.is_float_dtype(promoted_dtype): - scalar_func = float - else: - scalar_func = bool - for idx in ah.ndindex(shape): - aidx = a[idx] - x1idx = _x1[idx] - x2idx = _x2[idx] - # Sanity check - assert aidx.shape == x1idx.shape == x2idx.shape - assert bool(aidx) == (scalar_func(x1idx) >= scalar_func(x2idx)) @given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_isfinite(x): - a = ah.isfinite(x) - ph.assert_shape("isfinite", a.shape, x.shape) + res = ah.isfinite(x) + ph.assert_shape("isfinite", res.shape, x.shape) if dh.is_int_dtype(x.dtype): - ah.assert_exactly_equal(a, ah.true(x.shape)) + ah.assert_exactly_equal(res, ah.true(x.shape)) # Test that isfinite, isinf, and isnan are self-consistent. inf = ah.logical_or(xp.isinf(x), ah.isnan(x)) - ah.assert_exactly_equal(a, ah.logical_not(inf)) + ah.assert_exactly_equal(res, ah.logical_not(inf)) # Test the exact value by comparing to the math version if dh.is_float_dtype(x.dtype): for idx in ah.ndindex(x.shape): s = float(x[idx]) - assert bool(a[idx]) == math.isfinite(s) + assert bool(res[idx]) == math.isfinite(s) + @given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_isinf(x): - a = xp.isinf(x) + res = xp.isinf(x) - ph.assert_shape("isinf", a.shape, x.shape) + ph.assert_shape("isinf", res.shape, x.shape) if dh.is_int_dtype(x.dtype): - ah.assert_exactly_equal(a, ah.false(x.shape)) + ah.assert_exactly_equal(res, ah.false(x.shape)) finite_or_nan = ah.logical_or(ah.isfinite(x), ah.isnan(x)) - ah.assert_exactly_equal(a, ah.logical_not(finite_or_nan)) + ah.assert_exactly_equal(res, ah.logical_not(finite_or_nan)) # Test the exact value by comparing to the math version if dh.is_float_dtype(x.dtype): for idx in ah.ndindex(x.shape): s = float(x[idx]) - assert bool(a[idx]) == math.isinf(s) + assert bool(res[idx]) == math.isinf(s) + @given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_isnan(x): - a = ah.isnan(x) + res = ah.isnan(x) - ph.assert_shape("isnan", a.shape, x.shape) + ph.assert_shape("isnan", res.shape, x.shape) if dh.is_int_dtype(x.dtype): - ah.assert_exactly_equal(a, ah.false(x.shape)) + ah.assert_exactly_equal(res, ah.false(x.shape)) finite_or_inf = ah.logical_or(ah.isfinite(x), xp.isinf(x)) - ah.assert_exactly_equal(a, ah.logical_not(finite_or_inf)) + ah.assert_exactly_equal(res, ah.logical_not(finite_or_inf)) # Test the exact value by comparing to the math version if dh.is_float_dtype(x.dtype): for idx in ah.ndindex(x.shape): s = float(x[idx]) - assert bool(a[idx]) == math.isnan(s) - -@given(*hh.two_mutual_arrays(dh.numeric_dtypes)) -def test_less(x1, x2): - a = ah.less(x1, x2) - - # See the comments in test_equal() for a description of how this test - # works. - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("less", a.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) + assert bool(res[idx]) == math.isnan(s) + + +@pytest.mark.parametrize(binary_argnames, make_binary_params("less", dh.numeric_dtypes)) +@given(data=st.data()) +def test_less( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + out = func(left, right) + + if not right_is_scalar: + # TODO: generate indices without broadcasting arrays (see test_equal comment) + + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape(func_name, out.shape, shape) + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + promoted_dtype = dh.promotion_table[left.dtype, right.dtype] + _left = ah.asarray(_left, dtype=promoted_dtype) + _right = ah.asarray(_right, dtype=promoted_dtype) + + scalar_type = dh.get_scalar_type(promoted_dtype) + for idx in ah.ndindex(shape): + x1_idx = _left[idx] + x2_idx = _right[idx] + out_idx = out[idx] + assert out_idx.shape == x1_idx.shape == x2_idx.shape # sanity check + assert bool(out_idx) == (scalar_type(x1_idx) < scalar_type(x2_idx)) + + +@pytest.mark.parametrize( + binary_argnames, make_binary_params("less_equal", dh.numeric_dtypes) +) +@given(data=st.data()) +def test_less_equal( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + out = func(left, right) + + if not right_is_scalar: + # TODO: generate indices without broadcasting arrays (see test_equal comment) + + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape(func_name, out.shape, shape) + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + promoted_dtype = dh.promotion_table[left.dtype, right.dtype] + _left = ah.asarray(_left, dtype=promoted_dtype) + _right = ah.asarray(_right, dtype=promoted_dtype) + + scalar_type = dh.get_scalar_type(promoted_dtype) + for idx in ah.ndindex(shape): + x1_idx = _left[idx] + x2_idx = _right[idx] + out_idx = out[idx] + assert out_idx.shape == x1_idx.shape == x2_idx.shape # sanity check + assert bool(out_idx) == (scalar_type(x1_idx) <= scalar_type(x2_idx)) - promoted_dtype = dh.promotion_table[x1.dtype, x2.dtype] - _x1 = ah.asarray(_x1, dtype=promoted_dtype) - _x2 = ah.asarray(_x2, dtype=promoted_dtype) - - if dh.is_int_dtype(promoted_dtype): - scalar_func = int - elif dh.is_float_dtype(promoted_dtype): - scalar_func = float - else: - scalar_func = bool - for idx in ah.ndindex(shape): - aidx = a[idx] - x1idx = _x1[idx] - x2idx = _x2[idx] - # Sanity check - assert aidx.shape == x1idx.shape == x2idx.shape - assert bool(aidx) == (scalar_func(x1idx) < scalar_func(x2idx)) - -@given(*hh.two_mutual_arrays(dh.numeric_dtypes)) -def test_less_equal(x1, x2): - a = ah.less_equal(x1, x2) - - # See the comments in test_equal() for a description of how this test - # works. - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("less_equal", a.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) - - promoted_dtype = dh.promotion_table[x1.dtype, x2.dtype] - _x1 = ah.asarray(_x1, dtype=promoted_dtype) - _x2 = ah.asarray(_x2, dtype=promoted_dtype) - - if dh.is_int_dtype(promoted_dtype): - scalar_func = int - elif dh.is_float_dtype(promoted_dtype): - scalar_func = float - else: - scalar_func = bool - for idx in ah.ndindex(shape): - aidx = a[idx] - x1idx = _x1[idx] - x2idx = _x2[idx] - # Sanity check - assert aidx.shape == x1idx.shape == x2idx.shape - assert bool(aidx) == (scalar_func(x1idx) <= scalar_func(x2idx)) @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_log(x): - a = xp.log(x) + res = xp.log(x) - ph.assert_shape("log", a.shape, x.shape) + ph.assert_shape("log", res.shape, x.shape) INFINITY = ah.infinity(x.shape, x.dtype) ZERO = ah.zero(x.shape, x.dtype) domain = ah.inrange(x, ZERO, INFINITY) - codomain = ah.inrange(a, -INFINITY, INFINITY) + codomain = ah.inrange(res, -INFINITY, INFINITY) # log maps [0, inf] to [-inf, inf]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_log1p(x): - a = xp.log1p(x) - ph.assert_shape("log1p", a.shape, x.shape) + res = xp.log1p(x) + ph.assert_shape("log1p", res.shape, x.shape) INFINITY = ah.infinity(x.shape, x.dtype) NEGONE = -ah.one(x.shape, x.dtype) codomain = ah.inrange(x, NEGONE, INFINITY) - domain = ah.inrange(a, -INFINITY, INFINITY) + domain = ah.inrange(res, -INFINITY, INFINITY) # log1p maps [1, inf] to [-inf, inf]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_log2(x): - a = xp.log2(x) - ph.assert_shape("log2", a.shape, x.shape) + res = xp.log2(x) + ph.assert_shape("log2", res.shape, x.shape) INFINITY = ah.infinity(x.shape, x.dtype) ZERO = ah.zero(x.shape, x.dtype) domain = ah.inrange(x, ZERO, INFINITY) - codomain = ah.inrange(a, -INFINITY, INFINITY) + codomain = ah.inrange(res, -INFINITY, INFINITY) # log2 maps [0, inf] to [-inf, inf]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_log10(x): - a = xp.log10(x) - ph.assert_shape("log10", a.shape, x.shape) + res = xp.log10(x) + ph.assert_shape("log10", res.shape, x.shape) INFINITY = ah.infinity(x.shape, x.dtype) ZERO = ah.zero(x.shape, x.dtype) domain = ah.inrange(x, ZERO, INFINITY) - codomain = ah.inrange(a, -INFINITY, INFINITY) + codomain = ah.inrange(res, -INFINITY, INFINITY) # log10 maps [0, inf] to [-inf, inf]. Values outside this domain are # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) + @given(*hh.two_mutual_arrays(dh.float_dtypes)) def test_logaddexp(x1, x2): xp.logaddexp(x1, x2) @@ -674,68 +1089,96 @@ def test_logaddexp(x1, x2): # that this is indeed an approximation of log(exp(x1) + exp(x2)), but we # don't have tests for this sort of thing for any functions yet. + @given(*hh.two_mutual_arrays([xp.bool])) def test_logical_and(x1, x2): - a = ah.logical_and(x1, x2) + out = ah.logical_and(x1, x2) # See the comments in test_equal shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("logical_and", a.shape, shape) + ph.assert_shape("logical_and", out.shape, shape) _x1 = xp.broadcast_to(x1, shape) _x2 = xp.broadcast_to(x2, shape) for idx in ah.ndindex(shape): - assert a[idx] == (bool(_x1[idx]) and bool(_x2[idx])) + assert out[idx] == (bool(_x1[idx]) and bool(_x2[idx])) + @given(xps.arrays(dtype=xp.bool, shape=hh.shapes())) def test_logical_not(x): - a = ah.logical_not(x) - ph.assert_shape("logical_not", a.shape, x.shape) + out = ah.logical_not(x) + ph.assert_shape("logical_not", out.shape, x.shape) for idx in ah.ndindex(x.shape): - assert a[idx] == (not bool(x[idx])) + assert out[idx] == (not bool(x[idx])) + @given(*hh.two_mutual_arrays([xp.bool])) def test_logical_or(x1, x2): - a = ah.logical_or(x1, x2) + out = ah.logical_or(x1, x2) # See the comments in test_equal shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("logical_or", a.shape, shape) + ph.assert_shape("logical_or", out.shape, shape) _x1 = xp.broadcast_to(x1, shape) _x2 = xp.broadcast_to(x2, shape) for idx in ah.ndindex(shape): - assert a[idx] == (bool(_x1[idx]) or bool(_x2[idx])) + assert out[idx] == (bool(_x1[idx]) or bool(_x2[idx])) + @given(*hh.two_mutual_arrays([xp.bool])) def test_logical_xor(x1, x2): - a = xp.logical_xor(x1, x2) + out = xp.logical_xor(x1, x2) # See the comments in test_equal shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("logical_xor", a.shape, shape) + ph.assert_shape("logical_xor", out.shape, shape) _x1 = xp.broadcast_to(x1, shape) _x2 = xp.broadcast_to(x2, shape) for idx in ah.ndindex(shape): - assert a[idx] == (bool(_x1[idx]) ^ bool(_x2[idx])) - -@given(*hh.two_mutual_arrays(dh.numeric_dtypes)) -def test_multiply(x1, x2): - a = xp.multiply(x1, x2) - - b = xp.multiply(x2, x1) - # multiply is commutative - ah.assert_exactly_equal(a, b) - -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) -def test_negative(x): - out = ah.negative(x) - - ph.assert_shape("negative", out.shape, x.shape) + assert out[idx] == (bool(_x1[idx]) ^ bool(_x2[idx])) + + +@pytest.mark.parametrize( + binary_argnames, make_binary_params("multiply", dh.numeric_dtypes) +) +@given(data=st.data()) +def test_multiply( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + res = func(left, right) + + if not right_is_scalar: + # multiply is commutative + expected = func(right, left) + ah.assert_exactly_equal(res, expected) + + +@pytest.mark.parametrize( + unary_argnames, make_unary_params("negative", dh.numeric_dtypes) +) +@given(data=st.data()) +def test_negative(func_name, func, strat, data): + x = data.draw(strat, label="x") + + out = func(x) + + ph.assert_shape(func_name, out.shape, x.shape) # Negation is an involution - ah.assert_exactly_equal(x, ah.negative(out)) + ah.assert_exactly_equal(x, func(out)) mask = ah.isfinite(x) if dh.is_int_dtype(x.dtype): @@ -746,75 +1189,131 @@ def test_negative(x): # Additive inverse y = xp.add(x[mask], out[mask]) - ZERO = ah.zero(x[mask].shape, x.dtype) - ah.assert_exactly_equal(y, ZERO) - - -@given(*hh.two_mutual_arrays()) -def test_not_equal(x1, x2): - a = xp.not_equal(x1, x2) - - # See the comments in test_equal() for a description of how this test - # works. - shape = broadcast_shapes(x1.shape, x2.shape) - ph.assert_shape("not_equal", a.shape, shape) - _x1 = xp.broadcast_to(x1, shape) - _x2 = xp.broadcast_to(x2, shape) - - promoted_dtype = dh.promotion_table[x1.dtype, x2.dtype] - _x1 = ah.asarray(_x1, dtype=promoted_dtype) - _x2 = ah.asarray(_x2, dtype=promoted_dtype) - - if dh.is_int_dtype(promoted_dtype): - scalar_func = int - elif dh.is_float_dtype(promoted_dtype): - scalar_func = float - else: - scalar_func = bool - for idx in ah.ndindex(shape): - aidx = a[idx] - x1idx = _x1[idx] - x2idx = _x2[idx] - # Sanity check - assert aidx.shape == x1idx.shape == x2idx.shape - assert bool(aidx) == (scalar_func(x1idx) != scalar_func(x2idx)) - - -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) -def test_positive(x): - out = xp.positive(x) - ph.assert_shape("positive", out.shape, x.shape) + ah.assert_exactly_equal(y, ah.zero(x[mask].shape, x.dtype)) + + +@pytest.mark.parametrize( + binary_argnames, make_binary_params("not_equal", dh.all_dtypes) +) +@given(data=st.data()) +def test_not_equal( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + out = func(left, right) + + if not right_is_scalar: + # TODO: generate indices without broadcasting arrays (see test_equal comment) + + shape = broadcast_shapes(left.shape, right.shape) + ph.assert_shape(func_name, out.shape, shape) + _left = xp.broadcast_to(left, shape) + _right = xp.broadcast_to(right, shape) + + promoted_dtype = dh.promotion_table[left.dtype, right.dtype] + _left = ah.asarray(_left, dtype=promoted_dtype) + _right = ah.asarray(_right, dtype=promoted_dtype) + + scalar_type = dh.get_scalar_type(promoted_dtype) + for idx in ah.ndindex(shape): + out_idx = out[idx] + x1_idx = _left[idx] + x2_idx = _right[idx] + assert out_idx.shape == x1_idx.shape == x2_idx.shape # sanity check + assert bool(out_idx) == (scalar_type(x1_idx) != scalar_type(x2_idx)) + + +@pytest.mark.parametrize( + unary_argnames, make_unary_params("positive", dh.numeric_dtypes) +) +@given(data=st.data()) +def test_positive(func_name, func, strat, data): + x = data.draw(strat, label="x") + + out = func(x) + + ph.assert_shape(func_name, out.shape, x.shape) # Positive does nothing ah.assert_exactly_equal(out, x) -@given(*hh.two_mutual_arrays(dh.float_dtypes)) -def test_pow(x1, x2): - xp.pow(x1, x2) + +@pytest.mark.parametrize(binary_argnames, make_binary_params("pow", dh.float_dtypes)) +@given(data=st.data()) +def test_pow( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + try: + func(left, right) + except OverflowError: + reject() + # There isn't much we can test here. The spec doesn't require any behavior # beyond the special cases, and indeed, there aren't many mathematical # properties of exponentiation that strictly hold for floating-point # numbers. We could test that this does implement IEEE 754 pow, but we # don't yet have those sorts in general for this module. -@given(*hh.two_mutual_arrays(dh.numeric_dtypes)) -def test_remainder(x1, x2): - assume(len(x1.shape) <= len(x2.shape)) # TODO: rework same sign testing below to remove this - out = xp.remainder(x1, x2) - # out and x2 should have the same sign. - # ah.assert_same_sign returns False for nans - not_nan = ah.logical_not(ah.logical_or(ah.isnan(out), ah.isnan(x2))) - ah.assert_same_sign(out[not_nan], x2[not_nan]) +@pytest.mark.parametrize( + binary_argnames, make_binary_params("remainder", dh.numeric_dtypes) +) +@given(data=st.data()) +def test_remainder( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + # TODO: rework same sign testing below to remove this + if not right_is_scalar: + assume(len(left.shape) <= len(right.shape)) + + res = func(left, right) + + if not right_is_scalar: + # res and x2 should have the same sign. + # ah.assert_same_sign returns False for nans + not_nan = ah.logical_not(ah.logical_or(ah.isnan(res), ah.isnan(left))) + ah.assert_same_sign(res[not_nan], right[not_nan]) + @given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_round(x): - a = xp.round(x) + res = xp.round(x) - ph.assert_shape("round", a.shape, x.shape) + ph.assert_shape("round", res.shape, x.shape) # Test that the res is integral finite = ah.isfinite(x) - ah.assert_integral(a[finite]) + ah.assert_integral(res[finite]) # round(x) should be the nearest integer to x. The case where there is a # tie (round to even) is already handled by the special cases tests. @@ -828,60 +1327,89 @@ def test_round(x): under = xp.subtract(ceil, x) round_down = ah.less(over, under) round_up = ah.less(under, over) - ah.assert_exactly_equal(a[round_down], floor[round_down]) - ah.assert_exactly_equal(a[round_up], ceil[round_up]) + ah.assert_exactly_equal(res[round_down], floor[round_down]) + ah.assert_exactly_equal(res[round_up], ceil[round_up]) + @given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_sign(x): - out = xp.sign(x) - ph.assert_shape("sign", out.shape, x.shape) + res = xp.sign(x) + ph.assert_shape("sign", res.shape, x.shape) # TODO + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_sin(x): - out = xp.sin(x) - ph.assert_shape("sin", out.shape, x.shape) + res = xp.sin(x) + ph.assert_shape("sin", res.shape, x.shape) # TODO + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_sinh(x): - out = xp.sinh(x) - ph.assert_shape("sinh", out.shape, x.shape) + res = xp.sinh(x) + ph.assert_shape("sinh", res.shape, x.shape) # TODO + @given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_square(x): - out = xp.square(x) - ph.assert_shape("square", out.shape, x.shape) + res = xp.square(x) + ph.assert_shape("square", res.shape, x.shape) + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_sqrt(x): - out = xp.sqrt(x) - ph.assert_shape("sqrt", out.shape, x.shape) + res = xp.sqrt(x) + ph.assert_shape("sqrt", res.shape, x.shape) + + +@pytest.mark.parametrize( + binary_argnames, make_binary_params("subtract", dh.numeric_dtypes) +) +@given(data=st.data()) +def test_subtract( + func_name, + func, + left_sym, + left_strat, + right_sym, + right_strat, + right_is_scalar, + res_name, + data, +): + left = data.draw(left_strat, label=left_sym) + right = data.draw(right_strat, label=right_sym) + + try: + func(left, right) + except OverflowError: + reject() + + # TODO -@given(*hh.two_mutual_arrays(dh.numeric_dtypes)) -def test_subtract(x1, x2): - # out = xp.subtract(x1, x2) - pass @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_tan(x): - out = xp.tan(x) - ph.assert_shape("tan", out.shape, x.shape) + res = xp.tan(x) + ph.assert_shape("tan", res.shape, x.shape) # TODO + @given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_tanh(x): - out = xp.tanh(x) - ph.assert_shape("tanh", out.shape, x.shape) + res = xp.tanh(x) + ph.assert_shape("tanh", res.shape, x.shape) # TODO + @given(xps.arrays(dtype=hh.numeric_dtypes, shape=xps.array_shapes())) def test_trunc(x): - out = xp.trunc(x) - ph.assert_shape("bitwise_trunc", out.shape, x.shape) + res = xp.trunc(x) + ph.assert_shape("bitwise_trunc", res.shape, x.shape) if dh.is_int_dtype(x.dtype): - ah.assert_exactly_equal(out, x) + ah.assert_exactly_equal(res, x) else: finite = ah.isfinite(x) - ah.assert_integral(out[finite]) + ah.assert_integral(res[finite]) diff --git a/array_api_tests/test_linalg.py b/array_api_tests/test_linalg.py index fc8ddecf..ac9f3359 100644 --- a/array_api_tests/test_linalg.py +++ b/array_api_tests/test_linalg.py @@ -29,7 +29,7 @@ from . import dtype_helpers as dh from . import pytest_helpers as ph -from .test_broadcasting import broadcast_shapes +from .algos import broadcast_shapes from . import _array_module from ._array_module import linalg diff --git a/array_api_tests/test_type_promotion.py b/array_api_tests/test_type_promotion.py index 5e692632..d304071a 100644 --- a/array_api_tests/test_type_promotion.py +++ b/array_api_tests/test_type_promotion.py @@ -25,7 +25,7 @@ @given(hh.mutually_promotable_dtypes(None)) def test_result_type(dtypes): out = xp.result_type(*dtypes) - ph.assert_dtype("result_type", dtypes, out, out_name="out") + ph.assert_dtype("result_type", dtypes, out, repr_name="out") # The number and size of generated arrays is arbitrarily limited to prevent @@ -48,7 +48,7 @@ def test_meshgrid(dtypes, data): assert math.prod(x.size for x in arrays) <= hh.MAX_ARRAY_SIZE # sanity check out = xp.meshgrid(*arrays) for i, x in enumerate(out): - ph.assert_dtype("meshgrid", dtypes, x.dtype, out_name=f"out[{i}].dtype") + ph.assert_dtype("meshgrid", dtypes, x.dtype, repr_name=f"out[{i}].dtype") @given( @@ -312,7 +312,7 @@ def test_inplace_op_promotion(op, expr, in_dtypes, out_dtype, shapes, data): except OverflowError: reject() x1 = locals_["x1"] - ph.assert_dtype(op, in_dtypes, x1.dtype, out_dtype, out_name="x1.dtype") + ph.assert_dtype(op, in_dtypes, x1.dtype, out_dtype, repr_name="x1.dtype") op_scalar_params: List[Param[str, str, DataType, ScalarType, DataType]] = [] @@ -381,7 +381,7 @@ def test_inplace_op_scalar_promotion(op, expr, dtype, in_stype, data): reject() x = locals_["x"] assert x.dtype == dtype, f"{x.dtype=!s}, but should be {dtype}" - ph.assert_dtype(op, (dtype, in_stype), x.dtype, dtype, out_name="x.dtype") + ph.assert_dtype(op, (dtype, in_stype), x.dtype, dtype, repr_name="x.dtype") if __name__ == "__main__":