Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion graphblas/core/ss/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,5 +210,5 @@ def __repr__(self):
+ "})"
)

def _ipython_key_completions_(self): # pragma: no cover (ipython)
def _ipython_key_completions_(self):
return list(self)
8 changes: 7 additions & 1 deletion graphblas/io/_scipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ def from_scipy_sparse(A, *, dup_op=None, name=None):
A : scipy.sparse
Scipy sparse array or matrix
dup_op : BinaryOp, optional
Aggregation function for formats that allow duplicate entries (e.g. coo)
Aggregation function for duplicate entries, which coo allows (as do csr and csc
not in canonical format). Duplicates without it raise ValueError.
name : str, optional
Name of resulting Matrix

Expand All @@ -28,6 +29,11 @@ def from_scipy_sparse(A, *, dup_op=None, name=None):
dtype = lookup_dtype(A.dtype)
if A.nnz == 0:
return Matrix(dtype, nrows=nrows, ncols=ncols, name=name)
if A.format in {"csr", "csc"} and not A.has_canonical_format:
# An index may repeat within a row (column), and an import takes the indices
# unchecked: a repeat would make an invalid Matrix. Build from coo instead, where
# dup_op combines repeats.
A = A.tocoo()
if backend == "suitesparse" and A.format in {"csr", "csc"}:
data = A.data
is_iso = (data[[0]] == data).all()
Expand Down
13 changes: 11 additions & 2 deletions graphblas/io/_sparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ def from_pydata_sparse(s, *, dup_op=None, name=None):
s : sparse
PyData sparse array or matrix (see https://sparse.pydata.org)
dup_op : BinaryOp, optional
Aggregation function for formats that allow duplicate entries (e.g. coo)
Combines the values of an index repeated within a row (or column) of a
two-dimensional gcxs array. By default they are summed, as pydata's todense does.
(A coo array sums repeated coordinates when it is built.)
name : str, optional
Name of resulting Matrix

Expand Down Expand Up @@ -45,7 +47,14 @@ def from_pydata_sparse(s, *, dup_op=None, name=None):
)
# handle two-dimensional arrays
if isinstance(s, sparse.GCXS):
return from_scipy_sparse(s.to_scipy_sparse(), dup_op=dup_op, name=name)
A = s.to_scipy_sparse()
if dup_op is None and not A.has_canonical_format:
# pydata lets an index repeat within a row (column) and means the sum. scipy
# sums in any dtype it holds, which binary.plus may not take (a UDT). Copy
# first: A shares its arrays with s.
A = A.copy()
A.sum_duplicates()
return from_scipy_sparse(A, dup_op=dup_op, name=name)
if isinstance(s, (sparse.DOK, sparse.COO)):
_s = s.asformat("coo")
return Matrix.from_coo(
Expand Down
80 changes: 80 additions & 0 deletions graphblas/tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,31 @@ def test_from_scipy_sparse_duplicates():
assert a2.isequal(expected)


@pytest.mark.skipif("not ss")
@pytest.mark.parametrize("format", ["csr", "csc"])
def test_from_scipy_sparse_compressed_duplicates(format):
# Outside canonical format an index may repeat within a row (column); importing the
# arrays as they are made an invalid Matrix (nvals 4, two entries at one position).
cls, shape = (ss.csr_array, (2, 3)) if format == "csr" else (ss.csc_array, (3, 2))
a = cls(([1, 2, 3, 4], [1, 1, 0, 2], [0, 3, 4]), shape=shape)
assert not a.has_canonical_format
with pytest.raises(ValueError, match="Duplicate indices found"):
gb.io.from_scipy_sparse(a)
A = gb.io.from_scipy_sparse(a, dup_op=gb.binary.plus)
assert A.nvals == 3
np.testing.assert_array_equal(A.to_dense(fill_value=0), a.toarray())
A = gb.io.from_scipy_sparse(a, dup_op=gb.binary.min)
expected = np.array([[3, 1, 0], [0, 0, 4]])
if format == "csc":
expected = expected.T
np.testing.assert_array_equal(A.to_dense(fill_value=0), expected)
# Unsorted but distinct indices need no dup_op
b = cls(([1, 2, 3], [2, 1, 0], [0, 3, 3]), shape=shape)
assert not b.has_canonical_format
B = gb.io.from_scipy_sparse(b)
np.testing.assert_array_equal(B.to_dense(fill_value=0), b.toarray())


@pytest.mark.skipif("not ss")
@pytest.mark.parametrize("engine", ["auto", "scipy", "fast_matrix_market"])
def test_matrix_market_sparse_duplicates(engine):
Expand Down Expand Up @@ -388,6 +413,24 @@ def test_matrix_market_bad_engine():
gb.io.mmread(mm_out, engine="bad_engine")


@pytest.mark.skipif("not ss")
@pytest.mark.skipif("fmm is not None")
def test_matrix_market_fmm_engine_unavailable():
# Naming the deprecated engine warns before anything else, so the caller hears
# about the deprecation even when the install that would satisfy it is missing.
A = gb.Matrix.from_coo([0, 0, 3, 5], [1, 4, 0, 2], [1, 0, 2, -1], nrows=7, ncols=6)
with (
pytest.warns(DeprecationWarning, match="fast_matrix_market is no longer maintained"),
pytest.raises(ImportError, match="required to write Matrix Market files"),
):
gb.io.mmwrite(BytesIO(), A, engine="fmm")
with (
pytest.warns(DeprecationWarning, match="fast_matrix_market is no longer maintained"),
pytest.raises(ImportError, match="required to read Matrix Market files"),
):
gb.io.mmread(BytesIO(), engine="fast_matrix_market")


@pytest.mark.skipif("not ss")
def test_scipy_sparse():
a = np.arange(12).reshape(3, 4)
Expand Down Expand Up @@ -531,3 +574,40 @@ def test_matrix_to_from_pydata_sparse():
gb.io.to_pydata_sparse(w, format="badformat")
with pytest.raises(TypeError, match="sparse.pydata"):
gb.io.from_pydata_sparse(w)


@pytest.mark.skipif("not sparse")
@pytest.mark.slow
@pytest.mark.parametrize("axis", [0, 1])
@pytest.mark.parametrize("dtype", ["int64", "float64", "bool"])
def test_from_pydata_sparse_gcxs_duplicates(axis, dtype):
# pydata checks neither the order nor the distinctness of the indices in a GCXS row
# (column), and todense sums a repeat. Importing the arrays as they are made an invalid
# Matrix (nvals 4, two entries at one position).
data = np.array([1, 2, 3, 4], dtype=dtype)
indptr = np.array([0, 3, 4])
shape = (2, 3) if axis == 0 else (3, 2)
s = sparse.GCXS((data, np.array([1, 1, 0, 2]), indptr), shape=shape, compressed_axes=[axis])
A = gb.io.from_pydata_sparse(s)
assert A.nvals == 3
assert A.dtype == dtype
np.testing.assert_array_equal(A.to_dense(fill_value=0), s.todense())
A = gb.io.from_pydata_sparse(s, dup_op=gb.binary.min)
expected = np.array([[3, 1, 0], [0, 0, 4]], dtype=dtype)
if axis == 1:
expected = expected.T
np.testing.assert_array_equal(A.to_dense(fill_value=0), expected)
# Unsorted but distinct indices need no combining
s = sparse.GCXS((data, np.array([2, 1, 0, 2]), indptr), shape=shape, compressed_axes=[axis])
A = gb.io.from_pydata_sparse(s)
assert A.nvals == 4
np.testing.assert_array_equal(A.to_dense(fill_value=0), s.todense())
# The default sum needs no binary op for the dtype (complex is a UDT on vanilla),
# and it leaves the array alone
cdata = data + 1j
s = sparse.GCXS((cdata, np.array([1, 1, 0, 2]), indptr), shape=shape, compressed_axes=[axis])
indices = s.indices.copy()
A = gb.io.from_pydata_sparse(s)
assert A.nvals == 3
np.testing.assert_array_equal(A.to_dense(fill_value=0), s.todense())
np.testing.assert_array_equal(s.indices, indices)
16 changes: 16 additions & 0 deletions graphblas/tests/test_ss_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,22 @@ def test_global_config(monkeypatch):
assert "format" in repr(config)


def test_global_config_key_completions():
# IPython offers these for `config[<tab>]`, so they must be the keys that
# actually resolve, not the attributes of the mapping object.
config = gb.ss.config
completions = config._ipython_key_completions_()
assert set(completions) == set(config._options)
for key in completions:
config[key]
# About aliases the same hook onto its own __iter__ (gb.ss.about[<tab>])
about = gb.ss.about
completions = about._ipython_key_completions_()
assert set(completions) == set(about)
for key in completions:
about[key]


@pytest.mark.skipif("gb.core.ss._IS_SSGB7")
def test_context():
context = gb.ss.Context()
Expand Down
Loading