from numpy import inner, zeros, inf, finfo from numpy.linalg import norm from math import sqrt from .utils import make_system __all__ = ['minres'] def minres(A, b, x0=None, shift=0.0, tol=1e-5, maxiter=None, M=None, callback=None, show=False, check=False): """ Use MINimum RESidual iteration to solve Ax=b MINRES minimizes norm(Ax - b) for a real symmetric matrix A. Unlike the Conjugate Gradient method, A can be indefinite or singular. If shift != 0 then the method solves (A - shift*I)x = b Parameters ---------- A : {sparse matrix, ndarray, LinearOperator} The real symmetric N-by-N matrix of the linear system Alternatively, ``A`` can be a linear operator which can produce ``Ax`` using, e.g., ``scipy.sparse.linalg.LinearOperator``. b : ndarray Right hand side of the linear system. Has shape (N,) or (N,1). Returns ------- x : ndarray The converged solution. info : integer Provides convergence information: 0 : successful exit >0 : convergence to tolerance not achieved, number of iterations <0 : illegal input or breakdown Other Parameters ---------------- x0 : ndarray Starting guess for the solution. shift : float Value to apply to the system ``(A - shift * I)x = b``. Default is 0. tol : float Tolerance to achieve. The algorithm terminates when the relative residual is below `tol`. maxiter : integer Maximum number of iterations. Iteration will stop after maxiter steps even if the specified tolerance has not been achieved. M : {sparse matrix, ndarray, LinearOperator} Preconditioner for A. The preconditioner should approximate the inverse of A. Effective preconditioning dramatically improves the rate of convergence, which implies that fewer iterations are needed to reach a given error tolerance. callback : function User-supplied function to call after each iteration. It is called as callback(xk), where xk is the current solution vector. show : bool If ``True``, print out a summary and metrics related to the solution during iterations. Default is ``False``. check : bool If ``True``, run additional input validation to check that `A` and `M` (if specified) are symmetric. Default is ``False``. Examples -------- >>> import numpy as np >>> from scipy.sparse import csc_matrix >>> from scipy.sparse.linalg import minres >>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1]], dtype=float) >>> A = A + A.T >>> b = np.array([2, 4, -1], dtype=float) >>> x, exitCode = minres(A, b) >>> print(exitCode) # 0 indicates successful convergence 0 >>> np.allclose(A.dot(x), b) True References ---------- Solution of sparse indefinite systems of linear equations, C. C. Paige and M. A. Saunders (1975), SIAM J. Numer. Anal. 12(4), pp. 617-629. https://web.stanford.edu/group/SOL/software/minres/ This file is a translation of the following MATLAB implementation: https://web.stanford.edu/group/SOL/software/minres/minres-matlab.zip """ A, M, x, b, postprocess = make_system(A, M, x0, b) matvec = A.matvec psolve = M.matvec first = 'Enter minres. ' last = 'Exit minres. ' n = A.shape[0] if maxiter is None: maxiter = 5 * n msg = [' beta2 = 0. If M = I, b and x are eigenvectors ', # -1 ' beta1 = 0. The exact solution is x0 ', # 0 ' A solution to Ax = b was found, given rtol ', # 1 ' A least-squares solution was found, given rtol ', # 2 ' Reasonable accuracy achieved, given eps ', # 3 ' x has converged to an eigenvector ', # 4 ' acond has exceeded 0.1/eps ', # 5 ' The iteration limit was reached ', # 6 ' A does not define a symmetric matrix ', # 7 ' M does not define a symmetric matrix ', # 8 ' M does not define a pos-def preconditioner '] # 9 if show: print(first + 'Solution of symmetric Ax = b') print(first + 'n = %3g shift = %23.14e' % (n,shift)) print(first + 'itnlim = %3g rtol = %11.2e' % (maxiter,tol)) print() istop = 0 itn = 0 Anorm = 0 Acond = 0 rnorm = 0 ynorm = 0 xtype = x.dtype eps = finfo(xtype).eps # Set up y and v for the first Lanczos vector v1. # y = beta1 P' v1, where P = C**(-1). # v is really P' v1. if x0 is None: r1 = b.copy() else: r1 = b - A@x y = psolve(r1) beta1 = inner(r1, y) if beta1 < 0: raise ValueError('indefinite preconditioner') elif beta1 == 0: return (postprocess(x), 0) bnorm = norm(b) if bnorm == 0: x = b return (postprocess(x), 0) beta1 = sqrt(beta1) if check: # are these too strict? # see if A is symmetric w = matvec(y) r2 = matvec(w) s = inner(w,w) t = inner(y,r2) z = abs(s - t) epsa = (s + eps) * eps**(1.0/3.0) if z > epsa: raise ValueError('non-symmetric matrix') # see if M is symmetric r2 = psolve(y) s = inner(y,y) t = inner(r1,r2) z = abs(s - t) epsa = (s + eps) * eps**(1.0/3.0) if z > epsa: raise ValueError('non-symmetric preconditioner') # Initialize other quantities oldb = 0 beta = beta1 dbar = 0 epsln = 0 qrnorm = beta1 phibar = beta1 rhs1 = beta1 rhs2 = 0 tnorm2 = 0 gmax = 0 gmin = finfo(xtype).max cs = -1 sn = 0 w = zeros(n, dtype=xtype) w2 = zeros(n, dtype=xtype) r2 = r1 if show: print() print() print(' Itn x(1) Compatible LS norm(A) cond(A) gbar/|A|') while itn < maxiter: itn += 1 s = 1.0/beta v = s*y y = matvec(v) y = y - shift * v if itn >= 2: y = y - (beta/oldb)*r1 alfa = inner(v,y) y = y - (alfa/beta)*r2 r1 = r2 r2 = y y = psolve(r2) oldb = beta beta = inner(r2,y) if beta < 0: raise ValueError('non-symmetric matrix') beta = sqrt(beta) tnorm2 += alfa**2 + oldb**2 + beta**2 if itn == 1: if beta/beta1 <= 10*eps: istop = -1 # Terminate later # Apply previous rotation Qk-1 to get # [deltak epslnk+1] = [cs sn][dbark 0 ] # [gbar k dbar k+1] [sn -cs][alfak betak+1]. oldeps = epsln delta = cs * dbar + sn * alfa # delta1 = 0 deltak gbar = sn * dbar - cs * alfa # gbar 1 = alfa1 gbar k epsln = sn * beta # epsln2 = 0 epslnk+1 dbar = - cs * beta # dbar 2 = beta2 dbar k+1 root = norm([gbar, dbar]) Arnorm = phibar * root # Compute the next plane rotation Qk gamma = norm([gbar, beta]) # gammak gamma = max(gamma, eps) cs = gbar / gamma # ck sn = beta / gamma # sk phi = cs * phibar # phik phibar = sn * phibar # phibark+1 # Update x. denom = 1.0/gamma w1 = w2 w2 = w w = (v - oldeps*w1 - delta*w2) * denom x = x + phi*w # Go round again. gmax = max(gmax, gamma) gmin = min(gmin, gamma) z = rhs1 / gamma rhs1 = rhs2 - delta*z rhs2 = - epsln*z # Estimate various norms and test for convergence. Anorm = sqrt(tnorm2) ynorm = norm(x) epsa = Anorm * eps epsx = Anorm * ynorm * eps epsr = Anorm * ynorm * tol diag = gbar if diag == 0: diag = epsa qrnorm = phibar rnorm = qrnorm if ynorm == 0 or Anorm == 0: test1 = inf else: test1 = rnorm / (Anorm*ynorm) # ||r|| / (||A|| ||x||) if Anorm == 0: test2 = inf else: test2 = root / Anorm # ||Ar|| / (||A|| ||r||) # Estimate cond(A). # In this version we look at the diagonals of R in the # factorization of the lower Hessenberg matrix, Q @ H = R, # where H is the tridiagonal matrix from Lanczos with one # extra row, beta(k+1) e_k^T. Acond = gmax/gmin # See if any of the stopping criteria are satisfied. # In rare cases, istop is already -1 from above (Abar = const*I). if istop == 0: t1 = 1 + test1 # These tests work if tol < eps t2 = 1 + test2 if t2 <= 1: istop = 2 if t1 <= 1: istop = 1 if itn >= maxiter: istop = 6 if Acond >= 0.1/eps: istop = 4 if epsx >= beta1: istop = 3 # if rnorm <= epsx : istop = 2 # if rnorm <= epsr : istop = 1 if test2 <= tol: istop = 2 if test1 <= tol: istop = 1 # See if it is time to print something. prnt = False if n <= 40: prnt = True if itn <= 10: prnt = True if itn >= maxiter-10: prnt = True if itn % 10 == 0: prnt = True if qrnorm <= 10*epsx: prnt = True if qrnorm <= 10*epsr: prnt = True if Acond <= 1e-2/eps: prnt = True if istop != 0: prnt = True if show and prnt: str1 = '%6g %12.5e %10.3e' % (itn, x[0], test1) str2 = ' %10.3e' % (test2,) str3 = ' %8.1e %8.1e %8.1e' % (Anorm, Acond, gbar/Anorm) print(str1 + str2 + str3) if itn % 10 == 0: print() if callback is not None: callback(x) if istop != 0: break # TODO check this if show: print() print(last + ' istop = %3g itn =%5g' % (istop,itn)) print(last + ' Anorm = %12.4e Acond = %12.4e' % (Anorm,Acond)) print(last + ' rnorm = %12.4e ynorm = %12.4e' % (rnorm,ynorm)) print(last + ' Arnorm = %12.4e' % (Arnorm,)) print(last + msg[istop+1]) if istop == 6: info = maxiter else: info = 0 return (postprocess(x),info) if __name__ == '__main__': from numpy import arange from scipy.sparse import spdiags n = 10 residuals = [] def cb(x): residuals.append(norm(b - A@x)) # A = poisson((10,),format='csr') A = spdiags([arange(1,n+1,dtype=float)], [0], n, n, format='csr') M = spdiags([1.0/arange(1,n+1,dtype=float)], [0], n, n, format='csr') A.psolve = M.matvec b = zeros(A.shape[0]) x = minres(A,b,tol=1e-12,maxiter=None,callback=cb) # x = cg(A,b,x0=b,tol=1e-12,maxiter=None,callback=cb)[0]