Search code examples
pythonnumpyscipysparse-matrix

SciPy.sparse iterative solvers: No sparse right hand side support?


It seems like the iterative solvers of scipy.sparse.linalg do not support the sparse matrix data types of scipy.sparse as a right hand side of the equation system (whereas the direct solver does). Consider the following short example:

import numpy as np
import scipy
import scipy.sparse
import scipy.sparse.linalg

# Construct a random sparse but regular matrix A
A = scipy.sparse.rand(50,50,density=0.1)+scipy.sparse.coo_matrix(np.diag(np.random.rand(50,)))

# Construct a random sparse right hand side
b = scipy.sparse.rand(50,1,density=0.1).tocsc()

ud = scipy.sparse.linalg.spsolve(A,b) # Works as indented
ui = scipy.sparse.linalg.bicg(A,b)[0] # ValueError: A and b have incompatible dimensions

The shapes seem to be correct though:

print(A.shape)
print(b.shape)

returns

(50, 50)
(50, 1)

Defining b as a dense ndarray however works

b = scipy.sparse.rand(50,1,density=0.1).todense() 

It would be very surprising to me if there would be no support for sparse right hand sides (as arise in FEM for example) although the documentation requests b to be of type {array, matrix}.

So what am I doing wrong here or why would this be unsupported?


Solution

  • A two-part approach:

    The idea of LinearOperator is easy and powerful: it's a virtual linear operator, or actually two:

    Aop = Linop( A )  # see below
    A.dot(x) -> Aop.matvec(x)
    A.T.dot(y) -> Aop.rmatvec(y)
    x = solver( Aop, b ... )  # uses matvec and rmatvec, not A.dot A.T.dot
    

    Here matvec and rmatvec can do anything (within reason). For example, they could linearize horrible nonlinear equations near args x and y of any type whatever. Unfortunately aslinearoperator doesn't work asis for sparse x. The doc suggests two ways of implementing LinearOperator, but

    Whenever something can be done in two ways, someone will be confused.

    Anyway, Linop below works with sparse x -- with a patched lsmr.py, under gist.github.com/denis-bz . Other sparse iterative solvers ? Don't know.


    If what you really want to do is:
    minimize |A x - b|
    and also keep |x| small, a.k.a. regularization, in the L1 or L2 norm

    then you should definitely look at scikit-learn . It targets different corners of
    Speed - Accuracy - Problems - People (SAPP) space than scipy.sparse.isolve . I've found scikit-learn solid, pleasant, pretty well-documented, but haven't compared the two on real problems.

    How big, how sparse are your problems ? Could you point to test cases on the web ?


    """ Linop( A ): .matvec .rmatvec( sparse vecs )
    http://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.linalg.LinearOperator.html
    http://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.linalg.lsmr.html
    """
    
    from __future__ import division
    import numpy as np
    from scipy import sparse
    from scipy.sparse.linalg import LinearOperator  # $scipy/sparse/linalg/interface.py
    
    __version__ = "2015-12-24 dec  denis + safe_sparse_dot"
    
    
    #...............................................................................
    class Linop( LinearOperator ):  # subclass ?
        """ Aop = Linop( scipy sparse matrix A )
            ->  Aop.matvec(x) = A dot x, x ndarray or sparse
                Aop.rmatvec(x) = A.T dot x
            for scipy.sparse.linalg solvers like lsmr
        """
    
        def __init__( self, A ):
            self.A = A
    
        def matvec( self, x ):
            return safe_sparse_dot( self.A, x )
    
        def rmatvec( self, y ):
            return safe_sparse_dot( self.A.T, y )
    
            # LinearOperator subclass should implement at least one of _matvec and _matmat.
        def _matvec( self, b ):
            raise NotImplementedError( "_matvec" )
    
            # not _matvec only:
            # $scipy/sparse/linalg/interface.py
            # def matvec(self, x):
            #     x = np.asanyarray(x)  <-- kills sparse x, should raise an error
    
        def _rmatvec( self, b ):
            raise NotImplementedError( "_rmatvec" )
    
        @property
        def shape( self ):
            return self.A.shape
    
    
    def safe_sparse_dot( a, b ):
        """ -> a * b or np.dot(a, b) """
            # from sklearn
        if sparse.issparse(a) or sparse.issparse(b):
            try:
                return a * b
            except ValueError:  # dimension mismatch: print shapes
                print "error: %s %s  *  %s %s" % (
                        type(a).__name__, a.shape,
                        type(b).__name__, b.shape )
                raise
        else:
            return np.dot(a, b)
    
    #...........................................................................
    if __name__ == "__main__":
        import sys
        from lsmr import lsmr  # patched $scipy/sparse/linalg/lsmr.py
    
        np.set_printoptions( threshold=20, edgeitems=10, linewidth=100, suppress=True,
            formatter = dict( float = lambda x: "%.2g" % x ))
    
            # test sparse.rand A m n, x n 1, b m 1
        m = 10
        n = 100
        density = .1
        bdense = 0
        seed = 0
        damp = 1
    
            # to change these params in sh or ipython, run this.py  a=1  b=None  c=[3] ...
        for arg in sys.argv[1:]:
            exec( arg )
    
        np.random.seed(seed)
    
        print "\n", 80 * "-"
        paramstr = "%s  m %d  n %d  density %g  bdense %d  seed %d  damp %g " % (
                __file__, m, n, density, bdense, seed, damp )
        print paramstr
    
        A = sparse.rand( m, n, density, format="csr", random_state=seed )
        x = sparse.rand( n, 1, density, format="csr", random_state=seed )
        b = sparse.rand( m, 1, density, format="csr", random_state=seed )
        if bdense:
            b = b.toarray().squeeze()  # matrix (m,1) -> ndarray (m,)
    
        #...........................................................................
        Aop = Linop( A )
            # aslinearoperator( A ): not for sparse x
    
            # check Aop matvec rmatvec --
        Ax = Aop.matvec( x )
        bA = Aop.rmatvec( b )
        for nm in "A Aop x b Ax bA ".split():
            x = eval(nm)
            print "%s: %s %s " % (nm, x.shape, type(x))
        print ""
    
        print "lsmr( Aop, b )"
    
        #...........................................................................
        xetc = lsmr( Aop, b, damp=damp, show=1 )
        #...........................................................................
    
        x, istop, niter, normr, normar, norma, conda, normx = xetc
        x = getattr( x, "A", x ) .squeeze()
        print "x:", x.shape, x
    
        #     print "lsmr( A, b ) -- Valueerror in $scipy/sparse/linalg/interface.py"
        #     xetc = lsmr( A, b, damp=damp, show=1 )  # Valueerror
    
        safe_sparse_dot( A, b.T )  # ValueError: dimension mismatch