Lecture 13#

Time-dependent variational methods#

In lecture 11 and lecture 12 we have focused on the spatial discretization of steady (time-independent) differential equations. We will now add time-dependence and thus consider equations like

ut=L(u)+f,2ut2=L(u)+f.

The solution u(x,t) is now a function of both space x and time t, and naturally, the approximation will be as well. However, we will put all the time dependence in the expansion coefficients

u(x,t)uN(x,t)=j=0Nu^j(t)ψj(x),

and the spatial dependence in the basis functions. As such, we still use the same (time-independent) function space VN=span{ψj}j=0N, and the above sum is equivalent to the expansion uN(x,t)VN.

The most common approach for handling time-dependent problems with variational methods is to use a finite difference method for the time-derivative, and then subsequently apply the method of weighted residuals. For example, in order to solve

ut=L(u)+f,

with the forward Euler method in time, we discretize first as

(67)#un+1unΔt=L(un)+fn,

where un(x)=u(x,tn), fn(x)=f(x,tn) and tn=nΔt.

Note

For simplicity we normally drop the spatial dependence and write only un and fn, even though we mean un(x) and fn(x).

The solution un is known and un+1 is the unknown we are after. As always the Euler method needs to initialize the solution u0 in order to get started.

A residual can now be defined as

R(un+1;un)=un+1unΔtL(un)fn,

and as always we want this residual to be zero.

Note

The notation R(un+1;un) is used to indicate that un+1 is the unknown and un (right of the semicolon) is known.

We now introduce the approximations to uk(x)=u(x,tk), which for given integer k is found in the space VN

u(x,tk)uNk=j=0Nu^jkψj(x).

Inserted into the residual we get

RN=R(uNn+1;uNn)=uNn+1uNnΔtL(uNn)fn.

The method of weighted residuals is to find uNn+1VN such that

(RN,v)=0,vW,

and for the Galerkin method W=VN.

In order to solve the time-dependent problem we need to solve this Galerkin problem many times. The procedure is

  1. Initialize uN0(x)

  2. For n=0,1,,Nt1 find uNn+1VN such that

(RN,v)=0,vVN.

Here Nt is the number of time steps such that NtΔt=T and t(0,T].

Note

By initializing uN0(x)=u0(x), we need to project u0(x) to VN and store the expansion coefficients u^0. This is a regular function approximation, like in lectures 8 and 9. For the finite difference method, on the other hand, initialization is simply to evaluate (interpolate) the solution on the computational mesh: (u0(xi))i=0N. The finite element method can do either project or interpolate, because the FEM is using Lagrange polynomials such that uN(xi)=u^i.

Stability#

Just like for the finite difference method we need to be very careful with the time step Δt, in order to obtain a stable solution. A stable solution is one that remains bounded as t. However, there is one major difference from the finite difference or collocation methods. The Galerkin method described above does not involve a mesh size Δx! So the Courant number is not defined. How do we overcome this problem? In order to describe this problem it is helpful to first revisit the stability of the finite difference method.

Stability of the forward Euler method for finite difference spatial discretizations#

We start by considering (67) with finite difference discretizations for both temporal and spatial derivatives:

ujn+1ujn=ΔtL(un)j,

where, for example, L(un)j=(uj+1n2ujn+uj1n)/Δx2 if L represents the second derivative 2/x2. For simplicity we have assumed only one spatial dimension and the mesh function is thus ujn=u(xj,tn), where xj=jΔx and tn=nΔt. The solution vector at a given time step n is given as un=(ujn)j=0N.

With the finite difference method we assume that the solution can be written as an exponential decay in time and periodic in space (the Fourier wave eikxj is periodic in space)

ujn=(eaΔt)neikxjandun=(eaΔt)neikx,

where i=1 and k is a wavenumber. The spatial mesh is x=(xj)j=0N and we will write this ansatz as

ujn=gneikxj=gnuj0andun=gneikx=gnu0,

where g=eaΔt is the amplification factor, which is independent of space and time, and only depends on Δt and Δx. We now let the operator L be the finite difference m’th derivative operator D(m)=(dij(m))i,j=0N, such that

ujn+1ujn=Δtkdjk(m)ukn.

For example, if m=2 and we use second order accuracy, then kdjk(2)ukn=(uj+1n2ujn+uj1n)/Δx2. With matrix notation we can write

un+1un=ΔtD(m)un.

Now apply the ansatz un=gnu0, such that

