From 492c2798f7940afab1c7b7b77dcaf5fb2850279f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sok=C3=B3=C5=82?= <8431159+mtsokol@users.noreply.github.com> Date: Tue, 2 Jan 2024 11:30:15 +0100 Subject: [PATCH] API: Improve sparse.asarray (#615) --- sparse/__init__.py | 1 + sparse/_common.py | 32 ++++++++++++++--------------- sparse/_coo/common.py | 9 ++++++-- sparse/_sparse_array.py | 11 ++++++++++ sparse/_umath.py | 2 ++ sparse/tests/test_array_function.py | 7 ++++--- sparse/tests/test_coo.py | 2 +- sparse/tests/test_elemwise.py | 3 ++- 8 files changed, 44 insertions(+), 23 deletions(-) diff --git a/sparse/__init__.py b/sparse/__init__.py index f5940c1f..38533049 100644 --- a/sparse/__init__.py +++ b/sparse/__init__.py @@ -9,6 +9,7 @@ from ._version import get_versions __version__ = get_versions()["version"] +__array_api_version__ = "2022.12" del get_versions from numpy import ( diff --git a/sparse/_common.py b/sparse/_common.py index 576206c9..8c13f939 100644 --- a/sparse/_common.py +++ b/sparse/_common.py @@ -1819,7 +1819,7 @@ def zeros(shape, dtype=float, format="coo", **kwargs): array([[0, 0], [0, 0]]) """ - return full(shape, 0, np.dtype(dtype)).asformat(format, **kwargs) + return full(shape, fill_value=0, dtype=np.dtype(dtype), format=format, **kwargs) def zeros_like(a, dtype=None, shape=None, format=None, **kwargs): @@ -1848,7 +1848,7 @@ def zeros_like(a, dtype=None, shape=None, format=None, **kwargs): array([[0, 0, 0], [0, 0, 0]]) """ - return full_like(a, 0, dtype=dtype, shape=shape, format=format, **kwargs) + return full_like(a, fill_value=0, dtype=dtype, shape=shape, format=format, **kwargs) def ones(shape, dtype=float, format="coo", **kwargs): @@ -1880,7 +1880,7 @@ def ones(shape, dtype=float, format="coo", **kwargs): array([[1, 1], [1, 1]]) """ - return full(shape, 1, np.dtype(dtype)).asformat(format, **kwargs) + return full(shape, fill_value=1, dtype=np.dtype(dtype), format=format, **kwargs) def ones_like(a, dtype=None, shape=None, format=None, **kwargs): @@ -1909,18 +1909,18 @@ def ones_like(a, dtype=None, shape=None, format=None, **kwargs): array([[1, 1, 1], [1, 1, 1]]) """ - return full_like(a, 1, dtype=dtype, shape=shape, format=format, **kwargs) + return full_like(a, fill_value=1, dtype=dtype, shape=shape, format=format, **kwargs) def empty(shape, dtype=float, format="coo", **kwargs): - return full(shape, 0, np.dtype(dtype)).asformat(format, **kwargs) + return full(shape, fill_value=0, dtype=np.dtype(dtype), format=format, **kwargs) empty.__doc__ = zeros.__doc__ def empty_like(a, dtype=None, shape=None, format=None, **kwargs): - return full_like(a, 0, dtype=dtype, shape=shape, format=format, **kwargs) + return full_like(a, fill_value=0, dtype=dtype, shape=shape, format=format, **kwargs) empty_like.__doc__ = zeros_like.__doc__ @@ -2159,16 +2159,16 @@ def asarray( return obj elif isinstance(obj, spmatrix): - return format_dict[format].from_scipy_sparse( - obj.astype(dtype=dtype, copy=copy) - ) - - # check for scalars and 0-D arrays - elif np.isscalar(obj) or (isinstance(obj, np.ndarray) and obj.shape == ()): - return np.asarray(obj, dtype=dtype) - - elif isinstance(obj, np.ndarray): - return format_dict[format].from_numpy(obj).astype(dtype=dtype, copy=copy) + sparse_obj = format_dict[format].from_scipy_sparse(obj) + if dtype is None: + dtype = sparse_obj.dtype + return sparse_obj.astype(dtype=dtype, copy=copy) + + elif np.isscalar(obj) or isinstance(obj, (np.ndarray, Iterable)): + sparse_obj = format_dict[format].from_numpy(np.asarray(obj)) + if dtype is None: + dtype = sparse_obj.dtype + return sparse_obj.astype(dtype=dtype, copy=copy) else: raise ValueError(f"{type(obj)} not supported.") diff --git a/sparse/_coo/common.py b/sparse/_coo/common.py index f9f49a5f..4a3cc810 100644 --- a/sparse/_coo/common.py +++ b/sparse/_coo/common.py @@ -1145,11 +1145,16 @@ def _arg_minmax_common( assert mode in ("max", "min") max_mode_flag = mode == "max" + from .core import COO + + if not isinstance(x, COO): + raise ValueError(f"Only COO arrays are supported but {type(x)} was passed.") + if not isinstance(axis, (int, type(None))): - raise ValueError(f"axis must be int or None, but it's: {type(axis)}") + raise ValueError(f"`axis` must be `int` or `None`, but it's: {type(axis)}.") if isinstance(axis, int) and axis >= x.ndim: raise ValueError( - f"axis {axis} is out of bounds for array of dimension {x.ndim}" + f"`axis={axis}` is out of bounds for array of dimension {x.ndim}." ) if x.ndim == 0: raise ValueError("Input array must be at least 1-D, but it's 0-D.") diff --git a/sparse/_sparse_array.py b/sparse/_sparse_array.py index da2a100d..e1afb7c8 100644 --- a/sparse/_sparse_array.py +++ b/sparse/_sparse_array.py @@ -987,6 +987,17 @@ def conj(self): """ return np.conj(self) + def __array_namespace__(self, *, api_version=None): + if api_version is None: + api_version = "2022.12" + + if api_version in ("2021.12", "2022.12"): + import sparse + + return sparse + else: + raise ValueError(f'"{api_version}" Array API version not supported.') + def __bool__(self): """ """ return self._to_scalar(bool) diff --git a/sparse/_umath.py b/sparse/_umath.py index 0a5c4965..fec181f5 100644 --- a/sparse/_umath.py +++ b/sparse/_umath.py @@ -431,6 +431,8 @@ def __init__(self, func, *args, **kwargs): sparse_args = [arg for arg in args if isinstance(arg, SparseArray)] + if len(sparse_args) == 0: + raise ValueError(f"None of the args is sparse: {args}") if all(isinstance(arg, DOK) for arg in sparse_args): out_type = DOK elif all(isinstance(arg, GCXS) for arg in sparse_args): diff --git a/sparse/tests/test_array_function.py b/sparse/tests/test_array_function.py index 2bbbed28..b3634aa6 100644 --- a/sparse/tests/test_array_function.py +++ b/sparse/tests/test_array_function.py @@ -121,6 +121,10 @@ class TestAsarray: @pytest.mark.parametrize("dtype", [np.int64, np.float64, np.complex128]) @pytest.mark.parametrize("format", ["dok", "gcxs", "coo"]) def test_asarray(self, input, dtype, format): + if format == "dok" and (np.isscalar(input) or input.ndim == 0): + # scalars and 0-D arrays aren't supported in DOK format + return + s = sparse.asarray(input, dtype=dtype, format=format) actual = s.todense() if hasattr(s, "todense") else s @@ -132,9 +136,6 @@ def test_asarray_special_cases(self): with pytest.raises(ValueError, match="Taco not yet supported."): sparse.asarray(self.np_eye, backend="taco") - with pytest.raises(ValueError, match=" not supported."): - sparse.asarray([1, 2, 3]) - with pytest.raises(ValueError, match="any backend not supported."): sparse.asarray(self.np_eye, backend="any") diff --git a/sparse/tests/test_coo.py b/sparse/tests/test_coo.py index 1f811768..fc0ea675 100644 --- a/sparse/tests/test_coo.py +++ b/sparse/tests/test_coo.py @@ -1736,7 +1736,7 @@ def test_argmax_argmin_constraint(func): s = sparse.COO.from_numpy(np.full((2, 2), 2), fill_value=2) with pytest.raises( - ValueError, match="axis 2 is out of bounds for array of dimension 2" + ValueError, match="`axis=2` is out of bounds for array of dimension 2." ): func(s, axis=2) diff --git a/sparse/tests/test_elemwise.py b/sparse/tests/test_elemwise.py index 86cd6a05..1e2bd832 100644 --- a/sparse/tests/test_elemwise.py +++ b/sparse/tests/test_elemwise.py @@ -384,7 +384,8 @@ def test_elemwise_noargs(): def func(): return np.float_(5.0) - assert_eq(sparse.elemwise(func), func()) + with pytest.raises(ValueError, match=r"None of the args is sparse:"): + sparse.elemwise(func) @pytest.mark.parametrize(