Repository: scipy/scipy

Issue:
'eliminate_zeros' does not do anything on a 'bsr_matrix' entirely of zeros
### Summary

If the dense representation of a `bsr_matrix` is all zeros, `eliminate_zeros` becomes a no-op rather than removing the explicit zeros.

### Reproduction

```py3
import numpy as np
import scipy.sparse as sp
np.random.seed(0xcafebabe)

m = sp.bsr_matrix(np.random.random((27, 27)), blocksize=(3,3))
print(m.nnz, len(m.data))

# eliminate some blocks
m.data[np.absolute(m.data) < 0.8] = 0
m.eliminate_zeros()
print(m.nnz, len(m.data))

# eliminate all blocks
m.data[...] = 0
m.eliminate_zeros()
print(m.nnz, len(m.data))
```

**Output:**
```
729 81
603 67
603 67
```

**Expected output:**

I expect that everything should be truncated from the last matrix, like it is with `csr_matrix`.
```
729 81
603 67
0 0
```

### Scipy/Numpy/Python version information:

```
>>> import sys, scipy, numpy; print(scipy.__version__, numpy.__version__, sys.version_info)
1.2.0 1.15.4 sys.version_info(major=3, minor=7, micro=2, releaselevel='final', serial=0)
```

### Other notes

This looks suspicious:

https://github.com/scipy/scipy/blob/95029ad1452495bf3acfc7daa973cfd12e298064/scipy/sparse/bsr.py#L552-L553


Discussion:
Yep, looks like a bug to me. Impressively, it wasn't discovered for the 11 years since it was added by 8529b5b89ac17930222cc86787b72a8e8eb5b6a2.

If you want to send a PR with a fix, I'll be happy to take a look. We should add a test case as well, to prevent regressions.
Heh.  I noticed it when I was testing out the effect of different truncation thresholds on matrix density. (*that's weird, the density goes down... and then back up!?*)

I'm curious about what that fast path is even intended to achieve, and am tempted to simply remove it.  (or I guess I could replace it with `if not self.data.size`, which seems more sensible...)

Resolving patch:
diff --git a/scipy/sparse/bsr.py b/scipy/sparse/bsr.py
--- a/scipy/sparse/bsr.py
+++ b/scipy/sparse/bsr.py
@@ -542,6 +542,10 @@ def transpose(self, axes=None, copy=False):
 
     def eliminate_zeros(self):
         """Remove zero elements in-place."""
+
+        if not self.nnz:
+            return  # nothing to do
+
         R,C = self.blocksize
         M,N = self.shape
 
@@ -549,9 +553,6 @@ def eliminate_zeros(self):
 
         nonzero_blocks = mask.nonzero()[0]
 
-        if len(nonzero_blocks) == 0:
-            return  # nothing to do
-
         self.data[:len(nonzero_blocks)] = self.data[nonzero_blocks]
 
         # modifies self.indptr and self.indices *in place*


Test patch:
diff --git a/scipy/sparse/tests/test_base.py b/scipy/sparse/tests/test_base.py
--- a/scipy/sparse/tests/test_base.py
+++ b/scipy/sparse/tests/test_base.py
@@ -4239,6 +4239,30 @@ def test_eliminate_zeros(self):
         assert_array_equal(asp.nnz, 3*4)
         assert_array_equal(asp.todense(),bsp.todense())
 
+    # github issue #9687
+    def test_eliminate_zeros_all_zero(self):
+        np.random.seed(0)
+        m = bsr_matrix(np.random.random((12, 12)), blocksize=(2, 3))
+
+        # eliminate some blocks, but not all
+        m.data[m.data <= 0.9] = 0
+        m.eliminate_zeros()
+        assert_equal(m.nnz, 66)
+        assert_array_equal(m.data.shape, (11, 2, 3))
+
+        # eliminate all remaining blocks
+        m.data[m.data <= 1.0] = 0
+        m.eliminate_zeros()
+        assert_equal(m.nnz, 0)
+        assert_array_equal(m.data.shape, (0, 2, 3))
+        assert_array_equal(m.todense(), np.zeros((12,12)))
+
+        # test fast path
+        m.eliminate_zeros()
+        assert_equal(m.nnz, 0)
+        assert_array_equal(m.data.shape, (0, 2, 3))
+        assert_array_equal(m.todense(), np.zeros((12,12)))
+
     def test_bsr_matvec(self):
         A = bsr_matrix(arange(2*3*4*5).reshape(2*4,3*5), blocksize=(4,5))
         x = arange(A.shape[1]).reshape(-1,1)