Repository: cupy/cupy

Issue:
sparse min/max for negative `axis`
* Conditions (you can just paste the output of `python -c 'import cupy; cupy.show_config()'`)
```
CuPy Version          : 8.0.0b3
CUDA Root             : /usr/local/cuda
CUDA Build Version    : 9000
CUDA Driver Version   : 9000
CUDA Runtime Version  : 9000
cuBLAS Version        : 9000
cuFFT Version         : 9000
cuRAND Version        : 9000
cuSOLVER Version      : (9, 0, 0)
cuSPARSE Version      : 9000
NVRTC Version         : (9, 0)
Thrust Version        : 100900
cuDNN Build Version   : 7300
cuDNN Version         : 7300
NCCL Build Version    : 2005
NCCL Runtime Version  : (unknown)
CUB Version           : None
cuTENSOR Version      : None
```

* Code to reproduce

```
cupyx.scipy.sparse.csc_matrix(cupy.arange(9).astype('f').reshape(3, 3) - 4).min(axis=-1)
```

* Error messages, stack traces, or logs

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/kataoka/cupy/cupyx/scipy/sparse/data.py", line 284, in min
    return self._min_or_max(axis, out, cupy.min, sum_duplicates, nonzero)
  File "/home/kataoka/cupy/cupyx/scipy/sparse/data.py", line 163, in _min_or_max
    raise ValueError("axis out of range")
ValueError: axis out of range
```
while scipy supports it:
```
>>> scipy.sparse.csc_matrix(np.arange(9).astype('f').reshape(3, 3) - 4).min(axis=-1)
<3x1 sparse matrix of type '<class 'numpy.float32'>'
	with 3 stored elements in COOrdinate format>
```



Discussion:
There seems a bug around here.

https://github.com/cupy/cupy/blob/e9f8efcf36e4018cd92cf20c0bca666238d98203/cupyx/scipy/sparse/data.py#L159-L163

Resolving patch:
diff --git a/cupyx/scipy/sparse/data.py b/cupyx/scipy/sparse/data.py
--- a/cupyx/scipy/sparse/data.py
+++ b/cupyx/scipy/sparse/data.py
@@ -190,10 +190,10 @@ def _min_or_max(self, axis, out, min_or_max, explicit):
                     assert False
             return m
 
-        if axis == 0 or axis == 1:
-            return self._min_or_max_axis(axis, min_or_max, explicit)
-        else:
-            raise ValueError("axis out of range")
+        if axis < 0:
+            axis += 2
+
+        return self._min_or_max_axis(axis, min_or_max, explicit)
 
     def _arg_min_or_max_axis(self, axis, op):
         if self.shape[axis] == 0:
@@ -248,6 +248,9 @@ def _arg_min_or_max(self, axis, out, op, compare):
                         else:
                             return zero_ind
 
+        if axis < 0:
+            axis += 2
+
         return self._arg_min_or_max_axis(axis, op)
 
     def max(self, axis=None, out=None, *, explicit=False):


Test patch:
diff --git a/tests/cupyx_tests/scipy_tests/sparse_tests/test_csc.py b/tests/cupyx_tests/scipy_tests/sparse_tests/test_csc.py
--- a/tests/cupyx_tests/scipy_tests/sparse_tests/test_csc.py
+++ b/tests/cupyx_tests/scipy_tests/sparse_tests/test_csc.py
@@ -1021,9 +1021,8 @@ def test_getnnz(self, xp, sp):
 
 
 @testing.parameterize(*testing.product({
-    # TODO(takagi) Test dtypes
-    # TODO(takagi) Test negative axis
-    'axis': [None, 0, 1],
+    # TODO(takagi): Test dtypes
+    'axis': [None, -2, -1, 0, 1],
     'dense': [False, True],  # means a sparse matrix but all elements filled
 }))
 @testing.with_requires('scipy>=0.19.0')
@@ -1059,6 +1058,9 @@ def _make_data_min_explicit(self, xp, sp, axis):
                 if numpy.isinf(dm_data).all():
                     dm_data[0, 0] = 0
             else:
+                if axis < 0:
+                    axis += 2
+
                 # If all elements in a row/column are set to infinity, we make
                 # it have at least a zero so spmatrix.min(axis=axis) returns
                 # zero for the row/column.
diff --git a/tests/cupyx_tests/scipy_tests/sparse_tests/test_csr.py b/tests/cupyx_tests/scipy_tests/sparse_tests/test_csr.py
--- a/tests/cupyx_tests/scipy_tests/sparse_tests/test_csr.py
+++ b/tests/cupyx_tests/scipy_tests/sparse_tests/test_csr.py
@@ -1224,9 +1224,8 @@ def test_getnnz(self, xp, sp):
 
 
 @testing.parameterize(*testing.product({
-    # TODO(takagi) Test dtypes
-    # TODO(takagi) Test negative axis
-    'axis': [None, 0, 1],
+    # TODO(takagi): Test dtypes
+    'axis': [None, -2, -1, 0, 1],
     'dense': [False, True],  # means a sparse matrix but all elements filled
 }))
 @testing.with_requires('scipy>=0.19.0')
@@ -1261,6 +1260,9 @@ def _make_data_min_explicit(self, xp, sp, axis):
                 if numpy.isinf(dm_data).all():
                     dm_data[0, 0] = 0
             else:
+                if axis < 0:
+                    axis += 2
+
                 # If all elements in a row/column are set to infinity, we make
                 # it have at least a zero so spmatrix.min(axis=axis) returns
                 # zero for the row/column.