Repository: scipy/scipy

Issue:
scipy.sparse.linalg.use_solver(assumeSortedIndices=True) doesn't actually do anything
The documentation for `scipy.sparse.linalg.use_solver` [says](https://github.com/scipy/scipy/blob/28e2ee4d62ac07112c79f68818c435c197c7bd0e/scipy/sparse/linalg/dsolve/linsolve.py#L46):

> Umfpack requires a CSR/CSC matrix to have sorted column/row indices. If
    sure that the matrix fulfills this, pass ``assumeSortedIndices=True``
    to gain some speed.

But the flag is [not used](https://github.com/scipy/scipy/blob/28e2ee4d62ac07112c79f68818c435c197c7bd0e/scipy/sparse/linalg/dsolve/linsolve.py#L53) anywhere in the function. It seems it was removed by [this commit](https://github.com/scipy/scipy/commit/a5d38fc4d0268c32cdc56853d0ef1c4f557dc20c). 


Discussion:

Resolving patch:
diff --git a/scipy/sparse/linalg/dsolve/linsolve.py b/scipy/sparse/linalg/dsolve/linsolve.py
--- a/scipy/sparse/linalg/dsolve/linsolve.py
+++ b/scipy/sparse/linalg/dsolve/linsolve.py
@@ -35,6 +35,10 @@ def use_solver(**kwargs):
     useUmfpack : bool, optional
         Use UMFPACK over SuperLU. Has effect only if scikits.umfpack is
         installed. Default: True
+    assumeSortedIndices : bool, optional
+        Allow UMFPACK to skip the step of sorting indices for a CSR/CSC matrix.
+        Has effect only if useUmfpack is True and scikits.umfpack is installed.
+        Default: False
 
     Notes
     -----
@@ -50,8 +54,8 @@ def use_solver(**kwargs):
     """
     if 'useUmfpack' in kwargs:
         globals()['useUmfpack'] = kwargs['useUmfpack']
-
-    #TODO: pass other options to scikit
+    if useUmfpack and 'assumeSortedIndices' in kwargs:
+        umfpack.configure(assumeSortedIndices=kwargs['assumeSortedIndices'])
 
 def _get_umf_family(A):
     """Get umfpack family string given the sparse matrix dtype."""
@@ -402,7 +406,6 @@ def factorized(A):
             A = csc_matrix(A)
             warn('splu requires CSC matrix format', SparseEfficiencyWarning)
 
-        A.sort_indices()
         A = A.asfptype()  # upcast to a floating point format
 
         if A.dtype.char not in 'dD':


Test patch:
diff --git a/scipy/sparse/linalg/dsolve/tests/test_linsolve.py b/scipy/sparse/linalg/dsolve/tests/test_linsolve.py
--- a/scipy/sparse/linalg/dsolve/tests/test_linsolve.py
+++ b/scipy/sparse/linalg/dsolve/tests/test_linsolve.py
@@ -5,9 +5,12 @@
 import numpy as np
 from numpy import array, finfo, arange, eye, all, unique, ones, dot, matrix
 import numpy.random as random
-from numpy.testing import (run_module_suite,
+from numpy.testing import (run_module_suite, dec,
         assert_array_almost_equal, assert_raises, assert_almost_equal,
-        assert_equal, assert_array_equal, assert_, assert_allclose)
+        assert_equal, assert_array_equal, assert_, assert_allclose,
+        assert_warns)
+
+from scipy._lib._numpy_compat import assert_raises_regex
 
 import scipy.linalg
 from scipy.linalg import norm, inv
@@ -15,7 +18,7 @@
         csr_matrix, identity, isspmatrix, dok_matrix, lil_matrix, bsr_matrix)
 from scipy.sparse.linalg import SuperLU
 from scipy.sparse.linalg.dsolve import (spsolve, use_solver, splu, spilu,
-        MatrixRankWarning, _superlu, spsolve_triangular)
+        MatrixRankWarning, _superlu, spsolve_triangular, factorized)
 
 from scipy._lib._numpy_compat import suppress_warnings
 
@@ -23,9 +26,13 @@
 sup_sparse_efficiency = suppress_warnings()
 sup_sparse_efficiency.filter(SparseEfficiencyWarning)
 
-# TODO add more comprehensive tests
-use_solver(useUmfpack=False)
-
+# scikits.umfpack is not a SciPy dependency but it is optionally used in
+# dsolve, so check whether it's available
+try:
+    import scikits.umfpack as umfpack
+    has_umfpack = True
+except ImportError:
+    has_umfpack = False
 
 def toarray(a):
     if isspmatrix(a):
@@ -34,13 +41,131 @@ def toarray(a):
         return a
 
 
+class TestFactorized(object):
+    def setUp(self):
+        n = 5
+        d = arange(n) + 1
+        self.n = n
+        self.A = spdiags((d, 2*d, d[::-1]), (-3, 0, 5), n, n).tocsc()
+        random.seed(1234)
+
+    def _check_singular(self):
+        A = csc_matrix((5,5), dtype='d')
+        b = ones(5)
+        assert_array_almost_equal(0. * b, factorized(A)(b))
+
+    def _check_non_singular(self):
+        # Make a diagonal dominant, to make sure it is not singular
+        n = 5
+        a = csc_matrix(random.rand(n, n))
+        b = ones(n)
+
+        expected = splu(a).solve(b)
+        assert_array_almost_equal(factorized(a)(b), expected)
+
+    def test_singular_without_umfpack(self):
+        use_solver(useUmfpack=False)
+        assert_raises_regex(RuntimeError, "Factor is exactly singular", self._check_singular)
+
+    @dec.skipif(not has_umfpack)
+    def test_singular_with_umfpack(self):
+        use_solver(useUmfpack=True)
+        with suppress_warnings() as sup:
+            sup.filter(RuntimeWarning, "divide by zero encountered in double_scalars")
+            assert_warns(umfpack.UmfpackWarning, self._check_singular)
+
+    def test_non_singular_without_umfpack(self):
+        use_solver(useUmfpack=False)
+        self._check_non_singular()
+
+    @dec.skipif(not has_umfpack)
+    def test_non_singular_with_umfpack(self):
+        use_solver(useUmfpack=True)
+        self._check_non_singular()
+
+    def test_cannot_factorize_nonsquare_matrix_without_umfpack(self):
+        use_solver(useUmfpack=False)
+        assert_raises_regex(ValueError, "can only factor square matrices",
+                            factorized, self.A[:,:4])
+
+    @dec.skipif(not has_umfpack)
+    def test_factorizes_nonsquare_matrix_with_umfpack(self):
+        use_solver(useUmfpack=True)
+        # does not raise
+        factorized(self.A[:,:4])
+
+    def test_call_with_incorrectly_sized_matrix_without_umfpack(self):
+        use_solver(useUmfpack=False)
+        solve = factorized(self.A)
+        b = random.rand(4)
+        B = random.rand(4, 3)
+        BB = random.rand(self.n, 3, 9)
+
+        assert_raises_regex(ValueError, "is of incompatible size", solve, b)
+        assert_raises_regex(ValueError, "is of incompatible size", solve, B)
+        assert_raises_regex(ValueError, "object too deep for desired array", solve, BB)
+
+    @dec.skipif(not has_umfpack)
+    def test_call_with_incorrectly_sized_matrix_with_umfpack(self):
+        use_solver(useUmfpack=True)
+        solve = factorized(self.A)
+        b = random.rand(4)
+        B = random.rand(4, 3)
+        BB = random.rand(self.n, 3, 9)
+
+        # does not raise
+        solve(b)
+        assert_raises_regex(ValueError, "object too deep for desired array", solve, B)
+        assert_raises_regex(ValueError, "object too deep for desired array", solve, BB)
+
+    def test_call_with_cast_to_complex_without_umfpack(self):
+        use_solver(useUmfpack=False)
+        solve = factorized(self.A)
+        b = random.rand(4)
+        for t in [np.complex64, np.complex128]:
+            assert_raises_regex(TypeError, "Cannot cast array data", solve,
+                                b.astype(t))
+
+    @dec.skipif(not has_umfpack)
+    def test_call_with_cast_to_complex_with_umfpack(self):
+        use_solver(useUmfpack=True)
+        solve = factorized(self.A)
+        b = random.rand(4)
+        for t in [np.complex64, np.complex128]:
+            assert_warns(np.ComplexWarning, solve, b.astype(t))
+
+    @dec.skipif(not has_umfpack)
+    def test_assume_sorted_indices_flag(self):
+        # a sparse matrix with unsorted indices
+        unsorted_inds = np.array([2, 0, 1, 0])
+        data = np.array([10, 16, 5, 0.4])
+        indptr = np.array([0, 1, 2, 4])
+        A = csc_matrix((data, unsorted_inds, indptr), (3, 3))
+        b = ones(3)
+
+        # should raise when incorrectly assuming indices are sorted
+        use_solver(useUmfpack=True, assumeSortedIndices=True)
+        assert_raises_regex(RuntimeError, "UMFPACK_ERROR_invalid_matrix", factorized, A)
+
+        # should sort indices and succeed when not assuming indices are sorted
+        use_solver(useUmfpack=True, assumeSortedIndices=False)
+        expected = splu(A.copy()).solve(b)
+
+        assert_equal(A.has_sorted_indices, 0)
+        assert_array_almost_equal(factorized(A)(b), expected)
+        assert_equal(A.has_sorted_indices, 1)
+
+
 class TestLinsolve(object):
+    def setUp(self):
+        use_solver(useUmfpack=False)
+
     def test_singular(self):
         A = csc_matrix((5,5), dtype='d')
         b = array([1, 2, 3, 4, 5],dtype='d')
         with suppress_warnings() as sup:
             sup.filter(MatrixRankWarning, "Matrix is exactly singular")
-            x = spsolve(A, b, use_umfpack=False)
+            x = spsolve(A, b)
         assert_(not np.isfinite(x).any())
 
     def test_singular_gh_3312(self):
@@ -54,7 +179,7 @@ def test_singular_gh_3312(self):
         try:
             # should either raise a runtimeerror or return value
             # appropriate for singular input
-            x = spsolve(A, b, use_umfpack=False)
+            x = spsolve(A, b)
             assert_(not np.isfinite(x).any())
         except RuntimeError:
             pass
@@ -132,7 +257,9 @@ def test_example_comparison(self):
         assert_array_almost_equal(X, sX.todense())
 
     @sup_sparse_efficiency
+    @dec.skipif(not has_umfpack)
     def test_shape_compatibility(self):
+        use_solver(useUmfpack=True)
         A = csc_matrix([[1., 0], [0, 2]])
         bs = [
             [1, 6],
@@ -266,6 +393,7 @@ def test_dtype_cast(self):
 
 class TestSplu(object):
     def setUp(self):
+        use_solver(useUmfpack=False)
         n = 40
         d = arange(n) + 1
         self.n = n
@@ -520,6 +648,8 @@ def worker():
 
 
 class TestSpsolveTriangular(object):
+    def setUp(self):
+        use_solver(useUmfpack=False)
 
     def test_singular(self):
         n = 5