Skip to content

More 2023.12 test fixes #275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion array_api_tests/array_helpers.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions array_api_tests/dtype_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down
2 changes: 1 addition & 1 deletion array_api_tests/hypothesis_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions array_api_tests/shape_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

__all__ = [
"broadcast_shapes",
"normalise_axis",
"normalize_axis",
"ndindex",
"axis_ndindex",
"axes_ndindex",
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions array_api_tests/test_array_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 == ():
Expand Down
6 changes: 3 additions & 3 deletions array_api_tests/test_fft.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion array_api_tests/test_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 24 additions & 4 deletions array_api_tests/test_manipulation_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,27 @@ 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


_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})

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(
Expand All @@ -190,7 +210,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):
Expand Down Expand Up @@ -230,7 +250,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,
Expand Down Expand Up @@ -360,7 +380,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)

Expand Down
153 changes: 79 additions & 74 deletions array_api_tests/test_operators_and_elementwise_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -951,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(
Expand All @@ -972,80 +970,86 @@ 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 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))):
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()))
Expand All @@ -1062,7 +1066,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()))
Expand Down Expand Up @@ -1535,7 +1539,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))
Expand Down
Loading
Loading