diff --git a/doc/reference/ndarray.rst b/doc/reference/ndarray.rst index 559723120b1e..7981e47cc791 100644 --- a/doc/reference/ndarray.rst +++ b/doc/reference/ndarray.rst @@ -249,7 +249,7 @@ Comparison operators: dpnp.ndarray.__eq__ dpnp.ndarray.__ne__ -Truth value of an array (:func:`bool()`): +Truth value of an array (:class:`bool() `): .. autosummary:: :toctree: generated/ @@ -260,11 +260,11 @@ Truth value of an array (:func:`bool()`): Truth-value testing of an array invokes :meth:`dpnp.ndarray.__bool__`, which raises an error if the number of - elements in the array is larger than 1, because the truth value + elements in the array is not 1, because the truth value of such arrays is ambiguous. Use :meth:`.any() ` and :meth:`.all() ` instead to be clear about what is meant - in such cases. (If the number of elements is 0, the array evaluates - to ``False``.) + in such cases. (If you wish to check for whether an array is empty, + use for example ``.size > 0``.) Unary operations: @@ -300,6 +300,26 @@ Arithmetic: dpnp.ndarray.__xor__ +Arithmetic, reflected: + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + dpnp.ndarray.__radd__ + dpnp.ndarray.__rsub__ + dpnp.ndarray.__rmul__ + dpnp.ndarray.__rtruediv__ + dpnp.ndarray.__rfloordiv__ + dpnp.ndarray.__rmod__ + dpnp.ndarray.__rpow__ + dpnp.ndarray.__rlshift__ + dpnp.ndarray.__rrshift__ + dpnp.ndarray.__rand__ + dpnp.ndarray.__ror__ + dpnp.ndarray.__rxor__ + + Arithmetic, in-place: .. autosummary:: @@ -326,6 +346,8 @@ Matrix Multiplication: :toctree: generated/ dpnp.ndarray.__matmul__ + dpnp.ndarray.__rmatmul__ + dpnp.ndarray.__imatmul__ Special methods diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 09295a54e9d4..77f01c9a6fbe 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -25,6 +25,7 @@ # ***************************************************************************** import dpctl.tensor as dpt +from dpctl.tensor._numpy_helper import AxisError import dpnp @@ -205,6 +206,7 @@ def __bool__(self): return self._array_obj.__bool__() # '__class__', + # `__class_getitem__`, def __complex__(self): return self._array_obj.__complex__() @@ -335,6 +337,8 @@ def __getitem__(self, key): res._array_obj = item return res + # '__getstate__', + def __gt__(self, other): """Return ``self>value``.""" return dpnp.greater(self, other) @@ -361,7 +365,31 @@ def __ilshift__(self, other): dpnp.left_shift(self, other, out=self) return self - # '__imatmul__', + def __imatmul__(self, other): + """Return ``self@=value``.""" + + """ + Unlike `matmul(a, b, out=a)` we ensure that the result is not broadcast + if the result without `out` would have less dimensions than `a`. + Since the signature of matmul is '(n?,k),(k,m?)->(n?,m?)' this is the + case exactly when the second operand has both core dimensions. + We have to enforce this check by passing the correct `axes=`. + """ + if self.ndim == 1: + axes = [(-1,), (-2, -1), (-1,)] + else: + axes = [(-2, -1), (-2, -1), (-2, -1)] + + try: + dpnp.matmul(self, other, out=self, axes=axes) + except AxisError: + # AxisError should indicate that the axes argument didn't work out + # which should mean the second operand not being 2 dimensional. + raise ValueError( + "inplace matrix multiplication requires the first operand to " + "have at least one and the second at least two dimensions." + ) + return self def __imod__(self, other): """Return ``self%=value``.""" @@ -469,9 +497,11 @@ def __pow__(self, other): return dpnp.power(self, other) def __radd__(self, other): + """Return ``value+self``.""" return dpnp.add(other, self) def __rand__(self, other): + """Return ``value&self``.""" return dpnp.bitwise_and(other, self) # '__rdivmod__', @@ -483,27 +513,35 @@ def __repr__(self): return dpt.usm_ndarray_repr(self._array_obj, prefix="array") def __rfloordiv__(self, other): + """Return ``value//self``.""" return dpnp.floor_divide(self, other) def __rlshift__(self, other): + """Return ``value<>self``.""" return dpnp.right_shift(other, self) def __rshift__(self, other): @@ -511,12 +549,15 @@ def __rshift__(self, other): return dpnp.right_shift(self, other) def __rsub__(self, other): + """Return ``value-self``.""" return dpnp.subtract(other, self) def __rtruediv__(self, other): + """Return ``value/self``.""" return dpnp.true_divide(other, self) def __rxor__(self, other): + """Return ``value^self``.""" return dpnp.bitwise_xor(other, self) # '__setattr__', diff --git a/dpnp/dpnp_utils/dpnp_utils_linearalgebra.py b/dpnp/dpnp_utils/dpnp_utils_linearalgebra.py index ac432d595911..eb4ea10d803a 100644 --- a/dpnp/dpnp_utils/dpnp_utils_linearalgebra.py +++ b/dpnp/dpnp_utils/dpnp_utils_linearalgebra.py @@ -28,7 +28,7 @@ import dpctl.tensor._tensor_impl as ti import dpctl.utils as dpu import numpy -from dpctl.tensor._numpy_helper import normalize_axis_tuple +from dpctl.tensor._numpy_helper import AxisError, normalize_axis_tuple from dpctl.utils import ExecutionPlacementError import dpnp @@ -525,7 +525,7 @@ def _validate_internal(axes, i, ndim): ) if len(axes) != 1: - raise ValueError( + raise AxisError( f"Axes item {i} should be a tuple with a single element, or an integer." ) else: @@ -533,7 +533,7 @@ def _validate_internal(axes, i, ndim): if not isinstance(axes, tuple): raise TypeError(f"Axes item {i} should be a tuple.") if len(axes) != 2: - raise ValueError( + raise AxisError( f"Axes item {i} should be a tuple with 2 elements." ) @@ -563,7 +563,7 @@ def _validate_internal(axes, i, ndim): if x1_ndim == 1 and x2_ndim == 1: if axes[2] != (): - raise TypeError("Axes item 2 should be an empty tuple.") + raise AxisError("Axes item 2 should be an empty tuple.") elif x1_ndim == 1 or x2_ndim == 1: axes[2] = _validate_internal(axes[2], 2, 1) else: diff --git a/tests/test_mathematical.py b/tests/test_mathematical.py index ba74c8db1e30..b345fc55cc12 100644 --- a/tests/test_mathematical.py +++ b/tests/test_mathematical.py @@ -4168,25 +4168,91 @@ def test_matmul_with_offsets(self, sh1, sh2): assert_array_equal(result, expected) +class TestMatmulInplace: + ALL_DTYPES = get_all_dtypes(no_none=True) + DTYPES = {} + for i in ALL_DTYPES: + for j in ALL_DTYPES: + if numpy.can_cast(j, i): + DTYPES[f"{i}-{j}"] = (i, j) + + @pytest.mark.parametrize("dtype1, dtype2", DTYPES.values()) + def test_basic(self, dtype1, dtype2): + a = numpy.arange(10).reshape(5, 2).astype(dtype1) + b = numpy.ones((2, 2), dtype=dtype2) + ia, ib = dpnp.array(a), dpnp.array(b) + ia_id = id(ia) + + a @= b + ia @= ib + assert id(ia) == ia_id + assert_dtype_allclose(ia, a) + + @pytest.mark.parametrize( + "a_sh, b_sh", + [ + pytest.param((10**5, 10), (10, 10), id="2d_large"), + pytest.param((10**4, 10, 10), (1, 10, 10), id="3d_large"), + pytest.param((3,), (3,), id="1d"), + pytest.param((3, 3), (3,), id="2d_1d"), + pytest.param((3,), (3, 3), id="1d_2d"), + pytest.param((3, 3), (3, 1), id="2d_broadcast"), + pytest.param((1, 3), (3, 3), id="2d_broadcast_reverse"), + pytest.param((3, 3, 3), (1, 3, 1), id="3d_broadcast1"), + pytest.param((3, 3, 3), (1, 3, 3), id="3d_broadcast2"), + pytest.param((3, 3, 3), (3, 3, 1), id="3d_broadcast3"), + pytest.param((1, 3, 3), (3, 3, 3), id="3d_broadcast_reverse1"), + pytest.param((3, 1, 3), (3, 3, 3), id="3d_broadcast_reverse2"), + pytest.param((1, 1, 3), (3, 3, 3), id="3d_broadcast_reverse3"), + ], + ) + def test_shapes(self, a_sh, b_sh): + a_sz, b_sz = numpy.prod(a_sh), numpy.prod(b_sh) + a = numpy.arange(a_sz).reshape(a_sh).astype(numpy.float64) + b = numpy.arange(b_sz).reshape(b_sh) + + ia, ib = dpnp.array(a), dpnp.array(b) + ia_id = id(ia) + + expected = a @ b + if expected.shape != a_sh: + if len(b_sh) == 1: + # check the exception matches NumPy + match = "inplace matrix multiplication requires" + else: + match = None + + with pytest.raises(ValueError, match=match): + a @= b + + with pytest.raises(ValueError, match=match): + ia @= ib + else: + ia @= ib + assert id(ia) == ia_id + assert_dtype_allclose(ia, expected) + + class TestMatmulInvalidCases: + @pytest.mark.parametrize("xp", [numpy, dpnp]) @pytest.mark.parametrize( - "shape_pair", + "shape1, shape2", [ ((3, 2), ()), ((), (3, 2)), ((), ()), ], ) - def test_zero_dim(self, shape_pair): - for xp in (numpy, dpnp): - shape1, shape2 = shape_pair - x1 = xp.arange(numpy.prod(shape1), dtype=xp.float32).reshape(shape1) - x2 = xp.arange(numpy.prod(shape2), dtype=xp.float32).reshape(shape2) - with pytest.raises(ValueError): - xp.matmul(x1, x2) + def test_zero_dim(self, xp, shape1, shape2): + x1 = xp.arange(numpy.prod(shape1), dtype=xp.float32).reshape(shape1) + x2 = xp.arange(numpy.prod(shape2), dtype=xp.float32).reshape(shape2) + with pytest.raises(ValueError): + xp.matmul(x1, x2) + + @pytest.mark.parametrize("xp", [numpy, dpnp]) @pytest.mark.parametrize( - "shape_pair", + "shape1, shape2", [ ((3,), (4,)), ((2, 3), (4, 5)), @@ -4199,16 +4265,16 @@ def test_zero_dim(self, shape_pair): ((6, 5, 3, 2), (3, 2, 4)), ], ) - def test_invalid_shape(self, shape_pair): - for xp in (numpy, dpnp): - shape1, shape2 = shape_pair - x1 = xp.arange(numpy.prod(shape1), dtype=xp.float32).reshape(shape1) - x2 = xp.arange(numpy.prod(shape2), dtype=xp.float32).reshape(shape2) - with pytest.raises(ValueError): - xp.matmul(x1, x2) + def test_invalid_shape(self, xp, shape1, shape2): + x1 = xp.arange(numpy.prod(shape1), dtype=xp.float32).reshape(shape1) + x2 = xp.arange(numpy.prod(shape2), dtype=xp.float32).reshape(shape2) + with pytest.raises(ValueError): + xp.matmul(x1, x2) + + @pytest.mark.parametrize("xp", [numpy, dpnp]) @pytest.mark.parametrize( - "shape_pair", + "shape1, shape2, out_shape", [ ((5, 4, 3), (3, 1), (3, 4, 1)), ((5, 4, 3), (3, 1), (5, 6, 1)), @@ -4220,24 +4286,24 @@ def test_invalid_shape(self, shape_pair): ((4,), (3, 4, 5), (3, 6)), ], ) - def test_invalid_shape_out(self, shape_pair): - for xp in (numpy, dpnp): - shape1, shape2, out_shape = shape_pair - x1 = xp.arange(numpy.prod(shape1), dtype=xp.float32).reshape(shape1) - x2 = xp.arange(numpy.prod(shape2), dtype=xp.float32).reshape(shape2) - res = xp.empty(out_shape) - with pytest.raises(ValueError): - xp.matmul(x1, x2, out=res) + def test_invalid_shape_out(self, xp, shape1, shape2, out_shape): + x1 = xp.arange(numpy.prod(shape1), dtype=xp.float32).reshape(shape1) + x2 = xp.arange(numpy.prod(shape2), dtype=xp.float32).reshape(shape2) + res = xp.empty(out_shape) + with pytest.raises(ValueError): + xp.matmul(x1, x2, out=res) + + @pytest.mark.parametrize("xp", [numpy, dpnp]) @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)[:-2]) - def test_invalid_dtype(self, dtype): + def test_invalid_dtype(self, xp, dtype): dpnp_dtype = get_all_dtypes(no_none=True)[-1] - a1 = dpnp.arange(5 * 4, dtype=dpnp_dtype).reshape(5, 4) - a2 = dpnp.arange(7 * 4, dtype=dpnp_dtype).reshape(4, 7) - dp_out = dpnp.empty((5, 7), dtype=dtype) + a1 = xp.arange(5 * 4, dtype=dpnp_dtype).reshape(5, 4) + a2 = xp.arange(7 * 4, dtype=dpnp_dtype).reshape(4, 7) + dp_out = xp.empty((5, 7), dtype=dtype) with pytest.raises(TypeError): - dpnp.matmul(a1, a2, out=dp_out) + xp.matmul(a1, a2, out=dp_out) def test_exe_q(self): x1 = dpnp.ones((5, 4), sycl_queue=dpctl.SyclQueue()) @@ -4251,13 +4317,14 @@ def test_exe_q(self): with pytest.raises(ExecutionPlacementError): dpnp.matmul(x1, x2, out=out) - def test_matmul_casting(self): - a1 = dpnp.arange(2 * 4, dtype=dpnp.float32).reshape(2, 4) - a2 = dpnp.arange(4 * 3).reshape(4, 3) + @pytest.mark.parametrize("xp", [numpy, dpnp]) + def test_matmul_casting(self, xp): + a1 = xp.arange(2 * 4, dtype=xp.float32).reshape(2, 4) + a2 = xp.arange(4 * 3).reshape(4, 3) - res = dpnp.empty((2, 3), dtype=dpnp.int64) + res = xp.empty((2, 3), dtype=xp.int64) with pytest.raises(TypeError): - dpnp.matmul(a1, a2, out=res, casting="safe") + xp.matmul(a1, a2, out=res, casting="safe") def test_matmul_not_implemented(self): a1 = dpnp.arange(2 * 4).reshape(2, 4) @@ -4273,52 +4340,53 @@ def test_matmul_not_implemented(self): with pytest.raises(NotImplementedError): dpnp.matmul(a1, a2, axis=2) - def test_matmul_axes(self): - a1 = dpnp.arange(120).reshape(2, 5, 3, 4) - a2 = dpnp.arange(120).reshape(4, 2, 5, 3) + @pytest.mark.parametrize("xp", [numpy, dpnp]) + def test_matmul_axes(self, xp): + a1 = xp.arange(120).reshape(2, 5, 3, 4) + a2 = xp.arange(120).reshape(4, 2, 5, 3) # axes must be a list axes = ((3, 1), (2, 0), (0, 1)) with pytest.raises(TypeError): - dpnp.matmul(a1, a2, axes=axes) + xp.matmul(a1, a2, axes=axes) # axes must be be a list of three tuples axes = [(3, 1), (2, 0)] with pytest.raises(ValueError): - dpnp.matmul(a1, a2, axes=axes) + xp.matmul(a1, a2, axes=axes) # axes item should be a tuple axes = [(3, 1), (2, 0), [0, 1]] with pytest.raises(TypeError): - dpnp.matmul(a1, a2, axes=axes) + xp.matmul(a1, a2, axes=axes) # axes item should be a tuple with 2 elements axes = [(3, 1), (2, 0), (0, 1, 2)] - with pytest.raises(ValueError): - dpnp.matmul(a1, a2, axes=axes) + with pytest.raises(AxisError): + xp.matmul(a1, a2, axes=axes) # axes must be an integer axes = [(3, 1), (2, 0), (0.0, 1)] with pytest.raises(TypeError): - dpnp.matmul(a1, a2, axes=axes) + xp.matmul(a1, a2, axes=axes) # axes item 2 should be an empty tuple - a = dpnp.arange(3) + a = xp.arange(3) axes = [0, 0, 0] - with pytest.raises(TypeError): - dpnp.matmul(a, a, axes=axes) + with pytest.raises(AxisError): + xp.matmul(a, a, axes=axes) - a = dpnp.arange(3 * 4 * 5).reshape(3, 4, 5) - b = dpnp.arange(3) + a = xp.arange(3 * 4 * 5).reshape(3, 4, 5) + b = xp.arange(3) # list object cannot be interpreted as an integer axes = [(1, 0), (0), [0]] with pytest.raises(TypeError): - dpnp.matmul(a, b, axes=axes) + xp.matmul(a, b, axes=axes) # axes item should be a tuple with a single element, or an integer axes = [(1, 0), (0), (0, 1)] - with pytest.raises(ValueError): - dpnp.matmul(a, b, axes=axes) + with pytest.raises(AxisError): + xp.matmul(a, b, axes=axes) def test_elemenwise_nin_nout():