123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507 |
- # Copyright (C) 2015, Pauli Virtanen <pav@iki.fi>
- # Distributed under the same license as SciPy.
- import warnings
- import numpy as np
- from numpy.linalg import LinAlgError
- from scipy.linalg import (get_blas_funcs, qr, solve, svd, qr_insert, lstsq)
- from scipy.sparse.linalg._isolve.utils import make_system
- __all__ = ['gcrotmk']
- def _fgmres(matvec, v0, m, atol, lpsolve=None, rpsolve=None, cs=(), outer_v=(),
- prepend_outer_v=False):
- """
- FGMRES Arnoldi process, with optional projection or augmentation
- Parameters
- ----------
- matvec : callable
- Operation A*x
- v0 : ndarray
- Initial vector, normalized to nrm2(v0) == 1
- m : int
- Number of GMRES rounds
- atol : float
- Absolute tolerance for early exit
- lpsolve : callable
- Left preconditioner L
- rpsolve : callable
- Right preconditioner R
- cs : list of (ndarray, ndarray)
- Columns of matrices C and U in GCROT
- outer_v : list of ndarrays
- Augmentation vectors in LGMRES
- prepend_outer_v : bool, optional
- Whether augmentation vectors come before or after
- Krylov iterates
- Raises
- ------
- LinAlgError
- If nans encountered
- Returns
- -------
- Q, R : ndarray
- QR decomposition of the upper Hessenberg H=QR
- B : ndarray
- Projections corresponding to matrix C
- vs : list of ndarray
- Columns of matrix V
- zs : list of ndarray
- Columns of matrix Z
- y : ndarray
- Solution to ||H y - e_1||_2 = min!
- res : float
- The final (preconditioned) residual norm
- """
- if lpsolve is None:
- lpsolve = lambda x: x
- if rpsolve is None:
- rpsolve = lambda x: x
- axpy, dot, scal, nrm2 = get_blas_funcs(['axpy', 'dot', 'scal', 'nrm2'], (v0,))
- vs = [v0]
- zs = []
- y = None
- res = np.nan
- m = m + len(outer_v)
- # Orthogonal projection coefficients
- B = np.zeros((len(cs), m), dtype=v0.dtype)
- # H is stored in QR factorized form
- Q = np.ones((1, 1), dtype=v0.dtype)
- R = np.zeros((1, 0), dtype=v0.dtype)
- eps = np.finfo(v0.dtype).eps
- breakdown = False
- # FGMRES Arnoldi process
- for j in range(m):
- # L A Z = C B + V H
- if prepend_outer_v and j < len(outer_v):
- z, w = outer_v[j]
- elif prepend_outer_v and j == len(outer_v):
- z = rpsolve(v0)
- w = None
- elif not prepend_outer_v and j >= m - len(outer_v):
- z, w = outer_v[j - (m - len(outer_v))]
- else:
- z = rpsolve(vs[-1])
- w = None
- if w is None:
- w = lpsolve(matvec(z))
- else:
- # w is clobbered below
- w = w.copy()
- w_norm = nrm2(w)
- # GCROT projection: L A -> (1 - C C^H) L A
- # i.e. orthogonalize against C
- for i, c in enumerate(cs):
- alpha = dot(c, w)
- B[i,j] = alpha
- w = axpy(c, w, c.shape[0], -alpha) # w -= alpha*c
- # Orthogonalize against V
- hcur = np.zeros(j+2, dtype=Q.dtype)
- for i, v in enumerate(vs):
- alpha = dot(v, w)
- hcur[i] = alpha
- w = axpy(v, w, v.shape[0], -alpha) # w -= alpha*v
- hcur[i+1] = nrm2(w)
- with np.errstate(over='ignore', divide='ignore'):
- # Careful with denormals
- alpha = 1/hcur[-1]
- if np.isfinite(alpha):
- w = scal(alpha, w)
- if not (hcur[-1] > eps * w_norm):
- # w essentially in the span of previous vectors,
- # or we have nans. Bail out after updating the QR
- # solution.
- breakdown = True
- vs.append(w)
- zs.append(z)
- # Arnoldi LSQ problem
- # Add new column to H=Q@R, padding other columns with zeros
- Q2 = np.zeros((j+2, j+2), dtype=Q.dtype, order='F')
- Q2[:j+1,:j+1] = Q
- Q2[j+1,j+1] = 1
- R2 = np.zeros((j+2, j), dtype=R.dtype, order='F')
- R2[:j+1,:] = R
- Q, R = qr_insert(Q2, R2, hcur, j, which='col',
- overwrite_qru=True, check_finite=False)
- # Transformed least squares problem
- # || Q R y - inner_res_0 * e_1 ||_2 = min!
- # Since R = [R'; 0], solution is y = inner_res_0 (R')^{-1} (Q^H)[:j,0]
- # Residual is immediately known
- res = abs(Q[0,-1])
- # Check for termination
- if res < atol or breakdown:
- break
- if not np.isfinite(R[j,j]):
- # nans encountered, bail out
- raise LinAlgError()
- # -- Get the LSQ problem solution
- # The problem is triangular, but the condition number may be
- # bad (or in case of breakdown the last diagonal entry may be
- # zero), so use lstsq instead of trtrs.
- y, _, _, _, = lstsq(R[:j+1,:j+1], Q[0,:j+1].conj())
- B = B[:,:j+1]
- return Q, R, B, vs, zs, y, res
- def gcrotmk(A, b, x0=None, tol=1e-5, maxiter=1000, M=None, callback=None,
- m=20, k=None, CU=None, discard_C=False, truncate='oldest',
- atol=None):
- """
- Solve a matrix equation using flexible GCROT(m,k) algorithm.
- Parameters
- ----------
- A : {sparse matrix, ndarray, LinearOperator}
- The real or complex 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).
- x0 : ndarray
- Starting guess for the solution.
- tol, atol : float, optional
- Tolerances for convergence, ``norm(residual) <= max(tol*norm(b), atol)``.
- The default for ``atol`` is `tol`.
- .. warning::
- The default value for `atol` will be changed in a future release.
- For future compatibility, specify `atol` explicitly.
- maxiter : int, optional
- Maximum number of iterations. Iteration will stop after maxiter
- steps even if the specified tolerance has not been achieved.
- M : {sparse matrix, ndarray, LinearOperator}, optional
- Preconditioner for A. The preconditioner should approximate the
- inverse of A. gcrotmk is a 'flexible' algorithm and the preconditioner
- can vary from iteration to iteration. Effective preconditioning
- dramatically improves the rate of convergence, which implies that
- fewer iterations are needed to reach a given error tolerance.
- callback : function, optional
- User-supplied function to call after each iteration. It is called
- as callback(xk), where xk is the current solution vector.
- m : int, optional
- Number of inner FGMRES iterations per each outer iteration.
- Default: 20
- k : int, optional
- Number of vectors to carry between inner FGMRES iterations.
- According to [2]_, good values are around m.
- Default: m
- CU : list of tuples, optional
- List of tuples ``(c, u)`` which contain the columns of the matrices
- C and U in the GCROT(m,k) algorithm. For details, see [2]_.
- The list given and vectors contained in it are modified in-place.
- If not given, start from empty matrices. The ``c`` elements in the
- tuples can be ``None``, in which case the vectors are recomputed
- via ``c = A u`` on start and orthogonalized as described in [3]_.
- discard_C : bool, optional
- Discard the C-vectors at the end. Useful if recycling Krylov subspaces
- for different linear systems.
- truncate : {'oldest', 'smallest'}, optional
- Truncation scheme to use. Drop: oldest vectors, or vectors with
- smallest singular values using the scheme discussed in [1,2].
- See [2]_ for detailed comparison.
- Default: 'oldest'
- Returns
- -------
- x : ndarray
- The solution found.
- info : int
- Provides convergence information:
- * 0 : successful exit
- * >0 : convergence to tolerance not achieved, number of iterations
- Examples
- --------
- >>> import numpy as np
- >>> from scipy.sparse import csc_matrix
- >>> from scipy.sparse.linalg import gcrotmk
- >>> R = np.random.randn(5, 5)
- >>> A = csc_matrix(R)
- >>> b = np.random.randn(5)
- >>> x, exit_code = gcrotmk(A, b)
- >>> print(exit_code)
- 0
- >>> np.allclose(A.dot(x), b)
- True
- References
- ----------
- .. [1] E. de Sturler, ''Truncation strategies for optimal Krylov subspace
- methods'', SIAM J. Numer. Anal. 36, 864 (1999).
- .. [2] J.E. Hicken and D.W. Zingg, ''A simplified and flexible variant
- of GCROT for solving nonsymmetric linear systems'',
- SIAM J. Sci. Comput. 32, 172 (2010).
- .. [3] M.L. Parks, E. de Sturler, G. Mackey, D.D. Johnson, S. Maiti,
- ''Recycling Krylov subspaces for sequences of linear systems'',
- SIAM J. Sci. Comput. 28, 1651 (2006).
- """
- A,M,x,b,postprocess = make_system(A,M,x0,b)
- if not np.isfinite(b).all():
- raise ValueError("RHS must contain only finite numbers")
- if truncate not in ('oldest', 'smallest'):
- raise ValueError("Invalid value for 'truncate': %r" % (truncate,))
- if atol is None:
- warnings.warn("scipy.sparse.linalg.gcrotmk called without specifying `atol`. "
- "The default value will change in the future. To preserve "
- "current behavior, set ``atol=tol``.",
- category=DeprecationWarning, stacklevel=2)
- atol = tol
- matvec = A.matvec
- psolve = M.matvec
- if CU is None:
- CU = []
- if k is None:
- k = m
- axpy, dot, scal = None, None, None
- if x0 is None:
- r = b.copy()
- else:
- r = b - matvec(x)
- axpy, dot, scal, nrm2 = get_blas_funcs(['axpy', 'dot', 'scal', 'nrm2'], (x, r))
- b_norm = nrm2(b)
- if b_norm == 0:
- x = b
- return (postprocess(x), 0)
- if discard_C:
- CU[:] = [(None, u) for c, u in CU]
- # Reorthogonalize old vectors
- if CU:
- # Sort already existing vectors to the front
- CU.sort(key=lambda cu: cu[0] is not None)
- # Fill-in missing ones
- C = np.empty((A.shape[0], len(CU)), dtype=r.dtype, order='F')
- us = []
- j = 0
- while CU:
- # More memory-efficient: throw away old vectors as we go
- c, u = CU.pop(0)
- if c is None:
- c = matvec(u)
- C[:,j] = c
- j += 1
- us.append(u)
- # Orthogonalize
- Q, R, P = qr(C, overwrite_a=True, mode='economic', pivoting=True)
- del C
- # C := Q
- cs = list(Q.T)
- # U := U P R^-1, back-substitution
- new_us = []
- for j in range(len(cs)):
- u = us[P[j]]
- for i in range(j):
- u = axpy(us[P[i]], u, u.shape[0], -R[i,j])
- if abs(R[j,j]) < 1e-12 * abs(R[0,0]):
- # discard rest of the vectors
- break
- u = scal(1.0/R[j,j], u)
- new_us.append(u)
- # Form the new CU lists
- CU[:] = list(zip(cs, new_us))[::-1]
- if CU:
- axpy, dot = get_blas_funcs(['axpy', 'dot'], (r,))
- # Solve first the projection operation with respect to the CU
- # vectors. This corresponds to modifying the initial guess to
- # be
- #
- # x' = x + U y
- # y = argmin_y || b - A (x + U y) ||^2
- #
- # The solution is y = C^H (b - A x)
- for c, u in CU:
- yc = dot(c, r)
- x = axpy(u, x, x.shape[0], yc)
- r = axpy(c, r, r.shape[0], -yc)
- # GCROT main iteration
- for j_outer in range(maxiter):
- # -- callback
- if callback is not None:
- callback(x)
- beta = nrm2(r)
- # -- check stopping condition
- beta_tol = max(atol, tol * b_norm)
- if beta <= beta_tol and (j_outer > 0 or CU):
- # recompute residual to avoid rounding error
- r = b - matvec(x)
- beta = nrm2(r)
- if beta <= beta_tol:
- j_outer = -1
- break
- ml = m + max(k - len(CU), 0)
- cs = [c for c, u in CU]
- try:
- Q, R, B, vs, zs, y, pres = _fgmres(matvec,
- r/beta,
- ml,
- rpsolve=psolve,
- atol=max(atol, tol*b_norm)/beta,
- cs=cs)
- y *= beta
- except LinAlgError:
- # Floating point over/underflow, non-finite result from
- # matmul etc. -- report failure.
- break
- #
- # At this point,
- #
- # [A U, A Z] = [C, V] G; G = [ I B ]
- # [ 0 H ]
- #
- # where [C, V] has orthonormal columns, and r = beta v_0. Moreover,
- #
- # || b - A (x + Z y + U q) ||_2 = || r - C B y - V H y - C q ||_2 = min!
- #
- # from which y = argmin_y || beta e_1 - H y ||_2, and q = -B y
- #
- #
- # GCROT(m,k) update
- #
- # Define new outer vectors
- # ux := (Z - U B) y
- ux = zs[0]*y[0]
- for z, yc in zip(zs[1:], y[1:]):
- ux = axpy(z, ux, ux.shape[0], yc) # ux += z*yc
- by = B.dot(y)
- for cu, byc in zip(CU, by):
- c, u = cu
- ux = axpy(u, ux, ux.shape[0], -byc) # ux -= u*byc
- # cx := V H y
- hy = Q.dot(R.dot(y))
- cx = vs[0] * hy[0]
- for v, hyc in zip(vs[1:], hy[1:]):
- cx = axpy(v, cx, cx.shape[0], hyc) # cx += v*hyc
- # Normalize cx, maintaining cx = A ux
- # This new cx is orthogonal to the previous C, by construction
- try:
- alpha = 1/nrm2(cx)
- if not np.isfinite(alpha):
- raise FloatingPointError()
- except (FloatingPointError, ZeroDivisionError):
- # Cannot update, so skip it
- continue
- cx = scal(alpha, cx)
- ux = scal(alpha, ux)
- # Update residual and solution
- gamma = dot(cx, r)
- r = axpy(cx, r, r.shape[0], -gamma) # r -= gamma*cx
- x = axpy(ux, x, x.shape[0], gamma) # x += gamma*ux
- # Truncate CU
- if truncate == 'oldest':
- while len(CU) >= k and CU:
- del CU[0]
- elif truncate == 'smallest':
- if len(CU) >= k and CU:
- # cf. [1,2]
- D = solve(R[:-1,:].T, B.T).T
- W, sigma, V = svd(D)
- # C := C W[:,:k-1], U := U W[:,:k-1]
- new_CU = []
- for j, w in enumerate(W[:,:k-1].T):
- c, u = CU[0]
- c = c * w[0]
- u = u * w[0]
- for cup, wp in zip(CU[1:], w[1:]):
- cp, up = cup
- c = axpy(cp, c, c.shape[0], wp)
- u = axpy(up, u, u.shape[0], wp)
- # Reorthogonalize at the same time; not necessary
- # in exact arithmetic, but floating point error
- # tends to accumulate here
- for cp, up in new_CU:
- alpha = dot(cp, c)
- c = axpy(cp, c, c.shape[0], -alpha)
- u = axpy(up, u, u.shape[0], -alpha)
- alpha = nrm2(c)
- c = scal(1.0/alpha, c)
- u = scal(1.0/alpha, u)
- new_CU.append((c, u))
- CU[:] = new_CU
- # Add new vector to CU
- CU.append((cx, ux))
- # Include the solution vector to the span
- CU.append((None, x.copy()))
- if discard_C:
- CU[:] = [(None, uz) for cz, uz in CU]
- return postprocess(x), j_outer + 1
|