gn+1u0gnu0=ΔtgnD(m)u0.

Divide by gn to obtain

(68)#(g1)u0=ΔtD(m)u0.

Stability requires that |g|1. However, unlike the decay equation discussed in lectures 1 and 2, it is not always a requirement that g>0, because the solution may be oscillatory.

In order to say something more specific about stability, we need to define the finite difference matrix D(m). So lets assume for now that m=2 and use the derivative matrix with periodic boundary conditions

(69)#D(2)=1h2[210000011210000001210000000012100000012110000012]

where h=Δx and D(2)=h2D~(2), where D~(2) is the matrix in (69) without the h2 scaling. We get

(g1)u0=ΔtΔx2D~(2)u0,

and inserting for uj0=eikjΔx we get

(g1)eikjΔx=ΔtΔx2(eik(j+1)Δx2eikjΔx+eik(j1)Δx).

Divide by eikjΔx to obtain

g1=ΔtΔx2(eikΔx2+eikΔx),

and use eix+eix=2cosx, cos(2x)=cos2xsin2x and 1=cos2x+sin2x to obtain

g=14ΔtΔx2sin2(kΔx2).

We still want |g|1, so we need

(70)#|14ΔtΔx2sin2(kΔx2)|1,

and since sin2(kΔx/2)1 and positive, we get that 14ΔtΔx2 is always less than 1. However, 14ΔtΔx2 can be smaller than 1, and that gives us the limit

(71)#14ΔtΔx21,

which is satisfied if Δt/Δx21/2. This is the absolute stability limit for the forward Euler method for the diffusion equation. Note that it involves both Δt and Δx.

We have now found the absolute stability limit for the diffusion equation using the forward Euler method. However, it was quite complicated to compute since we had to discretize in both space and time. You might ask if there is an easier way, and the answer for once is yes. There is actually a generic approach we can take for any differentiation matrix (any value of m in D(m)), and that is to compute its eigenvalues λ, such that

D(m)u0=λu0.

We can insert this into (68) to obtain

(g1)u0=Δtλu0.

which indicates that g1=Δtλ. So we can compute the amplification factor g from the eigenvalues of the difference matrix! Since we require that |g|1, we get that

|1+Δtλ|1,

which is a generic result for the forward Euler method with any difference matrix D(m). Note that λ are the eigenvalues of D(m), which is a matrix that includes Δx. In the case of D(2), we could also find the eigenvalues of D~(2), which would be the same as for D(2), but divided by Δx2. With λ the eigenvalues of D~(2), we get the more specific result for the diffusion equation

|1+ΔtΔx2λ|1.

We can now compute the eigenvalues of D~(2)

import numpy as np
from scipy import sparse

N = 11
D2 = sparse.diags((1, 1, -2, 1, 1), (-N, -1, 0, 1, N), shape=(N+1, N+1))
Lambda = np.linalg.eig(D2.toarray())[0]
print(f"Minimum eigenvalue = {min(Lambda)}")
Minimum eigenvalue = -4.0

We find that the minimum eigenvalue is -4! And we get the same result using any odd N. Hence we get

|14ΔtΔx2|1.

which is the same result as (70) if we set sin2(kΔx/2) to its maximum value of 1.

Stability of the forward Euler method for variational forms#

As described in the start of Stability, the variational methods do not include a mesh size Δx. However, we can use the eigenvalue approach described above without defining a mesh size and we will now use this to find stability limits for variational forms.

First of all we make the same ansatz and assume that the solution uNn+1 can be computed from uNn as

(72)#uNn+1=guNnand thusuNn=gnuN0,

where g is an amplification factor independent of spatial coordinate x. As always a stable method is one where |g|<1.

For the forward Euler method we now consider the discretized Galerkin equation

(73)#(uNn+1,v)=(uNn,v)+Δt(L(uNn),v)

and insert for Eq. (72)

(gn+1uN0,v)=(gnuN0,v)+Δt(L(gnuN0),v)

Divide by gn (g is not dependent on x) to obtain

g(uN0,v)=(uN0,v)+Δt(L(uN0),v).

We now insert for uN0=j=0Nu^j0ψj and v=ψi to obtain

gj=0N(ψj,ψi)u^j0=j=0N(ψj,ψi)u^j0+Δtj=0N(L(ψj),ψi)u^j0

On matrix form this is

gAu^0=Au^0+ΔtMu^0

