diff --git a/decent_array/interoperability/__init__.py b/decent_array/interoperability/__init__.py index 0ca5bb7..0582d38 100644 --- a/decent_array/interoperability/__init__.py +++ b/decent_array/interoperability/__init__.py @@ -22,8 +22,8 @@ bitwise_xor, ) from ._iop.comparison 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 +from ._iop.creation import arange, empty, empty_like, eye, linspace, ones, ones_like, zeros, zeros_like +from ._iop.linalg import dot, eigvalsh, matmul, norm, solve, vecdot, vector_norm from ._iop.manipulations import ( asarray, astype, @@ -49,10 +49,12 @@ absolute, add, divide, + exp, floor_divide, isfinite, isinf, isnan, + logaddexp, multiply, negative, positive, @@ -60,6 +62,7 @@ remainder, sqrt, subtract, + where, ) from ._iop.operators import argmax, argmin, maximum, sign from ._iop.reductions import all, any, max, mean, min, sum # noqa: A004 @@ -84,6 +87,7 @@ "add", "all", "any", + "arange", "argmax", "argmin", "asarray", @@ -103,7 +107,11 @@ "diagonal", "divide", "dot", + "eigvalsh", + "empty", + "empty_like", "equal", + "exp", "expand_dims", "eye", "floor_divide", @@ -119,6 +127,8 @@ "isnan", "less", "less_equal", + "linspace", + "logaddexp", "matmul", "matrix_transpose", "max", @@ -144,6 +154,7 @@ "shape", "sign", "size", + "solve", "sqrt", "squeeze", "stack", @@ -156,6 +167,7 @@ "unsqueeze", "vecdot", "vector_norm", + "where", "zeros", "zeros_like", ] diff --git a/decent_array/interoperability/_abstracts/backend.py b/decent_array/interoperability/_abstracts/backend.py index 5a843ee..d756098 100644 --- a/decent_array/interoperability/_abstracts/backend.py +++ b/decent_array/interoperability/_abstracts/backend.py @@ -42,6 +42,14 @@ def __init__(self, device: Devices = Devices.CPU, name: str = "") -> None: # Array creation ------------------------------------------------------ + @abstractmethod + def empty(self, shape: int | tuple[int, ...]) -> Array: + """Create an uninitialized array with the given shape.""" + + @abstractmethod + def empty_like(self, x: Array, /) -> Array: + """Create an uninitialized array with the same shape as ``x``.""" + @abstractmethod def zeros(self, shape: int | tuple[int, ...]) -> Array: """Create an array of zeros with the given shape.""" @@ -59,8 +67,24 @@ def ones_like(self, x: Array) -> Array: """Create an array of ones matching the shape and type of ``x``.""" @abstractmethod - def eye(self, n: int) -> Array: - """Create an ``n x n`` identity matrix.""" + def eye(self, n_rows: int, n_cols: int | None = None, /, *, k: int = 0) -> Array: + """Create a two-dimensional array with ones on the diagonal at offset ``k``.""" + + @abstractmethod + def arange(self, start: int | float, /, stop: int | float | None = None, step: int | float = 1) -> Array: + """Return evenly spaced values within a half-open interval.""" + + @abstractmethod + def linspace( + self, + start: int | float | complex, + stop: int | float | complex, + /, + num: int, + *, + endpoint: bool = True, + ) -> Array: + """Return evenly spaced numbers over a specified interval.""" @abstractmethod def device_to_native(self, device: Devices) -> Any: # noqa: ANN401 @@ -186,6 +210,14 @@ def vector_norm( ) -> Array: """Compute the norm of ``x``.""" + @abstractmethod + def solve(self, x1: Array, x2: Array) -> Array: + """Solve the linear system ``x1 @ solution = x2``.""" + + @abstractmethod + def eigvalsh(self, x: Array) -> Array: + """Return eigenvalues of a symmetric/Hermitian matrix.""" + # Math reductions ----------------------------------------------------- @abstractmethod @@ -284,6 +316,23 @@ def absolute(self, x: Array) -> Array: def sqrt(self, x: Array) -> Array: """Element-wise square root.""" + @abstractmethod + def exp(self, x: Array) -> Array: + """Element-wise exponential.""" + + @abstractmethod + def logaddexp(self, x1: int | float | Array, x2: int | float | Array) -> Array: + """Element-wise log(exp(x1) + exp(x2)) computed stably.""" + + @abstractmethod + def where( + self, + condition: Array, + x1: bool | int | float | complex | Array, + x2: bool | int | float | complex | Array, + ) -> Array: + """Select elements from ``x1`` or ``x2`` according to ``condition``.""" + @abstractmethod def isfinite(self, x: Array) -> Array: """Element-wise test for finite values.""" diff --git a/decent_array/interoperability/_iop/creation.py b/decent_array/interoperability/_iop/creation.py index b849204..60280d7 100644 --- a/decent_array/interoperability/_iop/creation.py +++ b/decent_array/interoperability/_iop/creation.py @@ -61,8 +61,43 @@ def ones_like(x: Array) -> Array: return _BACKEND_INSTANCE.ones_like(x) -def eye(n: int) -> Array: - """Create an ``n x n`` identity matrix.""" +def empty(shape: int | tuple[int, ...]) -> Array: + """Create an uninitialized array with the given shape.""" if _BACKEND_INSTANCE is None: raise no_backend_error - return _BACKEND_INSTANCE.eye(n) + return _BACKEND_INSTANCE.empty(shape) + + +def empty_like(x: Array, /) -> Array: + """Create an uninitialized array with the same shape as ``x``.""" + if _BACKEND_INSTANCE is None: + raise no_backend_error + return _BACKEND_INSTANCE.empty_like(x) + + +def eye(n_rows: int, n_cols: int | None = None, /, *, k: int = 0) -> Array: + """Create a two-dimensional array with ones on the diagonal at offset ``k``.""" + if _BACKEND_INSTANCE is None: + raise no_backend_error + return _BACKEND_INSTANCE.eye(n_rows, n_cols, k=k) + + +def arange(start: int | float, /, stop: int | float | None = None, step: int | float = 1) -> Array: + """Return evenly spaced values within a half-open interval.""" + if _BACKEND_INSTANCE is None: + raise no_backend_error + return _BACKEND_INSTANCE.arange(start, stop, step) + + +def linspace( + start: int | float | complex, + stop: int | float | complex, + /, + num: int, + *, + endpoint: bool = True, +) -> Array: + """Return evenly spaced numbers over a specified interval.""" + if _BACKEND_INSTANCE is None: + raise no_backend_error + return _BACKEND_INSTANCE.linspace(start, stop, num, endpoint=endpoint) diff --git a/decent_array/interoperability/_iop/linalg.py b/decent_array/interoperability/_iop/linalg.py index 44b1792..bdfd9db 100644 --- a/decent_array/interoperability/_iop/linalg.py +++ b/decent_array/interoperability/_iop/linalg.py @@ -84,3 +84,17 @@ def norm( if _BACKEND_INSTANCE is None: raise no_backend_error return _BACKEND_INSTANCE.vector_norm(x, axis, keepdims, ord) + + +def solve(x1: Array, x2: Array) -> Array: + """Solve the linear system ``x1 @ solution = x2``.""" + if _BACKEND_INSTANCE is None: + raise no_backend_error + return _BACKEND_INSTANCE.solve(x1, x2) + + +def eigvalsh(x: Array) -> Array: + """Return eigenvalues of a symmetric/Hermitian matrix.""" + if _BACKEND_INSTANCE is None: + raise no_backend_error + return _BACKEND_INSTANCE.eigvalsh(x) diff --git a/decent_array/interoperability/_iop/math.py b/decent_array/interoperability/_iop/math.py index f933e8b..e909518 100644 --- a/decent_array/interoperability/_iop/math.py +++ b/decent_array/interoperability/_iop/math.py @@ -149,6 +149,31 @@ def sqrt(x: Array) -> Array: return _BACKEND_INSTANCE.sqrt(x) +def exp(x: Array) -> Array: + """Element-wise exponential.""" + if _BACKEND_INSTANCE is None: + raise no_backend_error + return _BACKEND_INSTANCE.exp(x) + + +def logaddexp(x1: int | float | Array, x2: int | float | Array) -> Array: + """Element-wise log(exp(x1) + exp(x2)) computed stably.""" + if _BACKEND_INSTANCE is None: + raise no_backend_error + return _BACKEND_INSTANCE.logaddexp(x1, x2) + + +def where( + condition: Array, + x1: bool | int | float | complex | Array, + x2: bool | int | float | complex | Array, +) -> Array: + """Select elements from ``x1`` or ``x2`` according to ``condition``.""" + if _BACKEND_INSTANCE is None: + raise no_backend_error + return _BACKEND_INSTANCE.where(condition, x1, x2) + + def isfinite(x: Array) -> Array: """Element-wise test for finite values.""" if _BACKEND_INSTANCE is None: diff --git a/decent_array/interoperability/_jax/jax_backend.py b/decent_array/interoperability/_jax/jax_backend.py index 9d10361..7da13eb 100644 --- a/decent_array/interoperability/_jax/jax_backend.py +++ b/decent_array/interoperability/_jax/jax_backend.py @@ -46,6 +46,12 @@ def __init__(self, device: Devices = Devices.CPU) -> None: # Array creation + def empty(self, shape: int | tuple[int, ...]) -> Array: + return Array(jnp.empty(shape, device=self._native_device)) + + def empty_like(self, x: Array, /) -> Array: + return Array(jnp.empty_like(x.value, device=self._native_device)) + def zeros(self, shape: int | tuple[int, ...]) -> Array: return Array(jnp.zeros(shape, device=self._native_device)) @@ -58,8 +64,22 @@ def ones(self, shape: int | tuple[int, ...]) -> Array: def ones_like(self, x: Array) -> Array: return Array(jnp.ones_like(x.value)) - def eye(self, n: int) -> Array: - return Array(jnp.eye(n, device=self._native_device)) + def eye(self, n_rows: int, n_cols: int | None = None, /, *, k: int = 0) -> Array: + return Array(jnp.eye(n_rows, n_cols, k=k, device=self._native_device)) + + def arange(self, start: int | float, /, stop: int | float | None = None, step: int | float = 1) -> Array: + return Array(jnp.arange(start, stop, step, device=self._native_device)) + + def linspace( + self, + start: int | float | complex, + stop: int | float | complex, + /, + num: int, + *, + endpoint: bool = True, + ) -> Array: + return Array(jnp.linspace(start, stop, num, endpoint=endpoint, device=self._native_device)) def device_to_native(self, device: Devices) -> jax.Device: if device == Devices.CPU: @@ -174,6 +194,12 @@ def vector_norm( ) -> Array: return Array(jnp.linalg.norm(x.value, ord=ord, axis=axis, keepdims=keepdims)) + def solve(self, x1: Array, x2: Array) -> Array: + return Array(jnp.linalg.solve(x1.value, x2.value)) + + def eigvalsh(self, x: Array) -> Array: + return Array(jnp.linalg.eigvalsh(x.value)) + # Math reductions def sum(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: @@ -256,6 +282,20 @@ def absolute(self, x: Array) -> Array: def sqrt(self, x: Array) -> Array: return Array(jnp.sqrt(x.value)) + def exp(self, x: Array) -> Array: + return Array(jnp.exp(x.value)) + + def logaddexp(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(jnp.logaddexp(unwrap(x1), unwrap(x2))) + + def where( + self, + condition: Array, + x1: bool | int | float | complex | Array, + x2: bool | int | float | complex | Array, + ) -> Array: + return Array(jnp.where(condition.value, unwrap(x1), unwrap(x2))) + def isfinite(self, x: Array) -> Array: """Element-wise test for finite values.""" return Array(jnp.isfinite(x.value)) diff --git a/decent_array/interoperability/_numpy/numpy_backend.py b/decent_array/interoperability/_numpy/numpy_backend.py index 37b7cc2..cda1625 100644 --- a/decent_array/interoperability/_numpy/numpy_backend.py +++ b/decent_array/interoperability/_numpy/numpy_backend.py @@ -40,6 +40,12 @@ def __init__(self, device: Devices = Devices.CPU) -> None: # Array creation + def empty(self, shape: int | tuple[int, ...]) -> Array: + return Array(np.empty(shape)) + + def empty_like(self, x: Array, /) -> Array: + return Array(np.empty_like(x.value)) + def zeros(self, shape: int | tuple[int, ...]) -> Array: return Array(np.zeros(shape)) @@ -52,8 +58,22 @@ def ones(self, shape: int | tuple[int, ...]) -> Array: def ones_like(self, x: Array) -> Array: return Array(np.ones_like(x.value)) - def eye(self, n: int) -> Array: - return Array(np.eye(n)) + def eye(self, n_rows: int, n_cols: int | None = None, /, *, k: int = 0) -> Array: + return Array(np.eye(n_rows, n_cols, k=k)) + + def arange(self, start: int | float, /, stop: int | float | None = None, step: int | float = 1) -> Array: + return Array(np.arange(start, stop, step)) + + def linspace( + self, + start: int | float | complex, + stop: int | float | complex, + /, + num: int, + *, + endpoint: bool = True, + ) -> Array: + return Array(np.linspace(start, stop, num, endpoint=endpoint)) def device_to_native(self, device: Devices) -> Any: # noqa: ANN401 # NumPy has no explicit device management; surface the request unchanged. @@ -169,6 +189,12 @@ def vector_norm( # but is still typed as int | tuple[int, int] | None, hence the ignore return Array(np.linalg.vector_norm(x.value, ord=ord, axis=axis, keepdims=keepdims)) # type: ignore[arg-type] + def solve(self, x1: Array, x2: Array) -> Array: + return Array(np.linalg.solve(x1.value, x2.value)) + + def eigvalsh(self, x: Array) -> Array: + return Array(np.linalg.eigvalsh(x.value)) + # Math reductions def sum(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: @@ -268,6 +294,20 @@ def absolute(self, x: Array) -> Array: def sqrt(self, x: Array) -> Array: return Array(np.sqrt(x.value)) + def exp(self, x: Array) -> Array: + return Array(np.exp(x.value)) + + def logaddexp(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(np.logaddexp(unwrap(x1), unwrap(x2))) + + def where( + self, + condition: Array, + x1: bool | int | float | complex | Array, + x2: bool | int | float | complex | Array, + ) -> Array: + return Array(np.where(condition.value, unwrap(x1), unwrap(x2))) + def isfinite(self, x: Array) -> Array: """Element-wise test for finite values.""" return Array(np.isfinite(x.value)) diff --git a/decent_array/interoperability/_pytorch/pytorch_backend.py b/decent_array/interoperability/_pytorch/pytorch_backend.py index 8c251b8..b78c2ce 100644 --- a/decent_array/interoperability/_pytorch/pytorch_backend.py +++ b/decent_array/interoperability/_pytorch/pytorch_backend.py @@ -33,6 +33,12 @@ def __init__(self, device: Devices = Devices.CPU) -> None: # Array creation + def empty(self, shape: int | tuple[int, ...]) -> Array: + return Array(torch.empty(shape, device=self._native_device)) + + def empty_like(self, x: Array, /) -> Array: + return Array(torch.empty_like(x.value)) + def zeros(self, shape: int | tuple[int, ...]) -> Array: return Array(torch.zeros(shape, device=self._native_device)) @@ -45,8 +51,40 @@ def ones(self, shape: int | tuple[int, ...]) -> Array: def ones_like(self, x: Array) -> Array: return Array(torch.ones_like(x.value)) - def eye(self, n: int) -> Array: - return Array(torch.eye(n, device=self._native_device)) + def eye(self, n_rows: int, n_cols: int | None = None, /, *, k: int = 0) -> Array: + columns = n_rows if n_cols is None else n_cols + result = torch.zeros((n_rows, columns), device=self._native_device) + if k >= 0: + length = min(n_rows, columns - k) + if length > 0: + indices = torch.arange(length, device=self._native_device) + result[indices, indices + k] = 1 + else: + length = min(n_rows + k, columns) + if length > 0: + indices = torch.arange(length, device=self._native_device) + result[indices - k, indices] = 1 + return Array(result) + + def arange(self, start: int | float, /, stop: int | float | None = None, step: int | float = 1) -> Array: + if stop is None: + return Array(torch.arange(start, device=self._native_device)) + return Array(torch.arange(start, stop, step, device=self._native_device)) + + def linspace( + self, + start: int | float | complex, + stop: int | float | complex, + /, + num: int, + *, + endpoint: bool = True, + ) -> Array: + if endpoint: + return Array(torch.linspace(start, stop, num, device=self._native_device)) + if num == 0: + return Array(torch.empty(0, device=self._native_device)) + return Array(torch.linspace(start, stop, num + 1, device=self._native_device)[:-1]) def device_to_native(self, device: Devices) -> str: if device == Devices.CPU: @@ -176,6 +214,16 @@ def vector_norm( ) -> Array: return Array(torch.linalg.norm(x.value, ord=ord, axis=axis, keepdim=keepdims)) + def solve(self, x1: Array, x2: Array) -> Array: + if x1.value.device.type == "mps": + return Array(torch.linalg.solve(x1.value.cpu(), x2.value.cpu()).to(x1.value.device)) + return Array(torch.linalg.solve(x1.value, x2.value)) + + def eigvalsh(self, x: Array) -> Array: + if x.value.device.type == "mps": + return Array(torch.linalg.eigvalsh(x.value.cpu()).to(x.value.device)) + return Array(torch.linalg.eigvalsh(x.value)) + # Math reductions def sum(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: @@ -269,6 +317,20 @@ def absolute(self, x: Array) -> Array: def sqrt(self, x: Array) -> Array: return Array(torch.sqrt(x.value)) + def exp(self, x: Array) -> Array: + return Array(torch.exp(x.value)) + + def logaddexp(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(torch.logaddexp(unwrap(x1), unwrap(x2))) + + def where( + self, + condition: Array, + x1: bool | int | float | complex | Array, + x2: bool | int | float | complex | Array, + ) -> Array: + return Array(torch.where(condition.value, unwrap(x1), unwrap(x2))) + def isfinite(self, x: Array) -> Array: """Element-wise test for finite values.""" return Array(torch.isfinite(x.value)) diff --git a/decent_array/interoperability/_tensorflow/tensorflow_backend.py b/decent_array/interoperability/_tensorflow/tensorflow_backend.py index 2d98f4f..8d961bb 100644 --- a/decent_array/interoperability/_tensorflow/tensorflow_backend.py +++ b/decent_array/interoperability/_tensorflow/tensorflow_backend.py @@ -44,6 +44,14 @@ def __init__(self, device: Devices = Devices.CPU) -> None: # Array creation + def empty(self, shape: int | tuple[int, ...]) -> Array: + with tf.device(self._native_device): + return Array(tf.experimental.numpy.empty(shape)) + + def empty_like(self, x: Array, /) -> Array: + with tf.device(self._native_device): + return Array(tf.experimental.numpy.empty_like(x.value)) + def zeros(self, shape: int | tuple[int, ...]) -> Array: with tf.device(self._native_device): return Array(tf.zeros(shape)) @@ -58,9 +66,25 @@ def ones(self, shape: int | tuple[int, ...]) -> Array: def ones_like(self, x: Array) -> Array: return Array(tf.ones_like(x.value)) - def eye(self, n: int) -> Array: + def eye(self, n_rows: int, n_cols: int | None = None, /, *, k: int = 0) -> Array: with tf.device(self._native_device): - return Array(tf.eye(n)) + return Array(tf.experimental.numpy.eye(n_rows, n_cols, k=k)) + + def arange(self, start: int | float, /, stop: int | float | None = None, step: int | float = 1) -> Array: + with tf.device(self._native_device): + return Array(tf.experimental.numpy.arange(start, stop, step)) + + def linspace( + self, + start: int | float | complex, + stop: int | float | complex, + /, + num: int, + *, + endpoint: bool = True, + ) -> Array: + with tf.device(self._native_device): + return Array(tf.experimental.numpy.linspace(start, stop, num=num, endpoint=endpoint)) def device_to_native(self, device: Devices) -> str: if device in {Devices.CPU, Devices.GPU}: @@ -197,6 +221,15 @@ def vector_norm( axis = axis if axis is not None else (-2, -1) if v.ndim == 2 else None return Array(tf.norm(v, ord=ord, axis=axis, keepdims=keepdims)) + def solve(self, x1: Array, x2: Array) -> Array: + rhs = x2.value + if rhs.shape.rank == x1.value.shape.rank - 1: + return Array(tf.squeeze(tf.linalg.solve(x1.value, tf.expand_dims(rhs, axis=-1)), axis=-1)) + return Array(tf.linalg.solve(x1.value, rhs)) + + def eigvalsh(self, x: Array) -> Array: + return Array(tf.linalg.eigvalsh(x.value)) + # Math reductions def sum(self, x: Array, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array: @@ -279,6 +312,20 @@ def absolute(self, x: Array) -> Array: def sqrt(self, x: Array) -> Array: return Array(tf.sqrt(x.value)) + def exp(self, x: Array) -> Array: + return Array(tf.exp(x.value)) + + def logaddexp(self, x1: int | float | Array, x2: int | float | Array) -> Array: + return Array(tf.keras.ops.logaddexp(unwrap(x1), unwrap(x2))) + + def where( + self, + condition: Array, + x1: bool | int | float | complex | Array, + x2: bool | int | float | complex | Array, + ) -> Array: + return Array(tf.where(condition.value, unwrap(x1), unwrap(x2))) + def isfinite(self, x: Array) -> Array: """Element-wise test for finite values.""" return Array(tf.math.is_finite(x.value)) diff --git a/pyproject.toml b/pyproject.toml index c4f7431..7313e52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "decent-array" -version = "0.2.6" +version = "0.2.7" 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_iop_functions.py b/tests/test_iop_functions.py index cad83dc..65c998f 100644 --- a/tests/test_iop_functions.py +++ b/tests/test_iop_functions.py @@ -42,11 +42,110 @@ def test_ones_like(backend: tuple) -> None: np.testing.assert_allclose(_np(arr), np.ones((2, 3))) +def test_empty(backend: tuple) -> None: + arr = iop.empty((2, 3)) + assert iop.shape(arr) == (2, 3) + + +def test_empty_scalar_shape(backend: tuple) -> None: + arr = iop.empty(4) + assert iop.shape(arr) == (4,) + + +def test_empty_like(backend: tuple) -> None: + src = iop.from_numpy(np.ones((2, 3), dtype=np.float32)) + arr = iop.empty_like(src) + assert iop.shape(arr) == iop.shape(src) + assert arr.dtype == src.dtype + + def test_eye(backend: tuple) -> None: arr = iop.eye(3) np.testing.assert_allclose(_np(arr), np.eye(3)) +def test_eye_rectangular(backend: tuple) -> None: + np.testing.assert_allclose(_np(iop.eye(2, 4)), np.eye(2, 4)) + + +def test_eye_positive_offset(backend: tuple) -> None: + np.testing.assert_allclose(_np(iop.eye(3, 4, k=1)), np.eye(3, 4, k=1)) + + +def test_eye_negative_offset(backend: tuple) -> None: + np.testing.assert_allclose(_np(iop.eye(4, 3, k=-1)), np.eye(4, 3, k=-1)) + + +def test_arange(backend: tuple) -> None: + np.testing.assert_array_equal(_np(iop.arange(5)), np.arange(5)) + + +def test_arange_start_stop_step(backend: tuple) -> None: + np.testing.assert_array_equal(_np(iop.arange(1, 8, 2)), np.arange(1, 8, 2)) + + +def test_arange_negative_step(backend: tuple) -> None: + np.testing.assert_array_equal(_np(iop.arange(5, 0, -2)), np.arange(5, 0, -2)) + + +def test_arange_float(backend: tuple) -> None: + np.testing.assert_allclose(_np(iop.arange(0.0, 1.0, 0.25)), np.arange(0.0, 1.0, 0.25)) + + +def test_linspace(backend: tuple) -> None: + np.testing.assert_allclose(_np(iop.linspace(0.0, 1.0, 5)), np.linspace(0.0, 1.0, 5)) + + +def test_linspace_without_endpoint(backend: tuple) -> None: + np.testing.assert_allclose(_np(iop.linspace(0.0, 1.0, 5, endpoint=False)), np.linspace(0.0, 1.0, 5, endpoint=False)) + + +def test_linspace_zero_samples(backend: tuple) -> None: + assert iop.shape(iop.linspace(0.0, 1.0, 0)) == (0,) + + +def test_exp(backend: tuple) -> None: + arr = iop.exp(iop.from_numpy(np.array([-2.0, 0.0, 2.0], dtype=np.float32))) + np.testing.assert_allclose(_np(arr), np.exp([-2.0, 0.0, 2.0])) + + +def test_where_arrays_and_broadcasting(backend: tuple) -> None: + condition = iop.from_numpy(np.array([[True], [False]], dtype=np.bool_)) + x1 = iop.from_numpy(np.array([1.0, 2.0, 3.0], dtype=np.float32)) + x2 = iop.from_numpy(np.array([4.0, 5.0, 6.0], dtype=np.float32)) + np.testing.assert_array_equal(_np(iop.where(condition, x1, x2)), [[1, 2, 3], [4, 5, 6]]) + + +def test_where_scalar_and_array_operands(backend: tuple) -> None: + condition = iop.from_numpy(np.array([True, False], dtype=np.bool_)) + x = iop.from_numpy(np.array([1.0, 2.0], dtype=np.float32)) + np.testing.assert_array_equal(_np(iop.where(condition, x, 0.0)), [1.0, 0.0]) + np.testing.assert_array_equal(_np(iop.where(condition, -1.0, x)), [-1.0, 2.0]) + + +def test_logaddexp_is_stable(backend: tuple) -> None: + values = iop.from_numpy(np.array([1000.0, -1000.0], dtype=np.float32)) + result = iop.logaddexp(values, values) + np.testing.assert_allclose(_np(result), [1000.0 + np.log(2.0), -1000.0 + np.log(2.0)]) + + +def test_logaddexp_infinities(backend: tuple) -> None: + values = iop.from_numpy(np.array([np.inf, -np.inf], dtype=np.float32)) + np.testing.assert_array_equal(_np(iop.logaddexp(values, values)), [np.inf, -np.inf]) + + +def test_solve_known_system(backend: tuple) -> None: + matrix = iop.from_numpy(np.array([[3.0, 1.0], [1.0, 2.0]], dtype=np.float32)) + rhs = iop.from_numpy(np.array([9.0, 8.0], dtype=np.float32)) + solution = iop.solve(matrix, rhs) + np.testing.assert_allclose(_np(iop.matmul(matrix, solution)), _np(rhs), rtol=1e-5, atol=1e-5) + + +def test_eigvalsh_known_symmetric_matrix(backend: tuple) -> None: + matrix = iop.from_numpy(np.array([[2.0, 1.0], [1.0, 2.0]], dtype=np.float32)) + np.testing.assert_allclose(_np(iop.eigvalsh(matrix)), [1.0, 3.0], rtol=1e-5, atol=1e-5) + + def test_device_to_native(backend: tuple) -> None: framework, device = backend native = device_to_native(device) @@ -62,6 +161,7 @@ def test_device_of(backend: tuple) -> None: # Array manipulation ----------------------------------------------------- + def test_asarray_existing_array_is_identity(backend: tuple) -> None: arr = Array(4)