import functools import operator import unittest import numpy import pytest import cupy from cupy import testing from cupyx import fallback_mode from cupyx.fallback_mode import fallback from cupyx.fallback_mode.notification import FallbackWarning ignore_fallback_warnings = pytest.mark.filterwarnings( "ignore", category=FallbackWarning) def numpy_fallback_equal(name='xp'): """ Decorator that checks fallback_mode results are equal to NumPy ones. Checks results that are non-ndarray. Args: name(str): Argument name whose value is either ``numpy`` or ``cupy`` module. """ def decorator(impl): @functools.wraps(impl) def test_func(self, *args, **kwargs): kwargs[name] = fallback_mode.numpy fallback_result = impl(self, *args, **kwargs) kwargs[name] = numpy numpy_result = impl(self, *args, **kwargs) assert numpy_result == fallback_result return test_func return decorator def numpy_fallback_array_equal(name='xp'): """ Decorator that checks fallback_mode results are equal to NumPy ones. Checks ndarrays. Args: name(str): Argument name whose value is either ``numpy`` or ``cupy`` module. """ def decorator(impl): @functools.wraps(impl) def test_func(self, *args, **kwargs): kwargs[name] = fallback_mode.numpy fallback_result = impl(self, *args, **kwargs) kwargs[name] = numpy numpy_result = impl(self, *args, **kwargs) if isinstance(numpy_result, numpy.ndarray): # if numpy returns ndarray, cupy must return ndarray assert isinstance(fallback_result, fallback.ndarray) fallback_mode.numpy.testing.assert_array_equal( numpy_result, fallback_result) assert fallback_result.dtype == numpy_result.dtype elif isinstance(numpy_result, numpy.ScalarType): # if numpy returns scalar # cupy may return 0-dim array assert numpy_result == fallback_result._cupy_array.item() or \ (numpy_result == fallback_result._numpy_array).all() else: assert False return test_func return decorator def numpy_fallback_array_allclose(name='xp', rtol=1e-07): """ Decorator that checks fallback_mode results are almost equal to NumPy ones. Checks ndarrays. Args: name(str): Argument name whose value is either ``numpy`` or ``cupy`` module. """ def decorator(impl): @functools.wraps(impl) def test_func(self, *args, **kwargs): kwargs[name] = fallback_mode.numpy fallback_result = impl(self, *args, **kwargs) kwargs[name] = numpy numpy_result = impl(self, *args, **kwargs) assert isinstance(fallback_result, fallback.ndarray) fallback_mode.numpy.testing.assert_allclose( numpy_result, fallback_result, rtol=rtol) assert fallback_result.dtype == numpy_result.dtype return test_func return decorator def enable_slice_copy(func): """ Decorator that enables CUPY_EXPERIMENTAL_SLICE_COPY. And then restores it to previous state. """ def decorator(*args, **kwargs): old = cupy._util.ENABLE_SLICE_COPY cupy._util.ENABLE_SLICE_COPY = True func(*args, **kwargs) cupy._util.ENABLE_SLICE_COPY = old return decorator def get_numpy_version(): v = numpy.lib.NumpyVersion(numpy.__version__) return (v.major, v.minor, v.bugfix) @ignore_fallback_warnings class TestFallbackMode(unittest.TestCase): def test_module_not_callable(self): pytest.raises(TypeError, fallback_mode.numpy) pytest.raises(TypeError, fallback_mode.numpy.linalg) def test_numpy_scalars(self): assert fallback_mode.numpy.inf is numpy.inf assert fallback_mode.numpy.pi is numpy.pi # True, because 'is' checks for reference # fallback_mode.numpy.nan and numpy.nan both have same reference assert fallback_mode.numpy.nan is numpy.nan def test_cupy_specific_func(self): with pytest.raises(AttributeError): func = fallback_mode.numpy.ElementwiseKernel # NOQA def test_func_not_in_numpy(self): with pytest.raises(AttributeError): func = fallback_mode.numpy.dummy # NOQA def test_same_reference(self): assert fallback_mode.numpy.int64 is numpy.int64 assert fallback_mode.numpy.float32 is numpy.float32 def test_isinstance(self): a = fallback_mode.numpy.float64(3) assert isinstance(a, fallback_mode.numpy.float64) abs = fallback_mode.numpy.vectorize(fallback_mode.numpy.abs) assert isinstance(abs, fallback_mode.numpy.vectorize) date = fallback_mode.numpy.datetime64('2019-07-18') assert isinstance(date, fallback_mode.numpy.datetime64) @testing.parameterize( {'func': 'min', 'shape': (3, 4), 'args': (), 'kwargs': {'axis': 0}}, {'func': 'argmin', 'shape': (3, 4), 'args': (), 'kwargs': {}}, {'func': 'roots', 'shape': (3,), 'args': (), 'kwargs': {}}, {'func': 'resize', 'shape': (2, 6), 'args': ((6, 2),), 'kwargs': {}}, {'func': 'resize', 'shape': (3, 4), 'args': ((4, 9),), 'kwargs': {}}, {'func': 'delete', 'shape': (5, 4), 'args': (1, 0), 'kwargs': {}}, {'func': 'append', 'shape': (2, 3), 'args': ([[7, 8, 9]],), 'kwargs': {'axis': 0}}, {'func': 'asarray_chkfinite', 'shape': (2, 4), 'args': (), 'kwargs': {'dtype': numpy.float64}} ) @ignore_fallback_warnings class TestFallbackMethodsArrayExternal(unittest.TestCase): @numpy_fallback_array_equal() def test_fallback_methods_array_external(self, xp): a = testing.shaped_random(self.shape, xp=xp, dtype=numpy.int64) return getattr(xp, self.func)(a, *self.args, **self.kwargs) @testing.parameterize( {'func': 'min', 'shape': (3, 4), 'args': (), 'kwargs': {'axis': 0}, 'numpy_version': None}, {'func': 'argmin', 'shape': (3, 4), 'args': (), 'kwargs': {}, 'numpy_version': (1, 10, 0)}, {'func': 'arccos', 'shape': (2, 3), 'args': (), 'kwargs': {}, 'numpy_version': None}, {'func': 'fabs', 'shape': (2, 3), 'args': (), 'kwargs': {}, 'numpy_version': None}, {'func': 'nancumsum', 'shape': (5, 3), 'args': (), 'kwargs': {'axis': 1}, 'numpy_version': (1, 12, 0)}, {'func': 'nanpercentile', 'shape': (3, 4), 'args': (50,), 'kwargs': {'axis': 0}, 'numpy_version': None} ) @ignore_fallback_warnings class TestFallbackMethodsArrayExternalOut(unittest.TestCase): @numpy_fallback_array_equal() def test_fallback_methods_array_external_out(self, xp): if self.numpy_version and get_numpy_version() < self.numpy_version: self.skipTest('Test not supported for this version of numpy') a = testing.shaped_random(self.shape, xp=xp) kwargs = self.kwargs.copy() res = getattr(xp, self.func)(a, *self.args, **kwargs) # to get the shape of out out = xp.zeros(res.shape, dtype=res.dtype) kwargs['out'] = out getattr(xp, self.func)(a, *self.args, **kwargs) return out @testing.parameterize( {'object': 'ndarray'}, {'object': 'ndarray.__add__'}, {'object': 'vectorize'}, {'object': 'linalg.eig'}, ) class TestDocs(unittest.TestCase): @numpy_fallback_equal() def test_docs(self, xp): return operator.attrgetter(self.object)(xp).__doc__ class TestFallbackArray(unittest.TestCase): def test_ndarray_creation_compatible(self): a = fallback_mode.numpy.array([[1, 2], [3, 4]]) assert isinstance(a, fallback.ndarray) assert a._supports_cupy b = fallback_mode.numpy.arange(9) assert isinstance(b, fallback.ndarray) assert a._supports_cupy def test_ndarray_creation_not_compatible(self): a = fallback_mode.numpy.array([1, 2, 3], dtype=object) assert isinstance(a, fallback.ndarray) assert not a._supports_cupy b = fallback_mode.numpy.array(['a', 'b', 'c', 'd'], dtype='|S1') assert isinstance(b, fallback.ndarray) assert not b._supports_cupy # Structured array will automatically be _numpy_array c = fallback_mode.numpy.array( [('Rex', 9, 81.0), ('Fido', 3, 27.0)], dtype=[('name', 'U10'), ('age', 'i4'), ('weight', 'f4')]) assert isinstance(c, fallback.ndarray) assert not c._supports_cupy def test_getitem(self): x = fallback_mode.numpy.array([1, 2, 3]) # single element assert int(x[2]) == 3 # slicing res = cupy.array([1, 2, 3]) testing.assert_array_equal(x[:2]._cupy_array, res[:2]) def test_setitem(self): x = fallback_mode.numpy.array([1, 2, 3]) # single element x[2] = 99 res = cupy.array([1, 2, 99]) testing.assert_array_equal(x._cupy_array, res) # slicing y = fallback_mode.numpy.array([11, 22]) x[:2] = y res = cupy.array([11, 22, 99]) testing.assert_array_equal(x._cupy_array, res) @numpy_fallback_equal() def test_ndarray_shape(self, xp): x = xp.arange(20) x = x.reshape(4, 5) return x.shape @numpy_fallback_equal() def test_ndarray_init(self, xp): a = xp.ndarray((3, 4)) return a.shape @numpy_fallback_equal() def test_ndarray_shape_creation(self, xp): a = xp.ndarray((4, 5)) return a.shape def test_instancecheck_ndarray(self): a = fallback_mode.numpy.array([1, 2, 3]) assert isinstance(a, fallback_mode.numpy.ndarray) b = fallback_mode.numpy.ndarray((2, 3)) assert isinstance(b, fallback_mode.numpy.ndarray) def test_instancecheck_type(self): a = fallback_mode.numpy.arange(3) assert isinstance(a, type(a)) @numpy_fallback_equal() def test_type_call(self, xp): a = xp.array([1]) b = type(a)((2, 3)) return b.shape @numpy_fallback_equal() def test_type_assert(self, xp): a = xp.array([1, 2, 3]) return type(a) == xp.ndarray @numpy_fallback_equal() def test_base(self, xp): a = xp.arange(7) b = a[2:] return b.base is a @testing.parameterize( {'func': 'min', 'shape': (5,), 'args': (), 'kwargs': {}}, {'func': 'argmax', 'shape': (5, 3), 'args': (), 'kwargs': {'axis': 0}}, {'func': 'ptp', 'shape': (3, 3), 'args': (), 'kwargs': {'axis': 1}}, {'func': 'compress', 'shape': (3, 2), 'args': ([False, True],), 'kwargs': {'axis': 0}} ) class TestFallbackArrayMethodsInternal(unittest.TestCase): @numpy_fallback_array_equal() def test_fallback_array_methods_internal(self, xp): a = testing.shaped_random(self.shape, xp=xp) return getattr(a, self.func)(*self.args, **self.kwargs) @numpy_fallback_array_equal() def test_fallback_array_methods_internal_out(self, xp): a = testing.shaped_random(self.shape, xp=xp) kwargs = self.kwargs.copy() res = getattr(a, self.func)(*self.args, **kwargs) # to get the shape of out out = xp.zeros(res.shape, dtype=res.dtype) kwargs['out'] = out getattr(a, self.func)(*self.args, **kwargs) return out @testing.parameterize( {'func': '__eq__', 'shape': (3, 4)}, {'func': '__ne__', 'shape': (3, 1)}, {'func': '__gt__', 'shape': (4,)}, {'func': '__lt__', 'shape': (1, 1)}, {'func': '__ge__', 'shape': (1, 2)}, {'func': '__le__', 'shape': (1,)} ) class TestArrayComparison(unittest.TestCase): @numpy_fallback_array_equal() def test_ndarray_comparison(self, xp): a = testing.shaped_random(self.shape, xp=xp) b = testing.shaped_random(self.shape, xp=xp, seed=3) return getattr(a, self.func)(b) @testing.parameterize( {'func': '__str__', 'shape': (5, 6)}, {'func': '__repr__', 'shape': (3, 4)}, {'func': '__int__', 'shape': ()}, {'func': '__float__', 'shape': ()}, {'func': '__len__', 'shape': (3, 3)}, {'func': '__bool__', 'shape': ()}, ) class TestArrayUnaryMethods(unittest.TestCase): @numpy_fallback_equal() def test_unary_methods(self, xp): a = testing.shaped_random(self.shape, xp=xp) return getattr(a, self.func)() @testing.parameterize( {'func': '__abs__', 'shape': (5, 6), 'dtype': numpy.float32}, {'func': '__copy__', 'shape': (3, 4), 'dtype': numpy.float32}, {'func': '__neg__', 'shape': (3, 3), 'dtype': numpy.float32}, {'func': '__invert__', 'shape': (2, 4), 'dtype': numpy.int32} ) class TestArrayUnaryMethodsArray(unittest.TestCase): @numpy_fallback_array_equal() def test_unary_methods_array(self, xp): a = testing.shaped_random(self.shape, xp=xp, dtype=self.dtype) return getattr(a, self.func)() @testing.parameterize( {'func': '__add__', 'shape': (3, 4), 'dtype': numpy.float32}, {'func': '__sub__', 'shape': (2, 2), 'dtype': numpy.float32}, {'func': '__mul__', 'shape': (5, 6), 'dtype': numpy.float32}, {'func': '__mod__', 'shape': (3, 4), 'dtype': numpy.float32}, {'func': '__iadd__', 'shape': (1,), 'dtype': numpy.float32}, {'func': '__imul__', 'shape': (1, 1), 'dtype': numpy.float32}, {'func': '__and__', 'shape': (3, 3), 'dtype': numpy.int32}, {'func': '__ipow__', 'shape': (4, 5), 'dtype': numpy.int32}, {'func': '__xor__', 'shape': (4, 4), 'dtype': numpy.int32}, {'func': '__lshift__', 'shape': (2,), 'dtype': numpy.int32}, {'func': '__irshift__', 'shape': (3, 2), 'dtype': numpy.int32}, ) class TestArrayArithmeticMethods(unittest.TestCase): @numpy_fallback_array_allclose(rtol=1e-6) def test_arithmetic_methods(self, xp): a = testing.shaped_random(self.shape, xp=xp, dtype=self.dtype) b = testing.shaped_random(self.shape, xp=xp, dtype=self.dtype, seed=5) return getattr(a, self.func)(b) class TestArrayMatmul(unittest.TestCase): @testing.with_requires('numpy>=1.16') @numpy_fallback_array_allclose(rtol=1e-05) def test_mm_matmul(self, xp): a = testing.shaped_random((4, 5), xp) b = testing.shaped_random((5, 3), xp, seed=5) return a.__matmul__(b) class TestVectorizeWrapper(unittest.TestCase): @numpy_fallback_array_equal() def test_pyfunc_custom_list(self, xp): def function(a, b): if a > b: return a - b return a + b return xp.vectorize(function)([1, 2, 3, 4], 2) @numpy_fallback_array_equal() def test_pyfunc_builtin(self, xp): a = testing.shaped_random((4, 5), xp) vabs = xp.vectorize(abs) return vabs(a) @numpy_fallback_array_equal() def test_pyfunc_numpy(self, xp): a = testing.shaped_random((4, 5), xp) vabs = xp.vectorize(numpy.abs) return vabs(a) @numpy_fallback_equal() def test_getattr(self, xp): vabs = xp.vectorize(numpy.abs) return vabs.pyfunc @numpy_fallback_array_equal() def test_setattr(self, xp): a = xp.array([-1, 2, -3]) vabs = xp.vectorize(abs) vabs.otypes = ['float'] return vabs(a) @numpy_fallback_equal() def test_doc(self, xp): vabs = xp.vectorize(abs) return vabs.__doc__ @ignore_fallback_warnings class TestInplaceSpecialMethods(unittest.TestCase): @numpy_fallback_array_equal() def test_resize_internal(self, xp): a = testing.shaped_random((3, 4), xp) a.resize(4, 5, refcheck=False) return a @numpy_fallback_array_equal() def test_ndarray_byteswap(self, xp): a = testing.shaped_random((4,), xp, dtype=xp.int16) return a.byteswap() @unittest.skipIf(get_numpy_version() < (1, 13, 0), 'inplace kwarg for byteswap was added in numpy v1.13.0') @numpy_fallback_array_equal() def test_ndarray_byteswap_inplace(self, xp): a = testing.shaped_random((4,), xp, dtype=xp.int16) a.byteswap(inplace=True) return a @numpy_fallback_array_equal() def test_putmask(self, xp): a = testing.shaped_random((3, 4), xp, dtype=xp.int8) xp.putmask(a, a > 2, a**2) return a @unittest.skipIf(get_numpy_version() < (1, 15, 0), 'put_along_axis introduced in numpy v1.15.0') @numpy_fallback_array_equal() def test_put_along_axis(self, xp): a = xp.array([[10, 30, 20], [60, 40, 50]]) ai = xp.expand_dims(xp.argmax(a, axis=1), axis=1) xp.put_along_axis(a, ai, 99, axis=1) return a @unittest.skipIf(get_numpy_version() < (1, 15, 0), 'quantile introduced in numpy v1.15.0') @numpy_fallback_array_equal() def test_out_is_returned_when_fallbacked(self, xp): a = testing.shaped_random((3, 4), xp) z = xp.zeros((4, )) res = xp.quantile(a, 0.5, axis=0, out=z) assert res is z return res @numpy_fallback_array_allclose() def test_out_is_returned_when_not_fallbacked(self, xp): a = testing.shaped_random((3, 4), xp, dtype=xp.float64) z = xp.zeros((4,)) res = xp.var(a, axis=0, out=z) assert res is z return res @ignore_fallback_warnings class TestArrayVariants(unittest.TestCase): @numpy_fallback_array_equal() def test_creation_masked(self, xp): mx = xp.ma.array([1, 2, 3, 4], mask=[1, 0, 1, 0]) return mx @numpy_fallback_equal() def test_method_internal(self, xp): mx = xp.ma.array([1, 2, 3, 4], mask=[1, 0, 1, 0]) return mx.min() @numpy_fallback_equal() def test_method_internal_not_callable(self, xp): mx = xp.ma.array([1, 2, 3, 4], mask=[1, 0, 1, 0]) return mx.shape @numpy_fallback_equal() def test_method_external_masked(self, xp): mx = xp.ma.array([1, 2, 3, 4], mask=[1, 0, 1, 0]) return xp.mean(mx) @numpy_fallback_array_equal() def test_magic_method_masked(self, xp): mx = xp.ma.array([1, 2, 3, 4], mask=[1, 0, 1, 0]) my = xp.ma.array([4, 2, 3, 1], mask=[1, 0, 1, 0]) return mx >= my @numpy_fallback_array_equal() def test_creation_char(self, xp): cx = xp.char.array(['a', 'b', 'c'], itemsize=3) return cx @numpy_fallback_array_equal() def test_method_external_char(self, xp): cx = xp.char.array(['a', 'b', 'c'], itemsize=3) cy = xp.char.array(['a', 'b', 'c'], itemsize=3) return xp.char.add(cx, cy) @numpy_fallback_array_equal() def test_magic_method_char(self, xp): cx = xp.char.array(['a', 'b', 'c'], itemsize=3) cy = xp.char.array(['a', 'b', 'c'], itemsize=3) return cx == cy @numpy_fallback_array_equal() def test_inplace(self, xp): x = xp.arange(12).reshape((3, 4)) mask = xp.zeros_like(x) mask[0, :] = 1 mx = xp.ma.array(x, mask=mask) z = xp.ma.zeros((4,)) xp.nanmean(mx, axis=0, out=z) return z @numpy_fallback_array_equal() def test_matrix_returned(self, xp): x = testing.shaped_random((2, 3), xp=xp) y = xp.asmatrix(x) if xp is fallback_mode.numpy: assert x._supports_cupy assert isinstance(y, fallback.ndarray) assert not y._supports_cupy assert y._numpy_array.__class__ is numpy.matrix return y @numpy_fallback_array_equal() def test_record_array(self, xp): ra = xp.rec.array([1, 2, 3]) return ra # changes in MaskedArray should be reflected in base ndarray @numpy_fallback_array_equal() def test_ma_func(self, xp): x = xp.array([1, 2, 3, 4]) x += x mx = xp.ma.array(x, mask=[1, 0, 1, 0]) assert mx.base is x mx += mx return x # changes in base ndarray should be reflected in MaskedArray @enable_slice_copy @numpy_fallback_array_equal() def test_ma_func_inverse(self, xp): x = xp.array([1, 2, 3, 4]) mx = xp.ma.array(x, mask=[1, 0, 1, 0]) assert mx.base is x mx += mx x += x return mx