From 23887cd9ca044911ae8d6789efbfb6623060be50 Mon Sep 17 00:00:00 2001 From: nicola-bastianello Date: Tue, 9 Jun 2026 17:26:52 +0200 Subject: [PATCH 1/7] enh: added dunder methods and dtype attribute to Array --- decent_array/_array.py | 87 ++++++++++++++++++++++++++++++++++++++++-- decent_array/types.py | 3 ++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/decent_array/_array.py b/decent_array/_array.py index 2799ad7..5ba4aec 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 @@ -34,7 +34,7 @@ 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, _STRING_TO_DTYPE _BACKEND_INSTANCE: Backend | None = None @@ -119,6 +119,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 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 +144,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: Array, /) -> Array: + """Exponentiate the array element-wise.""" + return Array(self.value ** other) + # Comparisons ---------------------------------------------------------- # # Element-wise comparisons return an :class:`Array` of bools. The ``__eq__`` and @@ -189,6 +209,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 Array(self.value << (other.value if type(other) is Array else other)) + + def __rlshift__(self, other: int | Array, /) -> Array: + """Element-wise bitwise left shift as specified by int/int array on the right.""" + return Array((other.value if type(other) is Array else other) << self.value) + + def __rshift__(self, other: int | Array, /) -> Array: + """Element-wise bitwise right shift as specified by int/int array.""" + return Array(self.value >> (other.value if type(other) is Array else other)) + + def __rrshift__(self, other: int | Array, /) -> Array: + """Element-wise bitwise right shift as specified by int/int array on the right.""" + return Array((other.value if type(other) is Array else other) >> self.value) + + 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 @@ -223,6 +279,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 @@ -278,6 +338,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", "tf.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.""" 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} From 31d75aef7d09c2c3c9bedf5f771558b5f69a56bc Mon Sep 17 00:00:00 2001 From: nicola-bastianello Date: Wed, 10 Jun 2026 10:48:20 +0200 Subject: [PATCH 2/7] enh: added more dunders to Array --- decent_array/_array.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/decent_array/_array.py b/decent_array/_array.py index 5ba4aec..eb7aee2 100644 --- a/decent_array/_array.py +++ b/decent_array/_array.py @@ -311,6 +311,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: @@ -348,7 +364,7 @@ def dtype(self) -> DTypes: """ # get framework-native dtype as string - # split takes care of types with names like "torch.float32", "tf.float32" + # 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) From e3c106a9b0a876d91444d721a7ba5673fd4ea99a Mon Sep 17 00:00:00 2001 From: nicola-bastianello Date: Wed, 10 Jun 2026 19:21:53 +0200 Subject: [PATCH 3/7] enh: added in-place operations to Array, added backend implementations corresponding to new dunders in Array --- decent_array/_array.py | 18 ++- decent_array/interoperability/__init__.py | 34 ++++- .../interoperability/_abstracts/backend.py | 68 ++++++++++ .../interoperability/_iop/bit_operators.py | 35 +++++ .../interoperability/_iop/manipulations.py | 7 + decent_array/interoperability/_iop/math.py | 19 +++ .../interoperability/_jax/jax_backend.py | 65 ++++++++- .../interoperability/_numpy/numpy_backend.py | 68 +++++++++- .../_pytorch/pytorch_backend.py | 65 ++++++++- .../_tensorflow/tensorflow_backend.py | 72 +++++++++- tests/test_array.py | 110 ++++++++++++++++ tests/test_iop_functions.py | 123 ++++++++++++++++++ 12 files changed, 672 insertions(+), 12 deletions(-) diff --git a/decent_array/_array.py b/decent_array/_array.py index eb7aee2..279b945 100644 --- a/decent_array/_array.py +++ b/decent_array/_array.py @@ -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, DTypes, SupportedArrayTypes, SupportedDevices, _STRING_TO_DTYPE + from decent_array.types import ArrayKey, DTypes, SupportedArrayTypes, SupportedDevices _BACKEND_INSTANCE: Backend | None = None @@ -152,7 +153,7 @@ def __pow__(self, other: int | float | complex | Array, /) -> Array: def __rpow__(self, other: Array, /) -> Array: """Exponentiate the array element-wise.""" - return Array(self.value ** other) + return Array(self.value**other) # Comparisons ---------------------------------------------------------- # @@ -227,19 +228,19 @@ def __rxor__(self, other: bool | int | Array, /) -> Array: def __lshift__(self, other: int | Array, /) -> Array: """Element-wise bitwise left shift as specified by int/int array.""" - return Array(self.value << (other.value if type(other) is Array else other)) + 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 Array((other.value if type(other) is Array else other) << self.value) + 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 Array(self.value >> (other.value if type(other) is Array else other)) + 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 Array((other.value if type(other) is Array else other) >> self.value) + return self._backend.bitwise_right_shift(other, self) def __invert__(self) -> Array: """Element-wise bitwise/logical NOT.""" @@ -383,6 +384,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..9f1be76 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) -> Array: + """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 | complex | Array) -> Array: + """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) -> Array: + """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) -> Array: + """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) -> Array: + """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) -> Array: + """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) -> Array: + """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) -> Array: + """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) -> Array: + """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..7a488f9 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,11 @@ def negative(x: Array) -> Array: return _BACKEND_INSTANCE.negative(x) +def positive(x: Array) -> Array: + """Return the array itself.""" + 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..99cb660 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) -> Array: + 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 | complex | Array) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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..849a047 100644 --- a/decent_array/interoperability/_numpy/numpy_backend.py +++ b/decent_array/interoperability/_numpy/numpy_backend.py @@ -117,6 +117,15 @@ 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") + mt = getattr(v, "mT", None) + if mt is not None: + return Array(mt) + return Array(np.swapaxes(v, -1, -2)) + def shape(self, x: Array) -> tuple[int, ...]: return tuple(x.value.shape) @@ -155,6 +164,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) -> Array: + x1.value @= x2.value + return x1 + def vector_norm( self, x: Array, @@ -215,9 +228,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 | complex | Array) -> Array: + 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) -> Array: + 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) -> Array: + x1.value **= _unwrap(x2) + return x1 + def negative(self, x: Array) -> Array: return Array(np.negative(x.value)) @@ -249,9 +280,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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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..54923d5 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) -> Array: + 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 | complex | Array) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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..499f9df 100644 --- a/decent_array/interoperability/_tensorflow/tensorflow_backend.py +++ b/decent_array/interoperability/_tensorflow/tensorflow_backend.py @@ -127,6 +127,15 @@ 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 None: + rank = int(tf.rank(v).numpy()) + if 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 +183,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) -> Array: + 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 +256,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 | complex | Array) -> Array: + 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) -> Array: + 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) -> Array: + x1.value = tf.pow(x1.value, _unwrap(x2)) + return x1 + def negative(self, x: Array) -> Array: return Array(tf.negative(x.value)) @@ -276,9 +311,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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + x1.value = tf.bitwise.right_shift(x1.value, _unwrap(x2)) + return x1 + # Operators def sign(self, x: Array) -> Array: 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) From 742717f4c8ec2968f67913fd1acea44f193e1ff8 Mon Sep 17 00:00:00 2001 From: nicola-bastianello Date: Thu, 11 Jun 2026 09:43:45 +0200 Subject: [PATCH 4/7] fix: simplify matrix transpose --- decent_array/interoperability/_numpy/numpy_backend.py | 3 --- .../interoperability/_tensorflow/tensorflow_backend.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/decent_array/interoperability/_numpy/numpy_backend.py b/decent_array/interoperability/_numpy/numpy_backend.py index 849a047..11b0a77 100644 --- a/decent_array/interoperability/_numpy/numpy_backend.py +++ b/decent_array/interoperability/_numpy/numpy_backend.py @@ -121,9 +121,6 @@ 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") - mt = getattr(v, "mT", None) - if mt is not None: - return Array(mt) return Array(np.swapaxes(v, -1, -2)) def shape(self, x: Array) -> tuple[int, ...]: diff --git a/decent_array/interoperability/_tensorflow/tensorflow_backend.py b/decent_array/interoperability/_tensorflow/tensorflow_backend.py index 499f9df..dd5b502 100644 --- a/decent_array/interoperability/_tensorflow/tensorflow_backend.py +++ b/decent_array/interoperability/_tensorflow/tensorflow_backend.py @@ -130,8 +130,6 @@ def transpose(self, x: Array, axis: tuple[int, ...] | None = None) -> Array: def matrix_transpose(self, x: Array) -> Array: v = x.value rank = v.shape.ndims - if rank is None: - rank = int(tf.rank(v).numpy()) if 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)) From bfc4f85d8166dfa6536d9d49af2f24ca09b7e6c5 Mon Sep 17 00:00:00 2001 From: nicola-bastianello Date: Thu, 11 Jun 2026 09:57:48 +0200 Subject: [PATCH 5/7] fix: types in ifloordiv --- decent_array/interoperability/_abstracts/backend.py | 2 +- decent_array/interoperability/_jax/jax_backend.py | 2 +- decent_array/interoperability/_numpy/numpy_backend.py | 2 +- decent_array/interoperability/_pytorch/pytorch_backend.py | 2 +- decent_array/interoperability/_tensorflow/tensorflow_backend.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/decent_array/interoperability/_abstracts/backend.py b/decent_array/interoperability/_abstracts/backend.py index 9f1be76..41c5465 100644 --- a/decent_array/interoperability/_abstracts/backend.py +++ b/decent_array/interoperability/_abstracts/backend.py @@ -229,7 +229,7 @@ def floor_divide(self, x1: int | float | Array, x2: int | float | Array) -> Arra """Element-wise floor division.""" @abstractmethod - def ifloordiv[T: Array](self, x1: T, x2: int | float | complex | Array) -> Array: + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> Array: """In-place floor division.""" @abstractmethod diff --git a/decent_array/interoperability/_jax/jax_backend.py b/decent_array/interoperability/_jax/jax_backend.py index 99cb660..65658e1 100644 --- a/decent_array/interoperability/_jax/jax_backend.py +++ b/decent_array/interoperability/_jax/jax_backend.py @@ -228,7 +228,7 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: 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 | complex | Array) -> Array: + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> Array: x1.value = jnp.floor_divide(x1.value, _unwrap(x2)) return x1 diff --git a/decent_array/interoperability/_numpy/numpy_backend.py b/decent_array/interoperability/_numpy/numpy_backend.py index 11b0a77..594b1e0 100644 --- a/decent_array/interoperability/_numpy/numpy_backend.py +++ b/decent_array/interoperability/_numpy/numpy_backend.py @@ -228,7 +228,7 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: 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 | complex | Array) -> Array: + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> Array: x1.value //= _unwrap(x2) return x1 diff --git a/decent_array/interoperability/_pytorch/pytorch_backend.py b/decent_array/interoperability/_pytorch/pytorch_backend.py index 54923d5..c54f057 100644 --- a/decent_array/interoperability/_pytorch/pytorch_backend.py +++ b/decent_array/interoperability/_pytorch/pytorch_backend.py @@ -249,7 +249,7 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: 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 | complex | Array) -> Array: + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> Array: x1.value.floor_divide_(_unwrap(x2)) return x1 diff --git a/decent_array/interoperability/_tensorflow/tensorflow_backend.py b/decent_array/interoperability/_tensorflow/tensorflow_backend.py index dd5b502..1f649b8 100644 --- a/decent_array/interoperability/_tensorflow/tensorflow_backend.py +++ b/decent_array/interoperability/_tensorflow/tensorflow_backend.py @@ -257,7 +257,7 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: 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 | complex | Array) -> Array: + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> Array: x1.value = tf.math.floordiv(x1.value, _unwrap(x2)) return x1 From a15466da71956b183b9f9fbbf999744e6ff7da9c Mon Sep 17 00:00:00 2001 From: nicola-bastianello Date: Thu, 11 Jun 2026 10:02:11 +0200 Subject: [PATCH 6/7] fix: rpow --- decent_array/_array.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/decent_array/_array.py b/decent_array/_array.py index 279b945..779b07e 100644 --- a/decent_array/_array.py +++ b/decent_array/_array.py @@ -151,9 +151,9 @@ def __pow__(self, other: int | float | complex | Array, /) -> Array: # call for no behavioral difference. return Array(self.value ** (other.value if type(other) is Array else other)) - def __rpow__(self, other: Array, /) -> Array: - """Exponentiate the array element-wise.""" - return Array(self.value**other) + def __rpow__(self, other: int | float | complex | Array, /) -> Array: + """Exponentiate other element-wise by array.""" + return Array(other**self.value) # Comparisons ---------------------------------------------------------- # From dbf063c9ce7472ebd180e427ffc14c077101e33f Mon Sep 17 00:00:00 2001 From: nicola-bastianello Date: Thu, 11 Jun 2026 10:46:05 +0200 Subject: [PATCH 7/7] fix: address PR comments --- decent_array/_array.py | 47 ++++++++++++++++++- .../interoperability/_abstracts/backend.py | 18 +++---- decent_array/interoperability/_iop/math.py | 2 + .../interoperability/_jax/jax_backend.py | 18 +++---- .../interoperability/_numpy/numpy_backend.py | 18 +++---- .../_pytorch/pytorch_backend.py | 18 +++---- .../_tensorflow/tensorflow_backend.py | 20 ++++---- 7 files changed, 94 insertions(+), 47 deletions(-) diff --git a/decent_array/_array.py b/decent_array/_array.py index 779b07e..4a0224a 100644 --- a/decent_array/_array.py +++ b/decent_array/_array.py @@ -133,7 +133,7 @@ def __mod__(self, other: int | float | Array, /) -> Array: return Array(self.value % (other.value if type(other) is Array else other)) def __rmod__(self, other: int | float | Array, /) -> Array: - """Return the floor division of ``other`` by the array.""" + """Return the remainder after floor division of ``other`` by the array.""" return Array(other % self.value) def __matmul__(self, other: Array, /) -> Array: @@ -272,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: diff --git a/decent_array/interoperability/_abstracts/backend.py b/decent_array/interoperability/_abstracts/backend.py index 41c5465..0d3789b 100644 --- a/decent_array/interoperability/_abstracts/backend.py +++ b/decent_array/interoperability/_abstracts/backend.py @@ -149,7 +149,7 @@ def matmul(self, x1: Array, x2: Array) -> Array: """Matrix multiplication of two arrays.""" @abstractmethod - def imatmul[T: Array](self, x1: T, x2: Array) -> Array: + def imatmul[T: Array](self, x1: T, x2: Array) -> T: """In-place matrix multiplication.""" @abstractmethod @@ -229,7 +229,7 @@ def floor_divide(self, x1: int | float | Array, x2: int | float | Array) -> Arra """Element-wise floor division.""" @abstractmethod - def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> Array: + def ifloordiv[T: Array](self, x1: T, x2: int | float | Array) -> T: """In-place floor division.""" @abstractmethod @@ -237,7 +237,7 @@ 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) -> Array: + def imod[T: Array](self, x1: T, x2: int | float | Array) -> T: """In-place remainder after floor division.""" @abstractmethod @@ -245,7 +245,7 @@ def pow(self, x1: int | float | complex | Array, x2: int | float | complex | Arr """Raise ``x1`` to power ``x2``.""" @abstractmethod - def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> Array: + def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: """In-place raise ``x1`` to power ``x2``.""" @abstractmethod @@ -295,7 +295,7 @@ 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) -> Array: + def iand[T: Array](self, x1: T, x2: bool | int | Array) -> T: """In-place bitwise/logical AND.""" @abstractmethod @@ -307,7 +307,7 @@ 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) -> Array: + def ior[T: Array](self, x1: T, x2: bool | int | Array) -> T: """In-place bitwise/logical OR.""" @abstractmethod @@ -315,7 +315,7 @@ 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) -> Array: + def ixor[T: Array](self, x1: T, x2: bool | int | Array) -> T: """In-place bitwise/logical XOR.""" @abstractmethod @@ -323,7 +323,7 @@ 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) -> Array: + def ilshift[T: Array](self, x1: T, x2: int | Array) -> T: """In-place bitwise left shift.""" @abstractmethod @@ -331,7 +331,7 @@ 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) -> Array: + def irshift[T: Array](self, x1: T, x2: int | Array) -> T: """In-place bitwise right shift.""" # Operators ----------------------------------------------------------- diff --git a/decent_array/interoperability/_iop/math.py b/decent_array/interoperability/_iop/math.py index 7a488f9..1b6bcc7 100644 --- a/decent_array/interoperability/_iop/math.py +++ b/decent_array/interoperability/_iop/math.py @@ -119,6 +119,8 @@ def negative(x: Array) -> Array: def positive(x: Array) -> Array: """Return the array itself.""" + if _BACKEND_INSTANCE is None: + raise _error return x diff --git a/decent_array/interoperability/_jax/jax_backend.py b/decent_array/interoperability/_jax/jax_backend.py index 65658e1..7173b92 100644 --- a/decent_array/interoperability/_jax/jax_backend.py +++ b/decent_array/interoperability/_jax/jax_backend.py @@ -160,7 +160,7 @@ 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) -> Array: + def imatmul[T: Array](self, x1: T, x2: Array) -> T: x1.value @= x2.value return x1 @@ -228,21 +228,21 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: 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) -> Array: + 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) -> Array: + 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) -> Array: + def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: x1.value = jnp.power(x1.value, _unwrap(x2)) return x1 @@ -280,7 +280,7 @@ 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: return Array(jnp.bitwise_and(_unwrap(x1), _unwrap(x2))) - def iand[T: Array](self, x1: T, x2: bool | int | Array) -> Array: + def iand[T: Array](self, x1: T, x2: bool | int | Array) -> T: x1.value = jnp.bitwise_and(x1.value, _unwrap(x2)) return x1 @@ -290,28 +290,28 @@ def bitwise_invert(self, x: Array) -> Array: 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + def irshift[T: Array](self, x1: T, x2: int | Array) -> T: x1.value = jnp.right_shift(x1.value, _unwrap(x2)) return x1 diff --git a/decent_array/interoperability/_numpy/numpy_backend.py b/decent_array/interoperability/_numpy/numpy_backend.py index 594b1e0..1c5f149 100644 --- a/decent_array/interoperability/_numpy/numpy_backend.py +++ b/decent_array/interoperability/_numpy/numpy_backend.py @@ -161,7 +161,7 @@ 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) -> Array: + def imatmul[T: Array](self, x1: T, x2: Array) -> T: x1.value @= x2.value return x1 @@ -228,21 +228,21 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: 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) -> Array: + 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) -> Array: + 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) -> Array: + def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: x1.value **= _unwrap(x2) return x1 @@ -280,7 +280,7 @@ 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: return Array(np.bitwise_and(_unwrap(x1), _unwrap(x2))) - def iand[T: Array](self, x1: T, x2: bool | int | Array) -> Array: + def iand[T: Array](self, x1: T, x2: bool | int | Array) -> T: x1.value &= _unwrap(x2) return x1 @@ -290,28 +290,28 @@ def bitwise_invert(self, x: Array) -> Array: 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + def irshift[T: Array](self, x1: T, x2: int | Array) -> T: x1.value >>= _unwrap(x2) return x1 diff --git a/decent_array/interoperability/_pytorch/pytorch_backend.py b/decent_array/interoperability/_pytorch/pytorch_backend.py index c54f057..a720d2d 100644 --- a/decent_array/interoperability/_pytorch/pytorch_backend.py +++ b/decent_array/interoperability/_pytorch/pytorch_backend.py @@ -170,7 +170,7 @@ 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) -> Array: + def imatmul[T: Array](self, x1: T, x2: Array) -> T: x1.value @= x2.value return x1 @@ -249,21 +249,21 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: 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) -> Array: + 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) -> Array: + 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) -> Array: + def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: x1.value.pow_(_unwrap(x2)) return x1 @@ -301,7 +301,7 @@ 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: return Array(torch.bitwise_and(_unwrap(x1), _unwrap(x2))) - def iand[T: Array](self, x1: T, x2: bool | int | Array) -> Array: + def iand[T: Array](self, x1: T, x2: bool | int | Array) -> T: x1.value.bitwise_and_(_unwrap(x2)) return x1 @@ -311,28 +311,28 @@ def bitwise_invert(self, x: Array) -> Array: 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + def irshift[T: Array](self, x1: T, x2: int | Array) -> T: x1.value.bitwise_right_shift_(_unwrap(x2)) return x1 diff --git a/decent_array/interoperability/_tensorflow/tensorflow_backend.py b/decent_array/interoperability/_tensorflow/tensorflow_backend.py index 1f649b8..4203b86 100644 --- a/decent_array/interoperability/_tensorflow/tensorflow_backend.py +++ b/decent_array/interoperability/_tensorflow/tensorflow_backend.py @@ -130,7 +130,7 @@ def transpose(self, x: Array, axis: tuple[int, ...] | None = None) -> Array: def matrix_transpose(self, x: Array) -> Array: v = x.value rank = v.shape.ndims - if rank < 2: + 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)) @@ -181,7 +181,7 @@ 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) -> Array: + 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) @@ -257,21 +257,21 @@ def idivide[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: 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) -> Array: + 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) -> Array: + 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) -> Array: + def ipow[T: Array](self, x1: T, x2: int | float | complex | Array) -> T: x1.value = tf.pow(x1.value, _unwrap(x2)) return x1 @@ -312,7 +312,7 @@ 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: return Array(_unwrap(x1) & _unwrap(x2)) - def iand[T: Array](self, x1: T, x2: bool | int | Array) -> Array: + def iand[T: Array](self, x1: T, x2: bool | int | Array) -> T: x1.value &= _unwrap(x2) return x1 @@ -322,28 +322,28 @@ def bitwise_invert(self, x: Array) -> Array: 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) -> Array: + 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) -> Array: + 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) -> Array: + 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) -> Array: + def irshift[T: Array](self, x1: T, x2: int | Array) -> T: x1.value = tf.bitwise.right_shift(x1.value, _unwrap(x2)) return x1