where A is the mass matrix and mij=(L(ψj),ψi) and M=(mij)i,j=0N. Multiply from the left by A1 to obtain

(74)#gu^0=u^0+ΔtA1Mu^0

We can compute the eigenvalues λ of the matrix H=A1M such that

Hu^0=λu^0,

which can be inserted into (74)

gu^0=u^0+λΔtu^0.

Since the vector u^0 is the same in all three terms, we get that

g=1+λΔt

and for stability |g|1, we need to have |1+λΔt|1.

So with the Galerkin method we do not need the mesh size Δx in order to compute stability limits. On the other hand, we do need to solve an eigenvalue problem to find the eigenvalues λ. In the stability computation, we use the largest eigenvalue. Lets try this out with a complete example, using the diffusion equation.

The diffusion equation#

Consider the diffusion equation with Dirichlet boundary conditions and a prescribed initial condition

(75)#ut=2ux2,x,t(0,L)×(0,T],u(0,t)=p(0),u(L,t)=p(L),u(x,0)=p(x).

In order to solve (75) we choose a global Galerkin method with mapped Legendre polynomials and basis functions ψi(x)=Pi(X)Pi+2(X) such that VN=span{ψi}i=0N. We then use Eq. (73) and create a linear algebra problem by inserting for uNn+1,uNn and v=ψi and using integrating by parts on the diffusion term:

j=0N(ψj,ψi)u^jn+1=j=0N(ψj,ψi)u^jnΔt(j=0N(ψj,ψi)u^jn[uNnψi]x=0x=L).

Since we are using basis functions that are all zero on the boundaries, the last term disappears. In order to incorporate the Dirichlet boundary condition we add a boundary term B(x) and solve for u~Nn+1=uNn+1B(x), where u~Nn+1(0,t)=u~Nn+1(L,t)=0. Note that we get exactly the same equation for uNn+1 as for u~Nn+1 since all terms with B(x) will be eliminated when inserting uNn+1=u~Nn+1+B(x) and uNn=u~Nn+B(x). But we solve the equation for u~Nn+1, with the zero boundary conditions, and then to get uNn+1 we simply add B(x).

We use Legendre basis functions with a mapping from x[0,L] to X[1,1] such that

x=L(1+X)/2anddxdX=L/2.

The stiffness matrix sij=(ψj,ψi) was given in Eq. (51), but it needs to be modified for the mapping

0Lψj(x)ψi(x)dx=11(Pj(X)Pj+2(X))dXdx(Pi(X)Pi+2(X))dXdxdxdXdX,=2L11(Pj(X)Pj+2(X))(Pi(X)Pi+2(X))dX,=2L(PjPj+2,PiPi+2)L2(1,1),=2L(4i+6)δij.

The mass matrix aij=(ψj,ψi) is similarly

0Lψj(x)ψi(x)dx=11(Pj(X)Pj+2(X))(Pi(X)Pi+2(X))dxdXdX,=L2(PjPj+2,PiPi+2)L2(1,1),=L2((Pj,Pi)(Pj+2,Pi)(Pj,Pi+2)+(Pj+2,Pi+2)).

The mass matrix is symmetric and we know that (Pj,Pi)=Pi2δij, where Pi2=22i+1 is the squared L2 norm of Pi. Hence with j=i we get

(ψi,ψi)=L2((Pi,Pi)+(Pi+2,Pi+2))=L2(Pi2+Pi+22),

and with j=i+2 we get

(ψi+2,ψi)=L2Pi+22.

The mass matrix is as such

