From 3d9f09cdbf761380da3d66222a5bb7590aac5b54 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Fri, 6 Jan 2023 12:10:19 +0300 Subject: [PATCH 1/3] MAINT: expand types/dtypes, adapt test_scalarmath --- torch_np/_dtypes.py | 1 + torch_np/_ndarray.py | 59 ++++++++- torch_np/_scalar_types.py | 12 +- torch_np/_wrapper.py | 17 +-- torch_np/testing/__init__.py | 1 + torch_np/testing/utils.py | 6 +- .../tests/numpy_tests/core/test_scalarmath.py | 118 ++++++++---------- 7 files changed, 130 insertions(+), 84 deletions(-) diff --git a/torch_np/_dtypes.py b/torch_np/_dtypes.py index 6777d2df..3d022bb2 100644 --- a/torch_np/_dtypes.py +++ b/torch_np/_dtypes.py @@ -67,6 +67,7 @@ def __repr__(self): __str__ = __repr__ + @property def itemsize(self): elem = self.type(1) return elem.get().element_size() diff --git a/torch_np/_ndarray.py b/torch_np/_ndarray.py index 7b1d532c..89c87a64 100644 --- a/torch_np/_ndarray.py +++ b/torch_np/_ndarray.py @@ -81,6 +81,18 @@ def base(self): def T(self): return self.transpose() + @property + def real(self): + return asarray(self._tensor.real) + + @property + def imag(self): + try: + return asarray(self._tensor.imag) + except RuntimeError: + zeros = torch.zeros_like(self._tensor) + return ndarray._from_tensor_and_base(zeros, None) + # ctors def astype(self, dtype): newt = ndarray() @@ -102,6 +114,13 @@ def __str__(self): ### comparisons ### def __eq__(self, other): + try: + t_other = asarray(other).get + except RuntimeError: + # Failed to convert other to array: definitely not equal. + # TODO: generalize, delegate to ufuncs + falsy = torch.full(self.shape, fill_value=False, dtype=bool) + return asarray(falsy) return asarray(self._tensor == asarray(other).get()) def __neq__(self, other): @@ -119,7 +138,6 @@ def __ge__(self, other): def __le__(self, other): return asarray(self._tensor <= asarray(other).get()) - def __bool__(self): try: return bool(self._tensor) @@ -167,7 +185,10 @@ def __iadd__(self, other): def __sub__(self, other): other_tensor = asarray(other).get() - return asarray(self._tensor.__sub__(other_tensor)) + try: + return asarray(self._tensor.__sub__(other_tensor)) + except RuntimeError as e: + raise TypeError(e.args) def __mul__(self, other): other_tensor = asarray(other).get() @@ -177,10 +198,30 @@ def __rmul__(self, other): other_tensor = asarray(other).get() return asarray(self._tensor.__rmul__(other_tensor)) + def __floordiv__(self, other): + other_tensor = asarray(other).get() + return asarray(self._tensor.__floordiv__(other_tensor)) + + def __ifloordiv__(self, other): + other_tensor = asarray(other).get() + return asarray(self._tensor.__ifloordiv__(other_tensor)) + def __truediv__(self, other): other_tensor = asarray(other).get() return asarray(self._tensor.__truediv__(other_tensor)) + def __itruediv__(self, other): + other_tensor = asarray(other).get() + return asarray(self._tensor.__itruediv__(other_tensor)) + + def __mod__(self, other): + other_tensor = asarray(other).get() + return asarray(self._tensor.__mod__(other_tensor)) + + def __imod__(self, other): + other_tensor = asarray(other).get() + return asarray(self._tensor.__imod__(other_tensor)) + def __or__(self, other): other_tensor = asarray(other).get() return asarray(self._tensor.__or__(other_tensor)) @@ -189,10 +230,22 @@ def __ior__(self, other): other_tensor = asarray(other).get() return asarray(self._tensor.__ior__(other_tensor)) - def __invert__(self): return asarray(self._tensor.__invert__()) + def __abs__(self): + return asarray(self._tensor.__abs__()) + + def __neg__(self): + try: + return asarray(self._tensor.__neg__()) + except RuntimeError as e: + raise TypeError(e.args) + + def __pow__(self, exponent): + exponent_tensor = asarray(exponent).get() + return asarray(self._tensor.__pow__(exponent_tensor)) + ### methods to match namespace functions def squeeze(self, axis=None): diff --git a/torch_np/_scalar_types.py b/torch_np/_scalar_types.py index 0c4429b3..b97c1921 100644 --- a/torch_np/_scalar_types.py +++ b/torch_np/_scalar_types.py @@ -25,7 +25,12 @@ def __new__(self, value): if isinstance(value, _ndarray.ndarray): tensor = value.get() else: - tensor = torch.as_tensor(value, dtype=torch_dtype) + try: + tensor = torch.as_tensor(value, dtype=torch_dtype) + except RuntimeError as e: + if "Overflow" in str(e): + raise OverflowError(e.args) + raise e # # With numpy: # >>> a = np.ones(3) @@ -135,6 +140,7 @@ class bool_(generic): half = float16 single = float32 double = float64 +float_ = float64 csingle = complex64 cdouble = complex128 @@ -169,8 +175,8 @@ class bool_(generic): __all__ = list(_typemap.keys()) __all__.remove('bool') -__all__ += ['bool_', 'intp', 'int_', 'intc', 'byte', 'short', 'longlong', 'ubyte', 'half', 'single', 'double', -'csingle', 'cdouble'] +__all__ += ['bool_', 'intp', 'int_', 'intc', 'byte', 'short', 'longlong', + 'ubyte', 'half', 'single', 'double', 'csingle', 'cdouble', 'float_'] __all__ += ['sctypes'] __all__ += ['generic', 'number', 'integer', 'signedinteger', 'unsignedinteger', diff --git a/torch_np/_wrapper.py b/torch_np/_wrapper.py index 9cabbba9..9070718f 100644 --- a/torch_np/_wrapper.py +++ b/torch_np/_wrapper.py @@ -510,6 +510,11 @@ def argwhere(a): return asarray(torch.argwhere(tensor)) +def abs(a): + # FIXME: should go the other way, together with other ufuncs + arr = asarray(a) + return a.__abs__() + from ._ndarray import axis_out_keepdims_wrapper @axis_out_keepdims_wrapper @@ -702,18 +707,14 @@ def angle(z, deg=False): return result -@asarray_replacer() def real(a): - return torch.real(a) + arr = asarray(a) + return arr.real -@asarray_replacer() def imag(a): - # torch.imag raises on real-valued inputs - if torch.is_complex(a): - return torch.imag(a) - else: - return torch.zeros_like(a) + arr = asarray(a) + return arr.imag @asarray_replacer() diff --git a/torch_np/testing/__init__.py b/torch_np/testing/__init__.py index b9f7581e..98755a5b 100644 --- a/torch_np/testing/__init__.py +++ b/torch_np/testing/__init__.py @@ -1,5 +1,6 @@ from .utils import (assert_equal, assert_array_equal, assert_almost_equal, assert_warns, assert_) +from .utils import _gen_alignment_data from .testing import assert_allclose # FIXME diff --git a/torch_np/testing/utils.py b/torch_np/testing/utils.py index 8f69032a..e33d5acd 100644 --- a/torch_np/testing/utils.py +++ b/torch_np/testing/utils.py @@ -869,9 +869,9 @@ def assert_array_almost_equal(x, y, decimal=6, err_msg='', verbose=True): """ __tracebackhide__ = True # Hide traceback for py.test - from numpy.core import number, float_, result_type, array - from numpy.core.numerictypes import issubdtype - from numpy.core.fromnumeric import any as npany + from torch_np import number, float_, result_type, array + from torch_np import issubdtype + from torch_np import any as npany def compare(x, y): try: diff --git a/torch_np/tests/numpy_tests/core/test_scalarmath.py b/torch_np/tests/numpy_tests/core/test_scalarmath.py index ca1b3a64..aacabc0b 100644 --- a/torch_np/tests/numpy_tests/core/test_scalarmath.py +++ b/torch_np/tests/numpy_tests/core/test_scalarmath.py @@ -16,6 +16,7 @@ import torch_np as np from torch_np.testing import ( assert_, assert_equal, assert_almost_equal, + _gen_alignment_data, # assert_array_equal, suppress_warnings, _gen_alignment_data, # assert_warns, ) @@ -64,8 +65,8 @@ def test_type_add(self): # skipped ahead based on the first argument, but that # does not produce properly symmetric results... assert_equal(c_scalar.dtype, c_array.dtype, - "error with types (%d/'%c' + %d/'%c')" % - (k, np.dtype(atype).char, l, np.dtype(btype).char)) + "error with types (%d/'%s' + %d/'%s')" % + (k, np.dtype(atype).name, l, np.dtype(btype).name)) def test_type_create(self): for k, atype in enumerate(types): @@ -149,6 +150,7 @@ def test_blocked(self): np.add(2, inp2, out=out) assert_almost_equal(out, exp1 + 2, err_msg=msg) + @pytest.mark.xfail(reason="pytorch does not have .view") def test_lower_align(self): # check data that is not aligned to element size # i.e doubles are aligned to 4 bytes on i386 @@ -179,15 +181,17 @@ def test_large_types(self): else: assert_almost_equal(b, 6765201, err_msg=msg) + @pytest.mark.xfail(reason='Value-based casting: (2)**(-2) -> 0 in pytorch.') def test_integers_to_negative_integer_power(self): # Note that the combination of uint64 with a signed integer # has common type np.float64. The other combinations should all # raise a ValueError for integer ** negative integer. - exp = [np.array(-1, dt)[()] for dt in 'bhilq'] + exp = [np.array(-1, dt)[()] for dt in 'bhil'] # 1 ** -1 possible special case - base = [np.array(1, dt)[()] for dt in 'bhilqBHILQ'] + base = [np.array(1, dt)[()] for dt in 'bhilB'] for i1, i2 in itertools.product(base, exp): + pass if i1.dtype != np.uint64: assert_raises(ValueError, operator.pow, i1, i2) else: @@ -196,7 +200,7 @@ def test_integers_to_negative_integer_power(self): assert_almost_equal(res, 1.) # -1 ** -1 possible special case - base = [np.array(-1, dt)[()] for dt in 'bhilq'] + base = [np.array(-1, dt)[()] for dt in 'bhil'] for i1, i2 in itertools.product(base, exp): if i1.dtype != np.uint64: assert_raises(ValueError, operator.pow, i1, i2) @@ -206,7 +210,7 @@ def test_integers_to_negative_integer_power(self): assert_almost_equal(res, -1.) # 2 ** -1 perhaps generic - base = [np.array(2, dt)[()] for dt in 'bhilqBHILQ'] + base = [np.array(2, dt)[()] for dt in 'bhilB'] for i1, i2 in itertools.product(base, exp): if i1.dtype != np.uint64: assert_raises(ValueError, operator.pow, i1, i2) @@ -258,7 +262,7 @@ class TestModulus: def test_modulus_basic(self): dt = np.typecodes['AllInteger'] + np.typecodes['Float'] - for op in [floordiv_and_mod, divmod]: + for op in [floordiv_and_mod,]: # TODO: divmod is not implemented for dt1, dt2 in itertools.product(dt, dt): for sg1, sg2 in itertools.product(_signs(dt1), _signs(dt2)): fmt = 'op: %s, dt1: %s, dt2: %s, sg1: %s, sg2: %s' @@ -272,6 +276,7 @@ def test_modulus_basic(self): else: assert_(b > rem >= 0, msg) + @pytest.mark.xfail(reason='divmod not implemented') def test_float_modulus_exact(self): # test that float results are exact for small integers. This also # holds for the same integers scaled by powers of two. @@ -302,7 +307,7 @@ def test_float_modulus_exact(self): def test_float_modulus_roundoff(self): # gh-6127 dt = np.typecodes['Float'] - for op in [floordiv_and_mod, divmod]: + for op in [floordiv_and_mod]: # TODO divmod is not implemented for dt1, dt2 in itertools.product(dt, dt): for sg1, sg2 in itertools.product((+1, -1), (+1, -1)): fmt = 'op: %s, dt1: %s, dt2: %s, sg1: %s, sg2: %s' @@ -317,6 +322,7 @@ def test_float_modulus_roundoff(self): else: assert_(b > rem >= 0, msg) + @pytest.mark.skip(reason='float16 on cpu is incomplete in pytorch') def test_float_modulus_corner_cases(self): # Check remainder magnitude. for dt in np.typecodes['Float']: @@ -352,18 +358,10 @@ def test_float_modulus_corner_cases(self): div, mod = op(fone, fzer) assert_(np.isinf(div)) and assert_(np.isnan(mod)) - def test_inplace_floordiv_handling(self): - # issue gh-12927 - # this only applies to in-place floordiv //=, because the output type - # promotes to float which does not fit - a = np.array([1, 2], np.int64) - b = np.array([1, 2], np.uint64) - with pytest.raises(TypeError, - match=r"Cannot cast ufunc 'floor_divide' output from"): - a //= b - class TestComplexDivision: + + @pytest.mark.xfail(reason='With pytorch, 1/(0+0j) is nan + nan*j, not inf + nan*j') def test_zero_division(self): with np.errstate(all="ignore"): for t in [np.complex64, np.complex128]: @@ -443,16 +441,15 @@ def test_int_from_long(self): a = np.array(l, dtype=T) assert_equal([int(_m) for _m in a], li) - a = np.array(l[:3], dtype=np.uint64) - assert_equal([int(_m) for _m in a], li[:3]) - - def test_iinfo_long_values(self): - for code in 'bBhH': + @pytest.mark.xfail(reason="pytorch does not emit this warning.") + def test_iinfo_long_values_1(self): + for code in 'bBh': with pytest.warns(DeprecationWarning): res = np.array(np.iinfo(code).max + 1, dtype=code) tgt = np.iinfo(code).min assert_(res == tgt) + def test_iinfo_long_values_2(self): for code in np.typecodes['AllInteger']: res = np.array(np.iinfo(code).max, dtype=code) tgt = np.iinfo(code).max @@ -467,7 +464,7 @@ def test_int_raise_behaviour(self): def overflow_error_func(dtype): dtype(np.iinfo(dtype).max + 1) - for code in [np.int_, np.uint, np.longlong, np.ulonglong]: + for code in [np.int_, np.longlong]: assert_raises(OverflowError, overflow_error_func, code) def test_numpy_scalar_relational_operators(self): @@ -483,13 +480,13 @@ def test_numpy_scalar_relational_operators(self): "type %s and %s failed" % (dt1, dt2)) #Unsigned integers - for dt1 in 'BHILQP': + for dt1 in 'B': assert_(-1 < np.array(1, dtype=dt1)[()], "type %s failed" % (dt1,)) assert_(not -1 > np.array(1, dtype=dt1)[()], "type %s failed" % (dt1,)) assert_(-1 != np.array(1, dtype=dt1)[()], "type %s failed" % (dt1,)) #unsigned vs signed - for dt2 in 'bhilqp': + for dt2 in 'bhil': assert_(np.array(1, dtype=dt1)[()] > np.array(-1, dtype=dt2)[()], "type %s and %s failed" % (dt1, dt2)) assert_(not np.array(1, dtype=dt1)[()] < np.array(-1, dtype=dt2)[()], @@ -498,12 +495,12 @@ def test_numpy_scalar_relational_operators(self): "type %s and %s failed" % (dt1, dt2)) #Signed integers and floats - for dt1 in 'bhlqp' + np.typecodes['Float']: + for dt1 in 'bhl' + np.typecodes['Float']: assert_(1 > np.array(-1, dtype=dt1)[()], "type %s failed" % (dt1,)) assert_(not 1 < np.array(-1, dtype=dt1)[()], "type %s failed" % (dt1,)) assert_(-1 == np.array(-1, dtype=dt1)[()], "type %s failed" % (dt1,)) - for dt2 in 'bhlqp' + np.typecodes['Float']: + for dt2 in 'bhl' + np.typecodes['Float']: assert_(np.array(1, dtype=dt1)[()] > np.array(-1, dtype=dt2)[()], "type %s and %s failed" % (dt1, dt2)) assert_(not np.array(1, dtype=dt1)[()] < np.array(-1, dtype=dt2)[()], @@ -517,20 +514,9 @@ def test_scalar_comparison_to_none(self): with warnings.catch_warnings(record=True) as w: warnings.filterwarnings('always', '', FutureWarning) assert_(not np.float32(1) == None) - assert_(not np.str_('test') == None) - # This is dubious (see below): - assert_(not np.datetime64('NaT') == None) - assert_(np.float32(1) != None) - assert_(np.str_('test') != None) - # This is dubious (see below): - assert_(np.datetime64('NaT') != None) assert_(len(w) == 0) - # For documentation purposes, this is why the datetime is dubious. - # At the time of deprecation this was no behaviour change, but - # it has to be considered when the deprecations are done. - assert_(np.equal(np.datetime64('NaT'), None)) #class TestRepr: @@ -542,6 +528,7 @@ def test_scalar_comparison_to_none(self): # assert_equal( val, val2 ) +@pytest.mark.xfail(reason="can delegate repr to pytorch") class TestRepr: def _test_type_repr(self, t): finfo = np.finfo(t) @@ -575,20 +562,7 @@ def test_float_repr(self): self._test_type_repr(t) -if not IS_PYPY: - # sys.getsizeof() is not valid on PyPy - class TestSizeOf: - - def test_equal_nbytes(self): - for type in types: - x = type(0) - assert_(sys.getsizeof(x) > x.nbytes) - - def test_error(self): - d = np.float32() - assert_raises(TypeError, d.__sizeof__, "a") - - +@pytest.mark.skip(reason="Array scalars do not decay to python scalars.") class TestMultiply: def test_seq_repeat(self): # Test that basic sequences get repeated when multiplied with @@ -646,9 +620,9 @@ def test_exceptions(self): assert_raises(TypeError, operator.neg, a) def test_result(self): - types = np.typecodes['AllInteger'] + np.typecodes['AllFloat'] - with suppress_warnings() as sup: - sup.filter(RuntimeWarning) + types = np.typecodes['AllInteger'] + np.typecodes['AllFloat'] +## with suppress_warnings() as sup: +## sup.filter(RuntimeWarning) for dt in types: a = np.ones((), dtype=dt)[()] if dt in np.typecodes['UnsignedInteger']: @@ -661,15 +635,16 @@ def test_result(self): class TestSubtract: def test_exceptions(self): a = np.ones((), dtype=np.bool_)[()] - assert_raises(TypeError, operator.sub, a, a) + with assert_raises(TypeError): + operator.sub(a, a) def test_result(self): types = np.typecodes['AllInteger'] + np.typecodes['AllFloat'] - with suppress_warnings() as sup: - sup.filter(RuntimeWarning) - for dt in types: - a = np.ones((), dtype=dt)[()] - assert_equal(operator.sub(a, a), 0) +# with suppress_warnings() as sup: +# sup.filter(RuntimeWarning) + for dt in types: + a = np.ones((), dtype=dt)[()] + assert_equal(operator.sub(a, a), 0) class TestAbs: @@ -687,10 +662,10 @@ def _test_abs_func(self, absfunc, test_dtype): x = test_dtype(np.finfo(test_dtype).max) assert_equal(absfunc(x), x.real) - with suppress_warnings() as sup: - sup.filter(UserWarning) - x = test_dtype(np.finfo(test_dtype).tiny) - assert_equal(absfunc(x), x.real) + # with suppress_warnings() as sup: + # sup.filter(UserWarning) + x = test_dtype(np.finfo(test_dtype).tiny) + assert_equal(absfunc(x), x.real) x = test_dtype(np.finfo(test_dtype).min) assert_equal(absfunc(x), -x.real) @@ -703,6 +678,8 @@ def test_builtin_abs(self, dtype): def test_numpy_abs(self, dtype): self._test_abs_func(np.abs, dtype) + +@pytest.mark.skip(reason='TODO: implement bit shifts') class TestBitShifts: @pytest.mark.parametrize('type_code', np.typecodes['AllInteger']) @@ -731,6 +708,7 @@ def test_shift_all_bits(self, type_code, op): assert_equal(res_arr, res_scl) +@pytest.mark.xfail(reason='Will rely on pytest for hashing') class TestHash: @pytest.mark.parametrize("type_code", np.typecodes['AllInteger']) def test_integer_hashes(self, type_code): @@ -809,6 +787,7 @@ def test_operator_scalars(op, type1, type2): ''' +@pytest.mark.xfail(reason="pytorch does not warn on overflow") @pytest.mark.parametrize("dtype", np.typecodes["AllInteger"]) @pytest.mark.parametrize("operation", [ lambda min, max: max + max, @@ -823,6 +802,7 @@ def test_scalar_integer_operation_overflow(dtype, operation): operation(min, max) +@pytest.mark.xfail(reason="pytorch does not warn on overflow") @pytest.mark.parametrize("dtype", np.typecodes["Integer"]) @pytest.mark.parametrize("operation", [ lambda min, neg_1: -min, @@ -841,6 +821,7 @@ def test_scalar_signed_integer_overflow(dtype, operation): operation(min, neg_1) +@pytest.mark.xfail(reason="pytorch does not warn on overflow") @pytest.mark.parametrize("dtype", np.typecodes["UnsignedInteger"]) def test_scalar_unsigned_integer_overflow(dtype): val = np.dtype(dtype).type(8) @@ -850,6 +831,8 @@ def test_scalar_unsigned_integer_overflow(dtype): zero = np.dtype(dtype).type(0) -zero # does not warn + +@pytest.mark.xfail(reason="pytorch raises RuntimeError on division by zero") @pytest.mark.parametrize("dtype", np.typecodes["AllInteger"]) @pytest.mark.parametrize("operation", [ lambda val, zero: val // zero, @@ -881,6 +864,7 @@ def test_scalar_integer_operation_divbyzero(dtype, operation): ] +@pytest.mark.skip(reason="We do not support subclassing scalars.") @pytest.mark.parametrize(["__op__", "__rop__", "op", "cmp"], ops_with_names) @pytest.mark.parametrize("sctype", [np.float32, np.float64]) def test_subclass_deferral(sctype, __op__, __rop__, op, cmp): @@ -919,7 +903,7 @@ def rop_func(self, other): assert op(myf_simple1(1), myf_op(2)) == op(1, 2) # inherited - +@pytest.mark.skip(reason="We do not support subclassing scalars.") @pytest.mark.parametrize(["__op__", "__rop__", "op", "cmp"], ops_with_names) @pytest.mark.parametrize("subtype", [float, int, complex, np.float16]) #@np._no_nep50_warning() From 1f1973d9a9a1c1202e98fdba9cd1620e90c17635 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Fri, 6 Jan 2023 18:42:27 +0300 Subject: [PATCH 2/3] MAINT: rationalize the array/asarray split --- torch_np/_dtypes.py | 27 ++++- torch_np/_ndarray.py | 106 ++++++++++-------- .../tests/numpy_tests/core/test_scalarmath.py | 4 +- torch_np/tests/test_reductions.py | 23 ++-- 4 files changed, 101 insertions(+), 59 deletions(-) diff --git a/torch_np/_dtypes.py b/torch_np/_dtypes.py index 3d022bb2..66c07267 100644 --- a/torch_np/_dtypes.py +++ b/torch_np/_dtypes.py @@ -194,6 +194,8 @@ def torch_dtype_from(dtyp): raise TypeError +# ### Defaults and dtype discovery + def default_int_type(): return dtype('int64') @@ -202,14 +204,33 @@ def default_float_type(): return dtype('float64') +def default_complex_type(): + return dtype('complex128') + + def is_floating(dtyp): dtyp = dtype(dtyp) - return dtyp.typecode in typecodes['AllFloat'] + return issubclass(dtyp.type, _scalar_types.floating) + def is_integer(dtyp): dtyp = dtype(dtyp) - return dtyp.typecode in typecodes['AllInteger'] - + return issubclass(dtyp.type, _scalar_types.integer) + + +def get_default_dtype_for(dtyp): + typ = dtype(dtyp).type + if issubclass(typ, _scalar_types.integer): + result = default_int_type() + elif issubclass(typ, _scalar_types.floating): + result = default_float_type() + elif issubclass(typ, _scalar_types.complexfloating): + result = default_complex_type() + elif issubclass(typ, _scalar_types.bool_): + result = dtype('bool') + else: + raise TypeError("dtype %s not understood." % dtyp) + return result def issubclass_(arg, klass): diff --git a/torch_np/_ndarray.py b/torch_np/_ndarray.py index 89c87a64..904d8229 100644 --- a/torch_np/_ndarray.py +++ b/torch_np/_ndarray.py @@ -159,6 +159,9 @@ def __hash__(self): def __float__(self): return float(self._tensor) + def __int__(self): + return int(self._tensor) + # XXX : are single-element ndarrays scalars? def is_integer(self): if self.shape == (): @@ -354,7 +357,7 @@ def mean(self, axis=None, dtype=None, out=None, keepdims=NoValue, *, where=NoVal if dtype is None: dtype = self.dtype - if not _dtypes.is_floating(dtype): + if _dtypes.is_integer(dtype): dtype = _dtypes.default_float_type() torch_dtype = _dtypes.torch_dtype_from(dtype) @@ -374,7 +377,7 @@ def sum(self, axis=None, dtype=None, out=None, keepdims=NoValue, if dtype is None: dtype = self.dtype - if not _dtypes.is_floating(dtype): + if _dtypes.is_integer(dtype): dtype = _dtypes.default_float_type() torch_dtype = _dtypes.torch_dtype_from(dtype) @@ -396,67 +399,80 @@ def __setitem__(self, index, value): return self._tensor.__setitem__(index, value) -def asarray(a, dtype=None, order=None, *, like=None): - _util.subok_not_ok(like) - if order is not None: +# This is the ideally the only place which talks to ndarray directly. +# The rest goes through asarray (preferred) or array. + +def array(object, dtype=None, *, copy=True, order='K', subok=False, ndmin=0, + like=None): + _util.subok_not_ok(like, subok) + if order != 'K': raise NotImplementedError - if isinstance(a, ndarray): - if dtype is not None and dtype != a.dtype: - a = a.astype(dtype) - return a + # a happy path + if isinstance(object, ndarray): + if copy is False and dtype is None and ndmin <= object.ndim: + return object - if isinstance(a, (list, tuple)): - # handle lists of ndarrays, [1, [2, 3], ndarray(4)] etc + # lists of ndarrays: [1, [2, 3], ndarray(4)] convert to lists of lists + if isinstance(object, (list, tuple)): a1 = [] - for elem in a: + for elem in object: if isinstance(elem, ndarray): a1.append(elem.get().tolist()) else: a1.append(elem) + object = a1 + + # get the tensor from "object" + if isinstance(object, ndarray): + tensor = object._tensor + base = object + elif isinstance(object, torch.Tensor): + tensor = object + base = None else: - a1 = a + tensor = torch.as_tensor(object) + base = None - torch_dtype = _dtypes.torch_dtype_from(dtype) + # At this point, `tensor.dtype` is the pytorch default. Our default may + # differ, so need to typecast. However, we cannot just do `tensor.to`, + # because if our desired dtype is wider then pytorch's, `tensor` + # may have lost precision: - # This and array(...) are the only places which talk to ndarray directly. - # The rest goes through asarray (preferred) or array. - out = ndarray() - tt = torch.as_tensor(a1, dtype=torch_dtype) - out._tensor = tt - return out + # int(torch.as_tensor(1e12)) - 1e12 equals -4096 (try it!) + # Therefore, we treat `tensor.dtype` as a hint, and convert the + # original object *again*, this time with an explicit dtype. + dtyp = _dtypes.dtype_from_torch(tensor.dtype) + default = _dtypes.get_default_dtype_for(dtyp) + torch_dtype = _dtypes.torch_dtype_from(default) -def array(object, dtype=None, *, copy=True, order='K', subok=False, ndmin=0, - like=None): - _util.subok_not_ok(like, subok) - if order != 'K': - raise NotImplementedError - - if isinstance(object, (list, tuple)): - obj = asarray(object) - return array(obj, dtype, copy=copy, order=order, subok=subok, - ndmin=ndmin, like=like) + tensor = torch.as_tensor(object, dtype=torch_dtype) - if isinstance(object, ndarray): - result = object._tensor - - if dtype != object.dtype: - torch_dtype = _dtypes.torch_dtype_from(dtype) - result = result.to(torch_dtype) - else: + # type cast if requested + if dtype is not None: torch_dtype = _dtypes.torch_dtype_from(dtype) - result = torch.as_tensor(object, dtype=torch_dtype) + tensor = tensor.to(torch_dtype) + base = None + # adjust ndim if needed + ndim_extra = ndmin - tensor.ndim + if ndim_extra > 0: + tensor = tensor.view((1,)*ndim_extra + tensor.shape) + base = None + + # copy if requested if copy: - result = result.clone() + tensor = tensor.clone() + base = None - ndim_extra = ndmin - result.ndim - if ndim_extra > 0: - result = result.reshape((1,)*ndim_extra + result.shape) - out = ndarray() - out._tensor = result - return out + return ndarray._from_tensor_and_base(tensor, base) + + +def asarray(a, dtype=None, order=None, *, like=None): + if order is None: + order = 'K' + return array(a, dtype=dtype, order=order, like=like, copy=False, ndmin=0) diff --git a/torch_np/tests/numpy_tests/core/test_scalarmath.py b/torch_np/tests/numpy_tests/core/test_scalarmath.py index aacabc0b..f02b48ef 100644 --- a/torch_np/tests/numpy_tests/core/test_scalarmath.py +++ b/torch_np/tests/numpy_tests/core/test_scalarmath.py @@ -191,7 +191,6 @@ def test_integers_to_negative_integer_power(self): # 1 ** -1 possible special case base = [np.array(1, dt)[()] for dt in 'bhilB'] for i1, i2 in itertools.product(base, exp): - pass if i1.dtype != np.uint64: assert_raises(ValueError, operator.pow, i1, i2) else: @@ -435,15 +434,18 @@ def test_branches(self): class TestConversion: def test_int_from_long(self): + # NB: this test assumes that the default fp type is float64 l = [1e6, 1e12, 1e18, -1e6, -1e12, -1e18] li = [10**6, 10**12, 10**18, -10**6, -10**12, -10**18] for T in [None, np.float64, np.int64]: a = np.array(l, dtype=T) assert_equal([int(_m) for _m in a], li) + @pytest.mark.xfail(reason="pytorch does not emit this warning.") def test_iinfo_long_values_1(self): for code in 'bBh': + with pytest.warns(DeprecationWarning): res = np.array(np.iinfo(code).max + 1, dtype=code) tgt = np.iinfo(code).min diff --git a/torch_np/tests/test_reductions.py b/torch_np/tests/test_reductions.py index 5a699345..108b358e 100644 --- a/torch_np/tests/test_reductions.py +++ b/torch_np/tests/test_reductions.py @@ -248,16 +248,19 @@ def test_mean_values(self): rmat = np.arange(20, dtype=float).reshape((4, 5)) cmat = rmat + 1j*rmat - for mat in [rmat, cmat]: - for axis in [0, 1]: - tgt = mat.sum(axis=axis) - res = np.mean(mat, axis=axis) * mat.shape[axis] - assert_allclose(res, tgt) - - for axis in [None]: - tgt = mat.sum(axis=axis) - res = np.mean(mat, axis=axis) * mat.size - assert_allclose(res, tgt) + import warnings + with warnings.catch_warnings(): + warnings.simplefilter('error') + for mat in [rmat, cmat]: + for axis in [0, 1]: + tgt = mat.sum(axis=axis) + res = np.mean(mat, axis=axis) * mat.shape[axis] + assert_allclose(res, tgt) + + for axis in [None]: + tgt = mat.sum(axis=axis) + res = np.mean(mat, axis=axis) * mat.size + assert_allclose(res, tgt) @pytest.mark.xfail(reason="see pytorch/gh-91597") def test_mean_float16(self): From 2b95ac252dd48cb915d18fd7fd91a75e33318aa5 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 11 Jan 2023 20:59:28 +0300 Subject: [PATCH 3/3] MAINT: address review comments --- .../tests/numpy_tests/core/test_scalarmath.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/torch_np/tests/numpy_tests/core/test_scalarmath.py b/torch_np/tests/numpy_tests/core/test_scalarmath.py index f02b48ef..7269637f 100644 --- a/torch_np/tests/numpy_tests/core/test_scalarmath.py +++ b/torch_np/tests/numpy_tests/core/test_scalarmath.py @@ -261,7 +261,11 @@ class TestModulus: def test_modulus_basic(self): dt = np.typecodes['AllInteger'] + np.typecodes['Float'] - for op in [floordiv_and_mod,]: # TODO: divmod is not implemented + for op in [floordiv_and_mod, divmod]: + + if op == divmod: + pytest.xfail(reason="__divmod__ not implemented") + for dt1, dt2 in itertools.product(dt, dt): for sg1, sg2 in itertools.product(_signs(dt1), _signs(dt2)): fmt = 'op: %s, dt1: %s, dt2: %s, sg1: %s, sg2: %s' @@ -306,7 +310,11 @@ def test_float_modulus_exact(self): def test_float_modulus_roundoff(self): # gh-6127 dt = np.typecodes['Float'] - for op in [floordiv_and_mod]: # TODO divmod is not implemented + for op in [floordiv_and_mod, divmod]: + + if op == divmod: + pytest.xfail(reason="__divmod__ not implemented") + for dt1, dt2 in itertools.product(dt, dt): for sg1, sg2 in itertools.product((+1, -1), (+1, -1)): fmt = 'op: %s, dt1: %s, dt2: %s, sg1: %s, sg2: %s' @@ -360,7 +368,7 @@ def test_float_modulus_corner_cases(self): class TestComplexDivision: - @pytest.mark.xfail(reason='With pytorch, 1/(0+0j) is nan + nan*j, not inf + nan*j') + @pytest.mark.skip(reason='With pytorch, 1/(0+0j) is nan + nan*j, not inf + nan*j') def test_zero_division(self): with np.errstate(all="ignore"): for t in [np.complex64, np.complex128]: @@ -441,7 +449,6 @@ def test_int_from_long(self): a = np.array(l, dtype=T) assert_equal([int(_m) for _m in a], li) - @pytest.mark.xfail(reason="pytorch does not emit this warning.") def test_iinfo_long_values_1(self): for code in 'bBh': @@ -681,7 +688,7 @@ def test_numpy_abs(self, dtype): self._test_abs_func(np.abs, dtype) -@pytest.mark.skip(reason='TODO: implement bit shifts') +@pytest.mark.xfail(reason='TODO: implement bit shifts') class TestBitShifts: @pytest.mark.parametrize('type_code', np.typecodes['AllInteger'])