linsolve: add linsolve package
diff --git a/linsolve/bicg.go b/linsolve/bicg.go
new file mode 100644
index 0000000..042f75e
--- /dev/null
+++ b/linsolve/bicg.go
@@ -0,0 +1,128 @@
+// 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/mat"
+)
+
+// BiCG implements the Bi-Conjugate Gradient method with preconditioning for
+// solving systems of linear equations
+// A * x = b,
+// where A is a nonsymmetric, nonsingular matrix. It uses limited memory storage
+// but the convergence may be irregular and the method may break down. BiCG
+// requires a multiplication with A and Aᵀ at each iteration. BiCGStab is a
+// related method that does not use Aᵀ.
+//
+// References:
+// - Barrett, R. et al. (1994). Section 2.3.5 BiConjugate Gradient (BiCG).
+// In Templates for the Solution of Linear Systems: Building Blocks
+// for Iterative Methods (2nd ed.) (pp. 19-20). Philadelphia, PA: SIAM.
+// Retrieved from http://www.netlib.org/templates/templates.pdf
+type BiCG struct {
+ x mat.VecDense
+ r, rt mat.VecDense
+ p, pt mat.VecDense
+ z, zt mat.VecDense
+
+ rho, rhoPrev float64
+
+ resume int
+}
+
+// Init initializes the data for a linear solve. See the Method interface for more details.
+func (b *BiCG) Init(x, residual *mat.VecDense) {
+ dim := x.Len()
+ if residual.Len() != dim {
+ panic("bicg: vector length mismatch")
+ }
+
+ b.x.CloneVec(x)
+ b.r.CloneVec(residual)
+ b.rt.CloneVec(&b.r)
+
+ b.p.Reset()
+ b.p.ReuseAsVec(dim)
+ b.pt.Reset()
+ b.pt.ReuseAsVec(dim)
+ b.z.Reset()
+ b.z.ReuseAsVec(dim)
+ b.zt.Reset()
+ b.zt.ReuseAsVec(dim)
+
+ b.rhoPrev = 1
+
+ b.resume = 1
+}
+
+// Iterate performs an iteration of the linear solve. See the Method interface for more details.
+//
+// BiCG will command the following operations:
+// MulVec
+// MulVec|Trans
+// PreconSolve
+// PreconSolve|Trans
+// CheckResidualNorm
+// MajorIteration
+// NoOperation
+func (b *BiCG) Iterate(ctx *Context) (Operation, error) {
+ switch b.resume {
+ case 1:
+ // Solve M^{-1} * r_{i-1}.
+ ctx.Src.CopyVec(&b.r)
+ b.resume = 2
+ return PreconSolve, nil
+ case 2:
+ b.z.CopyVec(ctx.Dst)
+ // Solve M^{-T} * rt_{i-1}.
+ ctx.Src.CopyVec(&b.rt)
+ b.resume = 3
+ return PreconSolve | Trans, nil
+ case 3:
+ b.zt.CopyVec(ctx.Dst)
+ b.rho = mat.Dot(&b.z, &b.rt)
+ if math.Abs(b.rho) < breakdownTol {
+ b.resume = 0
+ return NoOperation, &BreakdownError{math.Abs(b.rho), breakdownTol}
+ }
+ beta := b.rho / b.rhoPrev
+ b.p.AddScaledVec(&b.z, beta, &b.p)
+ b.pt.AddScaledVec(&b.zt, beta, &b.pt)
+ // Compute A * p.
+ ctx.Src.CopyVec(&b.p)
+ b.resume = 4
+ return MulVec, nil
+ case 4:
+ // z is overwritten and reused.
+ b.z.CopyVec(ctx.Dst)
+ // Compute Aᵀ * pt.
+ ctx.Src.CopyVec(&b.pt)
+ b.resume = 5
+ return MulVec | Trans, nil
+ case 5:
+ // zt is overwritten and reused.
+ b.zt.CopyVec(ctx.Dst)
+ alpha := b.rho / mat.Dot(&b.pt, &b.z)
+ ctx.X.AddScaledVec(ctx.X, alpha, &b.p)
+ b.rt.AddScaledVec(&b.rt, -alpha, &b.zt)
+ b.r.AddScaledVec(&b.r, -alpha, &b.z)
+ ctx.ResidualNorm = mat.Norm(&b.r, 2)
+ b.resume = 6
+ return CheckResidualNorm, nil
+ case 6:
+ if ctx.Converged {
+ b.resume = 0
+ return MajorIteration, nil
+ }
+ b.rhoPrev = b.rho
+ b.resume = 1
+ return MajorIteration, nil
+
+ default:
+ panic("bicg: Init not called")
+ }
+}
diff --git a/linsolve/bicgstab.go b/linsolve/bicgstab.go
new file mode 100644
index 0000000..e9046b4
--- /dev/null
+++ b/linsolve/bicgstab.go
@@ -0,0 +1,153 @@
+// 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/mat"
+)
+
+// BiCGStab implements the BiConjugate Gradient Stabilized method with
+// preconditioning for solving systems of linear equations
+// A * x = b,
+// where A is a nonsymmetric, nonsingular matrix. The method is a variant of
+// BiCG but offers a smoother convergence and does not require multiplication
+// with Aᵀ.
+//
+// References:
+// - Barrett, R. et al. (1994). Section 2.3.8 BiConjugate Gradient Stabilized (Bi-CGSTAB).
+// In Templates for the Solution of Linear Systems: Building Blocks
+// for Iterative Methods (2nd ed.) (pp. 24-25). Philadelphia, PA: SIAM.
+// Retrieved from http://www.netlib.org/templates/templates.pdf
+type BiCGStab struct {
+ x mat.VecDense
+ r, rt mat.VecDense
+ p mat.VecDense
+ phat mat.VecDense
+ shat mat.VecDense
+ t mat.VecDense
+ v mat.VecDense
+
+ rho, rhoPrev float64
+ alpha float64
+ omega float64
+
+ resume int
+}
+
+// Init initializes the data for a linear solve. See the Method interface for more details.
+func (b *BiCGStab) Init(x, residual *mat.VecDense) {
+ dim := x.Len()
+ if residual.Len() != dim {
+ panic("bicgstab: vector length mismatch")
+ }
+
+ b.x.CloneVec(x)
+ b.r.CloneVec(residual)
+ b.rt.CloneVec(&b.r)
+
+ b.p.Reset()
+ b.p.ReuseAsVec(dim)
+ b.phat.Reset()
+ b.phat.ReuseAsVec(dim)
+ b.shat.Reset()
+ b.shat.ReuseAsVec(dim)
+ b.t.Reset()
+ b.t.ReuseAsVec(dim)
+ b.v.Reset()
+ b.v.ReuseAsVec(dim)
+
+ b.rhoPrev = 1
+ b.alpha = 0
+ b.omega = 1
+
+ b.resume = 1
+}
+
+// Iterate performs an iteration of the linear solve. See the Method interface for more details.
+//
+// BiCGStab will command the following operations:
+// MulVec
+// PreconSolve
+// CheckResidualNorm
+// MajorIteration
+// NoOperation
+func (b *BiCGStab) Iterate(ctx *Context) (Operation, error) {
+ switch b.resume {
+ case 1:
+ b.rho = mat.Dot(&b.rt, &b.r)
+ if math.Abs(b.rho) < breakdownTol {
+ b.resume = 0
+ return NoOperation, &BreakdownError{math.Abs(b.rho), breakdownTol}
+ }
+ // p_i = r_{i-1} + beta*(p_{i-1} - omega * v_{i-1})
+ beta := (b.rho / b.rhoPrev) * (b.alpha / b.omega)
+ b.p.AddScaledVec(&b.p, -b.omega, &b.v)
+ b.p.AddScaledVec(&b.r, beta, &b.p)
+ // Solve M^{-1} * p_i.
+ ctx.Src.CopyVec(&b.p)
+ b.resume = 2
+ return PreconSolve, nil
+ case 2:
+ b.phat.CopyVec(ctx.Dst)
+ // Compute A * \hat{p}_i.
+ ctx.Src.CopyVec(&b.phat)
+ b.resume = 3
+ return MulVec, nil
+ case 3:
+ b.v.CopyVec(ctx.Dst)
+ rtv := mat.Dot(&b.rt, &b.v)
+ if rtv == 0 {
+ b.resume = 0
+ return NoOperation, &BreakdownError{}
+ }
+ b.alpha = b.rho / rtv
+ // Form the residual and X so that we can check for tolerance early.
+ ctx.X.AddScaledVec(ctx.X, b.alpha, &b.phat)
+ b.r.AddScaledVec(&b.r, -b.alpha, &b.v)
+ ctx.ResidualNorm = mat.Norm(&b.r, 2)
+ b.resume = 4
+ return CheckResidualNorm, nil
+ case 4:
+ if ctx.Converged {
+ b.resume = 0
+ return MajorIteration, nil
+ }
+ // Solve M^{-1} * r_i.
+ ctx.Src.CopyVec(&b.r)
+ b.resume = 5
+ return PreconSolve, nil
+ case 5:
+ b.shat.CopyVec(ctx.Dst)
+ // Compute A * \hat{s}_i.
+ ctx.Src.CopyVec(&b.shat)
+ b.resume = 6
+ return MulVec, nil
+ case 6:
+ b.t.CopyVec(ctx.Dst)
+ b.omega = mat.Dot(&b.t, &b.r) / mat.Dot(&b.t, &b.t)
+ ctx.X.AddScaledVec(ctx.X, b.omega, &b.shat)
+ b.r.AddScaledVec(&b.r, -b.omega, &b.t)
+ ctx.ResidualNorm = mat.Norm(&b.r, 2)
+ b.resume = 7
+ return CheckResidualNorm, nil
+ case 7:
+ if ctx.Converged {
+ b.resume = 0
+ return MajorIteration, nil
+ }
+ if math.Abs(b.omega) < breakdownTol {
+ b.resume = 0
+ return NoOperation, &BreakdownError{math.Abs(b.omega), breakdownTol}
+ }
+ b.rhoPrev = b.rho
+ b.resume = 1
+ return MajorIteration, nil
+
+ default:
+ panic("bicgstab: Init not called")
+ }
+}
diff --git a/linsolve/cg.go b/linsolve/cg.go
new file mode 100644
index 0000000..97e13f6
--- /dev/null
+++ b/linsolve/cg.go
@@ -0,0 +1,99 @@
+// 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 (
+ "gonum.org/v1/gonum/mat"
+)
+
+// CG implements the Conjugate Gradient iterative method with preconditioning
+// for solving systems of linear equations
+// A * x = b,
+// where A is a symmetric positive definite matrix. It requires minimal memory
+// storage and is a good choice for symmetric positive definite problems.
+//
+// References:
+// - Barrett, Richard et al. (1994). Section 2.3.1 Conjugate Gradient Method (CG).
+// In Templates for the Solution of Linear Systems: Building Blocks for
+// Iterative Methods (2nd ed.) (pp. 12-15). Philadelphia, PA: SIAM.
+// Retrieved from http://www.netlib.org/templates/templates.pdf
+// - Hestenes, M., and Stiefel, E. (1952). Methods of conjugate gradients for
+// solving linear systems. Journal of Research of the National Bureau of
+// Standards, 49(6), 409. doi:10.6028/jres.049.044
+// - Málek, J. and Strakoš, Z. (2015). Preconditioning and the Conjugate Gradient
+// Method in the Context of Solving PDEs. Philadelphia, PA: SIAM.
+type CG struct {
+ x mat.VecDense
+ r mat.VecDense
+ p mat.VecDense
+
+ rho, rhoPrev float64
+
+ resume int
+}
+
+// Init initializes the data for a linear solve. See the Method interface for more details.
+func (cg *CG) Init(x, residual *mat.VecDense) {
+ dim := x.Len()
+ if residual.Len() != dim {
+ panic("cg: vector length mismatch")
+ }
+
+ cg.x.CloneVec(x)
+ cg.r.CloneVec(residual)
+
+ cg.p.Reset()
+ cg.p.ReuseAsVec(dim)
+
+ cg.rhoPrev = 1
+
+ cg.resume = 1
+}
+
+// Iterate performs an iteration of the linear solve. See the Method interface for more details.
+//
+// CG will command the following operations:
+// MulVec
+// PreconSolve
+// CheckResidualNorm
+// MajorIteration
+func (cg *CG) Iterate(ctx *Context) (Operation, error) {
+ switch cg.resume {
+ case 1:
+ ctx.Src.CopyVec(&cg.r)
+ cg.resume = 2
+ // Compute z_{i-1} = M^{-1} * r_{i-1}.
+ return PreconSolve, nil
+ case 2:
+ z := ctx.Dst
+ cg.rho = mat.Dot(&cg.r, z) // ρ_{i-1} = r_{i-1} · z_{i-1}
+ beta := cg.rho / cg.rhoPrev // β_{i-1} = ρ_{i-1} / ρ_{i-2}
+ cg.p.AddScaledVec(z, beta, &cg.p) // p_i = z_{i-1} + β p_{i-1}
+ ctx.Src.CopyVec(&cg.p)
+ cg.resume = 3
+ // Compute A * p_i.
+ return MulVec, nil
+ case 3:
+ ap := ctx.Dst
+ alpha := cg.rho / mat.Dot(&cg.p, ap) // α_i = ρ_{i-1} / (p_i · A p_i)
+ cg.x.AddScaledVec(&cg.x, alpha, &cg.p) // x_i = x_{i-1} + α p_i
+ cg.r.AddScaledVec(&cg.r, -alpha, ap) // r_i = r_{i-1} - α A p_i
+ ctx.ResidualNorm = mat.Norm(&cg.r, 2)
+ cg.resume = 4
+ return CheckResidualNorm, nil
+ case 4:
+ ctx.X.CopyVec(&cg.x)
+ if ctx.Converged {
+ cg.resume = 0
+ return MajorIteration, nil
+ }
+ cg.rhoPrev = cg.rho
+ cg.resume = 1
+ return MajorIteration, nil
+
+ default:
+ panic("cg: Init not called")
+ }
+}
diff --git a/linsolve/doc.go b/linsolve/doc.go
new file mode 100644
index 0000000..d4b3364
--- /dev/null
+++ b/linsolve/doc.go
@@ -0,0 +1,118 @@
+// 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 provides iterative methods for solving linear systems.
+
+Background
+
+A system of linear equations can be written as
+
+ A * x = b,
+
+where A is a given n×n non-singular matrix, b is a given n-vector (the
+right-hand side), and x is an unknown n-vector.
+
+Direct methods such as the LU or QR decomposition compute (in the absence of
+roundoff errors) the exact solution after a finite number of steps. For a
+general matrix A they require O(n^3) arithmetic operations which becomes
+infeasible for large n due to excessive memory and time cost.
+
+Iterative methods, in contrast, generally do not compute the exact solution x.
+Starting from an initial estimate x_0, they instead compute a sequence x_i of
+increasingly accurate approximations to x. This iterative process is stopped
+when the estimated difference between x_i and the true x becomes smaller than a
+prescribed threshold. The number of iterations thus depends on the value of the
+threshold. If the desired threshold is very small, then the iterative methods
+may take as long or longer than a direct method. However, for many problems a
+decent approximation can be found in a small number of steps.
+
+The iterative methods implemented in this package do not access the elements of
+A directly, they instead ask for the result of matrix-vector products with A.
+For a general n×n matrix this requires O(n^2) operations, but can be much
+cheaper depending on the structure of the matrix (sparse, banded,
+block-stuctured, etc.). Such structure often arises in practical applications.
+An iterative method can thus be significantly cheaper than a direct method, by
+using a small number of iterations and taking advantage of matrix structure.
+
+Iterative methods are most often useful in the following situations:
+
+ - The system matrix A is sparse, blocked or has other special structure,
+ - The problem size is sufficiently large that a dense factorization of A is
+ costly in terms of compute time and/or memory storage,
+ - Computing the product of A (or A^T, if necessary) with a vector can be done
+ efficiently,
+ - An approximate solution is all that is required.
+
+Using linsolve
+
+The two most important elements of the API are the MulVecToer interface and the
+Iterative function.
+
+MulVecToer interface
+
+The MulVecToer interface represents the system matrix A. This abstracts the
+details of any particular matrix storage, and allows the user to exploit the
+properties of their particular matrix. Matrix types provided by gonum/mat and
+github.com/james-bowman/sparse packages implement this interface.
+
+Note that methods in this package have only limited means for checking whether
+the provided MulVecToer represents a matrix that satisfies all assumptions made
+by the chosen Method, for example if the matrix is actually symmetric positive
+definite.
+
+Iterative function
+
+The Iterative function is the entry point to the functionality provided by this
+package. It takes as parameters the matrix A (via the MulVecToer interface as
+discussed above), the right-hand side vector b, the iterative method and
+settings that control the iterative process and provide a way for reusing
+memory.
+
+Choosing an iterative method
+
+The choice of an iterative method is typically guided by the properties of the
+matrix A including symmetry, definiteness, sparsity, conditioning, and block
+structure. In general, performance on symmetric matrices is well understood (see
+the references below), with the conjugate gradient method being a good starting
+point. Non-symmetric matrices are much more difficult to assess, where any
+suggestion of a 'best' method is usually accompanied by a recommendation to use
+trial-and-error.
+
+Preconditioning
+
+Preconditioning is a family of techniques that attempt to transform the linear
+system into one that has the same solution but more favorable eigenspectrum. The
+transformation matrix is called a preconditioner. A good preconditioner will
+reduce the number of iterations needed to find a good approximate solution
+(hopefully enough to overcome the cost of applying the preconditioning!), and in
+some cases preconditioning is necessary to get any kind of convergence. In
+linsolve a preconditioner is specified by Settings.PreconSolve.
+
+Implementing Method interface
+
+This package allows external implementations of iterative solvers by means of
+the Method interface. It uses a reverse-communication style of API to
+"outsource" operations such as matrix-vector multiplication, preconditioner
+solve or convergence checks to the caller. The caller performs the commanded
+operation and passes the result again to Method. The matrix A and the right-hand
+side b are not directly available to Methods which encourages their cleaner
+implementation. See the documentation for Method, Operation, and Context for
+more information.
+
+References
+
+Further details about computational practice and mathematical theory of
+iterative methods can be found in the following references:
+
+ - Barrett, Richard et al. (1994). Templates for the Solution of Linear Systems:
+ Building Blocks for Iterative Methods (2nd ed.). Philadelphia, PA: SIAM.
+ Retrieved from http://www.netlib.org/templates/templates.pdf
+ - Saad, Yousef (2003). Iterative methods for sparse linear systems (2nd ed.).
+ Philadelphia, PA: SIAM. Retrieved from
+ http://www-users.cs.umn.edu/~saad/IterMethBook_2ndEd.pdf
+ - Greenbaum, A. (1997). Iterative methods for solving linear systems.
+ Philadelphia, PA: SIAM.
+*/
+package linsolve
diff --git a/linsolve/gmres.go b/linsolve/gmres.go
new file mode 100644
index 0000000..d1a174b
--- /dev/null
+++ b/linsolve/gmres.go
@@ -0,0 +1,271 @@
+// 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
+}
diff --git a/linsolve/internal/mmarket/reader.go b/linsolve/internal/mmarket/reader.go
new file mode 100644
index 0000000..eaf92d4
--- /dev/null
+++ b/linsolve/internal/mmarket/reader.go
@@ -0,0 +1,105 @@
+// 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 mmarket provides a type to read Matrix Market format files.
+package mmarket
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "gonum.org/v1/exp/linsolve/internal/triplet"
+)
+
+var (
+ errBadFormat = errors.New("mmarket: bad file format")
+ errUnsupported = errors.New("mmarket: matrix type not supported")
+)
+
+type Reader struct {
+ s *bufio.Scanner
+}
+
+func NewReader(r io.Reader) *Reader {
+ return &Reader{
+ s: bufio.NewScanner(r),
+ }
+}
+
+// Read a real matrix in coordinate format and return its triplet representation.
+func (r *Reader) Read() (*triplet.Matrix, error) {
+ r.s.Scan()
+ if err := r.s.Err(); err != nil {
+ return nil, err
+ }
+ header := strings.Fields(r.s.Text())
+ if header[0] != "%%MatrixMarket" {
+ return nil, errBadFormat
+ }
+ if header[1] != "matrix" {
+ return nil, errBadFormat
+ }
+ if header[2] != "coordinate" {
+ return nil, errBadFormat
+ }
+ if header[3] != "real" {
+ return nil, errUnsupported
+ }
+ sym := header[4] == "symmetric"
+
+ var nr, nc, nnz int
+ for r.s.Scan() {
+ line := r.s.Text()
+ if line[0] == '%' {
+ continue
+ }
+ n, err := fmt.Sscan(line, &nr, &nc, &nnz)
+ if err != nil {
+ return nil, err
+ }
+ if n != 3 {
+ return nil, errBadFormat
+ }
+ break
+ }
+ if err := r.s.Err(); err != nil {
+ return nil, err
+ }
+
+ if sym && nr != nc {
+ return nil, errBadFormat
+ }
+
+ m := triplet.NewMatrix(nr, nc)
+ for i := 0; i < nnz; i++ {
+ if !r.s.Scan() {
+ return nil, errBadFormat
+ }
+ var (
+ i, j int
+ v float64
+ )
+ n, err := fmt.Sscan(r.s.Text(), &i, &j, &v)
+ if err != nil {
+ return nil, err
+ }
+ if n != 3 {
+ return nil, errBadFormat
+ }
+ if i < 1 || nr < i {
+ return nil, errBadFormat
+ }
+ if j < 1 || nc < j {
+ return nil, errBadFormat
+ }
+ m.Append(i-1, j-1, v)
+ if sym && i != j {
+ m.Append(j-1, i-1, v)
+ }
+ }
+ return m, nil
+}
diff --git a/linsolve/internal/triplet/triplet.go b/linsolve/internal/triplet/triplet.go
new file mode 100644
index 0000000..e7718d4
--- /dev/null
+++ b/linsolve/internal/triplet/triplet.go
@@ -0,0 +1,74 @@
+// 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 triplet provides triplet representation for sparse matrices.
+package triplet
+
+import "gonum.org/v1/gonum/mat"
+
+type triplet struct {
+ i, j int
+ v float64
+}
+
+type Matrix struct {
+ r, c int
+ data []triplet
+}
+
+func NewMatrix(r, c int) *Matrix {
+ if r <= 0 || c <= 0 {
+ panic("triplet: invalid shape")
+ }
+ return &Matrix{
+ r: r,
+ c: c,
+ }
+}
+
+func (m *Matrix) Dims() (r, c int) {
+ return m.r, m.c
+}
+
+// Append appends a non-zero element to the list of matrix elements without
+// checking whether it already exists.
+func (m *Matrix) Append(i, j int, v float64) {
+ if i < 0 || m.r <= i {
+ panic("triplet: row index out of range")
+ }
+ if j < 0 || m.c <= j {
+ panic("triplet: column index out of range")
+ }
+ if v == 0 {
+ return
+ }
+ m.data = append(m.data, triplet{i, j, v})
+}
+
+func (m *Matrix) MulVecTo(dst *mat.VecDense, trans bool, x mat.Vector) {
+ dst.Zero()
+ if trans {
+ if m.c != dst.Len() || m.r != x.Len() {
+ panic("triplet: dimension mismatch")
+ }
+ for _, aij := range m.data {
+ dst.SetVec(aij.j, dst.AtVec(aij.j)+aij.v*x.AtVec(aij.i))
+ }
+ return
+ }
+ if m.c != x.Len() || m.r != dst.Len() {
+ panic("triplet: dimension mismatch")
+ }
+ for _, aij := range m.data {
+ dst.SetVec(aij.i, dst.AtVec(aij.i)+aij.v*x.AtVec(aij.j))
+ }
+}
+
+func (m *Matrix) DenseCopy() *mat.Dense {
+ d := mat.NewDense(m.r, m.c, nil)
+ for _, aij := range m.data {
+ d.Set(aij.i, aij.j, aij.v)
+ }
+ return d
+}
diff --git a/linsolve/iterative.go b/linsolve/iterative.go
new file mode 100644
index 0000000..c49f1c7
--- /dev/null
+++ b/linsolve/iterative.go
@@ -0,0 +1,266 @@
+// 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)
+}
diff --git a/linsolve/iterative_example_test.go b/linsolve/iterative_example_test.go
new file mode 100644
index 0000000..70b8d54
--- /dev/null
+++ b/linsolve/iterative_example_test.go
@@ -0,0 +1,81 @@
+// Copyright ©2019 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_test
+
+import (
+ "fmt"
+ "math"
+
+ "gonum.org/v1/gonum/floats"
+ "gonum.org/v1/gonum/linsolve"
+ "gonum.org/v1/gonum/mat"
+)
+
+// System represents a linear system with a symmetric band matrix
+// A*x = b
+type System struct {
+ A *mat.SymBandDense
+ B *mat.VecDense
+}
+
+// L2Projection returns a linear system whose solution is the L2 projection of f
+// into the space of piecewise linear functions defined on the given grid.
+//
+// References:
+// - M. Larson, F. Bengzon, The Finite Element Method: Theory,
+// Implementations, and Applications. Springer (2013), Section 1.3, also
+// available at:
+// http://www.springer.com/cda/content/document/cda_downloaddocument/9783642332869-c1.pdf
+func L2Projection(grid []float64, f func(float64) float64) System {
+ n := len(grid)
+
+ // Assemble the symmetric banded mass matrix by iterating over all elements.
+ A := mat.NewSymBandDense(n, 1, nil)
+ for i := 0; i < n-1; i++ {
+ // h is the length of the i-th element.
+ h := grid[i+1] - grid[i]
+ // Add contribution from the i-th element.
+ A.SetSymBand(i, i, A.At(i, i)+h/3)
+ A.SetSymBand(i, i+1, h/6)
+ A.SetSymBand(i+1, i+1, A.At(i+1, i+1)+h/3)
+ }
+
+ // Assemble the load vector by iterating over all elements.
+ b := mat.NewVecDense(n, nil)
+ for i := 0; i < n-1; i++ {
+ h := grid[i+1] - grid[i]
+ b.SetVec(i, b.AtVec(i)+f(grid[i])*h/2)
+ b.SetVec(i+1, b.AtVec(i+1)+f(grid[i+1])*h/2)
+ }
+
+ return System{A, b}
+}
+
+func ExampleIterative() {
+ const (
+ n = 10
+ x0 = 0.0
+ x1 = 1.0
+ )
+ // Make a uniform grid.
+ grid := make([]float64, n+1)
+ floats.Span(grid, x0, x1)
+ sys := L2Projection(grid, func(x float64) float64 {
+ return x * math.Sin(x)
+ })
+
+ result, err := linsolve.Iterative(sys.A, sys.B, &linsolve.CG{}, nil)
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+
+ fmt.Printf("# iterations: %v\n", result.Stats.Iterations)
+ fmt.Printf("Final solution: %.6f\n", mat.Formatted(result.X.T()))
+
+ // Output:
+ // # iterations: 11
+ // Final solution: [-0.003339 0.006677 0.036530 0.085606 0.152981 0.237072 0.337006 0.447616 0.578244 0.682719 0.920847]
+}
diff --git a/linsolve/iterative_test.go b/linsolve/iterative_test.go
new file mode 100644
index 0000000..4456a43
--- /dev/null
+++ b/linsolve/iterative_test.go
@@ -0,0 +1,304 @@
+// 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"
+ "testing"
+
+ "golang.org/x/exp/rand"
+
+ "gonum.org/v1/gonum/floats"
+ "gonum.org/v1/gonum/mat"
+)
+
+func TestDefaultMethodDefaultSettings(t *testing.T) {
+ rnd := rand.New(rand.NewSource(1))
+
+ testCases := spdTestCases(rnd)
+ testCases = append(testCases,
+ nonsym3x3(),
+ nonsymTridiag(100),
+ newGreenbaum54(1, 1, rnd),
+ newGreenbaum54(1, 2, rnd),
+ newGreenbaum54(2, 4, rnd),
+ newGreenbaum54(10, 0, rnd),
+ newGreenbaum54(10, 20, rnd),
+ newGreenbaum54(50, 3, rnd),
+ newGreenbaum73(16, 16, rnd),
+ newPDENonsymmetric(16, 16, rnd),
+ newPDEYang47(16, 16, rnd),
+ newPDEYang48(16, 16, rnd),
+ newPDEYang49(16, 16, rnd),
+ newPDEYang410(16, 16, rnd),
+ newPDEYang412(16, 16, rnd),
+ newPDEYang413(16, 16, rnd),
+ newPDEYang414(16, 16, rnd),
+ newPDEYang415(16, 16, rnd),
+ )
+ for _, tc := range testCases {
+ testMethodWithSettings(t, nil, nil, tc)
+ }
+}
+
+func TestCG(t *testing.T) {
+ rnd := rand.New(rand.NewSource(1))
+
+ testCases := spdTestCases(rnd)
+ for _, tc := range testCases {
+ s := newTestSettings(rnd, tc)
+ testMethodWithSettings(t, &CG{}, s, tc)
+ }
+}
+
+func TestCGDefaultSettings(t *testing.T) {
+ rnd := rand.New(rand.NewSource(1))
+
+ testCases := spdTestCases(rnd)
+ for _, tc := range testCases {
+ testMethodWithSettings(t, &CG{}, nil, tc)
+ }
+}
+
+func TestBiCG(t *testing.T) {
+ rnd := rand.New(rand.NewSource(1))
+
+ testCases := spdTestCases(rnd)
+ testCases = append(testCases,
+ nonsym3x3(),
+ nonsymTridiag(100),
+ newGreenbaum54(1, 1, rnd),
+ newGreenbaum54(1, 2, rnd),
+ newGreenbaum54(2, 4, rnd),
+ newGreenbaum54(10, 0, rnd),
+ newGreenbaum54(10, 20, rnd),
+ newGreenbaum54(50, 3, rnd),
+ newGreenbaum73(16, 16, rnd),
+ newPDEYang47(16, 16, rnd),
+ newPDEYang48(16, 16, rnd),
+ newPDEYang410(16, 16, rnd),
+ newPDEYang413(16, 16, rnd),
+ newPDEYang414(16, 16, rnd),
+ newPDEYang415(16, 16, rnd),
+ )
+ for _, tc := range testCases {
+ s := newTestSettings(rnd, tc)
+ s.Tolerance = 1e-10
+ testMethodWithSettings(t, &BiCG{}, s, tc)
+ }
+}
+
+func TestBiCGDefaultSettings(t *testing.T) {
+ rnd := rand.New(rand.NewSource(1))
+
+ testCases := spdTestCases(rnd)
+ testCases = append(testCases,
+ nonsym3x3(),
+ nonsymTridiag(100),
+ newGreenbaum54(1, 1, rnd),
+ newGreenbaum54(1, 2, rnd),
+ newGreenbaum54(2, 4, rnd),
+ newGreenbaum54(10, 0, rnd),
+ newGreenbaum54(10, 20, rnd),
+ newGreenbaum54(50, 3, rnd),
+ newGreenbaum73(16, 16, rnd),
+ newPDENonsymmetric(16, 16, rnd),
+ newPDEYang47(16, 16, rnd),
+ newPDEYang48(16, 16, rnd),
+ newPDEYang410(16, 16, rnd),
+ newPDEYang413(16, 16, rnd),
+ newPDEYang414(16, 16, rnd),
+ newPDEYang415(16, 16, rnd),
+ )
+ for _, tc := range testCases {
+ testMethodWithSettings(t, &BiCG{}, nil, tc)
+ }
+}
+
+func TestBiCGStab(t *testing.T) {
+ rnd := rand.New(rand.NewSource(1))
+
+ testCases := spdTestCases(rnd)
+ testCases = append(testCases,
+ nonsym3x3(),
+ nonsymTridiag(100),
+ newGreenbaum54(1, 1, rnd),
+ newGreenbaum54(1, 2, rnd),
+ newGreenbaum54(2, 4, rnd),
+ newGreenbaum54(10, 0, rnd),
+ newGreenbaum54(10, 20, rnd),
+ newGreenbaum54(50, 3, rnd),
+ newGreenbaum73(16, 16, rnd),
+ )
+ for _, tc := range testCases {
+ s := newTestSettings(rnd, tc)
+ testMethodWithSettings(t, &BiCGStab{}, s, tc)
+ }
+}
+
+func TestBiCGStabDefaultSettings(t *testing.T) {
+ rnd := rand.New(rand.NewSource(1))
+
+ testCases := spdTestCases(rnd)
+ testCases = append(testCases,
+ nonsym3x3(),
+ nonsymTridiag(100),
+ newGreenbaum54(1, 1, rnd),
+ newGreenbaum54(1, 2, rnd),
+ newGreenbaum54(2, 4, rnd),
+ newGreenbaum54(10, 0, rnd),
+ newGreenbaum54(10, 20, rnd),
+ newGreenbaum54(50, 3, rnd),
+ newGreenbaum73(16, 16, rnd),
+ )
+ for _, tc := range testCases {
+ testMethodWithSettings(t, &BiCGStab{}, nil, tc)
+ }
+}
+
+func TestGMRES(t *testing.T) {
+ rnd := rand.New(rand.NewSource(1))
+
+ testCases := spdTestCases(rnd)
+ testCases = append(testCases,
+ nonsym3x3(),
+ nonsymTridiag(100),
+ newGreenbaum54(1, 1, rnd),
+ newGreenbaum54(1, 2, rnd),
+ newGreenbaum54(2, 4, rnd),
+ newGreenbaum54(10, 0, rnd),
+ newGreenbaum54(10, 20, rnd),
+ newGreenbaum54(50, 3, rnd),
+ newGreenbaum73(16, 16, rnd),
+ newPDENonsymmetric(16, 16, rnd),
+ newPDEYang47(16, 16, rnd),
+ newPDEYang48(16, 16, rnd),
+ newPDEYang49(16, 16, rnd),
+ newPDEYang410(16, 16, rnd),
+ newPDEYang412(16, 16, rnd),
+ newPDEYang413(16, 16, rnd),
+ newPDEYang414(16, 16, rnd),
+ newPDEYang415(16, 16, rnd),
+ )
+ for _, tc := range testCases {
+ s := newTestSettings(rnd, tc)
+ testMethodWithSettings(t, &GMRES{}, s, tc)
+ }
+}
+
+func TestGMRESDefaultSettings(t *testing.T) {
+ rnd := rand.New(rand.NewSource(1))
+
+ testCases := spdTestCases(rnd)
+ testCases = append(testCases,
+ nonsym3x3(),
+ nonsymTridiag(100),
+ newGreenbaum54(1, 1, rnd),
+ newGreenbaum54(1, 2, rnd),
+ newGreenbaum54(2, 4, rnd),
+ newGreenbaum54(10, 0, rnd),
+ newGreenbaum54(10, 20, rnd),
+ newGreenbaum54(50, 3, rnd),
+ newGreenbaum73(16, 16, rnd),
+ newPDENonsymmetric(16, 16, rnd),
+ newPDEYang47(16, 16, rnd),
+ newPDEYang48(16, 16, rnd),
+ newPDEYang49(16, 16, rnd),
+ newPDEYang410(16, 16, rnd),
+ newPDEYang412(16, 16, rnd),
+ newPDEYang413(16, 16, rnd),
+ newPDEYang414(16, 16, rnd),
+ newPDEYang415(16, 16, rnd),
+ )
+ for _, tc := range testCases {
+ testMethodWithSettings(t, &GMRES{}, nil, tc)
+ }
+}
+
+func newTestSettings(rnd *rand.Rand, tc testCase) *Settings {
+ n := len(tc.b)
+
+ // Initial guess is a random vector.
+ initX := mat.NewVecDense(n, nil)
+ for i := 0; i < initX.Len(); i++ {
+ initX.SetVec(i, rnd.NormFloat64())
+ }
+
+ // Allocate a destination vector and fill it with NaN.
+ dst := mat.NewVecDense(n, nil)
+ for i := 0; i < dst.Len(); i++ {
+ dst.SetVec(i, math.NaN())
+ }
+
+ // Preallocate a work context and fill it with NaN.
+ work := NewContext(n)
+ for i := 0; i < work.X.Len(); i++ {
+ work.X.SetVec(i, math.NaN())
+ work.Src.SetVec(i, math.NaN())
+ work.Dst.SetVec(i, math.NaN())
+ }
+ work.ResidualNorm = math.NaN()
+
+ return &Settings{
+ InitX: initX,
+ Dst: dst,
+ Tolerance: tc.tol,
+ MaxIterations: 5 * n,
+ PreconSolve: tc.PreconSolve,
+ Work: work,
+ }
+}
+
+func testMethodWithSettings(t *testing.T, m Method, s *Settings, tc testCase) {
+ wantTol := 1e-9
+ if s == nil {
+ // The default value of Settings.Tolerance is not as low as the tolerance in
+ // individual test cases, therefore we must use a higher tolerance for
+ // the expected accuracy of the computed solution.
+ wantTol = 1e-7
+ }
+
+ n := len(tc.b)
+
+ b := make([]float64, n)
+ copy(b, tc.b)
+ bVec := mat.NewVecDense(n, b)
+
+ result, err := Iterative(&tc, bVec, m, s)
+ if err != nil {
+ t.Errorf("%v: unexpected error %v", tc.name, err)
+ return
+ }
+
+ if !floats.Equal(tc.b, b) {
+ t.Errorf("%v: unexpected modification of b", tc.name)
+ }
+
+ wantVec := mat.NewVecDense(n, tc.want)
+ var diff mat.VecDense
+ diff.SubVec(result.X, wantVec)
+ dist := mat.Norm(&diff, 2) / mat.Norm(wantVec, 2)
+ if dist > wantTol {
+ t.Errorf("%v: unexpected solution, |want-got|/|want|=%v", tc.name, dist)
+ }
+
+ if s == nil {
+ return
+ }
+
+ if s.MaxIterations > 0 && result.Stats.Iterations > s.MaxIterations {
+ t.Errorf("%v: Result.Stats.Iterations greater than Settings.MaxIterations", tc.name)
+ }
+
+ if s.Dst != nil {
+ if !mat.Equal(s.Dst, result.X) {
+ t.Errorf("%v: Settings.Dst and Result.X not equal\n%v\n%v", tc.name, s.Dst, result.X)
+ }
+ result.X.SetVec(0, 123456.7)
+ if s.Dst.AtVec(0) != result.X.AtVec(0) {
+ t.Errorf("%v: Settings.Dst and Result.X are not the same vector", tc.name)
+ }
+ }
+}
diff --git a/linsolve/linsolve.go b/linsolve/linsolve.go
new file mode 100644
index 0000000..ca95043
--- /dev/null
+++ b/linsolve/linsolve.go
@@ -0,0 +1,155 @@
+// 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 (
+ "fmt"
+
+ "gonum.org/v1/gonum/mat"
+)
+
+// BreakdownError signifies that a breakdown occured and the method cannot continue.
+type BreakdownError struct {
+ Value float64
+ Tolerance float64
+}
+
+func (e *BreakdownError) Error() string {
+ return fmt.Sprintf("linsolve: breakdown, value=%v tolerance=%v", e.Value, e.Tolerance)
+}
+
+// Method is an iterative method that produces a sequence of vectors that
+// converge to the solution of the system of linear equations
+// A * x = b,
+// where A is non-singular n×n matrix, and x and b are vectors of dimension n.
+//
+// Method uses a reverse-communication interface between the iterative algorithm
+// and the caller. Method acts as a client that commands the caller to perform
+// needed operations via an Operation returned from the Iterate method. This
+// provides independence of Method on representation of the matrix A, and
+// enables automation of common operations like checking for convergence and
+// maintaining statistics.
+type Method interface {
+ // Init initializes the method for solving an n×n
+ // linear system with an initial estimate x and
+ // the corresponding residual vector.
+ //
+ // Method will not retain x or residual.
+ Init(x, residual *mat.VecDense)
+
+ // Iterate performs a step in converging to the
+ // solution of a linear system.
+ //
+ // Iterate retrieves data from Context, updates it,
+ // and returns the next operation. The caller must
+ // perform the Operation using data in Context, and
+ // depending on the state call Iterate again.
+ Iterate(*Context) (Operation, error)
+}
+
+// Context mediates the communication between the Method and
+// the caller. The caller must not modify Context apart from
+// the commanded Operations.
+type Context struct {
+ // X will be set by Method to the current approximate
+ // solution when it commands ComputeResidual and MajorIteration.
+ X *mat.VecDense
+
+ // ResidualNorm is (an estimate of) a norm of
+ // the residual. Method will set it to the current
+ // value when it commands CheckResidualNorm.
+ ResidualNorm float64
+
+ // Converged indicates to Method whether ResidualNorm
+ // satisfies a stopping criterion as a result of
+ // CheckResidualNorm operation.
+ Converged bool
+
+ // Src and Dst are the source and destination vectors
+ // for various Operations. Src will be set by Method
+ // and the caller must store the result in Dst. See
+ // the Operation documentation for more information.
+ Src, Dst *mat.VecDense
+}
+
+// NewContext returns a new Context for work on problems of dimension n.
+// NewContext will panic if n is not positive.
+func NewContext(n int) *Context {
+ if n <= 0 {
+ panic("linsolve: context size is not positive")
+ }
+ return &Context{
+ X: mat.NewVecDense(n, nil),
+ Src: mat.NewVecDense(n, nil),
+ Dst: mat.NewVecDense(n, nil),
+ }
+}
+
+// Reset reinitializes the Context for work on problems of dimension n.
+// Reset will panic if n is not positive.
+func (ctx *Context) Reset(n int) {
+ if n <= 0 {
+ panic("linsolve: dimension not positive")
+ }
+ ctx.X.Reset()
+ ctx.X.ReuseAsVec(n)
+ ctx.Src.Reset()
+ ctx.Src.ReuseAsVec(n)
+ ctx.Dst.Reset()
+ ctx.Dst.ReuseAsVec(n)
+}
+
+// Operation specifies the type of operation.
+type Operation uint
+
+// Operations commanded by Method.Iterate.
+const (
+ NoOperation Operation = 0
+
+ // Compute A*x where x is stored in Context.Src. The
+ // result must be placed in Context.Dst.
+ MulVec Operation = 1 << (iota - 1)
+
+ // Perform a preconditioner solve
+ // M z = r
+ // where r is stored in Context.Src. The solution z
+ // must be placed in Context.Dst.
+ PreconSolve
+
+ // Trans indicates that MulVec or PreconSolve
+ // operation must be performed wih the transpose,
+ // that is, compute Aᵀ*x or solve Mᵀ*z = r. Method
+ // will command Trans only in bitwise OR combination
+ // with MulVec and PreconSolve.
+ Trans
+
+ // Compute b-A*x where x is stored in Context.X,
+ // and store the result in Context.Dst.
+ ComputeResidual
+
+ // Check convergence using (an estimate of) a
+ // residual norm in Context.ResidualNorm. Context.X
+ // does not need to be valid. The caller must set
+ // Context.Converged to indicate whether convergence
+ // has been determined, and then it must call
+ // Method.Iterate again.
+ CheckResidualNorm
+
+ // MajorIteration indicates that Method has finished
+ // what it considers to be one iteration. Method
+ // will make sure that Context.X is updated.
+ // If Context.Converged is true, the caller must
+ // terminate the iterative process, otherwise it
+ // should call Method.Iterate again.
+ MajorIteration
+)
+
+const (
+ // Machine epsilon.
+ eps = 1.0 / (1 << 53)
+
+ // Breakdown tolerance for BiCG and BiCGStab methods.
+ breakdownTol = eps * eps
+)
diff --git a/linsolve/linsolve_test.go b/linsolve/linsolve_test.go
new file mode 100644
index 0000000..f028bb8
--- /dev/null
+++ b/linsolve/linsolve_test.go
@@ -0,0 +1,762 @@
+// 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 (
+ "fmt"
+ "math"
+
+ "golang.org/x/exp/rand"
+
+ "gonum.org/v1/gonum/lapack/testlapack"
+ "gonum.org/v1/gonum/linsolve/internal/triplet"
+ "gonum.org/v1/gonum/mat"
+)
+
+const defaultTol = 1e-13
+
+type testCase struct {
+ name string
+
+ mulVecTo func(*mat.VecDense, bool, mat.Vector) // Matrix-vector multiplication
+
+ b []float64 // Right-hand side vector
+ diag []float64 // Diagonal for the Jacobi preconditioner
+ tol float64 // Tolerance for the convergence criterion
+
+ want []float64 // Expected solution
+}
+
+func (tc *testCase) MulVecTo(dst *mat.VecDense, trans bool, x mat.Vector) {
+ tc.mulVecTo(dst, trans, x)
+}
+
+func (tc *testCase) PreconSolve(dst *mat.VecDense, trans bool, rhs mat.Vector) error {
+ if tc.diag == nil {
+ dst.CopyVec(rhs)
+ } else {
+ n := len(tc.diag)
+ diag := mat.NewVecDense(n, tc.diag)
+ dst.DivElemVec(rhs, diag)
+ }
+ return nil
+}
+
+func spdTestCases(rnd *rand.Rand) []testCase {
+ return []testCase{
+ newRandomSPD(1, rnd),
+ newRandomSPD(2, rnd),
+ newRandomSPD(3, rnd),
+ newRandomSPD(4, rnd),
+ newRandomSPD(5, rnd),
+ newRandomSPD(10, rnd),
+ newRandomSPD(20, rnd),
+ newRandomSPD(50, rnd),
+ newRandomDiagonal(2, rnd),
+ newRandomDiagonal(3, rnd),
+ newRandomDiagonal(4, rnd),
+ newRandomDiagonal(5, rnd),
+ newRandomDiagonal(10, rnd),
+ newRandomDiagonal(20, rnd),
+ newRandomDiagonal(50, rnd),
+ newGreenbaum41(24, 0.001, 1, 0.4, rnd),
+ newGreenbaum41(24, 0.001, 1, 0.6, rnd),
+ newGreenbaum41(24, 0.001, 1, 0.8, rnd),
+ newGreenbaum41(24, 0.001, 1, 1, rnd),
+ newPoisson1D(32, random(rnd)),
+ newPoisson2D(32, 32, one),
+ }
+}
+
+// newRandomSPD returns a test case with a random symmetric positive-definite
+// matrix of order n, and a random right-hand side.
+func newRandomSPD(n int, rnd *rand.Rand) testCase {
+ // Generate a random matrix C.
+ c := make([]float64, n*n)
+ for i := range c {
+ c[i] = rnd.NormFloat64()
+ }
+ C := mat.NewDense(n, n, c)
+ // Use C to generate a SPD matrix A.
+ var A mat.SymDense
+ A.SymOuterK(1, C)
+ for i := 0; i < n; i++ {
+ A.SetSym(i, i, A.At(i, i)+float64(n))
+ }
+ // Generate the right-hand side.
+ b := make([]float64, n)
+ for i := range b {
+ b[i] = 1 / math.Sqrt(float64(n))
+ }
+ // Compute the solution using the Cholesky factorization.
+ var chol mat.Cholesky
+ ok := chol.Factorize(&A)
+ if !ok {
+ panic("bad test matrix")
+ }
+ want := make([]float64, n)
+ chol.SolveVecTo(mat.NewVecDense(n, want), mat.NewVecDense(n, b))
+ // Matrix-vector multiplication.
+ mulVecTo := func(dst *mat.VecDense, _ bool, x mat.Vector) {
+ if dst.Len() != n || x.Len() != n {
+ panic("mismatched vector length")
+ }
+ dst.MulVec(&A, x)
+ }
+ // Store the diagonal for preconditioning.
+ diag := make([]float64, n)
+ for i := range diag {
+ diag[i] = A.At(i, i)
+ }
+ return testCase{
+ name: fmt.Sprintf("Random SPD n=%v", n),
+ mulVecTo: mulVecTo,
+ b: b,
+ tol: defaultTol,
+ diag: diag,
+ want: want,
+ }
+}
+
+// newRandomDiagonal returns a test case with a diagonal matrix with random positive elements,
+// a random right-hand side and a known solution.
+func newRandomDiagonal(n int, rnd *rand.Rand) testCase {
+ // Generate a diagonal matrix with random positive elements.
+ a := make([]float64, n)
+ diag := make([]float64, n)
+ for i := range a {
+ a[i] = 1 + 10*rnd.Float64()
+ diag[i] = a[i]
+ }
+ A := mat.NewDiagDense(n, a)
+ // Generate the right-hand side.
+ b := make([]float64, n)
+ for i := range b {
+ b[i] = 1 / math.Sqrt(float64(n))
+ }
+ // Compute the reference solution.
+ want := make([]float64, n)
+ for i := range want {
+ want[i] = b[i] / a[i]
+ }
+ // Matrix-vector multiplication.
+ mulVecTo := func(dst *mat.VecDense, _ bool, x mat.Vector) {
+ if dst.Len() != n || x.Len() != n {
+ panic("mismatched vector length")
+ }
+ dst.MulVec(A, x)
+ }
+ return testCase{
+ name: fmt.Sprintf("Random diagonal n=%v", n),
+ mulVecTo: mulVecTo,
+ b: b,
+ tol: defaultTol,
+ diag: diag,
+ want: want,
+ }
+}
+
+// newGreenbaum41 returns a test case with a symmetric positive definite matrix
+// A defined as
+// A = U * D * Uᵀ,
+// where U is a random orthogonal matrix and D is a diagonal matrix with entries
+// given by
+// d_i = d_1 + (i-1)/(n-1)*(d_n-d_1)*rho^{n-i}, i = 2,...,n-1.
+//
+// This matrix is described in Section 4.1 of
+// Greenbaum, A. (1997). Iterative Methods for Solving Linear Systems. SIAM.
+func newGreenbaum41(n int, d1, dn, rho float64, rnd *rand.Rand) testCase {
+ if n < 2 || dn < d1 {
+ panic("bad test")
+ }
+ // Generate the diagonal.
+ d := make([]float64, n)
+ d[0] = d1
+ d[n-1] = dn
+ for i := 1; i < n-1; i++ {
+ d[i] = d1 + float64(i)/float64(n-1)*(dn-d1)*math.Pow(rho, float64(n-i-1))
+ }
+ // Generate the SPD matrix A.
+ a := make([]float64, n*n)
+ testlapack.Dlagsy(n, 0, d, a, n, rnd, make([]float64, 2*n))
+ A := mat.NewSymDense(n, a)
+ // Generate the right-hand side.
+ b := make([]float64, n)
+ for i := range b {
+ b[i] = rnd.NormFloat64()
+ }
+ // Compute the solution using the Cholesky factorization.
+ var chol mat.Cholesky
+ ok := chol.Factorize(A)
+ if !ok {
+ panic("bad test matrix")
+ }
+ want := make([]float64, n)
+ chol.SolveVecTo(mat.NewVecDense(n, want), mat.NewVecDense(n, b))
+ // Matrix-vector multiplication.
+ mulVecTo := func(dst *mat.VecDense, _ bool, x mat.Vector) {
+ if dst.Len() != n || x.Len() != n {
+ panic("mismatched vector length")
+ }
+ dst.MulVec(A, x)
+ }
+ // Store the diagonal for preconditioning.
+ diag := make([]float64, n)
+ for i := range diag {
+ diag[i] = A.At(i, i)
+ }
+ return testCase{
+ name: fmt.Sprintf("Greenbaum 4.1 n=%v,d_1=%v,d_n=%v,rho=%v", n, d1, dn, rho),
+ mulVecTo: mulVecTo,
+ b: b,
+ tol: defaultTol,
+ diag: diag,
+ want: want,
+ }
+}
+
+func nonsym3x3() testCase {
+ return testCase{
+ name: "nonsym 3x3",
+ mulVecTo: func(dst *mat.VecDense, trans bool, x mat.Vector) {
+ A := mat.NewDense(3, 3, []float64{
+ 5, -1, 3,
+ -1, 2, -2,
+ 3, -2, 3,
+ })
+ if trans {
+ dst.MulVec(A.T(), x)
+ } else {
+ dst.MulVec(A, x)
+ }
+ },
+ b: []float64{7, -1, 4},
+ diag: []float64{5, 2, -3},
+ tol: defaultTol,
+ want: []float64{1, 1, 1},
+ }
+}
+
+func nonsymTridiag(n int) testCase {
+ A := triplet.NewMatrix(n, n)
+ for i := 0; i < n; i++ {
+ if i > 0 {
+ A.Append(i, i-1, -2)
+ }
+ A.Append(i, i, 4)
+ if i < n-1 {
+ A.Append(i, i+1, -1)
+ }
+ }
+ b := make([]float64, n)
+ for i := range b {
+ switch i {
+ case 0:
+ b[i] = 3
+ default:
+ b[i] = 1
+ case n - 1:
+ b[i] = 2
+ }
+ }
+ want := make([]float64, n)
+ for i := range want {
+ want[i] = 1
+ }
+ return testCase{
+ name: fmt.Sprintf("Nonsym tridiag n=%v", n),
+ mulVecTo: A.MulVecTo,
+ b: b,
+ tol: defaultTol,
+ want: want,
+ }
+}
+
+// newPoisson1D returns a test case that arises from a finite-difference discretization
+// of the Poisson equation
+// - ∂_x ∂_x u = f
+// on the interval [0,1].
+func newPoisson1D(nx int, f func(float64, float64) float64) testCase {
+ tc := newPDE(nx, 1, negOne, nil, zero, nil, zero, f)
+ tc.name = fmt.Sprintf("Poisson 1D nx=%v", nx)
+ return tc
+}
+
+// newPoisson2D returns a test case that arises from a finite-difference discretization
+// of the Poisson equation
+// - Δu = f
+// on the unit square [0,1]×[0,1].
+func newPoisson2D(nx, ny int, f func(float64, float64) float64) testCase {
+ tc := newPDE(nx, ny, negOne, negOne, zero, zero, zero, f)
+ tc.name = fmt.Sprintf("Poisson 2D nx=%v,ny=%v", nx, ny)
+ tc.tol = 1e-12
+ return tc
+}
+
+// newGreenbaum54 returns a test case with a general unsymmetric matrix
+// A defined as
+// A = V*D*V^{-1},
+// where V is a random matrix and D is a block-diagonal matrix with n1 complex
+// and n2 real eigenvalues.
+//
+// This matrix is described in Section 5.4 of
+// Greenbaum, A. (1997). Iterative Methods for Solving Linear Systems. SIAM.
+func newGreenbaum54(n1, n2 int, rnd *rand.Rand) testCase {
+ n := 2*n1 + n2
+ // Generate the block-diagonal matrix D.
+ d := make([]float64, n*3)
+ // Generate n1 blocks with complex eigenvalues.
+ for i := 0; i < 2*n1; i += 2 {
+ // The 2x2 block will have eigenvalues a±b*i.
+ // Real part of the eigenvalue is in [1,2).
+ a := rnd.Float64() + 1
+ // Imaginary part is in [-1,1).
+ b := 2*rnd.Float64() - 1
+ d[i*3+1] = a
+ d[i*3+2] = b
+ d[(i+1)*3] = -b
+ d[(i+1)*3+1] = a
+ }
+ // Generate n2 real eigenvalues.
+ for i := 2 * n1; i < n; i++ {
+ r := 9*rnd.Float64() + 1
+ if rnd.Intn(2) == 0 {
+ r *= -1
+ }
+ d[i*3+1] = r
+ }
+ D := mat.NewBandDense(n, n, 1, 1, d)
+ // Generate a random matrix V.
+ v := make([]float64, n*n)
+ for i := range v {
+ v[i] = rnd.NormFloat64()
+ }
+ V := mat.NewDense(n, n, v)
+ var luV mat.LU
+ luV.Factorize(V)
+ // Generate the right-hand side.
+ b := make([]float64, n)
+ for i := range b {
+ b[i] = rnd.NormFloat64()
+ }
+ // Compute V*D and (V*D)^{-1} for computing the reference solution and for the matrix-vector operation.
+ var VD mat.Dense
+ VD.Mul(V, D)
+ var luVD mat.LU
+ luVD.Factorize(&VD)
+ // Compute the solution of V*D*V^{-1}*x = b.
+ // First, compute the solution of V*D*y = b.
+ want := make([]float64, n)
+ wantVec := mat.NewVecDense(n, want)
+ err := luVD.SolveVecTo(wantVec, false, mat.NewVecDense(n, b))
+ if err != nil {
+ panic("luVD.SolveVecTo(notrans) failed")
+ }
+ // Second, compute the solution of V^{-1}*x = y, which amounts to just
+ // computing x = V*y.
+ wantVec.MulVec(V, wantVec)
+ // Matrix-vector multiplication.
+ mulVecTo := func(dst *mat.VecDense, trans bool, x mat.Vector) {
+ if dst.Len() != n || x.Len() != n {
+ panic("mismatched vector length")
+ }
+ if trans {
+ // Multiply (V*D*V^{-1})ᵀ * x which can be
+ // rewritten as V^{-1}ᵀ * (V*D)ᵀ * x.
+ dst.MulVec(VD.T(), x)
+ err := luV.SolveVecTo(dst, true, dst)
+ if err != nil {
+ panic("luV.SolveVecTo(trans) failed")
+ }
+ } else {
+ // Multiply V*D*V^{-1} * x.
+ err := luV.SolveVecTo(dst, false, x)
+ if err != nil {
+ panic("luV.SolveVecTo(notrans) failed")
+ }
+ dst.MulVec(&VD, dst)
+ }
+ }
+ return testCase{
+ name: fmt.Sprintf("Greenbaum 5.4 n=%v,n1=%v,n2=%v", n, n1, n2),
+ mulVecTo: mulVecTo,
+ b: b,
+ tol: defaultTol,
+ want: want,
+ }
+}
+
+// newGreenbaum73 returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// - Δu + 40*(x*∂_x u + y*∂_y u) - 100*u = f
+//
+// This test problem is described in Section 7.3 of
+// Greenbaum, A. (1997). Iterative Methods for Solving Linear Systems. SIAM.
+func newGreenbaum73(nx, ny int, rnd *rand.Rand) testCase {
+ tc := newPDE(nx, ny,
+ negOne, negOne, // - ∂_x ∂_x u - ∂_y ∂_y u
+ func(x, _ float64) float64 { return 40 * x }, // 40 * x * ∂_x u
+ func(_, y float64) float64 { return 40 * y }, // 40 * y * ∂_y u
+ constant(-100), // -100 * u
+ random(rnd))
+ tc.name = fmt.Sprintf("Greenbaum 7.3 nx=%v,ny=%v", nx, ny)
+ return tc
+}
+
+// newPDENonsymmetric returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// Δu + henk*∂_x u + (∂_x henk/2)*u = f
+// where henk(x,y) := 20*exp(3.5*(x^2 + y^2))
+//
+// This test case is adapted from
+// http://www.netlib.org/templates/dftemplates.tgz
+func newPDENonsymmetric(nx, ny int, rnd *rand.Rand) testCase {
+ tc := newPDE(nx, ny, one, one, henk, zero, dhenkdx, random(rnd))
+ tc.name = fmt.Sprintf("PDE Nonsymmetric nx=%v,ny=%v", nx, ny)
+ return tc
+}
+
+func henk(x, y float64) float64 {
+ return 20 * math.Exp(3.5*(x*x+y*y))
+}
+
+func dhenkdx(x, y float64) float64 {
+ return 70 * x * math.Exp(3.5*(x*x+y*y))
+}
+
+// newPDEYang47 returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// Δu + 1000*∂_x u = f
+// The large coefficient of ∂_xu causes a loss of diagonal dominance in the system matrix.
+//
+// This test case corresponds to Eq. 4.7 in
+// Ulrike Meier Yang (1994), Preconditioned Conjugate Gradient-Like Methods for Nonsymmetric Linear Systems.
+func newPDEYang47(nx, ny int, rnd *rand.Rand) testCase {
+ tc := newPDE(nx, ny, one, one, constant(1000), zero, zero, random(rnd))
+ tc.name = fmt.Sprintf("PDE Yang, Eq. 4.7 nx=%v,ny=%v", nx, ny)
+ return tc
+}
+
+// newPDEYang48 returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// Δu + 1000*∂_y u = f
+// The large coefficient of ∂_yu causes a loss of diagonal dominance in the system matrix.
+//
+// This test case corresponds to Eq. 4.8 in
+// Ulrike Meier Yang (1994), Preconditioned Conjugate Gradient-Like Methods for Nonsymmetric Linear Systems.
+func newPDEYang48(nx, ny int, rnd *rand.Rand) testCase {
+ tc := newPDE(nx, ny, one, one, zero, constant(1000), zero, random(rnd))
+ tc.name = fmt.Sprintf("PDE Yang, Eq. 4.8 nx=%v,ny=%v", nx, ny)
+ return tc
+}
+
+// newPDEYang49 returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// Δu + 1000*exp(x*y)*(∂_x u - ∂_y u) = f
+//
+// This test case corresponds to Eq. 4.9 in
+// Ulrike Meier Yang (1994), Preconditioned Conjugate Gradient-Like Methods for Nonsymmetric Linear Systems.
+func newPDEYang49(nx, ny int, rnd *rand.Rand) testCase {
+ tc := newPDE(16, 16,
+ one, one,
+ func(x, y float64) float64 { return 1000 * math.Exp(x*y) },
+ func(x, y float64) float64 { return -1000 * math.Exp(x*y) },
+ zero, random(rnd))
+ tc.name = fmt.Sprintf("PDE Yang, Eq. 4.9 nx=%v,ny=%v", nx, ny)
+ return tc
+}
+
+// newPDEYang410 returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// ∂_x (exp(x*y) * ∂_x u) + ∂_y (exp(-x*y) * ∂_y u) - 500*(x + y)*∂_x u - (250*(1 + y) + 1/(1 + x + y))*u = f
+//
+// This test case corresponds to Eq. 4.10 in
+// Ulrike Meier Yang (1994), Preconditioned Conjugate Gradient-Like Methods for Nonsymmetric Linear Systems.
+func newPDEYang410(nx, ny int, rnd *rand.Rand) testCase {
+ tc := newPDE(nx, ny,
+ func(x, y float64) float64 { return math.Exp(x * y) }, // ∂_x (exp(x*y) * ∂_x u)
+ func(x, y float64) float64 { return math.Exp(-x * y) }, // ∂_y (exp(-x*y) * ∂_y u)
+ func(x, y float64) float64 { return -500 * (x + y) }, // -500*(x + y)*∂_x u
+ zero,
+ func(x, y float64) float64 { return -250*(1+y) - 1/(1+x+y) }, // - (250*(1 + y) + 1/(1+x+y)) * u
+ random(rnd))
+ tc.name = fmt.Sprintf("PDE Yang, Eq. 4.10 nx=%v,ny=%v", nx, ny)
+ return tc
+}
+
+// newPDEYang412 returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// Δu - 100000*x^2*(∂_x u + ∂_y u) = f
+//
+// This test case corresponds to Eq. 4.12 in
+// Ulrike Meier Yang (1994), Preconditioned Conjugate Gradient-Like Methods for Nonsymmetric Linear Systems.
+func newPDEYang412(nx, ny int, rnd *rand.Rand) testCase {
+ tc := newPDE(nx, ny,
+ one, one,
+ func(x, _ float64) float64 { return -100000 * x * x },
+ func(x, _ float64) float64 { return -100000 * x * x },
+ zero, random(rnd))
+ tc.name = fmt.Sprintf("PDE Yang, Eq. 4.12 nx=%v,ny=%v", nx, ny)
+ return tc
+}
+
+// newPDEYang413 returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// Δu - 1000*(1 + x^2)*∂_x u + 100*∂_y u = f
+//
+// This test case corresponds to Eq. 4.13 in
+// Ulrike Meier Yang (1994), Preconditioned Conjugate Gradient-Like Methods for Nonsymmetric Linear Systems.
+func newPDEYang413(nx, ny int, rnd *rand.Rand) testCase {
+ tc := newPDE(nx, ny,
+ one, one,
+ func(x, _ float64) float64 { return -1000 * (1 + x*x) },
+ constant(100),
+ zero, random(rnd))
+ tc.name = fmt.Sprintf("PDE Yang, Eq. 4.13 nx=%v,ny=%v", nx, ny)
+ return tc
+}
+
+// newPDEYang414 returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// Δu - 1000*x^2*∂_x u + 1000*u = f
+//
+// This test case corresponds to Eq. 4.14 in
+// Ulrike Meier Yang (1994), Preconditioned Conjugate Gradient-Like Methods for Nonsymmetric Linear Systems.
+func newPDEYang414(nx, ny int, rnd *rand.Rand) testCase {
+ tc := newPDE(nx, ny,
+ one, one,
+ func(x, _ float64) float64 { return -1000 * x * x },
+ zero,
+ constant(1000), random(rnd))
+ tc.name = fmt.Sprintf("PDE Yang, Eq. 4.14 nx=%v,ny=%v", nx, ny)
+ return tc
+}
+
+// newPDEYang415 returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// Δu - 1000*(1 - 2*x)*∂_x u - 1000*(1 - 2*y)*∂_y u = f
+//
+// This test case corresponds to Eq. 4.15 in
+// Ulrike Meier Yang (1994), Preconditioned Conjugate Gradient-Like Methods for Nonsymmetric Linear Systems.
+func newPDEYang415(nx, ny int, rnd *rand.Rand) testCase {
+ tc := newPDE(nx, ny,
+ one, one,
+ func(x, _ float64) float64 { return -1000 * (1 - 2*x) },
+ func(_, y float64) float64 { return -1000 * (1 - 2*y) },
+ zero, random(rnd))
+ tc.name = fmt.Sprintf("PDE Yang, Eq. 4.15 nx=%v,ny=%v", nx, ny)
+ return tc
+}
+
+// newPDE returns a test case that arises from a finite-difference discretization
+// of the partial differential equation
+// ∂_x (a ∂_x u) + ∂_y (b ∂_y u) + c ∂_x u + d ∂_y u + e u = f
+// on the unit square [0,1]×[0,1] with zero Dirichlet boundary conditions.
+//
+// nx and ny must be positive. If ny is 1, a 1D variant of the equation on [0,1]×{0} will be used,
+// and b and d will not be referenced.
+func newPDE(nx, ny int, a, b, c, d, e, f func(float64, float64) float64) testCase {
+ if nx <= 0 || ny <= 0 {
+ panic("invalid mesh size")
+ }
+
+ var (
+ A *triplet.Matrix
+ rhs []float64
+ diag []float64
+ )
+ if ny == 1 {
+ A, rhs, diag = newPDESystem1D(nx, a, c, e, f)
+ } else {
+ A, rhs, diag = newPDESystem2D(nx, ny, a, b, c, d, e, f)
+ }
+ // Use a dense solver to obtain a reference solution.
+ Ad := A.DenseCopy()
+ var lu mat.LU
+ lu.Factorize(Ad)
+ n := len(rhs)
+ want := make([]float64, n)
+ err := lu.SolveVecTo(mat.NewVecDense(n, want), false, mat.NewVecDense(n, rhs))
+ if err != nil {
+ panic("lu.SolveVec failed")
+ }
+ return testCase{
+ mulVecTo: A.MulVecTo,
+ b: rhs,
+ tol: defaultTol,
+ diag: diag,
+ want: want,
+ }
+}
+
+// newPDESystem1D assembles and returns the matrix A, the right-hand side vector,
+// and the diagonal of A for a 1-dimensional PDE problem.
+func newPDESystem1D(nx int, a, b, c, f func(float64, float64) float64) (A *triplet.Matrix, rhs, diag []float64) {
+ h := 1 / float64(nx+1)
+ A = triplet.NewMatrix(nx, nx)
+ rhs = make([]float64, nx)
+ diag = make([]float64, nx)
+ var i int
+ for ix := 0; ix < nx; ix++ {
+ s := newStencil1D(ix, h, a, b, c)
+ // Add stencil elements to the system matrix, skipping boundary nodes.
+ if ix > 0 {
+ A.Append(i, i-1, s.left)
+ }
+ A.Append(i, i, s.center)
+ diag[i] = s.center
+ if ix < nx-1 {
+ A.Append(i, i+1, s.right)
+ }
+ // Assemble the right-hand side.
+ x := float64(ix+1) * h
+ rhs[i] = f(x, 0) * h * h
+ i++
+ }
+ return A, rhs, diag
+}
+
+type stencil1D struct {
+ left, right float64
+ center float64
+}
+
+// newStencil1D returns a finite difference stencil that approximates the differential operator
+// ∂_x (a ∂_x u) + b ∂_x u + c u
+// at point [(i+1)*h] using the formula
+// a(i+1/2,0)*(u(i+1) - u(i)) + a(i-1/2,0)*(u(i-1) - u(i))
+// + (h/2)*b(i,0)*(u(i+1) - u(i-1))
+// + h^2*c(i,0)*u(i)
+func newStencil1D(i int, h float64, a, b, c func(float64, float64) float64) (s stencil1D) {
+ x := float64(i+1) * h
+
+ coeff := a(x+0.5*h, 0)
+ s.center -= coeff
+ s.right = coeff
+ coeff = a(x-0.5*h, 0)
+ s.center -= coeff
+ s.left = coeff
+
+ coeff = b(x, 0)
+ s.right += 0.5 * h * coeff
+ s.left -= 0.5 * h * coeff
+
+ s.center += h * h * c(x, 0)
+
+ return s
+}
+
+// newPDESystem2D assembles and returns the matrix A, the right-hand side vector,
+// and the diagonal of A for a 2-dimensional PDE problem.
+func newPDESystem2D(nx, ny int, a, b, c, d, e, f func(float64, float64) float64) (A *triplet.Matrix, rhs, diag []float64) {
+ // Finite difference stencil:
+ // * (ix,iy+1)
+ // |
+ // | (ix,iy)
+ // * ---- * ---- *
+ // (ix-1,iy) | (ix+1,iy)
+ // |
+ // * (ix,iy-1)
+ // Node (ix,iy) is mapped to the index ix+iy*k.
+
+ h := 1 / float64(nx+1)
+ n := nx * ny
+ A = triplet.NewMatrix(n, n)
+ rhs = make([]float64, n)
+ diag = make([]float64, n)
+ var i int
+ for iy := 0; iy < ny; iy++ {
+ y := float64(iy+1) * h
+ for ix := 0; ix < nx; ix++ {
+ s := newStencil2D(ix, iy, h, a, b, c, d, e)
+ // Add the coefficients from the stencil to the system matrix, skipping boundary nodes.
+ if iy > 0 {
+ A.Append(i, i-nx, s.down)
+ }
+ if ix > 0 {
+ A.Append(i, i-1, s.left)
+ }
+ A.Append(i, i, s.center)
+ diag[i] = s.center
+ if ix < nx-1 {
+ A.Append(i, i+1, s.right)
+ }
+ if iy < ny-1 {
+ A.Append(i, i+nx, s.up)
+ }
+ // Assemble the right-hand side.
+ x := float64(ix+1) * h
+ rhs[i] = f(x, y) * h * h
+ i++
+ }
+ }
+ return A, rhs, diag
+}
+
+type stencil2D struct {
+ left, right float64
+ up, down float64
+ center float64
+}
+
+// newStencil2D returns a finite difference stencil that approximates the differential operator
+// ∂_x (a ∂_x u) + ∂_y (b ∂_y u) + c ∂_x u + d ∂_y u + e u
+// at point [(i+1)*h,(j+1)*h] using the formula
+// a(i+1/2,j)*(u(i+1,j) - u(i,j)) + a(i-1/2,j)*(u(i-1,j) - u(i,j))
+// + b(i,j+1/2)*(u(i,j+1) - u(i,j)) + b(i,j-1/2)*(u(i,j-1) - u(i,j))
+// + (h/2)*c(i,j)*(u(i+1,j) - u(i-1,j)) + (h/2)*d(i,j)*(u(i,j+1) - u(i,j-1))
+// + h^2*e(i,j)*u(i,j)
+func newStencil2D(i, j int, h float64, a, b, c, d, e func(float64, float64) float64) (s stencil2D) {
+ x := float64(i+1) * h
+ y := float64(j+1) * h
+
+ coeff := a(x+0.5*h, y)
+ s.center -= coeff
+ s.right = coeff
+ coeff = a(x-0.5*h, y)
+ s.center -= coeff
+ s.left = coeff
+ coeff = b(x+0.5*h, y)
+ s.center -= coeff
+ s.up = coeff
+ coeff = b(x-0.5*h, y)
+ s.center -= coeff
+ s.down = coeff
+
+ coeff = c(x, y)
+ s.right += 0.5 * h * coeff
+ s.left -= 0.5 * h * coeff
+ coeff = d(x, y)
+ s.up += 0.5 * h * coeff
+ s.down -= 0.5 * h * coeff
+
+ s.center += h * h * c(x, y)
+
+ return s
+}
+
+func zero(_, _ float64) float64 {
+ return 0
+}
+
+func one(_, _ float64) float64 {
+ return 1
+}
+
+func negOne(_, _ float64) float64 {
+ return -1
+}
+
+func constant(c float64) func(_, _ float64) float64 {
+ return func(_, _ float64) float64 {
+ return c
+ }
+}
+
+func random(rnd *rand.Rand) func(_, _ float64) float64 {
+ return func(_, _ float64) float64 {
+ return rnd.NormFloat64()
+ }
+}
diff --git a/linsolve/pde_example_test.go b/linsolve/pde_example_test.go
new file mode 100644
index 0000000..292c1ac
--- /dev/null
+++ b/linsolve/pde_example_test.go
@@ -0,0 +1,256 @@
+// Copyright ©2019 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_test
+
+import (
+ "fmt"
+ "log"
+
+ "golang.org/x/exp/rand"
+
+ "gonum.org/v1/gonum/linsolve"
+ "gonum.org/v1/gonum/mat"
+ "gonum.org/v1/plot"
+ "gonum.org/v1/plot/plotter"
+ "gonum.org/v1/plot/plotutil"
+ "gonum.org/v1/plot/vg"
+)
+
+// AllenCahnFD implements a semi-implicit finite difference scheme for the
+// solution of the one-dimensional Allen-Cahn equation
+// u_t = u_xx - 1/ξ²·f'(u) in (0,L)×(0,T)
+// u_x = 0 on (0,T)
+// u(0) = u0 on (0,L)
+// where f is a double-well-shaped function with two minima at ±1
+// f(s) = 1/4·(s²-1)²
+//
+// The equation arises in materials science in the description of phase
+// transitions, e.g. solidification in crystal growth, but also in other areas
+// like image processing due to its connection to mean-curvature motion.
+// Starting the evolution from an initial distribution u0, the solution u
+// develops a thin steep layer, an interface between regions of the domain
+// (0,L) where u is constant and close to one of the minima of f.
+//
+// AllenCahnFD approximates derivatives by finite differences and the solution
+// is advanced in time by a semi-implicit Euler scheme where the nonlinear term
+// is taken from the previous time step. Therefore, at each time step a linear
+// system must be solved.
+type AllenCahnFD struct {
+ // Xi is the ξ parameter that determines the interface width.
+ Xi float64
+
+ // InitCond is the initial condition u0.
+ InitCond func(x float64) float64
+
+ h float64 // Spatial step size
+ tau float64 // Time step size
+
+ a *mat.SymBandDense
+ b *mat.VecDense
+ u *mat.VecDense
+
+ ls linsolve.Method
+ lssettings linsolve.Settings
+}
+
+// FPrime returns the value of the derivative of the double-well potential f at s.
+// f'(s) = s·(s²-1)
+func FPrime(s float64) float64 {
+ return s * (s*s - 1)
+}
+
+// Setup initializes the receiver for solving the Allen-Cahn equation on a
+// uniform grid with n+1 nodes on the spatial interval (0,L) and with the time
+// step size tau.
+func (ac *AllenCahnFD) Setup(n int, L float64, tau float64) {
+ ac.h = L / float64(n)
+ ac.tau = tau
+
+ // We solve this PDE numerically by replacing the derivatives with finite
+ // differences. For the spatial derivative, we use a central difference
+ // scheme, and for the time derivative derivative we use semi-implicit Euler
+ // integration where the non-linear term with f' is taken from the previous
+ // time step.
+ //
+ // After replacing the derivatives we get
+ // 1/tau*(u^{k+1}_i - u^k_i) = 1/h²*(u^{k+1}_{i-1} - 2*u^{k+1}_i + u^{k+1}_{i+1}) - 1/ξ²*f'(u^k_i)
+ // where tau is the time step size, h is the spatial step size, and u^k_i
+ // denotes the numerical solution that approximates u at time level k and
+ // grid node i, that is,
+ // u^k_i ≅ u(k*tau,i*h)
+ // Multiplying the equation by tau and collecting the terms from the same
+ // time level on each side gives
+ // -tau/h²*u^{k+1}_{i-1} + (1+2*tau/h²)*u^{k+1}_i - tau/h²*u^{k+1}_{i+1}) = u^k_i - tau/ξ²*f'(u^k_i)
+ // If we denote C:=tau/h² we can simplify this to
+ // -C*u^{k+1}_{i-1} + (1+2*C)*u^{k+1}_i - C*u^{k+1}_{i+1} = u^k_i - tau/ξ²*f'(u^k_i) (1)
+ // which must hold for all i=0,...,n.
+ //
+ // When i=0 or i=n, the expression (1) refers to values at nodes -1 and n+1
+ // which lie outside of the domain. We can eliminate them by using the fact
+ // that the first derivative is zero at the boundary. We approximate it by
+ // central difference:
+ // -1/h*(u^{k+1}_{-1} - u^{k+1}_1) = 0
+ // 1/h*(u^{k+1}_{n+1} - u^{k+1}_{n-1}) = 0
+ // which after simplifying gives
+ // u^{k+1}_{-1} = u^{k+1}_1
+ // u^{k+1}_{n+1} = u^{k+1}_{n-1}
+ // By substituting these two expressions into (1) at i=0 and i=n values at
+ // outside nodes do not appear in the expressions. If we further divide them
+ // by 2 (to obtain a symmetric matrix), we finally get
+ // (1/2+C)*u^{k+1}_0 - C*u^{k+1}_1 = 1/2*u^k_0 - 1/2*tau/ξ²*f'(u^k_0) (2)
+ // -C*u^{k+1}_{n-1} + (1/2+C)*u^{k+1}_n = 1/2*u^k_n - 1/2*tau/ξ²*f'(u^k_n) (3)
+ // Note that simply means that we treat values at the boundary nodes the
+ // same as the inner nodes.
+ //
+ // For a given k the equations (1),(2),(3) form a linear system for the
+ // unknown vector [u^{k+1}_i], i=0,...,n that must be solved at each step in
+ // order to advance the solution in time. The matrix of this system is
+ // tridiagonal and symmetric positive-definite:
+ // ⎛1/2+C -C 0 . . . . 0⎞
+ // ⎜ -C 1+2C -C .⎟
+ // ⎜ 0 -C 1+2C -C .⎟
+ // ⎜ . -C . . .⎟
+ // ⎜ . . . . .⎟
+ // ⎜ . . . -C 0⎟
+ // ⎜ . -C 1+2C -C⎟
+ // ⎝ 0 . . . . 0 -C 1/2+C⎠
+ // The right-hand side is:
+ // ⎛1/2*u^k_0 - 1/2*tau/ξ²*f'(u^k_0) ⎞
+ // ⎜ u^k_1 - tau/ξ²*f'(u^k_1) ⎟
+ // ⎜ u^k_2 - tau/ξ²*f'(u^k_2) ⎟
+ // ⎜ . ⎟
+ // ⎜ . ⎟
+ // ⎜ . ⎟
+ // ⎜ u^k_{n-1} - tau/ξ²*f'(u^k_{n-1})⎟
+ // ⎝1/2*u^k_n - 1/2*tau/ξ²*f'(u^k_n) ⎠
+
+ // Assemble the system matrix A based on the discretization scheme above.
+ // Since the matrix is symmetric, we only need to set elements in the upper
+ // triangle.
+ A := mat.NewSymBandDense(n+1, 1, nil)
+ c := ac.tau / ac.h / ac.h
+ // Boundary condition at the left node
+ A.SetSymBand(0, 0, 0.5+c)
+ A.SetSymBand(0, 1, -c)
+ // Interior nodes
+ for i := 1; i < n; i++ {
+ A.SetSymBand(i, i, 1+2*c)
+ A.SetSymBand(i, i+1, -c)
+ }
+ // Boundary condition at the right node
+ A.SetSymBand(n, n, 0.5+c)
+ ac.a = A
+
+ // Allocate the right-hand side b.
+ ac.b = mat.NewVecDense(n+1, nil)
+
+ // Allocate and set up the initial condition.
+ ac.u = mat.NewVecDense(n+1, nil)
+ for i := 0; i < ac.u.Len(); i++ {
+ ac.u.SetVec(i, ac.InitCond(float64(i)*ac.h))
+ }
+
+ // Allocate the linear solver and the settings.
+ ac.ls = &linsolve.CG{}
+ ac.lssettings = linsolve.Settings{
+ // Solution from the previous time step will be a good initial estimate.
+ InitX: ac.u,
+ // Store the solution into the existing vector.
+ Dst: ac.u,
+ // Provide context to reduce memory allocation and GC pressure.
+ Work: linsolve.NewContext(n + 1),
+ }
+}
+
+// Step advances the solution one step in time.
+func (ac *AllenCahnFD) Step() error {
+ // Assemble the right-hand side vector b.
+ tauXi2 := ac.tau / ac.Xi / ac.Xi
+ n := ac.u.Len()
+ for i := 0; i < ac.u.Len(); i++ {
+ ui := ac.u.AtVec(i)
+ bi := ui - tauXi2*FPrime(ui)
+ if i == 0 || i == n-1 {
+ bi *= 0.5
+ }
+ ac.b.SetVec(i, bi)
+ }
+ // Solve the system.
+ _, err := linsolve.Iterative(ac, ac.b, ac.ls, &ac.lssettings)
+ return err
+}
+
+// MulVecTo implements the MulVecToer interface.
+func (ac *AllenCahnFD) MulVecTo(dst *mat.VecDense, _ bool, x mat.Vector) {
+ dst.MulVec(ac.a, x)
+}
+
+func (ac *AllenCahnFD) Solution() *mat.VecDense {
+ return ac.u
+}
+
+func output(u mat.Vector, L float64, step int) error {
+ p, err := plot.New()
+ if err != nil {
+ return err
+ }
+
+ p.Title.Text = fmt.Sprintf("Step %d", step)
+ p.X.Label.Text = "x"
+ p.X.Min = 0
+ p.X.Max = L
+ p.Y.Min = -1.1
+ p.Y.Max = 1.1
+
+ n := u.Len()
+ h := L / float64(n-1)
+ pts := make(plotter.XYs, n)
+ for i := 0; i < u.Len(); i++ {
+ pts[i].X = float64(i) * h
+ pts[i].Y = u.AtVec(i)
+ }
+ err = plotutil.AddLines(p, "u", pts)
+ if err != nil {
+ return err
+ }
+ err = p.Save(20*vg.Centimeter, 10*vg.Centimeter, fmt.Sprintf("u%04d.png", step))
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func ExampleIterative_evolutionPDE() {
+ const (
+ L = 10.0
+ nx = 1000
+ nt = 200
+ tau = 0.1 * L / nx
+ xi = 6.0 * L / nx
+ )
+ rnd := rand.New(rand.NewSource(1))
+ ac := AllenCahnFD{
+ Xi: xi,
+ InitCond: func(x float64) float64 {
+ // Initial condition is a perturbation of the (unstable) zero state
+ // (the peak in the double-well function f).
+ return 0.01 * rnd.NormFloat64()
+ },
+ }
+ ac.Setup(nx, L, tau)
+ for i := 1; i <= nt; i++ {
+ err := ac.Step()
+ if err != nil {
+ log.Fatal(err)
+ }
+ // Generate plots of u as PNG images that can be converted to video
+ // using for example
+ // ffmpeg -i u%04d.png output.webm
+ err = output(ac.Solution(), L, i)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+}