aij=aji=L2{Pi2+Pi+22,i=j,Pi+22,j=i+2,0,otherwise.

On matrix form the equation to solve is

Au^n+1=Au^nΔtSu^n.

This equation is solved for n=0,1,,Nt1, after first initializing u^0. A solver is created in the class FE_Diffusion below, where we reuse the code from mandatory assignment 2. In the code we assemble the mass and stiffness matrices A and S, and compute the eigenvalues λ of H=A1S. We then use that |1λΔt|1 (where the minus is because we have used integration by parts) and the fact that here all eigenvalues are positive real numbers, to determine that

0λΔt2.

This means that for stability we need to choose a time step Δt2/max(λ).

import numpy as np
import sympy as sp
from scipy import sparse
from galerkin import TrialFunction, TestFunction, DirichletLegendre, DirichletChebyshev, \
    project, inner

x, t, c, L = sp.symbols('x,t,c,L')

class FE_Diffusion:
    """Class for solving the diffusion equation with

    Parameters
    ----------
    N : int
        Number of basis functions
    L0 : number
        The extent of the domain, which is [0, L0]
    bc : 2-tuple of numbers
        The Dirichlet boundary conditions at the two edges
    u0 : Sympy function of x, t, c and L
        Used for specifying initial condition
    """
    def __init__(self, N, L0=1, u0=sp.cos(sp.pi*x/L)+sp.cos(10*sp.pi*x/L)):
        self.N = N
        self.L = L0
        self.u0 = u0.subs({L: L0})
        self.bc = (self.u0.subs({x: 0, t: 0}).n(), self.u0.subs({x: L0, t: 0}).n())
        self.uh_np1 = np.zeros(N+1) # expansion coefficients \hat{u}^{n+1}
        self.uh_n = np.zeros(N+1)
        self.V = self.get_function_space()
        self.A, self.S = self.get_mats()
        self.lu = sparse.linalg.splu(self.A.tocsc())

    def get_function_space(self):
        return DirichletLegendre(self.N, domain=(0, self.L), bc=self.bc)

    def get_mats(self):
        """Return mass and stiffness matrices
        """
        N = self.N
        u = TrialFunction(self.V)
        v = TestFunction(self.V)
        a = inner(u, v)
        s = 2/self.L*sparse.diags([4*np.arange(N+1)+6], [0], shape=(N+1, N+1), format='csr')
        return a, s

    def get_eigenvalues(self):
        """Return eigenvalues of H=A^{-1}S"""
        return np.linalg.eig(np.linalg.inv(self.A.toarray()) @ self.S.toarray())[0]

    def __call__(self, Nt, dt=0.1, save_step=100):
        """Solve diffusion equation

        Parameters
        ----------
        Nt : int
            Number of time steps
        dt : number
            timestep
        save_step : int, optional
            Save solution every save_step time step

        Returns
        -------
        Dictionary with key, values as timestep, array of solution
        The number of items in the dictionary is Nt/save_step, and
        each value is an array of length N+1

        """
        # Initialize
        self.uh_n[:] = project(self.u0, self.V) # unm1 = u(x, 0)
        xj = self.V.mesh()
        plotdata = {0: self.V.eval(self.uh_n, xj)}

        # Solve
        f = np.zeros(self.N+1)
        for n in range(2, Nt+1):
            f[:] = self.A @ self.uh_n - dt*(self.S @ self.uh_n)
            self.uh_np1[:] = self.lu.solve(f)
            self.uh_n[:] = self.uh_np1
            if n % save_step == 0: # save every save_step timestep
                plotdata[n] = self.V.eval(self.uh_np1, xj)
        return plotdata

Lets solve the problem using initial condition u(x,0)=cos(πx/L)+cos(10πx/L) and dt=2/max(λ):

from utilities import plot_with_offset
sol = FE_Diffusion(40, L0=2)
dt = 2/max(sol.get_eigenvalues())
data = sol(1000, dt=dt, save_step=100)
plot_with_offset(data, sol.V.mesh(), figsize=(4, 3))
_images/c78052313924db192fb3f892d219ff85e706c8d6e6b6cff67f458783e6b3a6ee.png

The solution is nice and smooth and the time step is

print(f'Maximum time step = {dt}')
Maximum time step = 2.1980578790345177e-05

Lets solve it using a slightly larger dt to see if this really is the largest possible time step:

data = sol(1000, dt=1.01*dt, save_step=100)
plot_with_offset(data, sol.V.mesh(), figsize=(4, 3))
_images/cc8542b2c67cc99a2a426ba26f8cb30a691a7e8eff8b987a1f437f1f8dc8c9b1.png

Obviously, the solver is now unstable and the solution diverges.

Note

The diffusion equation is “stiff” and requires a very short time step with the forward Euler method in order to remain stable.

Changing from Legendre to Chebyshev makes the required time step even shorter.

class FE_Diffusion_Cheb(FE_Diffusion):

    def get_function_space(self):
        return DirichletChebyshev(self.N, domain=(0, self.L), bc=self.bc)

    def get_mats(self):
        """Return mass and stiffness matrices
        """
        N = self.N
        u = TrialFunction(self.V)
        v = TestFunction(self.V)
        a = inner(u, v)
        #s2 = -inner(u.diff(2), v) # slow
        k = np.arange(N+1, dtype=float)
        def diag(i):
            if i == 0:
                return 2*np.pi*(k+1)*(k+2)
            return 4*np.pi*(k[:-(i)]+1)
        diags = np.arange(0, N+1, 2)
        vals = [diag(i) for i in diags]
        s = 2/self.L * sparse.diags(vals, diags, shape=(N+1, N+1), format='csr')
        return a, s
sol = FE_Diffusion_Cheb(40, L0=2)
dt = 2/max(sol.get_eigenvalues())
data = sol(1000, dt=dt, save_step=100)
plot_with_offset(data, sol.V.mesh(), figsize=(4, 3))
_images/e1985bc29fef555b70ff1451b32f6546c6aa521525e3589943582c9659f7a8d7.png
print(f'Maximum time step = {dt}')
Maximum time step = 1.2332249161314778e-05

We can compare the absolute stability limits with the finite difference method, where Δt/Δx20.5 for stability. Using a uniform mesh with N=40, we get Δx=2/N=0.05 and Δt0.50.052=1.25103. Hence, the Galerkin method with Chebyshev basis functions requires approximately 100 times shorter time steps for stability!

Stiffness matrix for Chebyshev basis

Note that in the implementation we have used sij=(ψj,ψi)ω and

(76)#(ψj,ψi)ω=2L{2π(i+1)(i+2),i=j,4π(i+1),j=i+2,i+4,,N0,j<i or i+j odd

because it is quite expensive to compute the matrix using the inner(u.diff(2), v) function that loops over all i,jIN2. Equation (76) can be derived using

Tn=j=0j+n evenn21cjn(n2j2)Tj,

but it is a lot of work and only necessary in order to save some time on computing the stiffness matrix.

Also note that because of the non-constant weight function ω(x)=1/1x2 it is not common to use integration by parts with the Chebyshev basis.

Use a stencil matrix for the implementation#

It is rather messy to work with composite basis functions. A very smart trick is thus to use a stencil matrix S, such that (summation on repeated j index)

ψi=PiPi+2=sijPj.

This defines SR(N+1)×(N+3) for the Dirichlet problem as

sij={1j=i1j=i+2,S=[101000101000101000101].

With ψ={ψi}i=0N and P={Pi}i=0N+2 (note the longer vector) we get all basis functions in matrix form:

ψ=SP.

But why is the stencil matrix so smart? Consider the mass matrix

(ψj,ψi)=(PjPj+2,PiPi+2)=(Pj,Pi)(Pj,Pi+2)(Pj+2,Pi)+(Pj+2,Pi+2).

You need to sort out the four (diagonal) matrices on the right. This is not very difficult, since they are all diagonal, but you need to watch out for the indices in order for the mass matrix to turn out correctly. Now use stencil matrix instead (with summation on k and l):

(ψj,ψi)=(sjkPk,silPl)=sil(Pl,Pk)sjk.

Here you only need the one diagonal matrix P=((Pl,Pk))k,l=0N+2,N+2, where (Pl,Pk)=22l+1δkl. The great advantage only becomes apparent in matrix form though

A=((ψj,ψi))i,j=0N,N=SPST

And this works for any stencil matrix, not just the Dirichlet one. That is the main advantage of the stencil matrix approach. For the Neumann basis (see lecture 11)

ψi(x)=Pii(i+1)(i+2)(i+3)Pi+2,

the stencil matrix is

sij={1j=ii(i+1)/((i+2)(i+3))j=i+2,S=[100000101/6000103/1000010α],

where α=N(N+1)/((N+2)(N+3)). The stiffness matrix can also be computed through the stencil matrix:

(ψj,ψi)=(sjkPk,silPl)=sil(Pl,Pk)sjk

Again you need only the single matrix using orthogonal polynomials qkl=(Pl,Pk)=(Pl,Pk) and the matrix form is again much simpler:

M=((ψj,ψi))i,j=0N,N=SQST

Backward Euler#

The forward Euler method requires a very short time step for the diffusion equation. How about the backward Euler method

(77)#(uNn+1,v)=(uNn,v)+Δt(L(uNn+1),v),

where the only difference from forward is that (L(uNn+1),v) is making use of the unknown uNn+1 and not the known uNn? The linear algebra problem is now

j=0N((ψj,ψi)+Δt(ψj,ψi))u^jn+1=j=0N(ψj,ψi)u^jn.

If we continue with the ansatz uNn+1=guNn, and thus uNn+1=gn+1uN0, we get

j=0Ng((ψj,ψi)+Δt(ψj,ψi))u^j0=j=0N(ψj,ψi)u^j0,

which on matrix form reads

g(A+ΔtS)u^0=Au^0

If we multiply from left to right by A1 we get

g(I+ΔtA1S)u^0=u^0.

Here we can once again use H=A1S and Hu^0=λu^0 and thus we find

g=11+Δtλ.

Since all time steps and eigenvalues are real numbers larger than 0, we find that g is always less than 1 and the backward Euler method is as such unconditionally stable.

The Wave equation#

Lets consider the wave equation from lecture 5 in one spatial dimension

2ut2=c22ux2,(x,t)(0,L)×(0,T].

As you may recall, we solved the wave equation with Dirichlet, Neumann and open boundary conditions. The two time derivatives also required that the solution was initialized at two time-steps, or more mathematically precise, that both u(x,0) and ut(x,0) were given.

We now use finite differences in time such that

un+12un+un1Δt2=c22unx2.

A numerical residual RN=R(uNn+1;uNn,uNn1) can now be defined as

RN=uNn+12uNn+uNn1α2(uNn),

where we use α=cΔt and u=ux for simplicity, since there is only one spatial dimension. If we now initialize uN0 and uN1, the Galerkin method is to find uNn+1VN such that

(uNn+12uNn+uNn1α2(uNn),v)=0,vVN,

for n=1,,Nt1. We can also integrate the last term by parts

(uNn+12uNn+uNn1,v)+α2(((uNn),v)[(uNn)v]x=0x=L)=0,vVN.

In order to solve the Galerkin problem we need to formulate the linear algebra problem. Hence we insert for uNn1,uNn,uNn+1 and v=ψi to obtain

j=0N(ψj,ψi)(u^jn+12u^jn+u^jn1)+α2(j=0N(ψj,ψi)u^jn[(uNn)ψi]x=0x=L)=0.

We next rearrange such that the unknown u^jn+1 is on the left hand side and the rest of the known terms are on the right hand side

j=0N(ψj,ψi)u^jn+1=j=0N(ψj,ψi)(2u^jnu^jn1)α2(j=0N(ψj,ψi)u^jn[(uNn)ψi]x=0x=L).

With matrix notation, aij=(ψj,ψi) and sij=(ψj,ψi), we get

Au^n+1=A(2u^nu^n1)α2(Su^nbn),

where bn=((uNn)(L)ψi(L)(uNn)(0)ψi(0))i=0N. We can write this as

Au^n+1=fn,

where the vector fn=A(2u^nu^n1)α2(Su^nbn).

The boundary conditions determine the path forward. Here there are subtile differences between global Galerkin methods and the local finite element method, but the procedures are exactly as detailed in lecture 11 and lecture 12.

For Dirichlet boundary conditions u(0,t)=a and u(L,t)=b:

  • Global methods make use of basis functions that are all zero at the boundaries ψj(0)=ψj(L)=0. Hence the vector bn disappears. Nonzero values for a,b are incorporated using a boundary function, see lecture 11.

  • The finite element method makes use of Lagrange polynomials and ψi(0)=1 for i=0 and zero otherwise. Similarly, ψi(L)=1 for i=N and zero otherwise. Hence it is only b0n and bNn that potentially are different from zero. However, since u(0,t)=a=u^0n+1 and u(L,t)=b=u^Nn+1 we ident A and set f0n=a and fNn=b (see lecture 12). As such the boundary terms in bn will never enter the equation system.

For Neumann boundary conditions u(0,t)=g0 and u(L,t)=gL:

  • Global methods can make use of basis functions that all have ψj(0)=ψj(L)=0, and add a boundary function B(x) that satisfied B(0)=g0 and B(L)=gL. However, global methods can also make use of any basis functions (like pure Legendre polynomials) and incorporate the boundary conditions through bn. We get that bin=gLψi(L)g0ψi(0). Note that this approach is very easy to use if g0=gL=0, because in that case you only need to neglect the boundary vector bn.

  • The finite element method makes use of the weak form and specifies b0n=g0 and bNn=gL since the basis functions are Lagrange polynomials. No more is needed. In the event that g0=gL=0, the boundary vector can simply be neglected.

How about stability? We can make the same ansatz as for the diffusion equation and assume uNn+1=gn+1uN0. We get after some trivial manipulations

(gn+12gn+gn1)Au^0=α2gnSu^0,

which can be transformed by dividing by gn and multiplying by A1 from the left

(g2+g1)Iu^0=α2A1Su^0.

Using H=A1S and Hu^0=λu^0, we get

g+g1=β,

where β=α2λ+2. This is more difficult to conclude with than for the Euler method, because we have a quadratic equation for g. Nevertheless, we can find

g=β±β242,

and in order for |g|1 (g may be complex) we need 2β2 and thus 0α2λ4. The maximum time step that we can use is as such

Δt1c4max(λ).

Note

For all 2β2 we get that |g|=1. This makes sense if we consider g+g1=β, because if g is a root of the quadratic equation, then g1 must also be a root. Hence, if g<1, then g1>1, which is unstable. So all the possible β must here have |g|=1. We can also easily compute |β±β24|=2 when the real number 2β2.

Lets implement the wave equation using a global Galerkin method, homogeneous Dirichlet boundary conditions and an initial pulse u(x,t)=exp(200(xL/2+ct)2) used for both t=0 and t=Δt. Since we are using the same matrices as the diffusion problem, we can create a solver by subclassing the FE_Diffusion class and thus reuse much code

class Wave(FE_Diffusion):
    def __init__(self, N, L0=1, c0=1, u0=sp.cos(sp.pi*x/L)+sp.cos(10*sp.pi*x/L)):
        u0 = u0.subs({c: c0})
        FE_Diffusion.__init__(self, N, L0=L0, u0=u0)
        self.c = c0
        self.uh_nm1 = np.zeros_like(self.uh_n)

    def __call__(self, Nt, dt=0.1, save_step=100):
        # Initialize
        self.uh_nm1[:] = project(self.u0.subs(t, 0), self.V) # unm1 = u(x, 0)
        xj = self.V.mesh()
        plotdata = {0: self.V.eval(self.uh_nm1, xj)}
        self.uh_n[:] = project(self.u0.subs(t, dt), self.V)
        if save_step == 1:
            plotdata[1] = self.V.eval(self.uh_n, xj)

        # Solve
        f = np.zeros(self.N+1)
        alfa = self.c*dt
        for n in range(2, Nt+1):
            f[:] = self.A @ (2*self.uh_n-self.uh_nm1) - alfa**2*(self.S @ self.uh_n)
            self.uh_np1[:] = self.lu.solve(f)
            self.uh_nm1[:] = self.uh_n
            self.uh_n[:] = self.uh_np1
            if n % save_step == 0: # save every save_step timestep
                plotdata[n] = self.V.eval(self.uh_np1, xj)
        return plotdata
sol = Wave(40, L0=2, c0=1, u0=sp.exp(-50*(x-L/2+c*t)**2))
dt = 1/sol.c*np.sqrt(4/max(sol.get_eigenvalues()))
data = sol(400, dt=dt, save_step=40)
plot_with_offset(data, sol.V.mesh(), figsize=(4, 3))
_images/7199606952aa55009d1caab132b7090c41150910c4b31657020a3cb82e8bf221.png
print(f"Maximum time step = {dt}")
Maximum time step = 0.006630321076742087

This maximum time step should be compared with the maximum time step of a finite difference method. Remember the Courant number C=cΔt/Δx and the stability condition C1. For our case, with c=1, this limit is ΔtΔx. So with N=40 and Δx=2/N, we get Δt0.05. Again, the finite difference method can take much longer time steps than the global Galerkin method. But remember, the global Galerkin method is using a much more accurate spatial discretization and can actually get away with using a much lower N.

Weekly assignments#

  1. Implement the diffusion equation with the backward Euler scheme.

  2. Find the absolute stability limit of the diffusion equation discretized with a Crank-Nicolson scheme. Use both a finite difference method un+1un=Δt2D(2)(un+1+un) and a comparable Galerkin method.

  3. Implement the diffusion equation with a Crank-Nicolson scheme and verify the absolute stability limits found in 2.

  4. Consider the leap-frog finite difference scheme un+1un1=2ΔtD(2)un, suggested by Richardson in 1910. Explain why this scheme is unconditionally unstable. To this end you can use that all the eigenvalues of D(2) are real.

Note

Richardson was one of the great pioneers for using mathematical techniques in weather forecasting, but at the time (1910) he did not know about stability limits for finite difference schemes. The von Neumann stability analysis that made this possible was not discovered until 1947.