blob: 292c1ac6bf97c5f8b12c4979f33c0219e50cc8e6 [file] [log] [blame]
// 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)
}
}
}