We shall now take the ideas from previous chapters and put together such that we can solve PDEs using the flexible finite element basis functions. This is quite a machinery with many details, but the chapter is mostly an assembly of concepts and details we have already met.
The purpose of this section is to demonstrate in detail how the finite element method can then be applied to the model problem
with variational formulation
Any v∈V must obey v(0)=v(L)=0 because of the Dirichlet conditions on u.
We introduce a finite element mesh with Ne cells, all with length h, and number the cells from left to right. Choosing P1 elements, there are two nodes per cell, and the coordinates of the nodes become
Any node i is associated with a finite element basis function φi(x). When approximating a given function f by a finite element function u, we expand u using finite element basis functions associated with all nodes in the mesh. The parameter N, which counts the unknowns from 0 to N, is then equal to Nn−1 such that the total number of unknowns, N+1, is the total number of nodes. However, when solving differential equations we will often have N<Nn−1 because of Dirichlet boundary conditions. The reason is simple: we know what u are at some (here two) nodes, and the number of unknown parameters is naturally reduced.
In our case with homogeneous Dirichlet boundary conditions, we do not need any boundary function B(x), so we can work with the expansion
Because of the boundary conditions, we must demand ψi(0)=ψi(L)=0, i∈Is. When ψi for all i=0,…,N is to be selected among the finite element basis functions φj, j=0,…,Nn−1, we have to avoid using φj functions that do not vanish at x0=0 and xNn−1=L. However, all φj vanish at these two nodes for j=1,…,Nn−2. Only basis functions associated with the end nodes, φ0 and φNn−1, violate the boundary conditions of our differential equation. Therefore, we select the basis functions φi to be the set of finite element basis functions associated with all the interior nodes in the mesh:
The i index runs over all the unknowns ci in the expansion for u, and in this case N=Nn−3.
In the general case, and in particular on domains in higher dimensions, the nodes are not necessarily numbered from left to right, so we introduce a mapping from the node numbering, or more precisely the degree of freedom numbering, to the numbering of the unknowns in the final equation system. These unknowns take on the numbers 0,…,N. Unknown number j in the linear system corresponds to degree of freedom number ν(j), j∈Is. We can then write
With a regular numbering as in the present example, ν(j)=j+1, j=0,…,N=Nn−3.
We shall first perform a computation in the x coordinate system because the integrals can be easily computed here by simple, visual, geometric considerations. This is called a global approach since we work in the x coordinate system and compute integrals on the global domain [0,L].
The entries in the coefficient matrix and right-hand side are
Expressed in terms of finite element basis functions φi we get the alternative expressions
For the following calculations the subscripts on the finite element basis functions are more conveniently written as i and j instead of i+1 and j+1, so our notation becomes
where the i and j indices run as i,j=1,…,N+1=Nn−2.
The φi(x) function is a hat function with peak at x=xi and a linear variation in [xi−1,xi] and [xi,xi+1]. The derivative is 1/h to the left of xi and −1/h to the right, or more formally,
Figure shows φ′2(x) and φ′3(x).
Illustration of the derivative of piecewise linear basis functions associated with nodes in cell 2.
We realize that φ′i and φ′j has no overlap, and hence their product vanishes, unless i and j are nodes belonging to the same cell. The only nonzero contributions to the coefficient matrix are therefore
for i=1,…,N+1, but for i=1, Ai−1,i−2 is not defined, and for i=N+1, Ai−1,i is not defined.
From Figure, we see that φ′i−1(x) and φ′i(x) have overlap of one cell Ω(i−1)=[xi−1,xi] and that their product then is −1/h2. The integrand is constant and therefore Ai−1,i−2=−h−2h=−h−1. A similar reasoning can be applied to Ai−1,i, which also becomes −h−1. The integral of φ′i(x)2 gets contributions from two cells, Ω(i−1)=[xi−1,xi] and Ω(i)=[xi,xi+1], but φ′i(x)2=h−2 in both cells, and the length of the integration interval is 2h so we get Ai−1,i−1=2h−1.
The right-hand side involves an integral of 2φi(x), i=1,…,Nn−2, which is just the area under a hat function of height 1 and width 2h, i.e., equal to h. Hence, bi−1=2h.
To summarize the linear system, we switch from i to i+1 such that we can write
The equation system to be solved only involves the unknowns ci for i∈Is. With our numbering of unknowns and nodes, we have that ci equals u(xi+1). The complete matrix system then takes the following form:
Let us introduce the notation uj for the value of u at node j: uj=u(xj), since we have the interpretation u(xj)=∑jcjφ(xj)=∑jcjδij=cj. The unknowns c0,…,cN are u1,…,uNn−2. Shifting i with i+1 in (67) and inserting ui=ci−1, we get
A finite difference discretization of −u″(x)=2 by a centered, second-order finite difference approximation u″(xi)≈[DxDxu]i with Δx=h yields
which is, in fact, equal to (68) if (68) is divided by h. Therefore, the finite difference and the finite element method are equivalent in this simple test problem.
Sometimes a finite element method generates the finite difference equations on a uniform mesh, and sometimes the finite element method generates equations that are different. The differences are modest, but may influence the numerical quality of the solution significantly, especially in time-dependent problems. It depends on the problem at hand whether a finite element discretization is more or less accurate than a corresponding finite difference discretization.
Software for finite element computations normally employs the cell by cell computational procedure where an element matrix and vector are calculated for each cell and assembled in the global linear system. Let us go through the details of this type of algorithm.
All integrals are mapped to the local reference coordinate system X∈[−1,1]. In the present case, the matrix entries contain derivatives with respect to x,
where the global degree of freedom i is related to the local degree of freedom r through i=q(e,r). Similarly, j=q(e,s). The local degrees of freedom run as r,s=0,1 for a P1 element.
There are simple formulas for the basis functions ˜φr(X) as functions of X. However, we now need to find the derivative of ˜φr(X) with respect to x. Given
we can easily compute d˜φr/dX:
From the chain rule,
The transformed integral is then
The right-hand side is transformed according to
Specifically for P1 elements we arrive at the following calculations for the element matrix entries:
The element vector entries become
Expressing these entries in matrix and vector notation, we have
The first and last cell involve only one unknown and one basis function because of the Dirichlet boundary conditions at the first and last node. The element matrix therefore becomes a 1×1 matrix and there is only one entry in the element vector. On cell 0, only ψ0=φ1 is involved, corresponding to integration with ˜φ1. On cell Ne, only ψN=φNn−2 is involved, corresponding to integration with ˜φ0. We then get the special end-cell contributions
for e=0 and e=Ne. In these cells, we have only one degree of freedom, not two as in the interior cells.
The next step is to assemble the contributions from the various cells. The assembly of an element matrix and vector into the global matrix and right-hand side can be expressed as
for r and s running over all local degrees of freedom in cell e.
To make the assembly algorithm more precise, it is convenient to set up Python data structures and a code snippet for carrying out all details of the algorithm. For a mesh of four equal-sized P1 elements and L=2 we have
vertices = [0, 0.5, 1, 1.5, 2]
cells = [[0, 1], [1, 2], [2, 3], [3, 4]]
dof_map = [[0], [0, 1], [1, 2], [2]]
The total number of degrees of freedom is 3, being the function
values at the internal 3 nodes where u is unknown.
In cell 0 we have global degree of freedom 0, the next
cell has u unknown at its two nodes, which become
global degrees of freedom 0 and 1, and so forth according to
the dof_map
list. The mathematical q(e,r) quantity is nothing
but the dof_map
list.
Assume all element matrices are stored in a list Ae
such that
Ae[e][i,j]
is ˜A(e)i,j. A corresponding list
for the element vectors is named be
, where be[e][r]
is
˜b(e)r.
A Python code snippet
illustrates all details of the assembly algorithm:
# A[i,j]: coefficient matrix, b[i]: right-hand side
for e in range(len(Ae)):
for r in range(Ae[e].shape[0]):
for s in range(Ae[e].shape[1]):
A[dof_map[e,r],dof_map[e,s]] += Ae[e][i,j]
b[dof_map[e,r]] += be[e][i,j]
The general case with N_e
P1 elements of length h
has
N_n = N_e + 1
vertices = [i*h for i in range(N_n)]
cells = [[e, e+1] for e in range(N_e)]
dof_map = [[0]] + [[e-1, e] for i in range(1, N_e)] + [[N_n-2]]
Carrying out the assembly results in a linear system that is identical to (66), which is not surprising, since the procedures is mathematically equivalent to the calculations in the physical domain.
So far, our technique for computing the matrix system have assumed that u(0)=u(L)=0. The next section deals with the extension to nonzero Dirichlet conditions.
We have to take special actions to incorporate nonzero Dirichlet conditions, such as u(L)=D, into the computational procedures. The present section outlines alternative, yet mathematically equivalent, methods.
In previous sections, we introduced a boundary function B(x) to deal with nonzero Dirichlet boundary conditions for u. The construction of such a function is not always trivial, especially not in multiple dimensions. However, a simple and general constructive idea exists when the basis functions have the property
where xj is a boundary point. Examples on such functions are the Lagrange interpolating polynomials and finite element functions.
Suppose now that u has Dirichlet boundary conditions at nodes with numbers i∈Ib. For example, Ib={0,Nn−1} in a 1D mesh with node numbering from left to right and Dirichlet conditions at the end nodes i=0 and i=Nn−1. Let Ui be the corresponding prescribed values of u(xi). We can then, in general, use
It is easy to verify that B(xi)=∑j∈IbUjφj(xi)=Ui.
The unknown function can then be written as
where ν(j) maps unknown number j in the equation system to node ν(j), Ib is the set of indices corresponding to basis functions associated with nodes where Dirichlet conditions apply, and Is is the set of indices used to number the unknowns from zero to N. We can easily show that with this u, a Dirichlet condition u(xk)=Uk is fulfilled:
Some examples will further clarify the notation. With a regular left-to-right numbering of nodes in a mesh with P1 elements, and Dirichlet conditions at x=0, we use finite element basis functions associated with the nodes 1,2,…,Nn−1, implying that ν(j)=j+1, j=0,…,N, where N=Nn−2. Consider a particular mesh:
The expansion associated with this mesh becomes
Switching to the more standard case of left-to-right numbering and boundary conditions u(0)=C, u(L)=D, we have N=Nn−3 and
Finite element meshes in non-trivial 2D and 3D geometries usually leads to an irregular cell and node numbering. Let us therefore take a look at an irregular numbering in 1D:
Say we in this mesh have Dirichlet conditions on the left-most and right-most node, with numbers 3 and 1, respectively. We can number the unknowns at the interior nodes as we want, e.g., from left to right, resulting in ν(0)=0, ν(1)=4, ν(2)=5, ν(3)=2. This gives
and
The idea of constructing B, described here, generalizes almost trivially to 2D and 3D problems: B=∑j∈IbUjφj, where Ib is the index set containing the numbers of all the nodes on the boundaries where Dirichlet values are prescribed.
Let us see how the model problem −u″=2, u(0)=C, u(L)=D, is affected by a B(x) to incorporate boundary values. Inserting the expression
in −(u″,ψi)=(f,ψi) and integrating by parts results in a linear system with
We choose ψi=φi+1, i=0,…,N=Nn−3 if the node numbering is from left to right. (Later we also need the assumption that cells too are numbered from left to right.) The boundary function becomes
The expansion for u(x) is
We can write the matrix and right-hand side entries as
for i,j=1,…,N+1=Nn−2. Note that we have here used B′=Cφ′0+Dφ′Nn−1.
Most of the terms in the linear system have already been computed so we concentrate on the new contribution from the boundary function. The integral C∫L0φ′0(x))φ′i(x)dx, associated with the Dirichlet condition in x=0, can only get a nonzero contribution from the first cell, Ω(0)=[x0,x1] since φ′0(x)=0 on all other cells. Moreover, φ′0(x)φ′i(x)dx≠0 only for i=0 and i=1 (but node i=0 is excluded from the formulation), since φi=0 on the first cell if i>1. With a similar reasoning we realize that D∫L0φ′Nn−1(x))φ′i(x)dx can only get a nonzero contribution from the last cell.
With these expressions we get
As an equivalent alternative, we now turn to cellwise computations. The element matrices and vectors are calculated as in the section Cellwise computations, so we concentrate on the impact of the new term involving B(x). This new term, B′=Cφ′0+Dφ′Nn−1, vanishes on all cells except for e=0 and e=Ne. Over the first cell (e=0) the B′(x) function in local coordinates reads
while over the last cell (e=Ne) it looks like
For an arbitrary interior cell, we have the formula
for an entry in the local element vector. In the first cell, the value at local node 0 is known so only the value at local node 1 is unknown. The associated element vector entry becomes
The value at local node 1 in the last cell is known so the element vector here is
The contributions from the B(x) function to the global right-hand side vector becomes C/h for b0 and D/h for bN, exactly as we computed in the physical domain.
From an implementational point of view, there is a convenient alternative to adding the B(x) function and using only the basis functions associated with nodes where u is truly unknown. Instead of seeking
we use the sum over all degrees of freedom, including the known boundary values:
Note that the collections of unknowns {ci}i∈Is in (75) and (76) are different. The index set Is={0,…,N} always goes to N, and the number of unknowns is N+1, but in (75) the unknowns correspond to nodes where u is not known, while in (76) the unknowns cover u values at all the nodes. So, if the index set Ib contains Nb node numbers where u is prescribed, we have that N=Nn−Nb in (75) and N=Nn in (76).
The idea is to compute the entries in the linear system as if no Dirichlet values are prescribed. Afterwards, we modify the linear system to ensure that the known cj values are incorporated.
A potential problem arises for the boundary term [u′v]L0 from the integration by parts: imagining no Dirichlet conditions means that we no longer require v=0 at Dirichlet points, and the boundary term is then nonzero at these points. However, when we modify the linear system, we will erase whatever the contribution from [u′v]L0 should be at the Dirichlet points in the right-hand side of the linear system. We can therefore safely forget [u′v]L0 at any point where a Dirichlet condition applies.
Let us redo the computations in the example in the section General construction of a boundary function. We solve −u″=2 with u(0)=0 and u(L)=D. The expressions for Ai,j and bi are the same, but the numbering is different as the numbering of unknowns and nodes now coincide:
for i,j=0,…,N=Nn−1. The integrals involving basis functions corresponding to interior mesh nodes, i,j=1,…,Nn−2, are obviously the same as before. We concentrate on the contributions from φ0 and φNn−1:
The new terms on the right-hand side are also those involving φ0 and φNn−1:
The complete matrix system, involving all degrees of freedom, takes the form
Incorporation of Dirichlet values can now be done by replacing the first and last equation by the very simple equations c0=0 and cN=D, respectively. Note that the factor 1/h in front of the matrix then requires a factor h to be introduce appropriately on the diagonal in the first and last row of the matrix.
Note that because we do not require φi(0)=0 and φi(L)=0, i∈Is, the boundary term [u′v]L0, in principle, gives contributions u′(0)φ0(0) to b0 and u′(L)φN(L) to bN (u′φi vanishes for x=0 or x=L for i=1,…,N−1). Nevertheless, we erase these contributions in b0 and bN and insert boundary values instead. This argument shows why we can drop computing [u′v]L0 at Dirichlet nodes when we implement the Dirichlet values by modifying the linear system.
The original matrix system (66) is symmetric, but the modifications in (78) destroy this symmetry. Our described modification will in general destroy an initial symmetry in the matrix system. This is not a particular computational disadvantage for tridiagonal systems arising in 1D problems, but may be more serious in 2D and 3D problems when the systems are large and exploiting symmetry can be important for halving the storage demands and speeding up computations. Methods for solving symmetric matrix are also usually more stable and efficient than those for non-symmetric systems. Therefore, an alternative modification which preserves symmetry is attractive.
One can formulate a general algorithm for incorporating a Dirichlet condition in a symmetric way. Let ck be a coefficient corresponding to a known value u(xk)=Uk. We want to replace equation k in the system by ck=Uk, i.e., insert zeroes in row number k in the coefficient matrix, set 1 on the diagonal, and replace bk by Uk. A symmetry-preserving modification consists in first subtracting column number k in the coefficient matrix, i.e., Ai,k for i∈Is, times the boundary value Uk, from the right-hand side: bi←bi−Ai,kUk, i=0,…,N. Then we put zeroes in both row number k and column number k in the coefficient matrix, and finally set bk=Uk. The steps in algorithmic form becomes
bi←bi−Ai,kUk for i∈Is
Ai,k=Ak,i=0 for i∈Is
Ak,k=1
bi=Uk
This modification goes as follows for the specific linear system written out in (77) in the section Modification of the linear system. First we subtract the first column in the coefficient matrix, times the boundary value, from the right-hand side. Because c0=0, this subtraction has no effect. Then we subtract the last column, times the boundary value D, from the right-hand side. This action results in bN−1=2h+D/h and bN=h−2D/h. Thereafter, we place zeros in the first and last row and column in the coefficient matrix and 1 on the two corresponding diagonal entries. Finally, we set b0=0 and bN=D. The result becomes
The modifications of the global linear system can alternatively be done for the element matrix and vector. Let us perform the associated calculations in the computational example where the element matrix and vector is given by (71). The modifications are needed in cells where one of the degrees of freedom is known. In the present example, this means the first and last cell. We compute the element matrix and vector as if there were no Dirichlet conditions. The boundary term [u′v]L0 is simply forgotten at nodes that have Dirichlet conditions because the modification of the element vector will anyway erase the contribution from the boundary term. In the first cell, local degree of freedom number 0 is known and the modification becomes
In the last cell we set
We can also perform the symmetric modification. This operation affects only the last cell with a nonzero Dirichlet condition. The algorithm is the same as for the global linear system, resulting in
The reader is encouraged to assemble the element matrices and vectors and check that the result coincides with the system (79).
Suppose our model problem −u″(x)=f(x) features the boundary conditions u′(0)=C and u(L)=D. As already indicated, the former condition can be incorporated through the boundary term that arises from integration by parts. The details of this method will now be illustrated in the context of finite element basis functions.
Starting with the Galerkin method,
integrating u″ψi by parts results in
The first boundary term, u′(L)ψi(L), vanishes because u(L)=D. The second boundary term, u′(0)ψi(0), can be used to implement the condition u′(0)=C, provided ψi(0)≠0 for some i (but with finite elements we fortunately have ψ0(0)=1). The variational form of the differential equation then becomes
At points where u is known we may require ψi to vanish. Here, u(L)=D and then ψi(L)=0, i∈Is. Obviously, the boundary term u′(L)ψi(L) then vanishes.
The set of basis functions {ψi}i∈Is contains, in this case, all the finite element basis functions on the mesh, except the one that is 1 at x=L. The basis function that is left out is used in a boundary function B(x) instead. With a left-to-right numbering, ψi=φi, i=0,…,Nn−2, and B(x)=DφNn−1:
Inserting this expansion for u in the variational form (83) leads to the linear system
for i=0,…,N=Nn−2.
We may, as an alternative to the approach in the previous section, use a basis {ψi}i∈Is which contains all the finite element functions on the mesh: ψi=φi, i=0,…,Nn−1=N. In this case, u′(L)ψi(L)=u′(L)φi(L)≠0 for the i corresponding to the boundary node at x=L (where φi=1). The number of this node is i=Nn−1=N if a left-to-right numbering of nodes is utilized.
However, even though u′(L)φNn−1(L)≠0, we do not need to compute this term. For i<Nn−1 we realize that φi(L)=0. The only nonzero contribution to the right-hand side comes from i=N (bN). Without a boundary function we must implement the condition u(L)=D by the equivalent statement cN=D and modify the linear system accordingly. This modification will erase the last row and replace bN by another value. Any attempt to compute the boundary term u′(L)φNn−1(L) and store it in bN will be lost. Therefore, we can safely forget about boundary terms corresponding to Dirichlet boundary conditions also when we use the methods from the section Modification of the linear system or the section Symmetric modification of the linear system.
The expansion for u reads
Insertion in the variational form (83) leads to the linear system
After having computed the system, we replace the last row by cN=D, either straightforwardly as in the section Modification of the linear system or in a symmetric fashion as in the section Symmetric modification of the linear system. These modifications can also be performed in the element matrix and vector for the right-most cell.
We now turn to actual computations with P1 finite elements. The focus is on how the linear system and the element matrices and vectors are modified by the condition u′(0)=C.
Consider first the approach where Dirichlet conditions are incorporated by a B(x) function and the known degree of freedom CNn−1 is left out of the linear system (see the section Boundary term vanishes because of the test functions). The relevant formula for the linear system is given by (84). There are three differences compared to the extensively computed case where u(0)=0 in the sections Computation in the global physical domain and Cellwise computations. First, because we do not have a Dirichlet condition at the left boundary, we need to extend the linear system (66) with an equation associated with the node x0=0. According to the section Modification of the linear system, this extension consists of including A0,0=1/h, A0,1=−1/h, and b0=h. For i>0 we have Ai,i=2/h, Ai−1,i=Ai,i+1=−1/h. Second, we need to include the extra term −Cφi(0) on the right-hand side. Since all φi(0)=0 for i=1,…,N, this term reduces to −Cφ0(0)=−C and affects only the first equation (i=0). We simply add −C to b0 such that b0=h−C. Third, the boundary term −∫L0DφNn−1(x)φidx must be computed. Since i=0,…,N=Nn−2, this integral can only get a nonzero contribution with i=Nn−2 over the last cell. The result becomes −Dh/6. The resulting linear system can be summarized in the form
Next we consider the technique where we modify the linear system to incorporate Dirichlet conditions (see the section Boundary term vanishes because of linear system modifications). Now N=Nn−1. The two differences from the case above is that the −∫L0DφNn−1φidx term is left out of the right-hand side and an extra last row associated with the node xNn−1=L where the Dirichlet condition applies is appended to the system. This last row is anyway replaced by the condition cN=D or this condition can be incorporated in a symmetric fashion. Using the simplest, former approach gives
Now we compute with one element at a time, working in the reference coordinate system X∈[−1,1]. We need to see how the u′(0)=C condition affects the element matrix and vector. The extra term −Cφi(0) in the variational formulation only affects the element vector in the first cell. On the reference cell, −Cφi(0) is transformed to −C˜φr(−1), where r counts local degrees of freedom. We have ˜φ0(−1)=1 and ˜φ1(−1)=0 so we are left with the contribution −C˜φ0(−1)=−C to ˜b(0)0:
No other element matrices or vectors are affected by the −Cφi(0) boundary term.
There are two alternative ways of incorporating the Dirichlet condition. Following the section Boundary term vanishes because of the test functions, we get a 1×1 element matrix in the last cell and an element vector with an extra term containing D:
Alternatively, we include the degree of freedom at the node with u specified. The element matrix and vector must then be modified to constrain the ˜c1=cN value at local node r=1:
At this point, it is sensible to create a program with symbolic calculations to perform all the steps in the computational machinery, both for automating the work and for documenting the complete algorithms. As we have seen, there are quite many details involved with finite element computations and incorporation of boundary conditions. An implementation will also act as a structured summary of all these details.
Implementation of the finite element algorithms for differential equations follows closely the algorithm for approximation of functions. The new additional ingredients are
other types of integrands (as implied by the variational formulation)
additional boundary terms in the variational formulation for Neumann boundary conditions
modification of element matrices and vectors due to Dirichlet boundary conditions
Point 1 and 2 can be taken care of by letting the user supply functions defining the integrands and boundary terms on the left- and right-hand side of the equation system:
Integrand on the left-hand side: ilhs(e, phi, r, s, X, x, h)
Integrand on the right-hand side: irhs(e, phi, r, X, x, h)
Boundary term on the left-hand side: blhs (e, phi, r, s, X, x, h)
Boundary term on the right-hand side: brhs (e, phi, r, s, X, x, h)
Here, phi
is a dictionary where phi[q]
holds a list of the
derivatives of order q
of the basis functions with respect to the
physical coordinate x. The derivatives are available as Python
functions of X
. For example, phi[0][r](X)
means ˜φr(X),
and phi[1][s](X, h)
means d˜φs(X)/dx (we refer to
the file fe1D.py for details
regarding the function basis
that computes the phi
dictionary). The r
and s
arguments in the above functions correspond to the index in the
integrand contribution from an integration point to ˜A(e)r,s and ˜b(e)r. The variables e
and h
are
the current element number and the length of the cell, respectively.
Specific examples below will make it clear how to construct these
Python functions.
Given a mesh represented by vertices
, cells
, and dof_map
as
explained before, we can write a pseudo Python code to list all
the steps in the computational algorithm for finite element solution
of a differential equation.
<Declare global matrix and rhs: A, b>
for e in range(len(cells)):
# Compute element matrix and vector
n = len(dof_map[e]) # no of dofs in this element
h = vertices[cells[e][1]] - vertices[cells[e][1]]
<Initialize element matrix and vector: A_e, b_e>
# Integrate over the reference cell
points, weights = <numerical integration rule>
for X, w in zip(points, weights):
phi = <basis functions and derivatives at X>
detJ = h/2
dX = detJ*w
x = <affine mapping from X>
for r in range(n):
for s in range(n):
A_e[r,s] += ilhs(e, phi, r, s, X, x, h)*dX
b_e[r] += irhs(e, phi, r, X, x, h)*dX
# Add boundary terms
for r in range(n):
for s in range(n):
A_e[r,s] += blhs(e, phi, r, s, X, x)*dX
b_e[r] += brhs(e, phi, r, X, x, h)*dX
# Incorporate essential boundary conditions
for r in range(n):
global_dof = dof_map[e][r]
if global_dof in essbc:
# local dof r is subject to an essential condition
value = essbc[global_dof]
# Symmetric modification
b_e -= value*A_e[:,r]
A_e[r,:] = 0
A_e[:,r] = 0
A_e[r,r] = 1
b_e[r] = value
# Assemble
for r in range(n):
for s in range(n):
A[dof_map[e][r], dof_map[e][s]] += A_e[r,s]
b[dof_map[e][r] += b_e[r]
<solve linear system>
Having implemented this function, the user only has supply the ilhs
,
irhs
, blhs
, and brhs
functions that specify the PDE and boundary
conditions. The rest of the implementation forms a generic
computational engine. The big finite element packages Diffpack and FEniCS
are structured exactly the same way. A sample implementation of ilhs
for the 1D Poisson problem is:
def integrand_lhs(phi, i, j):
return phi[1][i]*phi[1][j]
which returns d˜φi(X)/dxd˜φj(X)/dx. Reducing the amount of code the user has to supply and making the code as close as possible to the mathematical formulation makes the software user-friendly and easy to debug.
A complete function finite_element1D_naive
for the 1D finite
algorithm above, is found in the file fe1D.py. The term "naive" refers to a version of the
algorithm where we use a standard dense square matrix as global matrix
A
. The implementation also has a verbose mode for printing out the
element matrices and vectors as they are computed. Below is the
complete function without the print statements. You should study in
detail since it contains all the steps in the finite element
algorithm.
def finite_element1D_naive(
vertices, cells, dof_map, # mesh
essbc, # essbc[globdof]=value
ilhs, # integrand left-hand side
irhs, # integrand right-hand side
blhs=lambda e, phi, r, s, X, x, h: 0,
brhs=lambda e, phi, r, X, x, h: 0,
intrule='GaussLegendre', # integration rule class
verbose=False, # print intermediate results?
):
N_e = len(cells)
N_n = np.array(dof_map).max() + 1
A = np.zeros((N_n, N_n))
b = np.zeros(N_n)
for e in range(N_e):
Omega_e = [vertices[cells[e][0]], vertices[cells[e][1]]]
h = Omega_e[1] - Omega_e[0]
d = len(dof_map[e]) - 1 # Polynomial degree
# Compute all element basis functions and their derivatives
phi = basis(d)
# Element matrix and vector
n = d+1 # No of dofs per element
A_e = np.zeros((n, n))
b_e = np.zeros(n)
# Integrate over the reference cell
if intrule == 'GaussLegendre':
points, weights = GaussLegendre(d+1)
elif intrule == 'NewtonCotes':
points, weights = NewtonCotes(d+1)
for X, w in zip(points, weights):
detJ = h/2
x = affine_mapping(X, Omega_e)
dX = detJ*w
# Compute contribution to element matrix and vector
for r in range(n):
for s in range(n):
A_e[r,s] += ilhs(phi, r, s, X, x, h)*dX
b_e[r] += irhs(phi, r, X, x, h)*dX
# Add boundary terms
for r in range(n):
for s in range(n):
A_e[r,s] += blhs(phi, r, s, X, x, h)
b_e[r] += brhs(phi, r, X, x, h)
# Incorporate essential boundary conditions
modified = False
for r in range(n):
global_dof = dof_map[e][r]
if global_dof in essbc:
# dof r is subject to an essential condition
value = essbc[global_dof]
# Symmetric modification
b_e -= value*A_e[:,r]
A_e[r,:] = 0
A_e[:,r] = 0
A_e[r,r] = 1
b_e[r] = value
modified = True
# Assemble
for r in range(n):
for s in range(n):
A[dof_map[e][r], dof_map[e][s]] += A_e[r,s]
b[dof_map[e][r]] += b_e[r]
c = np.linalg.solve(A, b)
return c, A, b, timing
The timing
object is a dictionary holding the CPU spent on computing
A
and the CPU time spent on solving the linear system. (We have
left out the timing statements.)
A potential efficiency problem with the finite_element1D_naive
function
is that it uses dense (N+1)×(N+1) matrices, while we know that
only 2d+1 diagonals around the main diagonal are different from zero.
Switching to a sparse matrix is very easy. Using the DOK (dictionary of
keys) format, we declare A
as
import scipy.sparse
A = scipy.sparse.dok_matrix((N_n, N_n))
Assignments or in-place arithmetics are done as for a dense matrix,
A[i,j] += term
A[i,j] = term
but only the index pairs (i,j)
we have used in assignments or
in-place arithmetics are actually stored.
A tailored solution algorithm is needed. The most reliable is
sparse Gaussian elimination. SciPy gives access to the
UMFPACK algorithm
for this purpose:
import scipy.sparse.linalg
c = scipy.sparse.linalg.spsolve(A.tocsr(), b, use_umfpack=True)
The declaration of A
and the solve statement are the only
changes needed in the finite_element1D_naive
to utilize
sparse matrices. The resulting modification is found in the
function finite_element1D
.
Let us demonstrate the finite element software on
This problem can be analytically solved by the
model2
function from the section "Simple model problems and their solutions" in previous chapter.
Let f(x)=x2. Calling model2(x**2, L, C, D)
gives
The variational formulation reads
The entries in the element matrix and vector,
which we need to set up the ilhs
, irhs
,
blhs
, and brhs
functions, becomes
where I(e) is an indicator function: I(e,q)=1 if e=q, otherwise I(e)=0. We use this indicator function to formulate that the boundary term Cv(0), which in the local element coordinate system becomes C˜φr(−1), is only included for the element e=0.
The functions for specifying the element matrix and vector entries
must contain the integrand, but without the detJdX term
as this term is taken care of by the quadrature loop, and
the derivatives d˜φr(X)/dx
with respect to the physical x coordinates are
contained in phi[1][r](X)
, computed by the function basis
.
def ilhs(e, phi, r, s, X, x, h):
return phi[1][r](X, h)*phi[1][s](X, h)
def irhs(e, phi, r, X, x, h):
return x**2*phi[0][r](X)
def blhs(e, phi, r, s, X, x, h):
return 0
def brhs(e, phi, r, X, x, h):
return -C*phi[0][r](-1) if e == 0 else 0
We can then make the call to finite_element1D_naive
or finite_element1D
to solve the problem with two P1 elements:
from src.fe1D import finite_element1D_naive, mesh_uniform
C = 5; D = 2; L = 4
d = 1
vertices, cells, dof_map = mesh_uniform(
N_e=2, d=d, Omega=[0,L], symbolic=False)
essbc = {}
essbc[dof_map[-1][-1]] = D
c, A, b, timing = finite_element1D_naive(
vertices, cells, dof_map, essbc,
ilhs=ilhs, irhs=irhs, blhs=blhs, brhs=brhs,
intrule='GaussLegendre')
It remains to plot the solution (with high resolution in each element).
To this end, we use the u_glob
function imported from
fe1D
, which imports it from fe_approx1D_numit
(the
u_glob
function in fe_approx1D.py
works with elements
and nodes
, while u_glob
in
fe_approx1D_numint
works with cells
, vertices
,
and dof_map
):
u_exact = lambda x: D + C*(x-L) + (1./6)*(L**3 - x**3)
from src.fe1D import u_glob
x, u, nodes = u_glob(c, cells, vertices, dof_map)
u_e = u_exact(x)
# print((u_exact(nodes) - c)) # difference at the nodes
# import matplotlib.pyplot as plt
# plt.plot(x, u, 'b-', x, u_e, 'r--')
# plt.legend(['finite elements, d=%d' %d, 'exact'], loc='upper left')
# plt.show()
The result is shown in Figure. We see that the solution using P1 elements is exact at the nodes, but feature considerable discrepancy between the nodes. Exercise 10: Investigate exact finite element solutions asks you to explore this problem further using other m and d values.
Finite element and exact solution using two cells.
The major difference between deriving variational formulations in 2D and 3D compared to 1D is the rule for integrating by parts. The cells have shapes different from an interval, so basis functions look a bit different, and there is a technical difference in actually calculating the integrals over cells. Otherwise, going to 2D and 3D is not a big step from 1D. All the fundamental ideas still apply.
A typical second-order term in a PDE may be written in dimension-independent notation as
The explicit forms in a 2D problem become
and
We shall continue with the latter operator as the former arises from just setting α=1.
The integration by parts formula for ∫∇⋅(α∇).
The general rule for integrating by parts is often referred to as Green's first identity:
where ∂Ω is the boundary of Ω and ∂u/∂n=n⋅∇u is the derivative of u in the outward normal direction, n being an outward unit normal to ∂Ω. The integrals ∫Ω()dx are area integrals in 2D and volume integrals in 3D, while ∫∂Ω()ds is a line integral in 2D and a surface integral in 3D.
It will be convenient to divide the boundary into two parts:
∂ΩN, where we have Neumann conditions −a∂u∂n=g, and
∂ΩD, where we have Dirichlet conditions u=u0.
The test functions v are (as usual) required to vanish on ∂ΩD.
Here is a quite general, stationary, linear PDE arising in many problems:
The vector field v and the scalar functions a, α, f, u0, and g may vary with the spatial coordinate x and must be known.
Such a second-order PDE needs exactly one boundary condition at each point of the boundary, so ∂ΩN∪∂ΩD must be the complete boundary ∂Ω.
Assume that the boundary function u0(x) is defined for all x∈Ω. The unknown function can then be expanded as
As long as any ψj=0 on ∂ΩD, we realize that u=u0 on ∂ΩD.
The variational formula is obtained from Galerkin's method, which technically means multiplying the PDE by a test function v and integrating over Ω:
The second-order term is integrated by parts, according to the formula (91):
Galerkin's method therefore leads to
The boundary term can be developed further by noticing that v≠0 only on ∂ΩN,
and that on ∂ΩN, we have the condition a∂u∂n=−g, so the term becomes
The final variational form is then
Instead of using the integral signs, we may use the inner product notation:
The subscript N in (g,v)N is a notation for a line or surface integral over ∂ΩN, while (⋅,⋅) is the area/volume integral over Ω.
We can derive explicit expressions for the linear system for {cj}j∈Is that arises from the variational formulation. Inserting the u expansion results in
This is a linear system with matrix entries
and right-hand side entries
for i,j∈Is.
In the finite element method, we usually express u0 in terms of basis functions and restrict i and j to run over the degrees of freedom that are not prescribed as Dirichlet conditions. However, we can also keep all the {cj}j∈Is as unknowns, drop the u0 in the expansion for u, and incorporate all the known cj values in the linear system. This has been explained in detail in the 1D case, and the technique is the same for 2D and 3D problems.
The real power of the finite element method first becomes evident when we want to solve partial differential equations posed on two- and three-dimensional domains of non-trivial geometric shape. As in 1D, the domain Ω is divided into Ne non-overlapping cells. The elements have simple shapes: triangles and quadrilaterals are popular in 2D, while tetrahedra and box-shapes elements dominate in 3D. The finite element basis functions φi are, as in 1D, polynomials over each cell. The integrals in the variational formulation are, as in 1D, split into contributions from each cell, and these contributions are calculated by mapping a physical cell, expressed in physical coordinates x, to a reference cell in a local coordinate system X. This mapping will now be explained in detail.
We consider an integral of the type
where the φi functions are finite element basis functions in 2D or 3D, defined in the physical domain. Suppose we want to calculate this integral over a reference cell, denoted by ˜Ωr, in a coordinate system with coordinates X=(X0,X1) (2D) or X=(X0,X1,X2) (3D). The mapping between a point X in the reference coordinate system and the corresponding point x in the physical coordinate system is given by a vector relation x(X). The corresponding Jacobian, J, of this mapping has entries
The change of variables requires dx to be replaced by detJdX. The derivatives in the ∇ operator in the variational form are with respect to x, which we may denote by ∇x. The φi(x) functions in the integral are replaced by local basis functions ˜φr(X) so the integral features ∇x˜φr(X). We readily have ∇X˜φr(X) from formulas for the basis functions in the reference cell, but the desired quantity ∇x˜φr(X) requires some efforts to compute. All the details are provided below.
Let i=q(e,r) and consider two space dimensions. By the chain rule,
and
We can write these two equations as a vector equation
Identifying
we have the relation
which we can solve with respect to ∇xφi:
On the reference cell, φi(x)=˜φr(X), so
This means that we have the following transformation of the integral in the physical domain to its counterpart over the reference cell:
Integrals are normally computed by numerical integration rules. For multi-dimensional cells, various families of rules exist. All of them are similar to what is shown in 1D: ∫fdx≈∑jwif(xj), where wj are weights and xj are corresponding points.
The file numint.py contains the functions
quadrature_for_triangles(n)
and quadrature_for_tetrahedra(n)
,
which returns lists of points and weights corresponding to integration
rules with n
points over the reference triangle
with vertices (0,0), (1,0), (0,1), and the reference tetrahedron
with vertices (0,0,0), (1,0,0), (0,1,0), (0,0,1),
respectively. For example, the first two rules for integration over
a triangle have 1 and 3 points:
import numint
x, w = numint.quadrature_for_triangles(num_points=1)
x
w
x, w = numint.quadrature_for_triangles(num_points=3)
x
w
Rules with 1, 3, 4, and 7 points over the triangle will exactly integrate polynomials of degree 1, 2, 3, and 4, respectively. In 3D, rules with 1, 4, 5, and 11 points over the tetrahedron will exactly integrate polynomials of degree 1, 2, 3, and 4, respectively.
We shall now provide some formulas for piecewise linear φi functions and their integrals in the physical coordinate system. These formulas make it convenient to compute with P1 elements without the need to work in the reference coordinate system and deal with mappings and Jacobians. A lot of computational and algorithmic details are hidden by this approach.
Let Ω(e) be cell number e, and let the three vertices have global vertex numbers I, J, and K. The corresponding coordinates are (xI,yI), (xJ,yJ), and (xK,yK). The basis function φI over Ω(e) have the explicit formula
where
and
The quantity Δ is the area of the cell.
The following formula is often convenient when computing element matrices and vectors:
(Note that the q in this formula is not to be mixed with the q(e,r) mapping of degrees of freedom.)
As an example, the element matrix entry ∫Ω(e)φIφJdx can be computed by setting p=q=1 and r=0, when I≠J, yielding Δ/12, and p=2 and q=r=0, when I=J, resulting in Δ/6. We collect these numbers in a local element matrix:
The common element matrix entry ∫Ω(e)∇φI⋅∇φJdx, arising from a Laplace term ∇2u, can also easily be computed by the formulas above. We have
so that the element matrix entry becomes 14Δ3(βIβJ+γIγJ).
From an implementational point of view, one will work with local vertex numbers r=0,1,2, parameterize the coefficients in the basis functions by r, and look up vertex coordinates through q(e,r).
Similar formulas exist for integration of P1 elements in 3D.
From a principle of view, we have seen that variational forms of the type: find a(u,v)=L ∀v∈V (and even general nonlinear problems F(u;v)=0), can apply the computational machinery of introduced for the approximation problem u=f. We actually need two extensions only:
specify Dirichlet boundary conditions as part of V
incorporate Neumann flux boundary conditions in the variational form
The algorithms are all the same in any space dimension, we only need to choose the element type and associated integration rule. Once we know how to compute things in 1D, and made the computer code sufficiently flexible, the method and code should work for any variational form in any number of space dimensions! This fact is exactly the idea behind the FEniCS finite element software.
Therefore, if we know how to set up an approximation problem in any dimension in FEniCS, and know how to derive variational forms in higher dimensions, we are (in principle!) very close to solving a PDE problem in FEniCS. We shall now solve a quite general 1D/2D/3D Poisson problem in FEniCS. There is much more to FEniCS than what is shown in this example, but it illustrates the fact that when we go beyond 1D, there exists software which leverage the full power of the finite element method as a method for solving any problem on any mesh in any number of space dimensions.
The following model describes the pressure u in the flow around a bore hole of radius a in a porous medium. If the hole is long in the vertical direction (z-direction) then it is natural to assume that the vertical changes are small and uz≈constant. Therefore, we can model it by a 2D domain in the cross section.
That is, we have a hollow circular 2D domain with inner radius a and outer radius b. The pressure is known on these two boundaries, so this is a pure Dirichlet problem.
The first thing we should observe is that the problem is radially symmetric, so we can change to polar coordinates and obtain a 1D problem in the radial direction:
This is not very exciting beyond being able to find an analytical solution and compute the true error of a finite element approximation.
However, many software packages solve problems in Cartesian coordinates, and FEniCS basically do this, so we want to take advantage of symmetry in Cartesian coordinates and reformulate the problem in a smaller domain.
Looking at the domain as a cake with a hole, any piece of the cake will be a candidate for a reduced-size domain. The solution is symmetric about any line θ=const in polar coordinates, so at such lines we have the symmetry boundary condition ∂u/∂n=0, i.e., a homogeneous Neumann condition. In Figure we have plotted a possible mesh of cells as triangles, here with dense refinement toward the bore hole, because we know the solution will decay most rapidly toward the origin. This mesh is a piece of the cake with four sides: Dirichlet conditions on the inner and outer boundary, named ΓDa and ΓDb, and ∂u/∂n=0 on the two other sides, named ΓN. In this particular example, the arc of the piece of the cake is 45 degrees, but any value of the arc will work.
Mesh of a hollow cylinder, with refinement and utilizing symmetry.
The boundary problem can then be expressed as
To obtain the variational formulation, we multiply the PDE by a test function v and integrate the second-order derivatives by part:
We are left with a problem of the form: find u such that a(u,v)=L(v) ∀v∈V, with
We write the integrand as 0vdx even though L=0, because it is necessary in FEniCS to specify L as a linear form (i.e., a test function and some form of integration need to be present) and not the number zero. The Dirichlet conditions make a nonzero solution.
We suppose that we have a function make_mesh
that can make the mesh for us. More
details about this function will be provided later.
A next step is then to define proper Dirichlet conditions.
This might seem a bit complicated, but we introduce markers at the
boundary for marking the Dirichlet boundaries. The inner boundary has
marker 1 and the outer has marker 2. In this way, we can recognize the
nodes that are on the boundary. It is usually a part of the mesh making
process to compute both the mesh and its markers, so make_mesh
returns
a Mesh
object as mesh
and a MeshFunction
object markers
.
Setting Dirichlet conditions in the solver is then a matter of
introducing DirichletBC
objects, one for each part of the boundary
marked by markers
, and then we collect all such Dirichlet conditions
in a list that is used by the assembly process to incorporate the
Dirichlet conditions in the linear system.
The code goes like this:
V = FunctionSpace(mesh, 'P', degree)
bc_inner = DirichletBC(V, u_a, markers, 1)
bc_outer = DirichletBC(V, u_b, markers, 2)
bcs = [bc_inner, bc_outer]
Here, u_a
and u_b
are constants (floats) set by the user. In general,
anything that can be evaluated pointwise can be used, such as Expression
,
Function
, and Constant
.
The next step is to define the variational problem and solve it:
# Define variational problem
u = TrialFunction(V)
v = TestFunction(V)
a = alpha*dot(grad(u), grad(v))*dx
L = Constant(0)*v*dx # L = 0*v*dx = 0 does not work...
# Compute solution
u = Function(V)
solve(a == L, u, bcs)
f = File("mesh.xml")
f << mesh
In order to avoid L=0
(L
equal to the float zero), we have to
tell FEniCS that is a linear form, so zero must be specified as Constant(0)
.
Note that everything is the same as for the approximation problem in the section fe:approx:fenics,
except for the Dirichlet conditions and the formulas for a
and
L
. FEniCS has, of course, access to very efficient solution methods,
so we could add arguments to the solve
call to apply
state-of-the-art iterative methods and preconditioners for large-scale
problems. However, for this little 2D case a standard sparse Gaussian
elimination, as implied by solve(a = L, u, bcs)
is a sufficient approach.
Finally, we can save the solution to file for using professional
visualization software and, if desired, add a quick plotting using the
built-in FEniCS tool plot
:
# Save solution to file in VTK format
vtkfile = File(filename + '.pvd')
vtkfile << u
u.rename('u', 'u'); plot(u); plot(mesh)
import matplotlib.pyplot as plt
plt.show()
(The u.rename
call is just for getting a more readable title in the plot.)
The above statements are collected in a function solver
in the
file borehole_fenics.py
:
def solver(
mesh,
markers, # MeshFunctions for Dirichlet conditions
alpha, # Diffusion coefficient
u_a, # Inner pressure
u_b, # Outer pressure
degree, # Element polynomial degree
filename, # Name of VTK file
):
V = FunctionSpace(mesh, 'P', degree)
bc_inner = DirichletBC(V, u_a, markers, 1)
bc_outer = DirichletBC(V, u_b, markers, 2)
bcs = [bc_inner, bc_outer]
# Define variational problem
u = TrialFunction(V)
v = TestFunction(V)
a = alpha*dot(grad(u), grad(v))*dx
L = Constant(0)*v*dx # L = 0*v*dx = 0 does not work...
# Compute solution
u = Function(V)
solve(a == L, u, bcs)
f = File("mesh.xml")
f << mesh
# Save solution to file in VTK format
vtkfile = File(filename + '.pvd')
vtkfile << u
u.rename('u', 'u'); plot(u); plot(mesh)
import matplotlib.pyplot as plt
plt.show()
return u
def problem():
mesh, markers = make_mesh(Theta=25*pi/180, a=1, b=2,
nr=20, nt=20, s=1.9)
beta = 5
solver(mesh, markers, alpha=1, u_a=1, u_b=0, degree=1, filename='tmp')
if __name__ == '__main__':
problem()
Be careful with name clashes!
It is easy when coding mathematics to use variable names that correspond
to one-letter names in the mathematics. For example, in the mathematics
of this problem there are two a variables: the radius of the inner
boundary and the bilinear form in the variational formulation.
Using a
for the inner boundary in solver
does not work: it is
quickly overwritten by the bilinear form. We therefore have to introduce
x_a
. Long variable names are to be preferred for safe programming,
though short names corresponding to the mathematics are often nicer.
The hardest part of a finite element problem is very often to make the
mesh. This is particularly the case in large industrial projects, but
also often academic projects quickly lead to time-consuming work with
constructing finite element meshes. In the present example we create
the mesh for the symmetric problem by deforming an originally
rectangular mesh. The rectangular mesh is made by the FEniCS object
RectangleMesh
on [a,b]×[0,1]. Therefore, we stretch the
mesh towards the left before we bend the rectangle onto to "a piece
of cake". Figure shows an example
on the resulting mesh. The stretching gives us refinement in the
radial direction because we expect the variations to be quite large in
this direction, but uniform in θ direction.
We first make the rectangle and set boundary markers here for the inner
and outer boundaries (since these are particularly simple: x=a and x=b).
Here is how we make
the rectangular mesh from lower left corner (a,0) to upper left corner
(b,1) with nr
quadrilateral cells in x direction (later to become
the radial direction) and nt
quadrilateral cells in the y direction:
mesh = RectangleMesh(Point(a, 0), Point(b, 1), nr, nt, 'crossed')
Each quadrilateral cell is divided into two triangles with right or left
going diagonals, or four triangles using both diagonals. These choices
of producing triangles from rectangles are named right
, left
, and
crossed
. Recall that FEniCS can only work with cells of triangular
shape only and where the sides are straight. This means that we need a
good resolution in θ direction to represent a circular boundary.
With isoparametric elements, it is easier to get a higher-order polynomial
approximation of curved boundaries.
We must then mark the boundaries for boundary conditions. Since we do not need
to do anything with the homogeneous Neumann conditions, we can just mark
the inner and outer boundary of the hole cylinder. This is very easy to do
as long as the mesh is a rectangle since then the specifications of the
boundaries are x=a and x=b. The relevant FEniCS code requires the
user to define a subclass of SubDomain
and implement a function inside
for indicating whether a given point x
is on the desired boundary or not:
Finite element mesh for a porous medium outside a bore hole (hollow cylinder).
# x=a becomes the inner borehole boundary
class Inner(SubDomain):
def inside(self, x, on_boundary):
return on_boundary and abs(x[0] - a) < tol
# x=b becomes the outer borehole boundary
class Outer(SubDomain):
def inside(self, x, on_boundary):
return on_boundary and abs(x[0] - b) < tol
inner = Inner(); outer = Outer();
markers = MeshFunction('size_t', mesh, mesh.topology().dim() - 1)
markers.set_all(0)
inner.mark(markers, 1)
outer.mark(markers, 2)
With the instances inner
and outer
we fill a marker object, called
MeshFunction
in FEniCS. For this purpose we must introduce our own
convention of numbering boundaries: here we use 1 for all points on
the inner boundary and 2 for the outer boundary, while all other points
are marked by 0. The solver applies the markers
object to set
the right Dirichlet boundary conditions.
The next step is to deform the mesh. Given coordinates x, we can map these onto a stretched coordinate ˉx by
where s is a parameter that controls the amount of stretching. The formula above gives a stretching towards x=a, while the next one stretches the coordinates towards x=b:
The code shown later shows the details of mapping coordinates in a FEniCS mesh object.
The final step is to map the rectangle to a part of a hollow cylinder. Mathematically, a point (x,y) in the rectangle is mapped onto ˉx,ˉy) in our final geometry:
The relevant FEniCS code becomes
# --- Deform mesh ---
# First make a denser mesh towards r=a
x = mesh.coordinates()[:,0]
y = mesh.coordinates()[:,1]
def denser(x, y):
return [a + (b-a)*((x-a)/(b-a))**s, y]
x_bar, y_bar = denser(x, y)
xy_bar_coor = np.array([x_bar, y_bar]).transpose()
mesh.coordinates()[:] = xy_bar_coor
# Then map onto to a "piece of cake"
def cylinder(r, s):
return [r*np.cos(Theta*s), r*np.sin(Theta*s)]
x_hat, y_hat = cylinder(x_bar, y_bar)
xy_hat_coor = np.array([x_hat, y_hat]).transpose()
mesh.coordinates()[:] = xy_hat_coor
return mesh, markers
Fortunately, the solver is independent of all the details of the mesh making.
We could also have used the mesh tool mshr
in FEniCS, but with our
approach here we have full control of the refinement towards the hole.
We assume that α is constant. Before solving such a specific problem, it can be wise to scale the problem since it often reduces the amount of input data in the model. Here, the variation in u is typically |ua−ub| so we use that as characteristic pressure. The coordinates may be naturally scaled by the bore hole radius, so we have new, scaled variables
Now, we expect ˉu∈[0,1], which is a goal of scaling. Inserting this in the problem gives the PDE
in a domain with inner radius 1 and ˉu=0, and outer radius
with ˉu=1. Our solver can solve this problem by setting
alpha=1
, u_a=1
, and u_b=0
. We see that the dimensionless
parameter β goes to the mesh and not to the solver.
Figure shows a solution for β=2
on a mesh with 4⋅20⋅20=1600 triangles, 25 degree opening,
and P1 elements.
Switching to higher-order, say P3, is a matter of changing the degree
parameter that goes to the function V
in the solver:
mesh, markers = make_mesh(Theta=25*pi/180, a=1, b=2,
nr=20, nt=20, s=1.9)
beta = 2
solver(mesh, markers,
alpha=1, u_a=1, u_b=0, degree=3, filename='borehole1')
The complete code is found in borehole_fenics.py
. All fluids flow in the same way as long as the geometry is the same!
Solution for (scaled) fluid pressure around a bore hole in a porous medium.
How can we solve a 3D version of this problem? Then we would make a long cylinder. The assumption is that nothing changes in the third direction, so ∂/∂z=0. This means that the cross sections at the end of the cylinder have homogeneous Neumann conditions ∂u/∂n=0. Therefore, nothing changes in the variational form. Actually, all we have to do is to a generate a 3D box and use the same stretching and mapping to make the cylinder, and run the solver without changes!
When approximating f by u=∑jcjφj, the least squares method and the Galerkin/projection method give the same result. The interpolation/collocation method is simpler and yields different (mostly inferior) results.
Fourier series expansion can be viewed as a least squares or Galerkin approximation procedure with sine and cosine functions.
Basis functions should optimally be orthogonal or almost orthogonal, because this makes the coefficient matrix become diagonal or sparse and results in little round-off errors when solving the linear system. One way to obtain almost orthogonal basis functions is to localize the basis by making the basis functions that have local support in only a few cells of a mesh. This is utilized in the finite element method.
Finite element basis functions are piecewise polynomials, normally with discontinuous derivatives at the cell boundaries. The basis functions overlap very little, leading to stable and efficient numerics involving sparse matrices.
To use the finite element method for differential equations, we use the Galerkin method or the method of weighted residuals to arrive at a variational form. Technically, the differential equation is multiplied by a test function and integrated over the domain. Second-order derivatives are integrated by parts to allow for typical finite element basis functions that have discontinuous derivatives.
The least squares method is not much used for finite element solution of differential equations of second order, because it then involves second-order derivatives which cause trouble for basis functions with discontinuous derivatives.
We have worked with two common finite element terminologies and associated data structures (both are much used, especially the first one, while the other is more general):
a. elements, nodes, and mapping between local and global node numbers
b. an extended element concept consisting of cell, vertices, degrees of freedom, local basis functions, geometry mapping, and mapping between local and global degrees of freedom
The meaning of the word "element" is multi-fold: the geometry of a finite element (also known as a cell), the geometry and its basis functions, or all information listed under point 2 above.
One normally computes integrals in the finite element method element by element (cell by cell), either in a local reference coordinate system or directly in the physical domain.
The advantage of working in the reference coordinate system is that the mathematical expressions for the basis functions depend on the element type only, not the geometry of that element in the physical domain. The disadvantage is that a mapping must be used, and derivatives must be transformed from reference to physical coordinates.
Element contributions to the global linear system are collected in
an element matrix and vector, which must be assembled into the
global system using the degree of freedom mapping (dof_map
) or
the node numbering mapping (elements
), depending on which terminology
that is used.
Dirichlet conditions, involving prescribed values of u at the boundary, are mathematically taken care of via a boundary function that takes on the right Dirichlet values, while the basis functions vanish at such boundaries. The finite element method features a general expression for the boundary function. In software implementations, it is easier to drop the boundary function and the requirement that the basis functions must vanish on Dirichlet boundaries and instead manipulate the global matrix system (or the element matrix and vector) such that the Dirichlet values are imposed on the unknown parameters.
Neumann conditions, involving prescribed values of the derivative (or flux) of u, are incorporated in boundary terms arising from integrating terms with second-order derivatives by part. Forgetting to account for the boundary terms implies the condition ∂u/∂n=0 at parts of the boundary where no Dirichlet condition is set.