diff --git a/decent_array/_array.py b/decent_array/_array.py index 2799ad7..4a0224a 100644 --- a/decent_array/_array.py +++ b/decent_array/_array.py @@ -11,8 +11,8 @@ Hot-path notes: -* ``__add__``/``__sub__``/``__mul__``/``__truediv__``/``__matmul__``, the unary - ``__neg__``/``__abs__``/``__pow__``, the comparisons ``__eq__``/``__ne__``/``__lt__``/ +* ``__add__``/``__sub__``/``__mul__``/``__truediv__``/``__matmul__``, ``__floordiv__``, ``__mod__``, + the unary ``__neg__``/``__abs__``/``__pow__``, the comparisons ``__eq__``/``__ne__``/``__lt__``/ ``__le__``/``__gt__``/``__ge__`` and the bitwise ``__and__``/``__rand__`` are inlined: every supported framework's tensor implements the equivalent operator natively with numpy-equivalent semantics, so routing through the interoperability layer @@ -29,12 +29,13 @@ from typing import TYPE_CHECKING, Any, Self from decent_array.interoperability._backend_manager import register_backend_listener +from decent_array.types import _STRING_TO_DTYPE if TYPE_CHECKING: from numpy.typing import NDArray from decent_array.interoperability._abstracts import Backend - from decent_array.types import ArrayKey, SupportedArrayTypes, SupportedDevices + from decent_array.types import ArrayKey, DTypes, SupportedArrayTypes, SupportedDevices _BACKEND_INSTANCE: Backend | None = None @@ -119,6 +120,22 @@ def __rtruediv__(self, other: int | float | complex | Array, /) -> Array: """Return the true division of ``other`` by the array.""" return Array(other / self.value) + def __floordiv__(self, other: int | float | Array, /) -> Array: + """Return the floor division of the array by ``other``.""" + return Array(self.value // (other.value if type(other) is Array else other)) + + def __rfloordiv__(self, other: int | float | Array, /) -> Array: + """Return the floor division of ``other`` by the array.""" + return Array(other // self.value) + + def __mod__(self, other: int | float | Array, /) -> Array: + """Return the remainder after floor division of the array by ``other``.""" + return Array(self.value % (other.value if type(other) is Array else other)) + + def __rmod__(self, other: int | float | Array, /) -> Array: + """Return the remainder after floor division of ``other`` by the array.""" + return Array(other % self.value) + def __matmul__(self, other: Array, /) -> Array: """Return the matrix multiplication of the array with ``other``.""" return Array(self.value @ other.value) @@ -128,12 +145,16 @@ def __rmatmul__(self, other: Array, /) -> Array: return Array(other.value @ self.value) def __pow__(self, other: int | float | complex | Array, /) -> Array: - """Exponentiate the array by a scalar power.""" + """Exponentiate the array element-wise.""" # numpy/torch/jax/tf all implement ``tensor ** p`` with semantics matching the # backend's ``pow``; routing through the backend would cost an extra method # call for no behavioral difference. return Array(self.value ** (other.value if type(other) is Array else other)) + def __rpow__(self, other: int | float | complex | Array, /) -> Array: + """Exponentiate other element-wise by array.""" + return Array(other**self.value) + # Comparisons ---------------------------------------------------------- # # Element-wise comparisons return an :class:`Array` of bools. The ``__eq__`` and @@ -189,6 +210,42 @@ def __rand__(self, other: bool | int | Array, /) -> Array: """Element-wise bitwise/logical AND with the array on the right.""" return Array((other.value if type(other) is Array else other) & self.value) + def __or__(self, other: bool | int | Array, /) -> Array: + """Element-wise bitwise/logical OR.""" + return Array(self.value | (other.value if type(other) is Array else other)) + + def __ror__(self, other: bool | int | Array, /) -> Array: + """Element-wise bitwise/logical OR with the array on the right.""" + return Array((other.value if type(other) is Array else other) | self.value) + + def __xor__(self, other: bool | int | Array, /) -> Array: + """Element-wise bitwise/logical XOR.""" + return Array(self.value ^ (other.value if type(other) is Array else other)) + + def __rxor__(self, other: bool | int | Array, /) -> Array: + """Element-wise bitwise/logical XOR with the array on the right.""" + return Array((other.value if type(other) is Array else other) ^ self.value) + + def __lshift__(self, other: int | Array, /) -> Array: + """Element-wise bitwise left shift as specified by int/int array.""" + return self._backend.bitwise_left_shift(self, other) + + def __rlshift__(self, other: int | Array, /) -> Array: + """Element-wise bitwise left shift as specified by int/int array on the right.""" + return self._backend.bitwise_left_shift(other, self) + + def __rshift__(self, other: int | Array, /) -> Array: + """Element-wise bitwise right shift as specified by int/int array.""" + return self._backend.bitwise_right_shift(self, other) + + def __rrshift__(self, other: int | Array, /) -> Array: + """Element-wise bitwise right shift as specified by int/int array on the right.""" + return self._backend.bitwise_right_shift(other, self) + + def __invert__(self) -> Array: + """Element-wise bitwise/logical NOT.""" + return Array(~self.value) + # In-place arithmetic -------------------------------------------------- # # The backend handles the framework's mutability semantics: numpy/pytorch mutate @@ -215,6 +272,51 @@ def __itruediv__(self, other: int | float | complex | Array, /) -> Self: self._backend.idivide(self, other) return self + def __ifloordiv__(self, other: int | float | Array) -> Self: + """In-place floor division.""" + self._backend.ifloordiv(self, other) + return self + + def __imod__(self, other: int | float | Array) -> Self: + """In-place remainder after floor division.""" + self._backend.imod(self, other) + return self + + def __ipow__(self, other: int | float | complex | Array) -> Self: + """In-place raise array to power ``other``.""" + self._backend.ipow(self, other) + return self + + def __imatmul__(self, other: Array) -> Self: + """In-place matrix multiplication.""" + self._backend.imatmul(self, other) + return self + + def __iand__(self, other: bool | int | Array) -> Self: + """In-place bitwise/logical AND.""" + self._backend.iand(self, other) + return self + + def __ior__(self, other: bool | int | Array) -> Self: + """In-place bitwise/logical OR.""" + self._backend.ior(self, other) + return self + + def __ixor__(self, other: bool | int | Array) -> Self: + """In-place bitwise/logical XOR.""" + self._backend.ixor(self, other) + return self + + def __ilshift__(self, other: int | Array) -> Self: + """In-place bitwise left shift.""" + self._backend.ilshift(self, other) + return self + + def __irshift__(self, other: int | Array) -> Self: + """In-place bitwise right shift.""" + self._backend.irshift(self, other) + return self + # Unary ---------------------------------------------------------------- def __neg__(self) -> Array: @@ -223,6 +325,10 @@ def __neg__(self) -> Array: # supported frameworks, so the indirection is not needed. return Array(-self.value) + def __pos__(self) -> Array: + """Return the array itself.""" + return self + def __abs__(self) -> Array: """Return the absolute value of the array.""" # Same rationale as ``__neg__`` — native ``abs(tensor)`` matches each @@ -251,6 +357,22 @@ def __float__(self) -> float: """Coerce a scalar array to a Python float.""" return float(self._backend.squeeze(self).value) + def __bool__(self) -> bool: + """Coerce a scalar array to a Python bool.""" + return bool(self._backend.squeeze(self).value) + + def __int__(self) -> int: + """Coerce a scalar array to a Python int.""" + return int(self._backend.squeeze(self).value) + + def __complex__(self) -> complex: + """Coerce a scalar array to a Python complex.""" + return complex(self._backend.squeeze(self).value) + + def __index__(self) -> int: + """Coerce a scalar array to a Python int.""" + return int(self._backend.squeeze(self).value) + # Repr ----------------------------------------------------------------- def __repr__(self) -> str: @@ -278,6 +400,25 @@ def ndim(self) -> int: """Return the number of dimensions of the array.""" return self._backend.ndim(self) + @property + def dtype(self) -> DTypes: + """ + Return dtype of the Array as item of DTypes enum. + + Raises: + ValueError: for dtypes that are not supported by all decent-array functions + + """ + # get framework-native dtype as string + # split takes care of types with names like "torch.float32" + dtype_name = str(self.value.dtype).split(".")[-1] + + dtype = _STRING_TO_DTYPE.get(dtype_name) + if dtype is None: + raise ValueError(f"dtype {self.value.dtype} is not supported by all decent-array functions.") + + return dtype + @property def transpose(self) -> Array: """Return a transposed view of the array.""" @@ -288,6 +429,11 @@ def T(self) -> Array: # noqa: N802 """Return a transposed view of the array.""" return self.transpose + @property + def mT(self) -> Array: # noqa: N802 + """Return the matrix transpose (last two dimensions swapped).""" + return self._backend.matrix_transpose(self) + @property def any(self) -> bool: """Return True if any element of the array is truthy.""" diff --git a/decent_array/interoperability/__init__.py b/decent_array/interoperability/__init__.py index a837416..8465bca 100644 --- a/decent_array/interoperability/__init__.py +++ b/decent_array/interoperability/__init__.py @@ -13,7 +13,14 @@ """ from ._backend_manager import default_device, set_backend -from ._iop.bit_operators import bitwise_and +from ._iop.bit_operators import ( + bitwise_and, + bitwise_invert, + bitwise_left_shift, + bitwise_or, + bitwise_right_shift, + bitwise_xor, +) from ._iop.comparasion import equal, greater, greater_equal, less, less_equal, not_equal from ._iop.creation import eye, ones, ones_like, zeros, zeros_like from ._iop.linalg import dot, matmul, norm, vecdot, vector_norm @@ -26,6 +33,7 @@ expand_dims, from_numpy, from_numpy_like, + matrix_transpose, ndim, reshape, shape, @@ -36,7 +44,20 @@ transpose, unsqueeze, ) -from ._iop.math import abs, absolute, add, divide, multiply, negative, pow, sqrt, subtract # noqa: A004 +from ._iop.math import ( + abs, # noqa: A004 + absolute, + add, + divide, + floor_divide, + multiply, + negative, + positive, + pow, # noqa: A004 + remainder, + sqrt, + subtract, +) from ._iop.operators import argmax, argmin, maximum, sign from ._iop.reductions import all, any, max, mean, min, sum # noqa: A004 from ._iop.rng import ( @@ -65,6 +86,11 @@ "asarray", "astype", "bitwise_and", + "bitwise_invert", + "bitwise_left_shift", + "bitwise_or", + "bitwise_right_shift", + "bitwise_xor", "choice", "copy", "default_device", @@ -77,6 +103,7 @@ "equal", "expand_dims", "eye", + "floor_divide", "from_numpy", "from_numpy_like", "get_numpy_rng", @@ -87,6 +114,7 @@ "less", "less_equal", "matmul", + "matrix_transpose", "max", "maximum", "mean", @@ -100,7 +128,9 @@ "not_equal", "ones", "ones_like", + "positive", "pow", + "remainder", "reshape", "set_backend", "set_rng_state", diff --git a/decent_array/interoperability/_abstracts/backend.py b/decent_array/interoperability/_abstracts/backend.py index cfa17b8..0d3789b 100644 --- a/decent_array/interoperability/_abstracts/backend.py +++ b/decent_array/interoperability/_abstracts/backend.py @@ -102,6 +102,10 @@ def reshape(self, x: Array, shape: tuple[int, ...]) -> Array: def transpose(self, x: Array, axis: tuple[int, ...] | None = None) -> Array: """Transpose ``x``; ``None`` reverses the dimensions.""" + @abstractmethod + def matrix_transpose(self, x: Array) -> Array: + """Transpose the innermost two dimensions of ``x``.""" + @abstractmethod def shape(self, x: Array) -> tuple[int, ...]: """Return the shape of ``x``.""" @@ -144,6 +148,10 @@ def vecdot(self, x1: Array, x2: Array) -> Array: def matmul(self, x1: Array, x2: Array) -> Array: """Matrix multiplication of two arrays.""" + @abstractmethod + def imatmul[T: Array](self, x1: T, x2: Array) -> T: + """In-place matrix multiplication.""" + @abstractmethod def vector_norm( self, @@ -216,10 +224,30 @@ def divide(self, x1: int | float | complex | Array, x2: int | float | complex | def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: """In-place element-wise division.""" + @abstractmethod + def floor_divide(self, x1: int | float | Array, x2: int | float | Array) -> Array: + """Element-wise floor division.""" + + @abstractmethod + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> T: + """In-place floor division.""" + + @abstractmethod + def remainder(self, x1: int | float | Array, x2: int | float | Array) -> Array: + """Element-wise remainder after floor division.""" + + @abstractmethod + def imod[T: Array](self, x1: T, x2: int | float | Array) -> T: + """In-place remainder after floor division.""" + @abstractmethod def pow(self, x1: int | float | complex | Array, x2: int | float | complex | Array) -> Array: """Raise ``x1`` to power ``x2``.""" + @abstractmethod + def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: + """In-place raise ``x1`` to power ``x2``.""" + @abstractmethod def negative(self, x: Array) -> Array: """Element-wise negation.""" @@ -266,6 +294,46 @@ def greater_equal(self, x1: int | float | complex | Array, x2: int | float | com def bitwise_and(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: """Element-wise bitwise/logical AND.""" + @abstractmethod + def iand[T: Array](self, x1: T, x2: bool | int | Array) -> T: + """In-place bitwise/logical AND.""" + + @abstractmethod + def bitwise_invert(self, x: Array) -> Array: + """Element-wise bitwise/logical NOT.""" + + @abstractmethod + def bitwise_or(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: + """Element-wise bitwise/logical OR.""" + + @abstractmethod + def ior[T: Array](self, x1: T, x2: bool | int | Array) -> T: + """In-place bitwise/logical OR.""" + + @abstractmethod + def bitwise_xor(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: + """Element-wise bitwise/logical XOR.""" + + @abstractmethod + def ixor[T: Array](self, x1: T, x2: bool | int | Array) -> T: + """In-place bitwise/logical XOR.""" + + @abstractmethod + def bitwise_left_shift(self, x1: int | Array, x2: int | Array) -> Array: + """Element-wise bitwise left shift.""" + + @abstractmethod + def ilshift[T: Array](self, x1: T, x2: int | Array) -> T: + """In-place bitwise left shift.""" + + @abstractmethod + def bitwise_right_shift(self, x1: int | Array, x2: int | Array) -> Array: + """Element-wise bitwise right shift.""" + + @abstractmethod + def irshift[T: Array](self, x1: T, x2: int | Array) -> T: + """In-place bitwise right shift.""" + # Operators ----------------------------------------------------------- @abstractmethod diff --git a/decent_array/interoperability/_iop/bit_operators.py b/decent_array/interoperability/_iop/bit_operators.py index aa58334..48362bd 100644 --- a/decent_array/interoperability/_iop/bit_operators.py +++ b/decent_array/interoperability/_iop/bit_operators.py @@ -38,3 +38,38 @@ def bitwise_and(x1: bool | int | Array, x2: bool | int | Array) -> Array: if _BACKEND_INSTANCE is None: raise _error return _BACKEND_INSTANCE.bitwise_and(x1, x2) + + +def bitwise_invert(x: Array) -> Array: + """Element-wise bitwise/logical NOT.""" + if _BACKEND_INSTANCE is None: + raise _error + return _BACKEND_INSTANCE.bitwise_invert(x) + + +def bitwise_or(x1: bool | int | Array, x2: bool | int | Array) -> Array: + """Element-wise bitwise/logical OR.""" + if _BACKEND_INSTANCE is None: + raise _error + return _BACKEND_INSTANCE.bitwise_or(x1, x2) + + +def bitwise_xor(x1: bool | int | Array, x2: bool | int | Array) -> Array: + """Element-wise bitwise/logical XOR.""" + if _BACKEND_INSTANCE is None: + raise _error + return _BACKEND_INSTANCE.bitwise_xor(x1, x2) + + +def bitwise_left_shift(x1: int | Array, x2: int | Array) -> Array: + """Element-wise bitwise left shift.""" + if _BACKEND_INSTANCE is None: + raise _error + return _BACKEND_INSTANCE.bitwise_left_shift(x1, x2) + + +def bitwise_right_shift(x1: int | Array, x2: int | Array) -> Array: + """Element-wise bitwise right shift.""" + if _BACKEND_INSTANCE is None: + raise _error + return _BACKEND_INSTANCE.bitwise_right_shift(x1, x2) diff --git a/decent_array/interoperability/_iop/manipulations.py b/decent_array/interoperability/_iop/manipulations.py index 55b4e89..c2450e2 100644 --- a/decent_array/interoperability/_iop/manipulations.py +++ b/decent_array/interoperability/_iop/manipulations.py @@ -93,6 +93,13 @@ def transpose(x: Array, axis: tuple[int, ...] | None = None) -> Array: return _BACKEND_INSTANCE.transpose(x, axis) +def matrix_transpose(x: Array) -> Array: + """Transpose the innermost two dimensions of ``x``.""" + if _BACKEND_INSTANCE is None: + raise _error + return _BACKEND_INSTANCE.matrix_transpose(x) + + def shape(x: Array) -> tuple[int, ...]: """Return the shape of ``x``.""" if _BACKEND_INSTANCE is None: diff --git a/decent_array/interoperability/_iop/math.py b/decent_array/interoperability/_iop/math.py index f9dd2bf..1b6bcc7 100644 --- a/decent_array/interoperability/_iop/math.py +++ b/decent_array/interoperability/_iop/math.py @@ -89,6 +89,20 @@ def idivide[T: Array](x1: T, x2: int | float | complex | Array) -> T: return _BACKEND_INSTANCE.idivide(x1, x2) +def floor_divide(x1: int | float | Array, x2: int | float | Array) -> Array: + """Element-wise floor division.""" + if _BACKEND_INSTANCE is None: + raise _error + return _BACKEND_INSTANCE.floor_divide(x1, x2) + + +def remainder(x1: int | float | Array, x2: int | float | Array) -> Array: + """Element-wise remainder after floor division.""" + if _BACKEND_INSTANCE is None: + raise _error + return _BACKEND_INSTANCE.remainder(x1, x2) + + def pow(x1: int | float | complex | Array, x2: int | float | complex | Array) -> Array: # noqa: A001 """Raise ``x`` to power ``p``.""" if _BACKEND_INSTANCE is None: @@ -103,6 +117,13 @@ def negative(x: Array) -> Array: return _BACKEND_INSTANCE.negative(x) +def positive(x: Array) -> Array: + """Return the array itself.""" + if _BACKEND_INSTANCE is None: + raise _error + return x + + def absolute(x: Array) -> Array: """Element-wise absolute value.""" if _BACKEND_INSTANCE is None: diff --git a/decent_array/interoperability/_jax/jax_backend.py b/decent_array/interoperability/_jax/jax_backend.py index 734b634..7173b92 100644 --- a/decent_array/interoperability/_jax/jax_backend.py +++ b/decent_array/interoperability/_jax/jax_backend.py @@ -116,6 +116,12 @@ def reshape(self, x: Array, shape: tuple[int, ...]) -> Array: def transpose(self, x: Array, axis: tuple[int, ...] | None = None) -> Array: return Array(jnp.transpose(x.value, axes=axis)) + def matrix_transpose(self, x: Array) -> Array: + v = x.value + if v.ndim < 2: + raise ValueError(f"matrix_transpose requires an array with at least 2 dimensions, got {v.ndim}-D") + return Array(jnp.swapaxes(v, -1, -2)) + def shape(self, x: Array) -> tuple[int, ...]: return tuple(x.value.shape) @@ -154,6 +160,10 @@ def vecdot(self, x1: Array, x2: Array) -> Array: def matmul(self, x1: Array, x2: Array) -> Array: return Array(x1.value @ x2.value) + def imatmul[T: Array](self, x1: T, x2: Array) -> T: + x1.value @= x2.value + return x1 + def vector_norm( self, x: Array, @@ -215,9 +225,27 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: x1.value = jnp.divide(x1.value, _unwrap(x2)) return x1 + def floor_divide(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(jnp.floor_divide(_unwrap(x1), _unwrap(x2))) + + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> T: + x1.value = jnp.floor_divide(x1.value, _unwrap(x2)) + return x1 + + def remainder(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(jnp.remainder(_unwrap(x1), _unwrap(x2))) + + def imod[T: Array](self, x1: T, x2: int | float | Array) -> T: + x1.value = jnp.remainder(x1.value, _unwrap(x2)) + return x1 + def pow(self, x1: int | float | complex | Array, x2: int | float | complex | Array) -> Array: return Array(jnp.power(_unwrap(x1), _unwrap(x2))) + def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: + x1.value = jnp.power(x1.value, _unwrap(x2)) + return x1 + def negative(self, x: Array) -> Array: return Array(jnp.negative(x.value)) @@ -249,9 +277,44 @@ def greater_equal(self, x1: int | float | complex | Array, x2: int | float | com # Bitwise - def bitwise_and(self, x1: int | Array, x2: int | Array) -> Array: + def bitwise_and(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: return Array(jnp.bitwise_and(_unwrap(x1), _unwrap(x2))) + def iand[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value = jnp.bitwise_and(x1.value, _unwrap(x2)) + return x1 + + def bitwise_invert(self, x: Array) -> Array: + return Array(jnp.bitwise_not(x.value)) + + def bitwise_or(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: + return Array(jnp.bitwise_or(_unwrap(x1), _unwrap(x2))) + + def ior[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value = jnp.bitwise_or(x1.value, _unwrap(x2)) + return x1 + + def bitwise_xor(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: + return Array(jnp.bitwise_xor(_unwrap(x1), _unwrap(x2))) + + def ixor[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value = jnp.bitwise_xor(x1.value, _unwrap(x2)) + return x1 + + def bitwise_left_shift(self, x1: int | Array, x2: int | Array) -> Array: + return Array(jnp.left_shift(_unwrap(x1), _unwrap(x2))) + + def ilshift[T: Array](self, x1: T, x2: int | Array) -> T: + x1.value = jnp.left_shift(x1.value, _unwrap(x2)) + return x1 + + def bitwise_right_shift(self, x1: int | Array, x2: int | Array) -> Array: + return Array(jnp.right_shift(_unwrap(x1), _unwrap(x2))) + + def irshift[T: Array](self, x1: T, x2: int | Array) -> T: + x1.value = jnp.right_shift(x1.value, _unwrap(x2)) + return x1 + # Operators def sign(self, x: Array) -> Array: diff --git a/decent_array/interoperability/_numpy/numpy_backend.py b/decent_array/interoperability/_numpy/numpy_backend.py index 72f363d..1c5f149 100644 --- a/decent_array/interoperability/_numpy/numpy_backend.py +++ b/decent_array/interoperability/_numpy/numpy_backend.py @@ -117,6 +117,12 @@ def reshape(self, x: Array, shape: tuple[int, ...]) -> Array: def transpose(self, x: Array, axis: tuple[int, ...] | None = None) -> Array: return Array(np.transpose(x.value, axes=axis)) + def matrix_transpose(self, x: Array) -> Array: + v = x.value + if v.ndim < 2: + raise ValueError(f"matrix_transpose requires an array with at least 2 dimensions, got {v.ndim}-D") + return Array(np.swapaxes(v, -1, -2)) + def shape(self, x: Array) -> tuple[int, ...]: return tuple(x.value.shape) @@ -155,6 +161,10 @@ def vecdot(self, x1: Array, x2: Array) -> Array: def matmul(self, x1: Array, x2: Array) -> Array: return Array(x1.value @ x2.value) + def imatmul[T: Array](self, x1: T, x2: Array) -> T: + x1.value @= x2.value + return x1 + def vector_norm( self, x: Array, @@ -215,9 +225,27 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: x1.value /= _unwrap(x2) return x1 + def floor_divide(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(np.floor_divide(_unwrap(x1), _unwrap(x2))) + + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> T: + x1.value //= _unwrap(x2) + return x1 + + def remainder(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(np.remainder(_unwrap(x1), _unwrap(x2))) + + def imod[T: Array](self, x1: T, x2: int | float | Array) -> T: + x1.value %= _unwrap(x2) + return x1 + def pow(self, x1: int | float | complex | Array, x2: int | float | complex | Array) -> Array: return Array(np.power(_unwrap(x1), _unwrap(x2))) + def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: + x1.value **= _unwrap(x2) + return x1 + def negative(self, x: Array) -> Array: return Array(np.negative(x.value)) @@ -249,9 +277,44 @@ def greater_equal(self, x1: int | float | complex | Array, x2: int | float | com # Bitwise - def bitwise_and(self, x1: int | Array, x2: int | Array) -> Array: + def bitwise_and(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: return Array(np.bitwise_and(_unwrap(x1), _unwrap(x2))) + def iand[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value &= _unwrap(x2) + return x1 + + def bitwise_invert(self, x: Array) -> Array: + return Array(np.bitwise_not(x.value)) + + def bitwise_or(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: + return Array(np.bitwise_or(_unwrap(x1), _unwrap(x2))) + + def ior[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value |= _unwrap(x2) + return x1 + + def bitwise_xor(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: + return Array(np.bitwise_xor(_unwrap(x1), _unwrap(x2))) + + def ixor[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value ^= _unwrap(x2) + return x1 + + def bitwise_left_shift(self, x1: int | Array, x2: int | Array) -> Array: + return Array(np.left_shift(_unwrap(x1), _unwrap(x2))) + + def ilshift[T: Array](self, x1: T, x2: int | Array) -> T: + x1.value <<= _unwrap(x2) + return x1 + + def bitwise_right_shift(self, x1: int | Array, x2: int | Array) -> Array: + return Array(np.right_shift(_unwrap(x1), _unwrap(x2))) + + def irshift[T: Array](self, x1: T, x2: int | Array) -> T: + x1.value >>= _unwrap(x2) + return x1 + # Operators def sign(self, x: Array) -> Array: diff --git a/decent_array/interoperability/_pytorch/pytorch_backend.py b/decent_array/interoperability/_pytorch/pytorch_backend.py index 1439f4f..a720d2d 100644 --- a/decent_array/interoperability/_pytorch/pytorch_backend.py +++ b/decent_array/interoperability/_pytorch/pytorch_backend.py @@ -123,6 +123,12 @@ def transpose(self, x: Array, axis: tuple[int, ...] | None = None) -> Array: dims = axis if axis is not None else tuple(reversed(range(v.ndim))) return Array(torch.permute(v, dims=dims)) + def matrix_transpose(self, x: Array) -> Array: + v = x.value + if v.ndim < 2: + raise ValueError(f"matrix_transpose requires an array with at least 2 dimensions, got {v.ndim}-D") + return Array(v.mT) + def shape(self, x: Array) -> tuple[int, ...]: return tuple(x.value.shape) @@ -164,6 +170,10 @@ def vecdot(self, x1: Array, x2: Array) -> Array: def matmul(self, x1: Array, x2: Array) -> Array: return Array(x1.value @ x2.value) + def imatmul[T: Array](self, x1: T, x2: Array) -> T: + x1.value @= x2.value + return x1 + def vector_norm( self, x: Array, @@ -236,9 +246,27 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: x1.value.div_(_unwrap(x2)) return x1 + def floor_divide(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(torch.floor_divide(_unwrap(x1), _unwrap(x2))) + + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> T: + x1.value.floor_divide_(_unwrap(x2)) + return x1 + + def remainder(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(torch.remainder(_unwrap(x1), _unwrap(x2))) + + def imod[T: Array](self, x1: T, x2: int | float | Array) -> T: + x1.value.remainder_(_unwrap(x2)) + return x1 + def pow(self, x1: int | float | complex | Array, x2: int | float | complex | Array) -> Array: return Array(torch.pow(_unwrap(x1), _unwrap(x2))) + def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: + x1.value.pow_(_unwrap(x2)) + return x1 + def negative(self, x: Array) -> Array: return Array(torch.neg(x.value)) @@ -270,9 +298,44 @@ def greater_equal(self, x1: int | float | complex | Array, x2: int | float | com # Bitwise - def bitwise_and(self, x1: int | Array, x2: int | Array) -> Array: + def bitwise_and(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: return Array(torch.bitwise_and(_unwrap(x1), _unwrap(x2))) + def iand[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value.bitwise_and_(_unwrap(x2)) + return x1 + + def bitwise_invert(self, x: Array) -> Array: + return Array(torch.bitwise_not(x.value)) + + def bitwise_or(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: + return Array(torch.bitwise_or(_unwrap(x1), _unwrap(x2))) + + def ior[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value.bitwise_or_(_unwrap(x2)) + return x1 + + def bitwise_xor(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: + return Array(torch.bitwise_xor(_unwrap(x1), _unwrap(x2))) + + def ixor[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value.bitwise_xor_(_unwrap(x2)) + return x1 + + def bitwise_left_shift(self, x1: int | Array, x2: int | Array) -> Array: + return Array(torch.bitwise_left_shift(_unwrap(x1), _unwrap(x2))) + + def ilshift[T: Array](self, x1: T, x2: int | Array) -> T: + x1.value.bitwise_left_shift_(_unwrap(x2)) + return x1 + + def bitwise_right_shift(self, x1: int | Array, x2: int | Array) -> Array: + return Array(torch.bitwise_right_shift(_unwrap(x1), _unwrap(x2))) + + def irshift[T: Array](self, x1: T, x2: int | Array) -> T: + x1.value.bitwise_right_shift_(_unwrap(x2)) + return x1 + # Operators def sign(self, x: Array) -> Array: diff --git a/decent_array/interoperability/_tensorflow/tensorflow_backend.py b/decent_array/interoperability/_tensorflow/tensorflow_backend.py index 586acae..4203b86 100644 --- a/decent_array/interoperability/_tensorflow/tensorflow_backend.py +++ b/decent_array/interoperability/_tensorflow/tensorflow_backend.py @@ -127,6 +127,13 @@ def reshape(self, x: Array, shape: tuple[int, ...]) -> Array: def transpose(self, x: Array, axis: tuple[int, ...] | None = None) -> Array: return Array(tf.transpose(x.value, perm=axis)) + def matrix_transpose(self, x: Array) -> Array: + v = x.value + rank = v.shape.ndims + if rank is not None and rank < 2: + raise ValueError(f"matrix_transpose requires an array with at least 2 dimensions, got {rank}-D") + return Array(tf.linalg.matrix_transpose(v)) + def shape(self, x: Array) -> tuple[int, ...]: return cast("tuple[int, ...]", tuple(x.value.shape)) @@ -174,6 +181,14 @@ def matmul(self, x1: Array, x2: Array) -> Array: return Array(tf.tensordot(a, b, axes=1)) return Array(a @ b) + def imatmul[T: Array](self, x1: T, x2: Array) -> T: + a, b = x1.value, x2.value + if a.shape.ndims is None or b.shape.ndims is None or a.shape.ndims < 2 or b.shape.ndims < 2: + x1.value = tf.tensordot(a, b, axes=1) + else: + x1.value = a @ b + return x1 + def vector_norm( self, x: Array, @@ -239,9 +254,27 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: x1.value = tf.divide(x1.value, _unwrap(x2)) return x1 + def floor_divide(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(tf.math.floordiv(_unwrap(x1), _unwrap(x2))) + + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> T: + x1.value = tf.math.floordiv(x1.value, _unwrap(x2)) + return x1 + + def remainder(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(tf.math.floormod(_unwrap(x1), _unwrap(x2))) + + def imod[T: Array](self, x1: T, x2: int | float | Array) -> T: + x1.value = tf.math.floormod(x1.value, _unwrap(x2)) + return x1 + def pow(self, x1: int | float | complex | Array, x2: int | float | complex | Array) -> Array: return Array(tf.pow(_unwrap(x1), _unwrap(x2))) + def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: + x1.value = tf.pow(x1.value, _unwrap(x2)) + return x1 + def negative(self, x: Array) -> Array: return Array(tf.negative(x.value)) @@ -276,9 +309,44 @@ def greater_equal(self, x1: int | float | complex | Array, x2: int | float | com # operator semantics. Calling either named function directly here would constrain # us to one dtype family. - def bitwise_and(self, x1: int | Array, x2: int | Array) -> Array: + def bitwise_and(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: return Array(_unwrap(x1) & _unwrap(x2)) + def iand[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value &= _unwrap(x2) + return x1 + + def bitwise_invert(self, x: Array) -> Array: + return Array(~x.value) + + def bitwise_or(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: + return Array(_unwrap(x1) | _unwrap(x2)) + + def ior[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value |= _unwrap(x2) + return x1 + + def bitwise_xor(self, x1: bool | int | Array, x2: bool | int | Array) -> Array: + return Array(_unwrap(x1) ^ _unwrap(x2)) + + def ixor[T: Array](self, x1: T, x2: bool | int | Array) -> T: + x1.value ^= _unwrap(x2) + return x1 + + def bitwise_left_shift(self, x1: int | Array, x2: int | Array) -> Array: + return Array(tf.bitwise.left_shift(_unwrap(x1), _unwrap(x2))) + + def ilshift[T: Array](self, x1: T, x2: int | Array) -> T: + x1.value = tf.bitwise.left_shift(x1.value, _unwrap(x2)) + return x1 + + def bitwise_right_shift(self, x1: int | Array, x2: int | Array) -> Array: + return Array(tf.bitwise.right_shift(_unwrap(x1), _unwrap(x2))) + + def irshift[T: Array](self, x1: T, x2: int | Array) -> T: + x1.value = tf.bitwise.right_shift(x1.value, _unwrap(x2)) + return x1 + # Operators def sign(self, x: Array) -> Array: diff --git a/decent_array/types.py b/decent_array/types.py index 7e39af0..b90fba0 100644 --- a/decent_array/types.py +++ b/decent_array/types.py @@ -70,3 +70,6 @@ class DTypes(Enum): FLOAT64 = "float64" COMPLEX64 = "complex64" COMPLEX128 = "complex128" + + +_STRING_TO_DTYPE = {dt.value: dt for dt in DTypes} diff --git a/tests/test_array.py b/tests/test_array.py index de0d72f..f2a6cb0 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -104,6 +104,38 @@ def test_rtruediv_scalar(backend: tuple) -> None: np.testing.assert_allclose(_np(8 / a), [8.0, 4.0, 2.0]) +def test_floordiv_array(backend: tuple) -> None: + a = _create_array([9.0, 10.0, 11.0]) + b = _create_array([2.0, 3.0, 5.0]) + np.testing.assert_allclose(_np(a // b), [4.0, 3.0, 2.0]) + + +def test_floordiv_scalar(backend: tuple) -> None: + a = _create_array([9.0, 10.0, 11.0]) + np.testing.assert_allclose(_np(a // 2), [4.0, 5.0, 5.0]) + + +def test_rfloordiv_scalar(backend: tuple) -> None: + a = _create_array([2.0, 3.0, 4.0]) + np.testing.assert_allclose(_np(20 // a), [10.0, 6.0, 5.0]) + + +def test_mod_array(backend: tuple) -> None: + a = _create_array([9.0, 10.0, 11.0]) + b = _create_array([2.0, 3.0, 5.0]) + np.testing.assert_allclose(_np(a % b), [1.0, 1.0, 1.0]) + + +def test_mod_scalar(backend: tuple) -> None: + a = _create_array([9.0, 10.0, 11.0]) + np.testing.assert_allclose(_np(a % 3), [0.0, 1.0, 2.0]) + + +def test_rmod_scalar(backend: tuple) -> None: + a = _create_array([2.0, 3.0, 4.0]) + np.testing.assert_allclose(_np(20 % a), [0.0, 2.0, 0.0]) + + def test_matmul_array(backend: tuple) -> None: a = _create_array([[1.0, 2.0], [3.0, 4.0]]) b = _create_array([[5.0, 6.0], [7.0, 8.0]]) @@ -240,6 +272,59 @@ def test_rand_scalar(backend: tuple) -> None: np.testing.assert_array_equal(_np(True & mask), [False, True, True]) +def test_or_array(backend: tuple) -> None: + a = _create_array([1.0, 2.0, 3.0]) + mask1 = a > 1.0 + mask2 = a < 2.0 + np.testing.assert_array_equal(_np(mask1 | mask2), [True, True, True]) + + +def test_ror_scalar(backend: tuple) -> None: + a = _create_array([1.0, 2.0, 3.0]) + mask = a > 1.0 + np.testing.assert_array_equal(_np(True | mask), [True, True, True]) + + +def test_xor_array(backend: tuple) -> None: + a = _create_array([1.0, 2.0, 3.0]) + mask1 = a > 1.0 + mask2 = a > 2.0 + np.testing.assert_array_equal(_np(mask1 ^ mask2), [False, True, False]) + + +def test_rxor_scalar(backend: tuple) -> None: + a = _create_array([1.0, 2.0, 3.0]) + mask = a > 1.0 + np.testing.assert_array_equal(_np(True ^ mask), [True, False, False]) + + +def test_lshift_array(backend: tuple) -> None: + a = iop.from_numpy(np.array([1, 3, 7], dtype=np.int32)) + b = iop.from_numpy(np.array([1, 2, 1], dtype=np.int32)) + np.testing.assert_array_equal(_np(a << b), [2, 12, 14]) + + +def test_rlshift_scalar(backend: tuple) -> None: + a = iop.from_numpy(np.array([1, 2, 3], dtype=np.int32)) + np.testing.assert_array_equal(_np(2 << a), [4, 8, 16]) + + +def test_rshift_array(backend: tuple) -> None: + a = iop.from_numpy(np.array([8, 12, 16], dtype=np.int32)) + b = iop.from_numpy(np.array([1, 2, 3], dtype=np.int32)) + np.testing.assert_array_equal(_np(a >> b), [4, 3, 2]) + + +def test_rrshift_scalar(backend: tuple) -> None: + a = iop.from_numpy(np.array([1, 2, 3], dtype=np.int32)) + np.testing.assert_array_equal(_np(64 >> a), [32, 16, 8]) + + +def test_invert_int_array(backend: tuple) -> None: + a = iop.from_numpy(np.array([0, 1, 2], dtype=np.int32)) + np.testing.assert_array_equal(_np(~a), np.bitwise_not(np.array([0, 1, 2], dtype=np.int32))) + + # In-place arithmetic ----------------------------------------------------- @@ -395,6 +480,31 @@ def test_T_alias(backend: tuple) -> None: np.testing.assert_allclose(_np(a.T), _np(a.transpose)) +def test_mT_property_2d(backend: tuple) -> None: + a = _create_array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) + np.testing.assert_allclose(_np(a.mT), [[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]]) + + +def test_mT_property_3d_swaps_last_two_axes(backend: tuple) -> None: + a = iop.from_numpy(np.arange(24, dtype=np.float32).reshape(2, 3, 4)) + out = _np(a.mT) + expected = np.swapaxes(np.arange(24, dtype=np.float32).reshape(2, 3, 4), -1, -2) + np.testing.assert_allclose(out, expected) + + +def test_mT_differs_from_T_for_3d(backend: tuple) -> None: + a = iop.from_numpy(np.arange(24, dtype=np.float32).reshape(2, 3, 4)) + assert _np(a.mT).shape == (2, 4, 3) + assert _np(a.T).shape == (4, 3, 2) + + +def test_mT_raises_for_rank_lt_2(backend: tuple) -> None: + for raw in (np.array(1.0, dtype=np.float32), np.array([1.0, 2.0], dtype=np.float32)): + a = iop.from_numpy(raw) + with pytest.raises(ValueError, match=r"at least 2 dimensions"): + _ = a.mT + + def test_any_true(backend: tuple) -> None: a = _create_array([0.0, 0.0, 1.0]) assert a.any is True diff --git a/tests/test_iop_functions.py b/tests/test_iop_functions.py index 671ecd1..e4c46ee 100644 --- a/tests/test_iop_functions.py +++ b/tests/test_iop_functions.py @@ -146,6 +146,24 @@ def test_transpose_explicit_dim(backend: tuple) -> None: assert iop.shape(out) == (3, 2, 4) +def test_matrix_transpose_2d(backend: tuple) -> None: + arr = iop.from_numpy(np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float32)) + np.testing.assert_allclose(_np(iop.matrix_transpose(arr)), [[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]]) + + +def test_matrix_transpose_3d_swaps_last_two_axes(backend: tuple) -> None: + raw = np.arange(24, dtype=np.float32).reshape(2, 3, 4) + arr = iop.from_numpy(raw) + np.testing.assert_allclose(_np(iop.matrix_transpose(arr)), np.swapaxes(raw, -1, -2)) + + +def test_matrix_transpose_raises_for_rank_lt_2(backend: tuple) -> None: + for raw in (np.array(1.0, dtype=np.float32), np.array([1.0, 2.0], dtype=np.float32)): + arr = iop.from_numpy(raw) + with pytest.raises(ValueError, match=r"at least 2 dimensions"): + iop.matrix_transpose(arr) + + def test_shape_function(backend: tuple) -> None: arr = iop.from_numpy(np.zeros((2, 3, 4), dtype=np.float32)) assert iop.shape(arr) == (2, 3, 4) @@ -363,6 +381,18 @@ def test_div(backend: tuple) -> None: np.testing.assert_allclose(_np(iop.divide(a, b)), [4.0, 2.0]) +def test_floor_divide_function(backend: tuple) -> None: + a = iop.from_numpy(np.array([9.0, 10.0], dtype=np.float32)) + b = iop.from_numpy(np.array([2.0, 3.0], dtype=np.float32)) + np.testing.assert_allclose(_np(iop.floor_divide(a, b)), [4.0, 3.0]) + + +def test_remainder_function(backend: tuple) -> None: + a = iop.from_numpy(np.array([9.0, 10.0], dtype=np.float32)) + b = iop.from_numpy(np.array([2.0, 3.0], dtype=np.float32)) + np.testing.assert_allclose(_np(iop.remainder(a, b)), [1.0, 1.0]) + + def test_iadd_func(backend: tuple) -> None: a = iop.from_numpy(np.array([1.0, 2.0], dtype=np.float32)) out = iadd(a, 10.0) @@ -399,6 +429,35 @@ def test_pow_function(backend: tuple) -> None: np.testing.assert_allclose(_np(iop.pow(arr, arr2)), [2.0, 9.0, 64.0]) +def test_backend_imatmul(backend: tuple) -> None: + a = iop.from_numpy(np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32)) + b = iop.from_numpy(np.array([[5.0, 6.0], [7.0, 8.0]], dtype=np.float32)) + out = a._backend.imatmul(a, b) + assert out is a + np.testing.assert_allclose(_np(a), [[19.0, 22.0], [43.0, 50.0]]) + + +def test_backend_ifloordiv(backend: tuple) -> None: + a = iop.from_numpy(np.array([9.0, 10.0], dtype=np.float32)) + out = a._backend.ifloordiv(a, 3.0) + assert out is a + np.testing.assert_allclose(_np(a), [3.0, 3.0]) + + +def test_backend_imod(backend: tuple) -> None: + a = iop.from_numpy(np.array([9.0, 10.0], dtype=np.float32)) + out = a._backend.imod(a, 3.0) + assert out is a + np.testing.assert_allclose(_np(a), [0.0, 1.0]) + + +def test_backend_ipow(backend: tuple) -> None: + a = iop.from_numpy(np.array([2.0, 3.0, 4.0], dtype=np.float32)) + out = a._backend.ipow(a, 2.0) + assert out is a + np.testing.assert_allclose(_np(a), [4.0, 9.0, 16.0]) + + def test_negative(backend: tuple) -> None: arr = iop.from_numpy(np.array([1.0, -2.0, 3.0], dtype=np.float32)) np.testing.assert_allclose(_np(iop.negative(arr)), [-1.0, 2.0, -3.0]) @@ -525,6 +584,70 @@ def test_bitwise_and_int_arrays(backend: tuple) -> None: np.testing.assert_array_equal(_np(iop.bitwise_and(a, b)), [0b1000, 0b0010]) +def test_bitwise_invert_int_array(backend: tuple) -> None: + a = iop.from_numpy(np.array([0, 1, 2], dtype=np.int32)) + np.testing.assert_array_equal(_np(iop.bitwise_invert(a)), np.bitwise_not(np.array([0, 1, 2], dtype=np.int32))) + + +def test_bitwise_or_int_arrays(backend: tuple) -> None: + a = iop.from_numpy(np.array([0b1100, 0b1010], dtype=np.int32)) + b = iop.from_numpy(np.array([0b1010, 0b0110], dtype=np.int32)) + np.testing.assert_array_equal(_np(iop.bitwise_or(a, b)), [0b1110, 0b1110]) + + +def test_bitwise_xor_int_arrays(backend: tuple) -> None: + a = iop.from_numpy(np.array([0b1100, 0b1010], dtype=np.int32)) + b = iop.from_numpy(np.array([0b1010, 0b0110], dtype=np.int32)) + np.testing.assert_array_equal(_np(iop.bitwise_xor(a, b)), [0b0110, 0b1100]) + + +def test_bitwise_left_shift_int_arrays(backend: tuple) -> None: + a = iop.from_numpy(np.array([1, 3], dtype=np.int32)) + b = iop.from_numpy(np.array([1, 2], dtype=np.int32)) + np.testing.assert_array_equal(_np(iop.bitwise_left_shift(a, b)), [2, 12]) + + +def test_bitwise_right_shift_int_arrays(backend: tuple) -> None: + a = iop.from_numpy(np.array([8, 12], dtype=np.int32)) + b = iop.from_numpy(np.array([1, 2], dtype=np.int32)) + np.testing.assert_array_equal(_np(iop.bitwise_right_shift(a, b)), [4, 3]) + + +def test_backend_iand(backend: tuple) -> None: + a = iop.from_numpy(np.array([0b1100, 0b1010], dtype=np.int32)) + out = a._backend.iand(a, 0b0110) + assert out is a + np.testing.assert_array_equal(_np(a), [0b0100, 0b0010]) + + +def test_backend_ior(backend: tuple) -> None: + a = iop.from_numpy(np.array([0b1100, 0b1010], dtype=np.int32)) + out = a._backend.ior(a, 0b0011) + assert out is a + np.testing.assert_array_equal(_np(a), [0b1111, 0b1011]) + + +def test_backend_ixor(backend: tuple) -> None: + a = iop.from_numpy(np.array([0b1100, 0b1010], dtype=np.int32)) + out = a._backend.ixor(a, 0b0110) + assert out is a + np.testing.assert_array_equal(_np(a), [0b1010, 0b1100]) + + +def test_backend_ilshift(backend: tuple) -> None: + a = iop.from_numpy(np.array([1, 3], dtype=np.int32)) + out = a._backend.ilshift(a, 2) + assert out is a + np.testing.assert_array_equal(_np(a), [4, 12]) + + +def test_backend_irshift(backend: tuple) -> None: + a = iop.from_numpy(np.array([8, 12], dtype=np.int32)) + out = a._backend.irshift(a, 2) + assert out is a + np.testing.assert_array_equal(_np(a), [2, 3]) + + def test_argmax_default(backend: tuple) -> None: arr = iop.from_numpy(np.array([3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0], dtype=np.float32)) np.testing.assert_allclose(_np(iop.argmax(arr)), 5)