Skip to content

Commit

Permalink
Make SciPy a soft dependency (#632)
Browse files Browse the repository at this point in the history
* Make SciPy a soft dependency.

* Fix packaging issue.

* Add scipy to doc deps.

* Add docs + __class__ -> __module__.
  • Loading branch information
hameerabbasi authored Jan 16, 2024
1 parent 16b46cd commit c57fed2
Show file tree
Hide file tree
Showing 9 changed files with 51 additions and 40 deletions.
15 changes: 0 additions & 15 deletions MANIFEST.in

This file was deleted.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "sparse"
dynamic = ["version"]
description = "Sparse n-dimensional arrays for the PyData ecosystem"
readme = "README.rst"
dependencies = ["numpy>=1.17", "scipy>=0.19", "numba>=0.49"]
dependencies = ["numpy>=1.17", "numba>=0.49"]
maintainers = [{ name = "Hameer Abbasi", email = "[email protected]" }]
requires-python = ">=3.8"
license = { file = "LICENSE" }
Expand All @@ -28,12 +28,13 @@ classifiers = [
]

[project.optional-dependencies]
docs = ["sphinx", "sphinx_rtd_theme"]
docs = ["sphinx", "sphinx_rtd_theme", "scipy"]
tests = [
"dask[array]",
"pytest>=3.5",
"pytest-cov",
"pre-commit",
"scipy",
]
tox = ["sparse[tests]", "tox"]
all = ["sparse[docs,tox]", "matrepr"]
Expand Down
27 changes: 21 additions & 6 deletions sparse/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from numba import literal_unroll

import numpy as np
import scipy.sparse
from scipy.sparse import spmatrix

from ._coo.common import asCOO
from ._sparse_array import SparseArray
Expand All @@ -22,6 +20,22 @@
)


def _is_scipy_sparse_obj(x):
"""
Tests if the supplied argument is a SciPy sparse object.
"""
if hasattr(x, "__module__") and x.__module__.startswith("scipy.sparse"):
return True
return False


def _is_sparse(x):
"""
Tests if the supplied argument is a SciPy sparse object, or one from this library.
"""
return isinstance(x, SparseArray) or _is_scipy_sparse_obj(x)


@numba.njit
def nan_check(*args):
"""
Expand Down Expand Up @@ -61,7 +75,7 @@ def check_class_nan(test):

if isinstance(test, (GCXS, COO)):
return nan_check(test.fill_value, test.data)
if isinstance(test, spmatrix):
if _is_scipy_sparse_obj(test):
return nan_check(test.data)
return nan_check(test)

Expand Down Expand Up @@ -99,9 +113,9 @@ def tensordot(a, b, axes=2, *, return_type=None):
# Please see license at https://github.com/numpy/numpy/blob/main/LICENSE.txt
check_zero_fill_value(a, b)

if scipy.sparse.issparse(a):
if _is_scipy_sparse_obj(a):
a = GCXS.from_scipy_sparse(a)
if scipy.sparse.issparse(b):
if _is_scipy_sparse_obj(b):
b = GCXS.from_scipy_sparse(b)

try:
Expand Down Expand Up @@ -2047,6 +2061,7 @@ def asarray(obj, /, *, dtype=None, format="coo", backend="pydata", device=None,
>>> sparse.asarray(x, format="COO")
<COO: shape=(8, 8), dtype=int64, nnz=8, fill_value=0>
"""

if format not in {"coo", "dok", "gcxs"}:
raise ValueError(f"{format} format not supported.")

