Repository: pydata/sparse

Issue:
Add resize function and method
Would be nice to have an analogous function and method to NumPy's `resize`.

ref: https://docs.scipy.org/doc/numpy/reference/generated/numpy.resize.html
ref: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.resize.html

Discussion:
hameerabbasi: I hope you don't mind I edited your comment with magic words that indicate a fix.

Would you mind adding tests? Just a small one for reshape and resize each, you can find them in https://github.com/pydata/sparse/blob/948cee79dbfba9c69ea52f04806e0f9dc5e05032/sparse/tests/test_coo.py

If you do `pip install -e .[all]` (assuming you're in the repo dir), and then run `pytest`, it'll run all the tests for you.
daletovar: I'm having a tough time figuring it out. I might not get a chance until tomorrow to try to add that. 
hameerabbasi: Of course! If you’re having trouble figuring it out, don’t hesitate to ask more questions. I’m here to help.
hameerabbasi: @daletovar If you're okay with it can I finish up this PR?
daletovar: @hameerabbasi I'm so sorry for getting to this so late. I have a numba-related issue that's causing tests to fail on my laptop. I can finish it up at work on Monday. But I wouldn't mind if you finished it up either. I certainly don't mean to drag this out.
hameerabbasi: No pressure! You can finish it up if you like. :-)
daletovar: Thanks! 
hameerabbasi: > I'm so sorry for getting to this so late. I have a numba-related issue that's causing tests to fail on my laptop. I can finish it up at work on Monday. But I wouldn't mind if you finished it up either. I certainly don't mean to drag this out.

