From 5f064d15fdcd9a4b4f4fd49e04d7fe5f0a76bdf8 Mon Sep 17 00:00:00 2001 From: Nils Werner Date: Wed, 10 Jan 2018 18:06:19 +0100 Subject: [PATCH 1/8] Implemented __array_interface__ --- sparse/core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sparse/core.py b/sparse/core.py index ecf59639..33cac726 100644 --- a/sparse/core.py +++ b/sparse/core.py @@ -488,6 +488,14 @@ def nbytes(self): """ return self.data.nbytes + self.coords.nbytes + @property + def __array_interface__(self): + return { + 'shape': self.shape, + 'data': self.todense(), + 'typestr': self.dtype.str, + } + def __len__(self): """ Get "length" of array, which is by definition the size of the first From 25885be36c16f9661b254f52f509c139ded142a5 Mon Sep 17 00:00:00 2001 From: Nils Werner Date: Wed, 10 Jan 2018 21:04:59 +0100 Subject: [PATCH 2/8] __array__ instead of __array_interface__ seems like the way to go --- sparse/core.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/sparse/core.py b/sparse/core.py index 33cac726..77459702 100644 --- a/sparse/core.py +++ b/sparse/core.py @@ -488,13 +488,30 @@ def nbytes(self): """ return self.data.nbytes + self.coords.nbytes - @property - def __array_interface__(self): - return { - 'shape': self.shape, - 'data': self.todense(), - 'typestr': self.dtype.str, - } + def __array__(self, dtype=None): + """ + Helper function to speed up :code:`np.array(x)` conversion + + Parameters + ---------- + dtype: type + Datatype of dense array. The sparse array is cast before densification. + + Returns + ------- + array_like + The dense array. + + See Also + -------- + numpy.ndarray.__array__ : Numpy equivalent function. + + """ + if dtype is not None: + tmp = self.astype(dtype) + else: + tmp = self + return tmp.todense() def __len__(self): """ From 8a1a4c230693351427451c6d71cb6b72f6a41352 Mon Sep 17 00:00:00 2001 From: Nils Werner Date: Thu, 11 Jan 2018 09:23:53 +0100 Subject: [PATCH 3/8] Simplified __array__ if a bit --- sparse/core.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sparse/core.py b/sparse/core.py index 77459702..19da8914 100644 --- a/sparse/core.py +++ b/sparse/core.py @@ -508,10 +508,9 @@ def __array__(self, dtype=None): """ if dtype is not None: - tmp = self.astype(dtype) + return self.astype(dtype).todense() else: - tmp = self - return tmp.todense() + return self.todense() def __len__(self): """ From 916e0168b610d791896e2ebd92d910981c4db357 Mon Sep 17 00:00:00 2001 From: Nils Werner Date: Thu, 11 Jan 2018 09:24:16 +0100 Subject: [PATCH 4/8] Test comparing np.array(x) and x.todense() --- sparse/tests/test_core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sparse/tests/test_core.py b/sparse/tests/test_core.py index 94800a26..9c751aec 100644 --- a/sparse/tests/test_core.py +++ b/sparse/tests/test_core.py @@ -887,3 +887,10 @@ def test_scalar_shape_construction(): def test_len(): s = sparse.random((20, 30, 40)) assert len(s) == 20 + + +def test_numpy_todense(): + # make sure that .astype(int) is never all zeros + s = sparse.random((20, 30, 40)) * 100 + assert np.allclose(np.array(s, dtype=int), s.astype(int).todense()) + assert np.allclose(np.array(s), s.todense()) From 120bf82e3701ee59082c18f3535e8c9747a8eaa5 Mon Sep 17 00:00:00 2001 From: Nils Werner Date: Thu, 11 Jan 2018 10:36:24 +0100 Subject: [PATCH 5/8] Use maybe_densify in __array__ --- sparse/core.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sparse/core.py b/sparse/core.py index 19da8914..7ce0cad8 100644 --- a/sparse/core.py +++ b/sparse/core.py @@ -508,9 +508,9 @@ def __array__(self, dtype=None): """ if dtype is not None: - return self.astype(dtype).todense() + return self.astype(dtype).maybe_densify() else: - return self.todense() + return self.maybe_densify() def __len__(self): """ @@ -2406,7 +2406,8 @@ def maybe_densify(self, allowed_nnz=1000, allowed_fraction=0.25): return self.todense() else: raise NotImplementedError("Operation would require converting " - "large sparse array to dense") + "large sparse array to dense. Use " + ".todense() to force densification.") def tensordot(a, b, axes=2): From f615b795fa298a0ee58124ff5ad3ad23802537a5 Mon Sep 17 00:00:00 2001 From: Nils Werner Date: Mon, 15 Jan 2018 11:24:02 +0100 Subject: [PATCH 6/8] __array__ returns NotImplemented --- sparse/core.py | 17 ++++++++--------- sparse/tests/test_core.py | 10 ++++++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/sparse/core.py b/sparse/core.py index 7ce0cad8..79a44316 100644 --- a/sparse/core.py +++ b/sparse/core.py @@ -490,27 +490,26 @@ def nbytes(self): def __array__(self, dtype=None): """ - Helper function to speed up :code:`np.array(x)` conversion + Helper function that gets called during :code:`np.array(x)` conversion. + We deliberately return :code:`NotImplemented` to prevent accidental + densification. Parameters ---------- dtype: type - Datatype of dense array. The sparse array is cast before densification. + Datatype requested by :code:`np.array(x)`. Has no effect on + output. Returns ------- - array_like - The dense array. - + NotImplemented + We do not implement this function, so this is what we return. See Also -------- numpy.ndarray.__array__ : Numpy equivalent function. """ - if dtype is not None: - return self.astype(dtype).maybe_densify() - else: - return self.maybe_densify() + return NotImplemented def __len__(self): """ diff --git a/sparse/tests/test_core.py b/sparse/tests/test_core.py index 9c751aec..b9e7513b 100644 --- a/sparse/tests/test_core.py +++ b/sparse/tests/test_core.py @@ -892,5 +892,11 @@ def test_len(): def test_numpy_todense(): # make sure that .astype(int) is never all zeros s = sparse.random((20, 30, 40)) * 100 - assert np.allclose(np.array(s, dtype=int), s.astype(int).todense()) - assert np.allclose(np.array(s), s.todense()) + with pytest.raises(ValueError): + assert np.array(s) + + with pytest.raises(ValueError): + assert np.array(s, dtype=int) + + with pytest.raises(ValueError): + assert np.allclose(s, s.todense()) From 7a9b597ad68c198d2ca7d38c9ee0c476dfd4e9c8 Mon Sep 17 00:00:00 2001 From: Nils Werner Date: Mon, 15 Jan 2018 16:30:09 +0100 Subject: [PATCH 7/8] Fix docstring --- sparse/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sparse/core.py b/sparse/core.py index 79a44316..d67a7e8d 100644 --- a/sparse/core.py +++ b/sparse/core.py @@ -2397,7 +2397,8 @@ def maybe_densify(self, allowed_nnz=1000, allowed_fraction=0.25): >>> s.maybe_densify(allowed_nnz=5, allowed_fraction=0.25) Traceback (most recent call last): ... - NotImplementedError: Operation would require converting large sparse array to dense + NotImplementedError: Operation would require converting large sparse \ +array to dense. Use .todense() to force densification. """ elements = np.prod(self.shape) From 13f7841060cbbacb3a2ab8ee17099099c69076c5 Mon Sep 17 00:00:00 2001 From: Nils Werner Date: Mon, 15 Jan 2018 16:56:16 +0100 Subject: [PATCH 8/8] Docs --- docs/generated/sparse.COO.__array__.rst | 6 ++++++ docs/generated/sparse.COO.rst | 1 + sparse/core.py | 4 +++- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 docs/generated/sparse.COO.__array__.rst diff --git a/docs/generated/sparse.COO.__array__.rst b/docs/generated/sparse.COO.__array__.rst new file mode 100644 index 00000000..d2ff7ba3 --- /dev/null +++ b/docs/generated/sparse.COO.__array__.rst @@ -0,0 +1,6 @@ +COO\.\_\_array\_\_ +================== + +.. currentmodule:: sparse + +.. automethod:: COO.__array__ diff --git a/docs/generated/sparse.COO.rst b/docs/generated/sparse.COO.rst index 99fcb719..21826cfb 100644 --- a/docs/generated/sparse.COO.rst +++ b/docs/generated/sparse.COO.rst @@ -68,6 +68,7 @@ COO COO.to_scipy_sparse COO.tocsc COO.tocsr + COO.__array__ .. rubric:: :doc:`Other operations <../user_manual/operations/other>` .. autosummary:: diff --git a/sparse/core.py b/sparse/core.py index d67a7e8d..47a98fdf 100644 --- a/sparse/core.py +++ b/sparse/core.py @@ -503,7 +503,9 @@ def __array__(self, dtype=None): Returns ------- NotImplemented - We do not implement this function, so this is what we return. + We do not implement this function, so we return + :obj:`NotImplemented`. + See Also -------- numpy.ndarray.__array__ : Numpy equivalent function.