Expand All @@ -2065,7 +2080,7 @@ def asarray(obj, /, *, dtype=None, format="coo", backend="pydata", device=None,
if isinstance(obj, (COO, DOK, GCXS)):
return obj.asformat(format)

if isinstance(obj, spmatrix):
if _is_scipy_sparse_obj(obj):
sparse_obj = format_dict[format].from_scipy_sparse(obj)
if dtype is None:
dtype = sparse_obj.dtype
Expand Down
10 changes: 6 additions & 4 deletions sparse/_compressed/compressed.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from functools import reduce

import numpy as np
import scipy.sparse as ss
from numpy.lib.mixins import NDArrayOperatorsMixin

from .._coo.common import linear_loc
Expand Down Expand Up @@ -140,7 +139,9 @@ def __init__(
fill_value=0,
idx_dtype=None,
):
if isinstance(arg, ss.spmatrix):
from .._common import _is_scipy_sparse_obj

if _is_scipy_sparse_obj(arg):
arg = self.from_scipy_sparse(arg)

if isinstance(arg, np.ndarray):
Expand Down Expand Up @@ -482,16 +483,17 @@ def to_scipy_sparse(self):
ValueError
If all the array doesn't zero fill-values.
"""
import scipy.sparse

check_zero_fill_value(self)

if self.ndim != 2:
raise ValueError("Can only convert a 2-dimensional array to a Scipy sparse matrix.")

if 0 in self.compressed_axes:
return ss.csr_matrix((self.data, self.indices, self.indptr), shape=self.shape)
return scipy.sparse.csr_matrix((self.data, self.indices, self.indptr), shape=self.shape)

return ss.csc_matrix((self.data, self.indices, self.indptr), shape=self.shape)
return scipy.sparse.csc_matrix((self.data, self.indices, self.indptr), shape=self.shape)

def asformat(self, format, **kwargs):
"""
Expand Down
12 changes: 7 additions & 5 deletions sparse/_coo/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import numba

import numpy as np
import scipy.sparse

from .._sparse_array import SparseArray
from .._utils import (
Expand Down Expand Up @@ -43,9 +42,10 @@ def asCOO(x, name="asCOO", check=True):
ValueError
If ``check`` is true and a dense input is supplied.
"""
from .._common import _is_sparse
from .core import COO

if check and not isinstance(x, (SparseArray, scipy.sparse.spmatrix)):
if check and not _is_sparse(x):
raise ValueError(f"Performing this operation would produce a dense result: {name}")

if not isinstance(x, COO):
Expand Down Expand Up @@ -94,13 +94,14 @@ def kron(a, b):
[0, 0, 0, 1, 2, 3, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 2, 3]], dtype=int64)
"""
from .._common import _is_sparse
from .._umath import _cartesian_product
from .core import COO

check_zero_fill_value(a, b)

a_sparse = isinstance(a, (SparseArray, scipy.sparse.spmatrix))
b_sparse = isinstance(b, (SparseArray, scipy.sparse.spmatrix))
a_sparse = _is_sparse(a)
b_sparse = _is_sparse(b)
a_ndim = np.ndim(a)
b_ndim = np.ndim(b)

Expand Down Expand Up @@ -1346,9 +1347,10 @@ def take(x, indices, /, *, axis=None):


def _validate_coo_input(x: Any):
from .._common import _is_scipy_sparse_obj
from .core import COO

if isinstance(x, scipy.sparse.spmatrix):
if _is_scipy_sparse_obj(x):
x = COO.from_scipy_sparse(x)
elif not isinstance(x, SparseArray):
raise ValueError(f"Input must be an instance of SparseArray, but it's {type(x)}.")
Expand Down
9 changes: 7 additions & 2 deletions sparse/_coo/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import numba

import numpy as np
import scipy.sparse
from numpy.lib.mixins import NDArrayOperatorsMixin

from .._sparse_array import SparseArray
Expand Down Expand Up @@ -1177,6 +1176,8 @@ def to_scipy_sparse(self):
COO.tocsr : Convert to a :obj:`scipy.sparse.csr_matrix`.
COO.tocsc : Convert to a :obj:`scipy.sparse.csc_matrix`.
"""
import scipy.sparse

check_zero_fill_value(self)

if self.ndim != 2:
Expand All @@ -1187,6 +1188,8 @@ def to_scipy_sparse(self):
return result

def _tocsr(self):
import scipy.sparse

if self.ndim != 2:
raise ValueError("This array must be two-dimensional for this conversion to work.")
row, col = self.coords
Expand Down Expand Up @@ -1557,6 +1560,8 @@ def as_coo(x, shape=None, fill_value=None, idx_dtype=None):
COO.from_iter :
Convert an iterable to :obj:`COO`.
"""
from .._common import _is_scipy_sparse_obj

if hasattr(x, "shape") and shape is not None:
raise ValueError("Cannot provide a shape in combination with something that already has a shape.")

Expand All @@ -1569,7 +1574,7 @@ def as_coo(x, shape=None, fill_value=None, idx_dtype=None):
if isinstance(x, np.ndarray) or np.isscalar(x):
return COO.from_numpy(x, fill_value=fill_value, idx_dtype=idx_dtype)

if isinstance(x, scipy.sparse.spmatrix):
if _is_scipy_sparse_obj(x):
return COO.from_scipy_sparse(x)

if isinstance(x, (Iterable, Iterator)):
Expand Down
4 changes: 2 additions & 2 deletions sparse/_dok.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from numbers import Integral

import numpy as np
import scipy.sparse
from numpy.lib.mixins import NDArrayOperatorsMixin

from ._slicing import normalize_index
Expand Down Expand Up @@ -92,6 +91,7 @@ class DOK(SparseArray, NDArrayOperatorsMixin):
"""

def __init__(self, shape, data=None, dtype=None, fill_value=None):
from ._common import _is_scipy_sparse_obj
from ._coo import COO

self.data = {}
Expand All @@ -106,7 +106,7 @@ def __init__(self, shape, data=None, dtype=None, fill_value=None):
self._make_shallow_copy_of(ar)
return

if isinstance(shape, scipy.sparse.spmatrix):
if _is_scipy_sparse_obj(shape):
ar = DOK.from_scipy_sparse(shape)
self._make_shallow_copy_of(ar)
return
Expand Down
5 changes: 3 additions & 2 deletions sparse/_sparse_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from typing import Callable

import numpy as np
import scipy.sparse as ss

from ._umath import elemwise
from ._utils import _zero_of_dtype, equivalent, html_table, normalize_axis
Expand Down Expand Up @@ -298,10 +297,12 @@ def __array_function__(self, func, types, args, kwargs):

@staticmethod
def _reduce(method, *args, **kwargs):
from ._common import _is_scipy_sparse_obj

assert len(args) == 1

self = args[0]
if isinstance(self, ss.spmatrix):
if _is_scipy_sparse_obj(self):
self = type(self).from_scipy_sparse(self)

return self.reduce(method, **kwargs)
Expand Down
4 changes: 2 additions & 2 deletions sparse/_umath.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import numba

import numpy as np
import scipy.sparse

from ._utils import _zero_of_dtype, equivalent, isscalar

Expand Down Expand Up @@ -399,6 +398,7 @@ def __init__(self, func, *args, **kwargs):
**kwargs : dict
Extra arguments to pass to the function.
"""
from ._common import _is_scipy_sparse_obj
from ._compressed import GCXS
from ._coo import COO
from ._dok import DOK
Expand All @@ -422,7 +422,7 @@ def __init__(self, func, *args, **kwargs):
out_type = COO

for arg in args:
if isinstance(arg, scipy.sparse.spmatrix):
if _is_scipy_sparse_obj(arg):
processed_args.append(COO.from_scipy_sparse(arg))
elif isscalar(arg) or isinstance(arg, np.ndarray):
# Faster and more reliable to pass ()-shaped ndarrays as scalars.
Expand Down

0 comments on commit c57fed2

Please sign in to comment.