Could you tell me your exact version and environment? I've seen this issue reported but have been unable to reproduce it myself...
daletovar: I'm having a tough time running the tests. I added a resize test but because I can't seem to run them I don't have a good sense of what's happening. Thanks so much for your patience on all of this. Would you mind having a look and running it? ^^The issue with my laptop had to do with not being able to properly update llvmlite.
codecov[bot]: # [Codecov](https://codecov.io/gh/pydata/sparse/pull/251?src=pr&el=h1) Report
> Merging [#251](https://codecov.io/gh/pydata/sparse/pull/251?src=pr&el=desc) into [master](https://codecov.io/gh/pydata/sparse/commit/948cee79dbfba9c69ea52f04806e0f9dc5e05032?src=pr&el=desc) will **decrease** coverage by `0.27%`.
> The diff coverage is `78.26%`.


```diff
@@            Coverage Diff             @@
##           master     #251      +/-   ##
==========================================
- Coverage    97.6%   97.32%   -0.28%     
==========================================
  Files          10       10              
  Lines        1585     1608      +23     
==========================================
+ Hits         1547     1565      +18     
- Misses         38       43       +5
```



hameerabbasi: Okay, there were a few issues here:

1. The `test_caching` test was wrong... That's legacy code. :) I fixed it up.
2. Your code wasn't properly formatted, it was tripping the linter.
3. There were garbage values in the dense array but not the sparse one, they had to be gotten rid of.
4. If the shape being resized to was smaller, we had to discard some values.

Patch:
diff --git a/.gitignore b/.gitignore
index 2c48ede..c498256 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,6 +47,7 @@ coverage.xml
 *,cover
 .pytest_cache/
 test_results/
+junit/
 
 # Airspeed velocity
 .asv/
diff --git a/docs/generated/sparse.COO.resize.rst b/docs/generated/sparse.COO.resize.rst
new file mode 100644
index 0000000..7c0155d
--- /dev/null
+++ b/docs/generated/sparse.COO.resize.rst
@@ -0,0 +1,6 @@
+COO.resize
+==========
+
+.. currentmodule:: sparse
+
+.. automethod:: COO.resize
\ No newline at end of file
diff --git a/docs/generated/sparse.COO.rst b/docs/generated/sparse.COO.rst
index 689d922..e33fc07 100644
--- a/docs/generated/sparse.COO.rst
+++ b/docs/generated/sparse.COO.rst
@@ -73,6 +73,7 @@ COO
       COO.copy
       COO.dot
       COO.reshape
+      COO.resize
       COO.transpose
       COO.nonzero
 
diff --git a/sparse/coo/core.py b/sparse/coo/core.py
index f7cdbe1..730e04e 100644
--- a/sparse/coo/core.py
+++ b/sparse/coo/core.py
@@ -1705,6 +1705,9 @@ class COO(SparseArray, NDArrayOperatorsMixin):  # lgtm [py/missing-equals]
         if self.shape == shape:
             return self
 
+        if self.size != reduce(operator.mul, shape, 1):
+            raise ValueError('cannot reshape array of size {} into shape {}'.format(self.size, shape))
+
         if self._cache is not None:
             for sh, value in self._cache['reshape']:
                 if sh == shape:
@@ -1728,6 +1731,49 @@ class COO(SparseArray, NDArrayOperatorsMixin):  # lgtm [py/missing-equals]
             self._cache['reshape'].append((shape, result))
         return result
 
+    def resize(self, *args, refcheck=True):
+        """
+        This method changes the shape and size of an array in-place.
+
+        Parameters
+        ----------
+        args : tuple, or series of integers
+            The desired shape of the output array.
+
+        See Also
+        --------
+        numpy.ndarray.resize : The equivalent Numpy function.
+
+        """
+        if len(args) == 1 and isinstance(args[0], tuple):
+            shape = args[0]
+        elif all(isinstance(arg, int) for arg in args):
+            shape = tuple(args)
+        else:
+            raise ValueError('Invalid input')
+
+        if any(d < 0 for d in shape):
+            raise ValueError('negative dimensions not allowed')
+
+        new_size = reduce(operator.mul, shape, 1)
+
+        # TODO: this self.size enforces a 2**64 limit to array size
+        linear_loc = self.linear_loc()
+        end_idx = np.searchsorted(linear_loc, new_size, side='left')
+        linear_loc = linear_loc[:end_idx]
+
+        coords = np.empty((len(shape), len(linear_loc)), dtype=np.intp)
+        strides = 1
+        for i, d in enumerate(shape[::-1]):
+            coords[-(i + 1), :] = (linear_loc // strides) % d
+            strides *= d
+
+        self.shape = shape
+        self.coords = coords
+
+        if len(self.data) != len(linear_loc):
+            self.data = self.data[:end_idx].copy()
+
     def to_scipy_sparse(self):
         """
         Converts this :obj:`COO` object into a :obj:`scipy.sparse.coo_matrix`.


Test patch:
diff --git a/sparse/tests/test_coo.py b/sparse/tests/test_coo.py
index 0f53841..c9992d4 100644
--- a/sparse/tests/test_coo.py
+++ b/sparse/tests/test_coo.py
@@ -212,6 +212,30 @@ def test_transpose_error(axis):
         x.transpose(axis)
 
 
+@pytest.mark.parametrize('a,b', [
+    [(3, 4), (5, 5)],
+    [(12,), (3, 4)],
+    [(12,), (3, 6)],
+    [(5, 5, 5), (6, 6, 6)],
+    [(3, 4), (9, 4)],
+    [(5,), (4,)],
+    [(2, 3, 4, 5), (2, 3, 4, 5, 6)],
+    [(100,), (5, 5)],
+    [(2, 3, 4, 5), (20, 6)],
+    [(), ()],
+])
+def test_resize(a, b):
+    s = sparse.random(a, density=0.5)
+    orig_size = s.size
+    x = s.todense()
+    x = np.resize(x, b)
+    s.resize(b)
+    temp = x.reshape(x.size)
+    temp[orig_size:] = s.fill_value
+    assert isinstance(s, sparse.SparseArray)
+    assert_eq(x, s)
+
+
 @pytest.mark.parametrize('a,b', [
     [(3, 4), (3, 4)],
     [(12,), (3, 4)],
@@ -1534,16 +1558,16 @@ def test_add_many_sparse_arrays():
 
 
 def test_caching():
-    x = COO({(10, 10, 10): 1})
+    x = COO({(9, 9, 9): 1})
     assert x[:].reshape((100, 10)).transpose().tocsr() is not x[:].reshape((100, 10)).transpose().tocsr()
 
-    x = COO({(10, 10, 10): 1}, cache=True)
+    x = COO({(9, 9, 9): 1}, cache=True)
     assert x[:].reshape((100, 10)).transpose().tocsr() is x[:].reshape((100, 10)).transpose().tocsr()
 
     x = COO({(1, 1, 1, 1, 1, 1, 1, 2): 1}, cache=True)
 
     for i in range(x.ndim):
-        x.reshape((1,) * i + (2,) + (1,) * (x.ndim - i - 1))
+        x.reshape(x.size)
 
     assert len(x._cache['reshape']) < 5