Source code for sharpy.linear.src.libsparse

```'''
Collect tools to manipulate sparse and/or mixed dense/sparse matrices.

author: S. Maraniello
date: Dec 2018

Comment: manipulating large linear system may require using both dense and sparse
matrices. While numpy/scipy automatically handle most operations between mixed
dense/sparse arrays, some (e.g. dot product) require more attention. This
library collects methods to handle these situations.

Classes:
scipy.sparse matrices are wrapped so as to ensure compatibility with numpy arrays
upon conversion to dense.
- csc_matrix: this is a wrapper of scipy.csc_matrix.
- SupportedTypes: types supported for operations
- WarningTypes: due to some bugs in scipy (v.1.1.0), sum (+) operations between
np.ndarray and scipy.sparse matrices can result in numpy.matrixlib.defmatrix.matrix
types. This list contains such undesired types that can result from dense/sparse
operations and raises a warning if required.
(b) convert these types into numpy.ndarrays.

Methods:
- dot: handles matrix dot products across different types.
- solve: solves linear systems Ax=b with A and b dense, sparse or mixed.
- dense: convert matrix to numpy array

Warning:
- only sparse types into SupportedTypes are supported!

To Do:
- move these methods into an algebra module?
'''

import warnings
import numpy as np
import scipy.sparse as sparse
from scipy.sparse._sputils import upcast_char
import scipy.sparse.linalg as spalg

# --------------------------------------------------------------------- Classes

[docs]class csc_matrix(sparse.csc_matrix):
'''
Wrapper of scipy.csc_matrix that ensures best compatibility with numpy.ndarray.
The following methods have been overwritten to ensure that numpy.ndarray are
- todense

Warning: this format is memory inefficient to allocate new sparse matrices.
Consider using:
- scipy.sparse.lil_matrix, which supports slicing, or
- scipy.sparse.coo_matrix, though slicing is not supported :(

'''

def __init__(self,arg1, shape=None, dtype=None, copy=False):
super().__init__(arg1, shape=shape, dtype=dtype, copy=copy)

[docs]	def todense(self):
''' As per scipy.spmatrix.todense but returns a numpy.ndarray. '''
return super().toarray()

if other.shape != self.shape:
raise ValueError('Incompatible shapes.')
dtype = upcast_char(self.dtype.char, other.dtype.char)
order = self._swap('CF')[0]
result = np.array(other, dtype=dtype, order=order, copy=True)
M, N = self._swap(self.shape)
y = result if result.flags.c_contiguous else result.T
sparse._sparsetools.csr_todense(M, N, self.indptr, self.indices, self.data, y)
return result #np.matrix(result, copy=False)

SupportedTypes=[np.ndarray,csc_matrix]
WarningTypes=[np.matrixlib.defmatrix.matrix]

# --------------------------------------------------------------------- Methods

def block_dot(A, B):
'''
dot product between block matrices.

Inputs:
A, B: are nested lists of dense/sparse matrices of compatible shape for
block matrices product. Empty blocks can be defined with None. (see numpy.block)
'''

rA, cA = len(A), len(A[0])
rB, cB = len(B), len(B[0])

for arow,brow in zip(A,B):
assert len(brow) == cB,\
'B rows do not contain the same number of column blocks'
assert len(arow) == cA,\
'A rows do not contain the same number of column blocks'
assert cA==rB, 'Columns of A not equal to rows of B!'

P=[]
for ii in range(rA):
prow = cB * [None]
for jj in range(cB):
# check first that the result will not be None
Continue = False
for kk in range(cA):
if A[ii][kk] is not None and B[kk][jj] is not None:
Continue = True
break
if Continue:
prow[jj] = 0.
for kk in range(cA):
if A[ii][kk] is not None and B[kk][jj] is not None:
prow[jj] += dot( A[ii][kk], B[kk][jj] )
P.append(prow)

return P

def block_matrix_dot_vector(A, v):
'''
dot product between block matrix and block vector

Inputs:
A, v: are nested lists of dense/sparse matrices of compatible shape for
block matrices product. Empty blocks can be defined with None. (see numpy.block)
'''

rA, cA = len(A), len(A[0])
rv = len(B)

for arow in A:
assert len(arow) == cA,\
'A rows do not contain the same number of column blocks'
assert cA==rv, 'Columns of A not equal to rows of v!'

P=[None]*rA
for ii in range(rA):
for jj in range(cA):
# check first that the result will not be None
if A[ii][jj] is not None and B[jj] is not None:
P[ii] += dot(A[ii][jj], B[jj])
return P

def block_sum(A, B, factA = None, factB = None):
'''
dot product between block matrices.

Inputs:
A, B: are nested lists of dense/sparse matrices of compatible shape for
block matrices product. Empty blocks can be defined with None. (see numpy.block)
'''

rA, cA = len(A), len(A[0])
rB, cB = len(B), len(B[0])

assert cA==cB and rA==rB, 'Block matrices do not have same size'

for arow,brow in zip(A,B):
assert len(brow) == cB,\
'B rows do not contain the same number of column blocks'
assert len(arow) == cA,\
'A rows do not contain the same number of column blocks'

P=[]
for ii in range(rA):
prow = cA * [None]

for jj in range(cA):

if A[ii][jj] is None:
if B[ii][jj] is None:
prow[jj] = None
else:
if factB is None:
prow[jj] = B[ii][jj]
else:
prow[jj] = factB*B[ii][jj]
else:
if B[ii][jj] is None:
if factA is None:
prow[jj] = A[ii][jj]
else:
prow[jj] = factA*A[ii][jj]
else:
if factA is None and factA is None:
prow[jj] = A[ii][jj] + B[ii][jj]
elif factA is None:
prow[jj] = A[ii][jj] + factB*B[ii][jj]
elif factB is None:
prow[jj] = factA*A[ii][jj] + B[ii][jj]
else:
prow[jj] = factA*A[ii][jj] + factB*B[ii][jj]

P.append(prow)

return P

def dot(A,B,type_out=None):
'''
Method to compute
C = A*B ,
where * is the matrix product, with dense/sparse/mixed matrices.

The format (sparse or dense) of C is specified through 'type_out'. If
type_out==None, the output format is sparse if both A and B are sparse, dense
otherwise.

The following formats are supported:
- numpy.ndarray
- scipy.csc_matrix
'''

# determine types:
tA=type(A)
tB=type(B)

assert tA in SupportedTypes, 'Type of A matrix (%s) not supported'%tA
assert tB in SupportedTypes, 'Type of B matrix (%s) not supported'%tB
if type_out == None:
type_out=tA
else:
assert type_out in SupportedTypes, 'type_out not supported'

# multiply
# if tA==float or tb==float:
# 	C = A*B
# else:
if tA==np.ndarray and tB==csc_matrix:
C=(B.transpose()).dot(A.transpose()).transpose()
# C=A.dot(B.todense())
else:
C=A.dot(B)

# format output
if tA != type_out:
if type_out==csc_matrix:
return csc_matrix(C)
else:
return C.toarray()

return C

def solve(A,b):
'''
Wrapper of
numpy.linalg.solve and scipy.sparse.linalg.spsolve
for solution of the linear system A x = b.
- if A is a dense numpy array np.linalg.solve is called for solution. Note
that if B is sparse, this requires convertion to dense. In this case,
solution through LU factorisation of A should be considered to exploit the
sparsity of B.
- if A is sparse, scipy.sparse.linalg.spsolve is used.
'''

# determine types:
tA=type(A)
tB=type(b)

assert tA in SupportedTypes, 'Type of A matrix (%s) not supported'%tA
assert tB in SupportedTypes, 'Type of B matrix (%s) not supported'%tB
# multiply
if tA==np.ndarray:
if tB==csc_matrix:
x=np.linalg.solve(A,b.toarray())
else:
x=np.linalg.solve(A,b)
else:
x=spalg.spsolve(A,b)

assert type(x) in SupportedTypes, 'Unexpected output type!'

return x

def dense(M):
''' If required, converts sparse array to dense. '''
if type(M) == csc_matrix:
return np.array(M.toarray())
elif type(M) == csc_matrix:
return M.toarray()
return M

def eye_as(M):
''' Produces an identity matrix as per M, in shape and type '''

tM=type(M)
assert tM in SupportedTypes, 'Type %s not supported!'%tM
nrows=M.shape[0]
assert nrows==M.shape[1], 'Not a square matrix!'

if tM==csc_matrix:
D=csc_matrix((nrows,nrows))
D.setdiag(1.)
elif tM==np.ndarray:
D=np.eye(nrows)

return D

def zeros_as(M):
''' Produces an identity matrix as per M, in shape and type '''

tM=type(M)
assert tM in SupportedTypes, 'Type %s not supported!'%tM
nrows,ncols=M.shape

if tM==csc_matrix:
D=csc_matrix((nrows,ncols))
elif tM==np.ndarray:
D=np.zeros_like(M)

return D

# -----------------------------------------------------------------------------

if __name__=='__main__':
import unittest

class Test_module(unittest.TestCase):
''' Test methods into this module '''

def setUp(self):
self.A=np.random.rand(3,4)
self.B=np.random.rand(4,2)

def test_dense_plus_csc_matrix_type(self):
A=self.A
Asp=csc_matrix(A)

for aa in [A,Asp]:
for bb in [A,Asp]:
tsum=type(aa+bb)
tdiff=type(aa-bb)
for tout,strout in zip([tsum,tdiff],['(+)','(-)']):
if tout not in SupportedTypes:
if tout in WarningTypes:
warnings.warn(
'Undesired type (%s) resulting from %s operations between %s and %s types'\
%(tout,strout,type(aa),type(bb)))
else:
raise NameError(
'Unexpected type (%s) resulting from %s operations between %s and %s types'\
%(tout,strout,type(aa),type(bb)))

def test_zeros_as(self):
A=np.zeros((4,2))
A1=zeros_as(A)
A2=zeros_as(csc_matrix(A))
assert np.max(np.abs(A-A1))<1e-16, 'Error in libsparse.zeros_as'
assert np.max(np.abs(A-A2))<1e-16, 'Error in libsparse.zeros_as'

def test_eye_as(self):
A=np.random.rand(4,4)
D0=np.eye(4)
D1=eye_as(A)
D2=eye_as(csc_matrix(A))
assert np.max(np.abs(D0-D1))<1e-12, 'Error in libsparse.eye_as'
assert np.max(np.abs(D0-D2))<1e-12, 'Error in libsparse.eye_as'

def test_dot(self):
A,B=self.A,self.B
C0=np.dot(A,B)		# reference
C1=dot(A,B)
C2=dot(A,csc_matrix(B))
C3=dot(csc_matrix(A),B)
C4=dot(csc_matrix(A),csc_matrix(B))
assert np.max(np.abs(C0-C1))<1e-12, 'Error in libsparse.dot'
assert np.max(np.abs(C0-C2))<1e-12, 'Error in libsparse.dot'
assert np.max(np.abs(C0-C3))<1e-12, 'Error in libsparse.dot'

def test_solve(self):
A=np.random.rand(4,4)
B=np.random.rand(4,2)
Asp=csc_matrix(A)
Bsp=csc_matrix(B)

X0=np.linalg.solve(A,B)
X1=solve(A,B)
X2=solve(A,Bsp)
X3=solve(Asp,B)
X4=solve(Asp,Bsp)

assert np.max(np.abs(X0-X1))<1e-12, 'Error in libsparse.solve'
assert np.max(np.abs(X0-X2))<1e-12, 'Error in libsparse.solve'
assert np.max(np.abs(X0-X3))<1e-12, 'Error in libsparse.solve'
assert np.max(np.abs(X0-X4))<1e-12, 'Error in libsparse.solve'

outprint='Testing libsparse'
print('\n' + 70*'-')
print((70-len(outprint))*' ' + outprint )
print(70*'-')
unittest.main()
```