Skip to content
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

More improvements to test_linalg #101

Merged
merged 61 commits into from Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
360df9f
Add shape testing for vector_norm()
asmeurer Feb 17, 2022
e34f492
Test the dtype and stacks in the vector_norm() test
asmeurer Feb 18, 2022
111c237
Remove an ununsed variable
asmeurer Feb 18, 2022
4f3aa54
Use a simpler strategy for ord in test_vector_norm
asmeurer Feb 19, 2022
979b81b
Skip the test_vector_norm test on the NumPy CI
asmeurer Feb 25, 2022
8df237a
Fix syntax error
asmeurer Feb 25, 2022
d11a685
Fix the input strategies for test_tensordot()
asmeurer Feb 26, 2022
a776cd4
Add a test for the tensordot result shape
asmeurer Apr 9, 2022
45b36d6
Test stacking for tensordot
asmeurer Apr 12, 2022
414b322
Add allclose() and assert_allclose() helper functions
asmeurer Apr 25, 2022
9bb8c7a
Use assert_allclose() in the linalg tests for float inputs
asmeurer Apr 25, 2022
b3fb4ec
Remove skip from test_eigh
asmeurer Apr 26, 2022
241220e
Disable eigenvectors stack test
asmeurer May 6, 2022
ca70fbe
Reduce the relative tolerance in assert_allclose
asmeurer May 6, 2022
720b309
Sort the eigenvalues when testing stacks
asmeurer May 6, 2022
17d93bf
Merge branch 'more-linalg2' of github.com:asmeurer/array-api-tests in…
asmeurer May 9, 2022
f439259
Sort the results in eigvalsh before comparing
asmeurer Jun 3, 2022
75ca73a
Remove the allclose testing in linalg
asmeurer Jun 13, 2022
d86a0a1
Add (commented out) stacking tests for solve()
asmeurer Jun 16, 2022
9bccfa5
Remove unused none standin in the linalg tests
asmeurer Jun 16, 2022
f494b45
Don't compare float elements in test_tensordot
asmeurer Jun 16, 2022
74add08
Fix test_vecdot
asmeurer Jun 24, 2022
f12be47
Fix typo in test_vecdot
asmeurer Jul 5, 2022
d41d0bd
Expand vecdot tests
asmeurer Jul 5, 2022
1220d6e
Merge branch 'master' into more-linalg2
asmeurer Sep 27, 2022
a96a5df
Merge branch 'master' into more-linalg2
asmeurer Oct 20, 2022
48a8442
Check specially that the result of linalg functions is not a unnamed …
asmeurer Nov 29, 2022
fd6367f
Use a more robust fallback helper for matrix_transpose
asmeurer Mar 17, 2023
7017797
Be more constrained about constructing symmetric matrices
asmeurer Mar 20, 2023
335574e
Merge branch 'more-linalg2' of github.com:asmeurer/array-api-tests in…
asmeurer Mar 21, 2023
246e38a
Don't require the arguments to assert_keepdimable_shape to be positio…
asmeurer Mar 23, 2023
02542ff
Show the arrays in the error message for assert_exactly_equal
asmeurer Mar 29, 2023
72974e0
Allow passing an extra assertion message to assert_equal in linalg an…
asmeurer Mar 29, 2023
1daba5d
Fix the true_value check for test_vecdot
asmeurer Mar 29, 2023
bbfe50f
Fix the test_diagonal true value check
asmeurer Mar 29, 2023
64b0342
Use a function instead of operation
asmeurer Mar 29, 2023
9cb58a1
Add a comment
asmeurer Apr 18, 2023
0b3e170
Merge branch 'master' into more-linalg2
asmeurer Feb 3, 2024
c51216b
Remove flaky skips from linalg tests
asmeurer Feb 3, 2024
cffd076
Fix some issues in linalg tests from recent merge
asmeurer Feb 3, 2024
3501116
Fix vector_norm to not use our custom arrays strategy
asmeurer Feb 3, 2024
5c1aa45
Update _test_stacks to use updated ndindex behavior
asmeurer Feb 3, 2024
7a46e6b
Further limit the size of n in test_matrix_power
asmeurer Feb 3, 2024
6d154f2
Fix test_trace
asmeurer Feb 3, 2024
257aa13
Fix test_vecdot to only generate axis in [-min(x1.ndim, x2.ndim), -1]
asmeurer Feb 3, 2024
afc8a25
Update test_cross to test broadcastable shapes
asmeurer Feb 3, 2024
3cb9912
Fix test_cross to use assert_dtype and assert_shape helpers
asmeurer Feb 3, 2024
012ca19
Remove some completed TODO comments
asmeurer Feb 3, 2024
5ceb81d
Update linalg tests to test complex dtypes
asmeurer Feb 3, 2024
a4d419f
Update linalg tests to use assert_dtype and assert_shape helpers
asmeurer Feb 3, 2024
6f9db94
Factor out dtype logic from test_sum() and test_prod() and apply it t…
asmeurer Feb 3, 2024
5aa9083
Remove unused allclose and assert_allclose helpers
asmeurer Feb 7, 2024
938f086
Update ndindex version requirement
asmeurer Feb 16, 2024
3856b8f
Fix linting issue
asmeurer Feb 16, 2024
ccc6ca3
Skip `test_cross` in CI
honno Feb 20, 2024
3092422
Test matmul, matrix_transpose, tensordot, and vecdot for the main and…
asmeurer Feb 23, 2024
2d918e4
Merge branch 'more-linalg2' of github.com:asmeurer/array-api-tests in…
asmeurer Feb 23, 2024
3fefd20
Remove need for filtering in `invertible_matrices()`
honno Feb 26, 2024
a76e051
Merge branch 'master' into more-linalg2
honno Feb 26, 2024
268682d
Skip flaky `test_reshape`
honno Feb 26, 2024
0ddb0cd
Less filtering in `positive_definitive_matrices`
honno Feb 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/numpy.yml
Expand Up @@ -37,6 +37,9 @@ jobs:
# The return dtype for trace is not consistent in the spec
# https://github.com/data-apis/array-api/issues/202#issuecomment-952529197
array_api_tests/test_linalg.py::test_trace
# Various fixes to vector_norm are in
# https://github.com/numpy/numpy/pull/21084.
array_api_tests/test_linalg.py::test_vector_norm
# waiting on NumPy to allow/revert distinct NaNs for np.unique
# https://github.com/numpy/numpy/issues/20326#issuecomment-1012380448
array_api_tests/test_set_functions.py
Expand Down
41 changes: 40 additions & 1 deletion array_api_tests/array_helpers.py
Expand Up @@ -9,6 +9,9 @@
from ._array_module import logical_not, subtract, floor, ceil, where
from . import dtype_helpers as dh

