blob: d1a174bf5aa7390736759ed51387f59a7f685458 [file] [log] [blame]
// Copyright ©2017 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package linsolve
import (
"math"
"gonum.org/v1/gonum/blas"
"gonum.org/v1/gonum/blas/blas64"
"gonum.org/v1/gonum/mat"
)
// GMRES implements the Generalized Minimum Residual method with the modified
// Gram-Schmidt orthogonalization for solving systems of linear equations
// A * x = b,
// where A is a nonsymmetric, nonsingular matrix. It may find a solution in
// fewer iterations and with fewer matrix-vector products compared to BiCG or
// BiCGStab at the price of much large memory storage. This implementation uses
// restarts to limit the memory requirements. GMRES does not need the
// multiplication with Aᵀ.
//
// References:
// - Barrett, R. et al. (1994). Section 2.3.4 Generalized Minimal Residual
// (GMRES). In Templates for the Solution of Linear Systems: Building Blocks
// for Iterative Methods (2nd ed.) (pp. 17-19). Philadelphia, PA: SIAM.
// Retrieved from http://www.netlib.org/templates/templates.pdf
// - Saad, Y., and Schultz, M. (1986). GMRES: A generalized minimal residual
// algorithm for solving nonsymmetric linear systems. SIAM J. Sci. Stat.
// Comput., 7(3), 856. doi:10.6028/jres.049.044
// Retrieved from https://web.stanford.edu/class/cme324/saad-schultz.pdf
type GMRES struct {
// Restart is the restart parameter which limits the computation and
// storage costs. It must hold that
// 1 <= Restart <= n
// where n is the dimension of the problem. If Restart is 0, n will be
// used instead. This guarantees convergence of GMRES and increases
// robustness. Many specific problems however, particularly for large
// n, will benefit in efficiency by setting Restart to
// a problem-dependent value less than n.
Restart int
// m is the used value of Restart.
m int
// v is an n×(m+1) matrix V whose columns form an orthonormal basis of the
// Krylov subspace.
v mat.Dense
// h is an (m+1)×m upper Hessenberg matrix H.
h mat.Dense
// givs holds Givens rotations that are used to reduce H to upper triangular
// form.
givs []givens
x mat.VecDense
y mat.VecDense
s mat.VecDense
k int // Loop variable for inner iterations.
resume int
}
// Init initializes the data for a linear solve. See the Method interface for more details.
func (g *GMRES) Init(x, residual *mat.VecDense) {
dim := x.Len()
if residual.Len() != dim {
panic("gmres: vector length mismatch")
}
g.m = g.Restart
if g.m == 0 {
g.m = dim
}
if g.m <= 0 || dim < g.m {
panic("gmres: invalid value of Restart")
}
g.v.Reset()
g.v.ReuseAs(dim, g.m+1)
// Store the residual in the first column of V.
g.vcol(0).CopyVec(residual)
g.h.Reset()
g.h.ReuseAs(g.m+1, g.m)
if cap(g.givs) < g.m {
g.givs = make([]givens, g.m)
} else {
g.givs = g.givs[:g.m]
for i := range g.givs {
g.givs[i].c = 0
g.givs[i].s = 0
}
}
g.x.CloneVec(x)
g.y.Reset()
g.y.ReuseAsVec(g.m + 1)
g.s.Reset()
g.s.ReuseAsVec(g.m + 1)
g.resume = 1
}
// Iterate performs an iteration of the linear solve. See the Method interface for more details.
//
// GMRES will command the following operations:
// MulVec
// PreconSolve
// CheckResidualNorm
// MajorIteration
// NoOperation
func (g *GMRES) Iterate(ctx *Context) (Operation, error) {
switch g.resume {
case 1:
// The initial residual is in the first column of V.
ctx.Src.CopyVec(g.vcol(0))
g.resume = 2
// Solve M^{-1} * r_0.
return PreconSolve, nil
case 2:
// v_0 = M^{-1} * r_0
v0 := g.vcol(0)
v0.CopyVec(ctx.Dst)
// Normalize v_0.
norm := mat.Norm(v0, 2)
v0.ScaleVec(1/norm, v0)
// Initialize s to the elementary vector e_1 scaled by norm.
g.s.Zero()
g.s.SetVec(0, norm)
// Begin the inner for-loop for k going from 0 to m-1.
g.k = 0
fallthrough
case 3:
ctx.Src.CopyVec(g.vcol(g.k))
g.resume = 4
// Compute A * v_k.
return MulVec, nil
case 4:
ctx.Src.CopyVec(ctx.Dst)
g.resume = 5
// Solve M^{-1} * (A * v_k).
return PreconSolve, nil
case 5:
// v_{k+1} = M^{-1} * (A * v_k)
vk1 := g.vcol(g.k + 1)
vk1.CopyVec(ctx.Dst)
// Construct the k-th column of the upper Hessenberg matrix H
// using the modified Gram-Schmidt process to make v_{k+1}
// orthonormal to the first k+1 columns of V.
g.modifiedGS(g.k, &g.h, &g.v, vk1)
// Reduce H back to upper triangular form and update the vector s.
g.qr(g.k, g.givs, &g.h, &g.s)
// Check the approximate residual norm.
ctx.ResidualNorm = math.Abs(g.s.AtVec(g.k + 1))
g.resume = 6
return CheckResidualNorm, nil
case 6:
g.k++
if g.k < g.m && !ctx.Converged {
// Continue the inner for-loop.
g.resume = 3
return NoOperation, nil
}
// Either restarting or converged, we have to update the solution.
// Solve the upper triangular system H*y=s.
g.solveLeastSquares(g.k, &g.y, &g.h, &g.s)
// Compute x as a linear combination of columns of V.
g.updateSolution(g.k, &g.x, &g.v, &g.y)
ctx.X.CopyVec(&g.x)
if ctx.Converged {
g.resume = 0
return MajorIteration, nil
}
// We are restarting, so we have to also compute the residual.
g.resume = 7
return ComputeResidual, nil
case 7:
// Store the residual again in the first column of V.
g.vcol(0).CopyVec(ctx.Dst)
g.resume = 1
return MajorIteration, nil
default:
panic("gmres: Init not called")
}
}
// modifiedGS orthonormalizes the vector w with respect to the first k+1 columns
// of V using the modified Gram-Schmidt algorithm, and stores the computed
// coefficients in the k-th column of H.
func (g *GMRES) modifiedGS(k int, h, v *mat.Dense, w *mat.VecDense) {
hk := h.ColView(k).(*mat.VecDense)
for j := 0; j <= k; j++ {
vj := v.ColView(j).(*mat.VecDense)
hkj := mat.Dot(vj, w)
hk.SetVec(j, hkj) // H[j,k] = v_j · w
w.AddScaledVec(w, -hkj, vj) // w -= H[j,k] * v_j
}
norm := mat.Norm(w, 2)
hk.SetVec(k+1, norm) // H[k+1,k] = |w|
w.ScaleVec(1/norm, w) // Normalize w.
}
// qr applies previous Givens rotations to the k-th column of H, computes the
// next Givens rotation to zero out H[k+1,k] and applies it also to the vector s.
func (g *GMRES) qr(k int, givs []givens, h *mat.Dense, s *mat.VecDense) {
// Apply previous Givens rotations to the k-th column of H.
hk := h.ColView(k).(*mat.VecDense)
for i, giv := range givs[:k] {
hki, hki1 := giv.apply(hk.AtVec(i), hk.AtVec(i+1))
hk.SetVec(i, hki)
hk.SetVec(i+1, hki1)
}
// Compute the k-th Givens rotation that zeros H[k+1,k].
givs[k].c, givs[k].s, _, _ = blas64.Rotg(hk.AtVec(k), hk.AtVec(k+1))
// Apply the k-th Givens rotation to (H[k,k], H[k+1,k]).
hkk, _ := givs[k].apply(hk.AtVec(k), hk.AtVec(k+1))
hk.SetVec(k, hkk)
// We don't call hk.SetVec(k+1, 0) because the element won't be accessed.
// Apply the k-th Givens rotation to (s[k], s[k+1]).
sk, sk1 := givs[k].apply(s.AtVec(k), s.AtVec(k+1))
s.SetVec(k, sk)
s.SetVec(k+1, sk1)
}
// solveLeastSquares solves the upper triangular linear system
// H * y = s
func (g *GMRES) solveLeastSquares(k int, y *mat.VecDense, h *mat.Dense, s *mat.VecDense) {
// Copy the first k elements of s into y.
y.CopyVec(s.SliceVec(0, k))
// Convert H into an upper triangular matrix.
hraw := h.RawMatrix()
htri := blas64.Triangular{
Uplo: blas.Upper,
Diag: blas.NonUnit,
N: k,
Data: hraw.Data,
Stride: hraw.Stride,
}
// Solve the upper triangular system storing the solution into y.
blas64.Trsv(blas.NoTrans, htri, y.RawVector())
}
// updateSolution updates the current solution vector x with a linear
// combination of the first k columns of V:
// x = x + V * y = x + \sum y_j * v_j
func (g *GMRES) updateSolution(k int, x *mat.VecDense, v *mat.Dense, y *mat.VecDense) {
for j := 0; j < k; j++ {
vj := v.ColView(j)
x.AddScaledVec(x, y.AtVec(j), vj)
}
}
// vcol returns a view of the j-th column of the matrix V.
func (g *GMRES) vcol(j int) *mat.VecDense {
return g.v.ColView(j).(*mat.VecDense)
}
// givens is a Givens rotation.
type givens struct {
c, s float64
}
func (giv *givens) apply(x, y float64) (float64, float64) {
return giv.c*x + giv.s*y, giv.c*y - giv.s*x
}