blob: c49f1c7a8ffa42313ad3d296a76f88659d80ab3c [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 (
"errors"
"gonum.org/v1/gonum/mat"
)
const defaultTolerance = 1e-8
// ErrIterationLimit is returned when a maximum number of iterations were done
// without converging to a solution.
var ErrIterationLimit = errors.New("linsolve: iteration limit reached")
// MulVecToer represents a square matrix A by means of a matrix-vector
// multiplication.
type MulVecToer interface {
// MulVecTo computes A*x or Aᵀ*x and stores the result into dst.
MulVecTo(dst *mat.VecDense, trans bool, x mat.Vector)
}
// Settings holds settings for solving a linear system.
type Settings struct {
// InitX holds the initial guess. If it is nil or empty, the zero vector
// will be used, otherwise its length must be equal to the dimension of
// the system.
InitX *mat.VecDense
// Dst, if not nil, will be used for storing the approximate solution,
// otherwise a new vector will be allocated. In both cases the vector will
// also be returned in Result. If Dst is not empty, its length must be equal
// to the dimension of the system.
Dst *mat.VecDense
// Tolerance specifies error tolerance for the final (approximate)
// solution produced by the iterative method. The iteration will be
// stopped when
// |r_i| < Tolerance * |b|
// where r_i is the residual at i-th iteration.
//
// If Tolerance is zero, a default value of 1e-8 will be used, otherwise
// it must be positive and less than 1.
Tolerance float64
// MaxIterations is the limit on the number of iterations. If it is
// zero, a default value of twice the dimension of the system will be
// used.
MaxIterations int
// PreconSolve describes a preconditioner solve that stores into dst the
// solution of the system
// M * dst = rhs, or
// Mᵀ * dst = rhs,
// where M is the preconditioning matrix. If PreconSolve is nil, no
// preconditioning will be used (M is the identity).
PreconSolve func(dst *mat.VecDense, trans bool, rhs mat.Vector) error
// Work context can be provided to reduce memory allocation when solving
// multiple linear systems. If Work is not nil, its fields must be
// either empty or their length must be equal to the dimension of the
// system.
Work *Context
}
// defaultSettings fills zero fields of s with default values.
func defaultSettings(s *Settings, dim int) {
if s.InitX != nil && s.InitX.Len() == 0 {
s.InitX.ReuseAsVec(dim)
}
if s.Dst == nil {
s.Dst = mat.NewVecDense(dim, nil)
} else if s.Dst.Len() == 0 {
s.Dst.ReuseAsVec(dim)
}
if s.Tolerance == 0 {
s.Tolerance = defaultTolerance
}
if s.MaxIterations == 0 {
s.MaxIterations = 4 * dim
}
if s.PreconSolve == nil {
s.PreconSolve = NoPreconditioner
}
if s.Work == nil {
s.Work = NewContext(dim)
} else {
if s.Work.X.Len() == 0 {
s.Work.X.ReuseAsVec(dim)
}
if s.Work.Src.Len() == 0 {
s.Work.Src.ReuseAsVec(dim)
}
if s.Work.Dst.Len() == 0 {
s.Work.Dst.ReuseAsVec(dim)
}
}
}
func checkSettings(s *Settings, dim int) {
if s.InitX != nil && s.InitX.Len() != dim {
panic("linsolve: mismatched length of initial guess")
}
if s.Dst.Len() != dim {
panic("linsolve: mismatched destination length")
}
if s.Tolerance <= 0 || 1 <= s.Tolerance {
panic("linsolve: invalid tolerance")
}
if s.MaxIterations <= 0 {
panic("linsolve: negative iteration limit")
}
if s.Work.X.Len() != dim || s.Work.Src.Len() != dim || s.Work.Dst.Len() != dim {
panic("linsolve: mismatched work context length")
}
}
// Result holds the result of an iterative solve.
type Result struct {
// X is the approximate solution.
X *mat.VecDense
// ResidualNorm is an approximation to the norm of the final residual.
ResidualNorm float64
// Stats holds statistics about the iterative solve.
Stats Stats
}
// Stats holds statistics about an iterative solve.
type Stats struct {
// Iterations is the number of iterations performed by Method.
Iterations int
// MulVec is the number of MulVec operations commanded by Method.
MulVec int
// PreconSolve is the number of PreconSolve operations commanded by Method.
PreconSolve int
}
// Iterative finds an approximate solution of the system of n linear equations
// A*x = b,
// where A is a nonsingular square matrix of order n and b is the right-hand
// side vector, using an iterative method m. If m is nil, default GMRES will be
// used.
//
// settings provide means for adjusting parameters of the iterative process. See
// the Settings documentation for more information. Iterative will not modify
// the fields of Settings. If settings is nil, default settings will be used.
//
// Note that the default choices of Method and Settings were chosen to provide
// accuracy and robustness, rather than speed. There are many algorithms for
// iterative linear solutions that have different tradeoffs, and can exploit
// special structure in the A matrix. Similarly, in many cases the number of
// iterations can be significantly reduced by using an appropriate
// preconditioner or increasing the error tolerance. Combined, these choices can
// significantly reduce computation time. Thus, while Iterative has supplied
// defaults, users are strongly encouraged to adjust these defaults for their
// problem.
func Iterative(a MulVecToer, b *mat.VecDense, m Method, settings *Settings) (*Result, error) {
n := b.Len()
var s Settings
if settings != nil {
s = *settings
}
defaultSettings(&s, n)
checkSettings(&s, n)
var stats Stats
ctx := s.Work
rInit := mat.NewVecDense(n, nil)
if s.InitX != nil {
// Initial x is provided.
ctx.X.CloneVec(s.InitX)
computeResidual(rInit, a, b, ctx.X, &stats)
} else {
// Initial x is the zero vector.
ctx.X.Zero()
// Residual b-A*x is then equal to b.
rInit.CopyVec(b)
}
if m == nil {
m = &GMRES{}
}
var err error
ctx.ResidualNorm = mat.Norm(rInit, 2)
if ctx.ResidualNorm >= s.Tolerance {
err = iterate(a, b, rInit, s, m, &stats)
} else {
s.Dst.CopyVec(ctx.X)
}
return &Result{
X: s.Dst,
ResidualNorm: ctx.ResidualNorm,
Stats: stats,
}, err
}
func iterate(a MulVecToer, b, initRes *mat.VecDense, settings Settings, method Method, stats *Stats) error {
bNorm := mat.Norm(b, 2)
if bNorm == 0 {
bNorm = 1
}
ctx := settings.Work
settings.Dst.CopyVec(ctx.X)
method.Init(ctx.X, initRes)
for {
op, err := method.Iterate(ctx)
if err != nil {
return err
}
switch op {
case NoOperation:
case MulVec, MulVec | Trans:
stats.MulVec++
a.MulVecTo(ctx.Dst, op&Trans == Trans, ctx.Src)
case PreconSolve, PreconSolve | Trans:
stats.PreconSolve++
err = settings.PreconSolve(ctx.Dst, op&Trans == Trans, ctx.Src)
if err != nil {
return err
}
case CheckResidualNorm:
ctx.Converged = ctx.ResidualNorm < settings.Tolerance*bNorm
case ComputeResidual:
computeResidual(ctx.Dst, a, b, ctx.X, stats)
case MajorIteration:
stats.Iterations++
if ctx.Converged {
settings.Dst.CopyVec(ctx.X)
return nil
}
if stats.Iterations == settings.MaxIterations {
settings.Dst.CopyVec(ctx.X)
return ErrIterationLimit
}
default:
panic("linsolve: invalid operation")
}
}
}
// NoPreconditioner implements the identity preconditioner.
func NoPreconditioner(dst *mat.VecDense, trans bool, rhs mat.Vector) error {
if dst.Len() != rhs.Len() {
panic("linsolve: mismatched vector length")
}
dst.CloneVec(rhs)
return nil
}
func computeResidual(dst *mat.VecDense, a MulVecToer, b, x *mat.VecDense, stats *Stats) {
stats.MulVec++
a.MulVecTo(dst, false, x)
dst.AddScaledVec(b, -1, dst)
}