from ndindex import iter_indices

import math

__all__ = ['all', 'any', 'logical_and', 'logical_or', 'logical_not', 'less',
'less_equal', 'greater', 'subtract', 'negative', 'floor', 'ceil',
Expand Down Expand Up @@ -146,6 +149,43 @@ def exactly_equal(x, y):

return equal(x, y)

def allclose(x, y, rel_tol=0.25, abs_tol=1, return_indices=False):
"""
Return True all elements of x and y are within tolerance

If return_indices=True, returns (False, (i, j)) when the arrays are not
close, where i and j are the indices into x and y of corresponding
non-close elements.
"""
for i, j in iter_indices(x.shape, y.shape):
i, j = i.raw, j.raw
a = x[i]
b = y[j]
if not (math.isfinite(a) and math.isfinite(b)):
# TODO: If a and b are both infinite, require the same type of infinity
continue
close = math.isclose(a, b, rel_tol=rel_tol, abs_tol=abs_tol)
if not close:
if return_indices:
return (False, (i, j))
return False
return True
Copy link
Member

@honno honno Apr 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep looks good, this is really how we have to do it if we can't use masking.

Maybe move this and assert_allclose to test_linalg.py for now, as ideally what we'd be doing is standardising these utils across function groups then, and scoping these utils for each function group makes it easier to distinguish where they're used and their subtle differences. (standardising seems quite doable, I just need to get round to it)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One problem here is represents a pretty significant performance hit. Before this commit (with the exact equality test), the linalg tests take 15 seconds on my computer. After, they take 44 seconds. Performance isn't our top priority, but maybe we should try array operations and only fallback to this when there are nonfinite values.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this and assert_allclose to test_linalg.py for now, as ideally what we'd be doing is standardising these utils across function groups then, and scoping these utils for each function group makes it easier to distinguish where they're used and their subtle differences. (standardising seems quite doable, I just need to get round to it)

Think I'd still like this for now, esp as I'm generally rethinking the pytest helpers for #200/general look at vectorisation. Not blocking tho.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I'm just going to delete them. I'm not using them anymore, because testing float arrays from linalg functions like this turned out to be too difficult.


def assert_allclose(x, y, rel_tol=1, abs_tol=0.):
"""
Test that x and y are approximately equal to each other.

Also asserts that x and y have the same shape and dtype.
"""
assert x.shape == y.shape, f"The input arrays do not have the same shapes ({x.shape} != {y.shape})"

assert x.dtype == y.dtype, f"The input arrays do not have the same dtype ({x.dtype} != {y.dtype})"

c = allclose(x, y, rel_tol=rel_tol, abs_tol=abs_tol, return_indices=True)
if c is not True:
_, (i, j) = c
raise AssertionError(f"The input arrays are not close with {rel_tol = } and {abs_tol = } at indices {i = } and {j = }")

def notequal(x, y):
"""
Same as not_equal(x, y) except it gives False when both values are nan.
Expand Down Expand Up @@ -305,4 +345,3 @@ def same_sign(x, y):

def assert_same_sign(x, y):
assert all(same_sign(x, y)), "The input arrays do not have the same sign"

197 changes: 175 additions & 22 deletions array_api_tests/test_linalg.py
Expand Up @@ -15,19 +15,22 @@

import pytest
from hypothesis import assume, given
from hypothesis.strategies import (booleans, composite, none, tuples, integers,
shared, sampled_from, one_of, data, just)
from hypothesis.strategies import (booleans, composite, none, tuples, floats,
integers, shared, sampled_from, one_of,
data, just)
from ndindex import iter_indices

from .array_helpers import assert_exactly_equal, asarray
import itertools

from .array_helpers import assert_exactly_equal, asarray, assert_allclose
from .hypothesis_helpers import (xps, dtypes, shapes, kwargs, matrix_shapes,
square_matrix_shapes, symmetric_matrices,
positive_definite_matrices, MAX_ARRAY_SIZE,
invertible_matrices, two_mutual_arrays,
mutually_promotable_dtypes, one_d_shapes,
two_mutually_broadcastable_shapes,
SQRT_MAX_ARRAY_SIZE, finite_matrices,
rtol_shared_matrix_shapes, rtols)
rtol_shared_matrix_shapes, rtols, axes)
from . import dtype_helpers as dh
from . import pytest_helpers as ph
from . import shape_helpers as sh
Expand All @@ -41,9 +44,23 @@
# Standin strategy for not yet implemented tests
todo = none()

def assert_equal(x, y):
if x.dtype in dh.float_dtypes:
# It's too difficult to do an approximately equal test here because
# different routines can give completely different answers, and even
# when it does work, the elementwise comparisons are too slow. So for
# floating-point dtypes only test the shape and dtypes.

# assert_allclose(x, y)

assert x.shape == y.shape, f"The input arrays do not have the same shapes ({x.shape} != {y.shape})"
assert x.dtype == y.dtype, f"The input arrays do not have the same dtype ({x.dtype} != {y.dtype})"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep this seems reasonable.

else:
assert_exactly_equal(x, y)

def _test_stacks(f, *args, res=None, dims=2, true_val=None,
matrix_axes=(-2, -1),
assert_equal=assert_exactly_equal, **kw):
assert_equal=assert_equal, **kw):
"""
Test that f(*args, **kw) maps across stacks of matrices

Expand Down Expand Up @@ -225,7 +242,6 @@ def true_diag(x_stack):

_test_stacks(linalg.diagonal, x, **kw, res=res, dims=1, true_val=true_diag)

@pytest.mark.skip(reason="Inputs need to be restricted") # TODO
@pytest.mark.xp_extension('linalg')
@given(x=symmetric_matrices(finite=True))
def test_eigh(x):
Expand All @@ -242,8 +258,15 @@ def test_eigh(x):
assert eigenvectors.dtype == x.dtype, "eigh().eigenvectors did not return the correct dtype"
assert eigenvectors.shape == x.shape, "eigh().eigenvectors did not return the correct shape"

# Note: _test_stacks here is only testing the shape and dtype. The actual
# eigenvalues and eigenvectors may not be equal at all, since there is not
# requirements about how eigh computes an eigenbasis, or about the order
# of the eigenvalues
_test_stacks(lambda x: linalg.eigh(x).eigenvalues, x,
res=eigenvalues, dims=1)

# TODO: Test that eigenvectors are orthonormal.

_test_stacks(lambda x: linalg.eigh(x).eigenvectors, x,
res=eigenvectors, dims=2)

Expand All @@ -258,9 +281,14 @@ def test_eigvalsh(x):
assert res.dtype == x.dtype, "eigvalsh() did not return the correct dtype"
assert res.shape == x.shape[:-1], "eigvalsh() did not return the correct shape"

# Note: _test_stacks here is only testing the shape and dtype. The actual
# eigenvalues may not be equal at all, since there is not requirements or
# about the order of the eigenvalues, and the stacking code may use a
# different code path.
_test_stacks(linalg.eigvalsh, x, res=res, dims=1)

# TODO: Should we test that the result is the same as eigh(x).eigenvalues?
# (probably no because the spec doesn't actually require that)

# TODO: Test that res actually corresponds to the eigenvalues of x

Expand Down Expand Up @@ -309,8 +337,6 @@ def test_matmul(x1, x2):
assert res.shape == stack_shape + (x1.shape[-2], x2.shape[-1])
_test_stacks(_array_module.matmul, x1, x2, res=res)

matrix_norm_shapes = shared(matrix_shapes())

@pytest.mark.xp_extension('linalg')
@given(
x=finite_matrices(),
Expand Down Expand Up @@ -571,22 +597,118 @@ def test_svdvals(x):

# TODO: Check that svdvals() is the same as svd().s.

_tensordot_pre_shapes = shared(two_mutually_broadcastable_shapes)

@composite
def _tensordot_axes(draw):
shape1, shape2 = draw(_tensordot_pre_shapes)
ndim1, ndim2 = len(shape1), len(shape2)
isint = draw(booleans())

if isint:
N = min(ndim1, ndim2)
return draw(integers(0, N))
else:
if ndim1 < ndim2:
first = draw(xps.valid_tuple_axes(ndim1))
second = draw(xps.valid_tuple_axes(ndim2, min_size=len(first),
max_size=len(first)))
else:
second = draw(xps.valid_tuple_axes(ndim2))
first = draw(xps.valid_tuple_axes(ndim1, min_size=len(second),
max_size=len(second)))
return (tuple(first), tuple(second))

tensordot_kw = shared(kwargs(axes=_tensordot_axes()))

@composite
def tensordot_shapes(draw):
_shape1, _shape2 = map(list, draw(_tensordot_pre_shapes))
ndim1, ndim2 = len(_shape1), len(_shape2)
kw = draw(tensordot_kw)
if 'axes' not in kw:
assume(ndim1 >= 2 and ndim2 >= 2)
axes = kw.get('axes', 2)

if isinstance(axes, int):
axes = [list(range(-axes, 0)), list(range(0, axes))]

first, second = axes
for i, j in zip(first, second):
try:
if -ndim2 <= j < ndim2 and _shape2[j] != 1:
_shape1[i] = _shape2[j]
if -ndim1 <= i < ndim1 and _shape1[i] != 1:
_shape2[j] = _shape1[i]
except:
raise

shape1, shape2 = map(tuple, [_shape1, _shape2])
return (shape1, shape2)

def _test_tensordot_stacks(x1, x2, kw, res):
"""
Variant of _test_stacks for tensordot

tensordot doesn't stack directly along the non-contracted dimensions like
the other linalg functions. Rather, it is stacked along the product of
each non-contracted dimension. These dimensions are independent of one
another and do not broadcast.
"""
shape1, shape2 = x1.shape, x2.shape

axes = kw.get('axes', 2)

if isinstance(axes, int):
res_axes = axes
axes = [list(range(-axes, 0)), list(range(0, axes))]
else:
# Convert something like (0, 4, 2) into (0, 2, 1)
res_axes = []
for a, s in zip(axes, [shape1, shape2]):
indices = [range(len(s))[i] for i in a]
repl = dict(zip(sorted(indices), range(len(indices))))
res_axes.append(tuple(repl[i] for i in indices))

for ((i,), (j,)), (res_idx,) in zip(
itertools.product(
iter_indices(shape1, skip_axes=axes[0]),
iter_indices(shape2, skip_axes=axes[1])),
iter_indices(res.shape)):
i, j, res_idx = i.raw, j.raw, res_idx.raw

res_stack = res[res_idx]
x1_stack = x1[i]
x2_stack = x2[j]
decomp_res_stack = xp.tensordot(x1_stack, x2_stack, axes=res_axes)
assert_exactly_equal(res_stack, decomp_res_stack)

@given(
dtypes=mutually_promotable_dtypes(dtypes=dh.numeric_dtypes),
shape=shapes(),
data=data(),
*two_mutual_arrays(dh.numeric_dtypes, two_shapes=tensordot_shapes()),
tensordot_kw,
)
def test_tensordot(dtypes, shape, data):
def test_tensordot(x1, x2, kw):
# TODO: vary shapes, vary contracted axes, test different axes arguments
x1 = data.draw(xps.arrays(dtype=dtypes[0], shape=shape), label="x1")
x2 = data.draw(xps.arrays(dtype=dtypes[1], shape=shape), label="x2")
res = xp.tensordot(x1, x2, **kw)

out = xp.tensordot(x1, x2, axes=len(shape))
ph.assert_dtype("tensordot", [x1.dtype, x2.dtype], res.dtype)

ph.assert_dtype("tensordot", dtypes, out.dtype)
# TODO: assert shape and elements
axes = _axes = kw.get('axes', 2)

if isinstance(axes, int):
_axes = [list(range(-axes, 0)), list(range(0, axes))]

_shape1 = list(x1.shape)
_shape2 = list(x2.shape)
for i, j in zip(*_axes):
_shape1[i] = _shape2[j] = None
_shape1 = tuple([i for i in _shape1 if i is not None])
_shape2 = tuple([i for i in _shape2 if i is not None])
result_shape = _shape1 + _shape2
ph.assert_result_shape('tensordot', [x1.shape, x2.shape], res.shape,
expected=result_shape)
# TODO: assert stacking and elements
_test_tensordot_stacks(x1, x2, kw, res)

@pytest.mark.xp_extension('linalg')
@given(
Expand Down Expand Up @@ -645,11 +767,42 @@ def test_vecdot(dtypes, shape, data):
# TODO: assert shape and elements


# Insanely large orders might not work. There isn't a limit specified in the
# spec, so we just limit to reasonable values here.
max_ord = 100

@pytest.mark.xp_extension('linalg')
@given(
x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()),
kw=kwargs(axis=todo, keepdims=todo, ord=todo)
x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes(min_side=1)),
data=data(),
)
def test_vector_norm(x, kw):
# res = linalg.vector_norm(x, **kw)
pass
def test_vector_norm(x, data):
kw = data.draw(
# We use data because axes is parameterized on x.ndim
kwargs(axis=axes(x.ndim),
keepdims=booleans(),
ord=one_of(
sampled_from([2, 1, 0, -1, -2, float("inf"), float("-inf")]),
integers(-max_ord, max_ord),
floats(-max_ord, max_ord),
)), label="kw")


res = linalg.vector_norm(x, **kw)
axis = kw.get('axis', None)
keepdims = kw.get('keepdims', False)
# TODO: Check that the ord values give the correct norms.
# ord = kw.get('ord', 2)

_axes = sh.normalise_axis(axis, x.ndim)

ph.assert_keepdimable_shape('linalg.vector_norm', res.shape, x.shape,
_axes, keepdims, **kw)
ph.assert_dtype('linalg.vector_norm', x.dtype, res.dtype)

_kw = kw.copy()
_kw.pop('axis', None)
_test_stacks(linalg.vector_norm, x, res=res,
dims=x.ndim if keepdims else 0,
matrix_axes=_axes, **_kw
)