From c0b0b2e4e97a753b17f4f0cba9e376a62ad1b666 Mon Sep 17 00:00:00 2001 From: nicola-bastianello Date: Wed, 23 Sep 2026 15:58:04 +0200 Subject: [PATCH] ref: change asarray to properly handle scalars, Arrays, and introduce support for nested sequences and buffer objects. fixed also any and all that were returning bool instead of Array --- decent_array/_array.py | 12 +-- .../interoperability/_abstracts/backend.py | 28 +++++-- .../interoperability/_iop/manipulations.py | 11 ++- .../interoperability/_iop/reductions.py | 8 +- .../interoperability/_jax/jax_backend.py | 15 ++-- .../interoperability/_numpy/numpy_backend.py | 19 ++--- .../_pytorch/pytorch_backend.py | 15 ++-- .../_tensorflow/tensorflow_backend.py | 19 ++--- decent_array/types/_types.py | 12 ++- docs/source/conf.py | 1 + pyproject.toml | 2 +- tests/test_array.py | 47 ++++++----- tests/test_iop_functions.py | 81 ++++++++++++++++++- 13 files changed, 188 insertions(+), 82 deletions(-) diff --git a/decent_array/_array.py b/decent_array/_array.py index 45dbbe0..73047b7 100644 --- a/decent_array/_array.py +++ b/decent_array/_array.py @@ -89,7 +89,7 @@ def __init__(self, value: ArrayTypes) -> None: "Call set_backend() to initialize the interoperability layer." ) - self.value: Any = value + self.value: Any = _BACKEND_INSTANCE.native_asarray(value) self._backend: Backend = _BACKEND_INSTANCE # Binary arithmetic ---------------------------------------------------- @@ -445,16 +445,6 @@ 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.""" - return self._backend.any(self) - - @property - def all(self) -> bool: - """Return True if all elements of the array are truthy.""" - return self._backend.all(self) - @property def device(self) -> Devices: """Return the device of the array.""" diff --git a/decent_array/interoperability/_abstracts/backend.py b/decent_array/interoperability/_abstracts/backend.py index 45d5eda..5a843ee 100644 --- a/decent_array/interoperability/_abstracts/backend.py +++ b/decent_array/interoperability/_abstracts/backend.py @@ -24,7 +24,7 @@ from numpy.typing import NDArray from decent_array import Array - from decent_array.types import ArrayKey, ArrayTypes + from decent_array.types import ArrayKey, ArrayLike, ArrayTypes from decent_array.types._dtypes import dtype @@ -88,9 +88,23 @@ def from_numpy(self, x: NDArray[Any]) -> Array: def from_numpy_like(self, x: NDArray[Any], like: Array) -> Array: """Convert a Numpy array to an :class:`Array` on this backend, matching shape and type of ``like``.""" + def asarray(self, x: ArrayTypes | Array) -> Array: + """ + Convert `x` into an :class:`~decent_array.Array` on the active backend. + + `x` can be a scalar, a backend-native array/tensor, or :class:`~decent_array.Array`, in which case it acts as + the identity. + + """ + from decent_array._array import Array # noqa: PLC0415 + + if isinstance(x, Array): + return x + return Array(self.native_asarray(x)) + @abstractmethod - def asarray(self, x: bool | int | float | complex) -> Array: - """Convert a Python scalar to an :class:`Array` on this backend.""" + def native_asarray(self, x: ArrayTypes) -> ArrayLike: + """Wrap the backend-native asarray operation.""" @abstractmethod def to_scalar(self, x: Array) -> Any: # noqa: ANN401 @@ -191,12 +205,12 @@ def max(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: boo """Maximum of ``x`` along ``axis``.""" @abstractmethod - def any(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: - """Return True if any element of ``x`` is truthy.""" + def any(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: + """Test whether any array element along axis is truthy.""" @abstractmethod - def all(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: - """Return True if all elements of ``x`` are truthy.""" + def all(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: + """Test whether all array elements along axis are truthy.""" # Math elementwise — both operands may be Array or scalar (operator dunders pass # either). ``Array | float`` covers both because PEP 484's numeric tower implicitly diff --git a/decent_array/interoperability/_iop/manipulations.py b/decent_array/interoperability/_iop/manipulations.py index e04bf28..31b1815 100644 --- a/decent_array/interoperability/_iop/manipulations.py +++ b/decent_array/interoperability/_iop/manipulations.py @@ -66,8 +66,15 @@ def from_numpy_like(x: NDArray[Any], like: Array) -> Array: return _BACKEND_INSTANCE.from_numpy_like(x, like) -def asarray(x: float | bool) -> Array: - """Convert a Python scalar to an :class:`~decent_array.Array` on the active backend.""" +def asarray(x: ArrayTypes | Array) -> Array: + """ + Convert `x` into an :class:`~decent_array.Array` on the active backend. + + `x` can be a scalar, a native array, a nested sequence, an object supporting Python's buffer protocol. Note that `x` + is passed directly to the equivalent backend-native asarray function. If `x` is an :class:`~decent_array.Array` + instance, it is returned unchanged. + + """ if _BACKEND_INSTANCE is None: raise no_backend_error return _BACKEND_INSTANCE.asarray(x) diff --git a/decent_array/interoperability/_iop/reductions.py b/decent_array/interoperability/_iop/reductions.py index 20540cc..c37bb0a 100644 --- a/decent_array/interoperability/_iop/reductions.py +++ b/decent_array/interoperability/_iop/reductions.py @@ -77,15 +77,15 @@ def max( # noqa: A001 return _BACKEND_INSTANCE.max(x, axis, keepdims) -def any(x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: # noqa: A001 - """Return True if any element of ``x`` is truthy.""" +def any(x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: # noqa: A001 + """Test whether any array element along axis is truthy.""" if _BACKEND_INSTANCE is None: raise no_backend_error return _BACKEND_INSTANCE.any(x, axis, keepdims) -def all(x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: # noqa: A001 - """Return True if all elements of ``x`` are truthy.""" +def all(x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: # noqa: A001 + """Test whether all array elements along axis are truthy.""" if _BACKEND_INSTANCE is None: raise no_backend_error return _BACKEND_INSTANCE.all(x, axis, keepdims) diff --git a/decent_array/interoperability/_jax/jax_backend.py b/decent_array/interoperability/_jax/jax_backend.py index c572e8b..9d10361 100644 --- a/decent_array/interoperability/_jax/jax_backend.py +++ b/decent_array/interoperability/_jax/jax_backend.py @@ -30,7 +30,7 @@ from decent_array._utils import is_scalar, unwrap from decent_array.interoperability._abstracts import Backend from decent_array.interoperability._backend_manager import register_backend -from decent_array.types import ArrayKey, ArrayTypes, Devices, Frameworks +from decent_array.types import ArrayKey, ArrayLike, ArrayTypes, Devices, Frameworks from decent_array.types._dtypes import dtype @@ -91,8 +91,9 @@ def from_numpy_like(self, x: NDArray[Any], like: Array) -> Array: v = like.value return Array(jnp.asarray(x, dtype=v.dtype, device=v.device)) - def asarray(self, x: bool | int | float | complex) -> Array: - return Array(jnp.array(x, device=self._native_device)) + def native_asarray(self, x: ArrayTypes) -> ArrayLike: + """Wrap the backend-native asarray operation.""" + return jnp.asarray(x, device=self._native_device) def to_scalar(self, x: Array) -> Any: # noqa: ANN401 """ @@ -187,11 +188,11 @@ def min(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: boo def max(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: return Array(jnp.max(x.value, axis=axis, keepdims=keepdims)) - def any(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: - return bool(jnp.any(x.value, axis=axis, keepdims=keepdims)) + def any(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: + return Array(jnp.any(x.value, axis=axis, keepdims=keepdims)) - def all(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: - return bool(jnp.all(x.value, axis=axis, keepdims=keepdims)) + def all(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: + return Array(jnp.all(x.value, axis=axis, keepdims=keepdims)) # Math elementwise — JAX arrays are immutable; "in-place" ops rebind the wrapper. # Operands may be Array or scalar (operator dunders pass either); ``Array | float`` diff --git a/decent_array/interoperability/_numpy/numpy_backend.py b/decent_array/interoperability/_numpy/numpy_backend.py index c0dd17a..37b7cc2 100644 --- a/decent_array/interoperability/_numpy/numpy_backend.py +++ b/decent_array/interoperability/_numpy/numpy_backend.py @@ -25,7 +25,7 @@ from decent_array._utils import is_scalar, unwrap from decent_array.interoperability._abstracts import Backend from decent_array.interoperability._backend_manager import register_backend -from decent_array.types import ArrayKey, ArrayTypes, Devices, Frameworks +from decent_array.types import ArrayKey, ArrayLike, ArrayTypes, Devices, Frameworks from decent_array.types._dtypes import dtype @@ -84,8 +84,9 @@ def from_numpy_like(self, x: NDArray[Any], like: Array) -> Array: # NumPy has no device dimension, so only the dtype of ``like`` matters. return Array(np.asarray(x, dtype=like.value.dtype)) - def asarray(self, x: bool | int | float | complex) -> Array: - return Array(np.array(x)) + def native_asarray(self, x: ArrayTypes) -> ArrayLike: + """Wrap the backend-native asarray operation.""" + return np.asarray(x) def to_scalar(self, x: Array) -> Any: # noqa: ANN401 """ @@ -194,17 +195,17 @@ def max(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: boo return Array(np.max(v, axis=axis, keepdims=True)) return Array(np.max(v, axis=axis)) - def any(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: + def any(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: v = cast("np.ndarray[Any, Any]", x.value) if keepdims: - return bool(np.any(v, axis=axis, keepdims=True)) - return bool(np.any(v, axis=axis)) + return Array(np.any(v, axis=axis, keepdims=True)) + return Array(np.any(v, axis=axis, keepdims=False)) - def all(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: + def all(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: v = cast("np.ndarray[Any, Any]", x.value) if keepdims: - return bool(np.all(v, axis=axis, keepdims=True)) - return bool(np.all(v, axis=axis)) + return Array(np.all(v, axis=axis, keepdims=True)) + return Array(np.all(v, axis=axis, keepdims=False)) # Math elementwise — operands may be Array or scalar (operator dunders pass either). # ``Array | float`` covers both: PEP 484's numeric tower implicitly admits ``int``. diff --git a/decent_array/interoperability/_pytorch/pytorch_backend.py b/decent_array/interoperability/_pytorch/pytorch_backend.py index a5e0673..8c251b8 100644 --- a/decent_array/interoperability/_pytorch/pytorch_backend.py +++ b/decent_array/interoperability/_pytorch/pytorch_backend.py @@ -19,7 +19,7 @@ from decent_array._utils import is_scalar, unwrap from decent_array.interoperability._abstracts import Backend from decent_array.interoperability._backend_manager import register_backend -from decent_array.types import ArrayKey, ArrayTypes, Devices, Frameworks +from decent_array.types import ArrayKey, ArrayLike, ArrayTypes, Devices, Frameworks from decent_array.types._dtypes import dtype @@ -88,8 +88,9 @@ def from_numpy_like(self, x: NDArray[Any], like: Array) -> Array: v = like.value return Array(torch.from_numpy(x).to(dtype=v.dtype, device=v.device)) - def asarray(self, x: bool | int | float | complex) -> Array: - return Array(torch.tensor(x, device=self._native_device)) + def native_asarray(self, x: ArrayTypes) -> ArrayLike: + """Wrap the backend-native asarray operation.""" + return torch.as_tensor(x, device=self._native_device) def to_scalar(self, x: Array) -> Any: # noqa: ANN401 """ @@ -201,11 +202,11 @@ def max(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: boo return Array(torch.max(v)) return Array(torch.amax(v, dim=axis, keepdim=keepdims)) - def any(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: - return bool(torch.any(x.value, dim=axis, keepdim=keepdims).item()) + def any(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: + return Array(torch.any(x.value, dim=axis, keepdim=keepdims)) - def all(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: - return bool(torch.all(x.value, dim=axis, keepdim=keepdims).item()) + def all(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: + return Array(torch.all(x.value, dim=axis, keepdim=keepdims)) # Math elementwise — operands may be Array or scalar (operator dunders pass either). # ``Array | float`` covers both: PEP 484's numeric tower implicitly admits ``int``. diff --git a/decent_array/interoperability/_tensorflow/tensorflow_backend.py b/decent_array/interoperability/_tensorflow/tensorflow_backend.py index 8d1baa9..2d98f4f 100644 --- a/decent_array/interoperability/_tensorflow/tensorflow_backend.py +++ b/decent_array/interoperability/_tensorflow/tensorflow_backend.py @@ -28,7 +28,7 @@ from decent_array._utils import is_scalar, unwrap from decent_array.interoperability._abstracts import Backend from decent_array.interoperability._backend_manager import register_backend -from decent_array.types import ArrayKey, ArrayTypes, Devices, Frameworks +from decent_array.types import ArrayKey, ArrayLike, ArrayTypes, Devices, Frameworks from decent_array.types._dtypes import dtype @@ -98,11 +98,12 @@ def from_numpy_like(self, x: NDArray[Any], like: Array) -> Array: with tf.device(v.device): return Array(tf.convert_to_tensor(x, dtype=v.dtype)) - def asarray(self, x: bool | int | float | complex) -> Array: - """Convert a Python scalar to an :class:`Array` on this backend.""" + def native_asarray(self, x: ArrayTypes) -> ArrayLike: + """Wrap the backend-native asarray operation.""" + # TensorFlow's type stubs do not cover all inputs accepted by + # convert_to_tensor at runtime. with tf.device(self._native_device): - # Its not a tf tensor but mypyc doesn't import tf so it complains about unsude type-ignores - return Array(tf.convert_to_tensor(cast("tf.Tensor", x))) + return tf.convert_to_tensor(cast("Any", x)) def to_scalar(self, x: Array) -> Any: # noqa: ANN401 """ @@ -210,11 +211,11 @@ def min(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: boo def max(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: return Array(tf.reduce_max(x.value, axis=axis, keepdims=keepdims)) - def any(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: - return bool(tf.reduce_any(tf.cast(x.value, tf.bool), axis=axis, keepdims=keepdims).numpy()) + def any(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: + return Array(tf.reduce_any(tf.cast(x.value, tf.bool), axis=axis, keepdims=keepdims)) - def all(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> bool: - return bool(tf.reduce_all(tf.cast(x.value, tf.bool), axis=axis, keepdims=keepdims).numpy()) + def all(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: + return Array(tf.reduce_all(tf.cast(x.value, tf.bool), axis=axis, keepdims=keepdims)) # Math elementwise — TF Tensors are immutable; "in-place" ops rebind the wrapper. # Operands may be Array or scalar (operator dunders pass either); ``Array | float`` diff --git a/decent_array/types/_types.py b/decent_array/types/_types.py index 10c6a1b..522dfbb 100644 --- a/decent_array/types/_types.py +++ b/decent_array/types/_types.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Buffer, Sequence from enum import Enum from typing import TYPE_CHECKING, SupportsIndex, TypeAlias, Union @@ -14,6 +15,10 @@ from decent_array._array import Array +Scalar: TypeAlias = bool | int | float | complex | numpy.generic # noqa: UP040 +""" +Type alias for scalar types supported in decent-array. +""" ArrayLike: TypeAlias = Union["numpy.ndarray", "torch.Tensor", "tf.Tensor", "jax.Array"] # noqa: UP040 """ @@ -21,7 +26,12 @@ PyTorch tensors, TensorFlow tensors, and JAX arrays. """ -ArrayTypes: TypeAlias = bool | int | float | complex | numpy.generic | ArrayLike # noqa: UP040 +NestedSequence: TypeAlias = "Sequence[Scalar | NestedSequence]" # noqa: UP040 +""" +Type alias for nested sequences supported in decent-array. +""" + +ArrayTypes: TypeAlias = Scalar | ArrayLike | NestedSequence | Buffer # noqa: UP040 """ Type alias for supported scalar/array types in decent-array. """ diff --git a/docs/source/conf.py b/docs/source/conf.py index 4d475a0..659b3ec 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -38,6 +38,7 @@ ("py:class", "float64"), ("py:class", "numpy._typing._array_like._SupportsArray"), ("py:class", "numpy._typing._nested_sequence._NestedSequence"), + ("py:class", "Sequence[Scalar | NestedSequence]"), ("py:class", "T"), ] diff --git a/pyproject.toml b/pyproject.toml index a983687..c4f7431 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "decent-array" -version = "0.2.5" +version = "0.2.6" authors = [{name = "Simon Granström"}, {name = "Nicola Bastianello"}] maintainers = [{name = "Team Decent"}] description = "A library of array operations and linear algebra primitives for interoperability across ML frameworks." diff --git a/tests/test_array.py b/tests/test_array.py index de7078e..13d6519 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -37,6 +37,33 @@ def test_init_records_active_backend(backend: tuple) -> None: assert isinstance(arr, Array) +@pytest.mark.parametrize( + "value", + [ + True, + 1, + 1.0, + 1.0 + 2.0j, + np.int32(1), + np.float32(1.0), + np.complex64(1.0 + 2.0j), + ], +) +def test_init_converts_to_backend_array(value, backend: tuple) -> None: + arr = Array(value) + + assert isinstance(arr, Array) + assert arr.ndim == 0 + assert arr.size == 1 + assert arr.shape == () + + +def test_init_scalar_value(backend: tuple) -> None: + arr = Array(4) + + np.testing.assert_array_equal(iop.to_numpy(arr), np.array(4)) + + # Binary arithmetic ------------------------------------------------------- @@ -528,26 +555,6 @@ def test_mT_raises_for_rank_lt_2(backend: tuple) -> None: _ = a.mT -def test_any_true(backend: tuple) -> None: - a = _create_array([0.0, 0.0, 1.0]) - assert a.any is True - - -def test_any_false(backend: tuple) -> None: - a = _create_array([0.0, 0.0, 0.0]) - assert a.any is False - - -def test_all_true(backend: tuple) -> None: - a = _create_array([1.0, 2.0, 3.0]) - assert a.all is True - - -def test_all_false(backend: tuple) -> None: - a = _create_array([1.0, 0.0, 3.0]) - assert a.all is False - - def test_device_property(backend: tuple) -> None: _framework, device = backend a = iop.zeros((3,)) diff --git a/tests/test_iop_functions.py b/tests/test_iop_functions.py index e5f2f84..cad83dc 100644 --- a/tests/test_iop_functions.py +++ b/tests/test_iop_functions.py @@ -62,6 +62,59 @@ def test_device_of(backend: tuple) -> None: # Array manipulation ----------------------------------------------------- +def test_asarray_existing_array_is_identity(backend: tuple) -> None: + arr = Array(4) + + result = iop.asarray(arr) + + assert result is arr + + +def test_asarray_scalar(backend: tuple) -> None: + result = iop.asarray(4) + + assert isinstance(result, Array) + assert result.shape == () + assert result.size == 1 + assert result.ndim == 0 + + +def test_asarray_native_array(backend: tuple) -> None: + arr = iop.from_numpy(np.array([1.0, 2.0, 3.0], dtype=np.float32)) + + result = iop.asarray(arr.value) + + assert isinstance(result, Array) + np.testing.assert_array_equal(_np(result), _np(arr)) + + +def test_asarray_numpy_array(backend: tuple) -> None: + raw = np.array([1.0, 2.0, 3.0], dtype=np.float32) + + result = iop.asarray(raw) + + assert isinstance(result, Array) + np.testing.assert_array_equal(_np(result), raw) + + +def test_asarray_nested_sequence(backend: tuple) -> None: + raw = [[1, 2, 3], [4, 5, 6]] + + result = iop.asarray(raw) + + assert isinstance(result, Array) + np.testing.assert_array_equal(_np(result), raw) + + +def test_asarray_buffer(backend: tuple) -> None: + raw = np.array([1, 2, 3], dtype=np.int32) + buffer = memoryview(raw) + + result = iop.asarray(buffer) + + assert isinstance(result, Array) + np.testing.assert_array_equal(_np(result), raw) + def test_copy_independent(backend: tuple) -> None: src = iop.from_numpy(np.array([1.0, 2.0, 3.0], dtype=np.float32)) @@ -331,22 +384,42 @@ def test_max_dim(backend: tuple) -> None: def test_any_true(backend: tuple) -> None: arr = iop.from_numpy(np.array([0.0, 1.0, 0.0], dtype=np.float32)) - assert iop.any(arr) is True + np.testing.assert_array_equal(_np(iop.any(arr)), True) def test_any_false(backend: tuple) -> None: arr = iop.from_numpy(np.array([0.0, 0.0, 0.0], dtype=np.float32)) - assert iop.any(arr) is False + np.testing.assert_array_equal(_np(iop.any(arr)), False) def test_all_true(backend: tuple) -> None: arr = iop.from_numpy(np.array([1.0, 2.0, 3.0], dtype=np.float32)) - assert iop.all(arr) is True + np.testing.assert_array_equal(_np(iop.all(arr)), True) def test_all_false(backend: tuple) -> None: arr = iop.from_numpy(np.array([1.0, 0.0, 3.0], dtype=np.float32)) - assert iop.all(arr) is False + np.testing.assert_array_equal(_np(iop.all(arr)), False) + + +def test_any_dim(backend: tuple) -> None: + arr = iop.from_numpy(np.array([[0.0, 1.0], [0.0, 0.0]], dtype=np.float32)) + np.testing.assert_array_equal(_np(iop.any(arr, axis=0)), [False, True]) + + +def test_all_dim(backend: tuple) -> None: + arr = iop.from_numpy(np.array([[1.0, 1.0], [1.0, 0.0]], dtype=np.float32)) + np.testing.assert_array_equal(_np(iop.all(arr, axis=0)), [True, False]) + + +def test_any_dim_keepdims(backend: tuple) -> None: + arr = iop.from_numpy(np.array([[0.0, 1.0], [0.0, 0.0]], dtype=np.float32)) + np.testing.assert_array_equal(_np(iop.any(arr, axis=0, keepdims=True)), [[False, True]]) + + +def test_all_dim_keepdims(backend: tuple) -> None: + arr = iop.from_numpy(np.array([[1.0, 1.0], [1.0, 0.0]], dtype=np.float32)) + np.testing.assert_array_equal(_np(iop.all(arr, axis=0, keepdims=True)), [[True, False]]) # Math elementwise -------------------------------------------------------