In this note, we look at constructing a finite element approximation to $$
First, let's read in a mesh. Here we use a list of vertices (in x- and y-coordinates) and triangular elements (ie, a list of three vertices). An example on [−1,1] is given here: https://gist.github.com/lukeolson/3705bf0b770476b66d61
%matplotlib inline
import numpy as np
import scipy.linalg as la
import scipy.sparse as sparse
import scipy.sparse.linalg as sla
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
V = np.loadtxt('mesh.v')
E = np.loadtxt('mesh.e', dtype=int)
#import refine_mesh
#V, E = refine_mesh.refine2dtri(V, E)
nv = V.shape[0]
ne = E.shape[0]
print(V.shape)
print(E.shape)
print(E.max())
X, Y = V[:, 0], V[:, 1]
plt.triplot(X, Y, E)
plt.show()
Now do some things to the mesh
G
is the adjacency matrix (V to E connections)E2E
identifies element-to-element connectionsV2V
identifies vertex-to-vertex connectionsE2E = G.T * G
E2E.toarray()[:10][:10]
print('V shape: ', V.shape)
print('E shape: ', E.shape)
ID = np.kron(np.arange(0, ne), np.ones((3,)))
G = sparse.coo_matrix((np.ones((ne*3,)), (E.ravel(), ID,)))
print('G shape: ', G.shape)
E2E = G.T * G
V2V = G * G.T
V2V = V2V.todia()
print('V2V shape: ', V2V.shape)
print('E2E shape: ', E2E.shape)
plt.scatter(X, Y, c=V2V.diagonal(), clip_on=False)
plt.colorbar()
plt.show()
E2E.data = 0.0 * E2E.data + 1.0
nbrs = np.array(E2E.sum(axis=0)).ravel()
plt.tripcolor(X, Y, triangles=E, facecolors=nbrs)
plt.colorbar()
plt.show()
At this point we have nv
vertices and ne
elements. We seek a solution of the form
uh=nv∑j=iuiϕi(x,y)
where ϕi(x,y) are linear basis functions and {ϕi}nvi=1 spans Vh, the space of piecewise linear, continous functions on the mesh.
To do this, we consider the weak form of this problem: find uh∈Vh such that ∫Ωκ(x,y)∇uh⋅∇ϕjdx=∫Ωfϕjdx, for all ϕj. Or, nv∑i=1ui∫Ωκ(x,y)∇ϕi⋅∇ϕjdx=∫Ωfϕjdx,
To simplify the evaluation of these integrals, we consider them over each mesh element τ. To do this, we write a Lagrange basis function ϕ(x,y) on mesh element τ in terms of the linear nodal basis function on a reference element, ˆτ, call this λ(α,β). Let (0,0), (1,0), and (0,1) represented the ˆτ be the reference element with basis functions λ1(α,β)=1−α−β,λ2(α,β)=α,λ3(α,β)=β.
Notice that the gradient of these basis functions are ∇λ1=[11],∇λ2=[10],∇λ3=[01]
The key is that if T(α,β)=C[αβ]+c is a transformation from ˆτ to τ, then we can define the linear nodal basis functions on τ by ϕr(x,y)=λr(T−1(x,y)) where r=1,2,3.
This transformation is straightforward to define: the reference coordinate system maps to the coordinate system at v1 with axis v2−v1 and v3−v1 by T(α,β)=(v2−v1)α+(v3−v1)β+v1 where vr=[xryr] for each vertex r=1,2,3.
Let's take a look. Let's map the reference triangle to (1,1), (3,1), (2,2).
XX = np.random.rand(1000)
YY = np.random.rand(1000)
I = np.where((XX + YY)<1.0)[0]
alph = XX[I]
beta = YY[I]
v1 = np.array([1.0, 1.0])
v2 = np.array([3.0, 1.0])
v3 = np.array([2.0, 8.0])
Tx = (v2[0] - v1[0]) * alph + (v3[0] - v1[0]) * beta + v1[0]
Ty = (v2[1] - v1[1]) * alph + (v3[1] - v1[1]) * beta + v1[1]
plt.plot(alph, beta,'r.')
plt.plot(Tx,Ty, 'b.')
plt.axis('square')
plt.show()
Let's define κ(x,y), f(x,y), and g(x,y).
κ={25if√x2+y2≤0.250.1elsef={25if√x2+y2≤0.250elseg=10(1−x2)def kappa(x, y):
if (x**2 + y**2)**0.5 <= 0.25:
return 25.0
else:
return 1.0
def f(x, y):
if (x**2 + y**2)**0.5 <= 0.25:
return 100.0
else:
return 0.0
def g(x):
return 1 * (1 - x**2)
The Jacobian J of transformation T is important since ∫ˆτf(x)dx=∫τf(T(x))|J|,dx. In our case, $$ J = \begin{bmatrix} v_3 - v_1 & v_2 - v_1
\nabla \phi_r = \nabla \lambda_r(T^{-1}(x, y)) = J^{-T} \nabla \lambda_r $$ Since $\lambda_r$ is constant and since $J$ is constant per triangle, these are taken outside of the integral.
If we visit each element to compute the contributions of nv∑i=1ui∫Ωκ(x,y)∇ϕi⋅∇ϕjdx=∫Ωfϕjdx, then there are three basis functions ϕi interacting with three basis functions ϕj. Thus nine entries per element. Let's allocate that space:
AA = np.zeros((ne, 9))
IA = np.zeros((ne, 9))
JA = np.zeros((ne, 9))
bb = np.zeros((ne, 3))
ib = np.zeros((ne, 3))
jb = np.zeros((ne, 3))
Here we have created space for the data, row, and column indices of the matrix (AA
, IA
, JA
) and of the right-hand side (bb
, ib
).
The loop over elements follows several steps:
For each element τ do the following
Get the coordinates of the element
Compute the Jacobian J, the inverse transpose J−T, and the determinant |J|). Call theese J
, invJ
, and detJ
.
Define the gradient of the reference basis functions ∇ϕr (this is actually constant for each element...see above). Call these dlambda
.
Compute the gradient of the real basis functions ∇ϕr=J−Tλr. Call these dphi
.
Now calculate the bilinear form on the element: ∫τκ(x)∇ϕr⋅∇ϕsdx=∫τκ(x)(J−T∇λr)T(J−T∇λs)dx=(J−T∇λr)T(J−T∇λs)|J|∫τκ(T(α,β))dαdβ Compute ∫τκ using a 1-point Gauss Quadrature rule ≈(1/2)κ(ˉx,ˉy)
Compute the right-hand side: ∫Ef(x)ϕrdx=|J|∫τf(T(α))λr(α)dα Now using a 1-point Gauss Quadrature rule gives ≈(1/2)f(ˉx,ˉy)λr(ˉα,ˉβ)=(1/6)f(ˉx,ˉy)
Add these to the matrix depending i and j
for ei in range(0, ne):
# Step 1
K = E[ei, :]
x0, y0 = X[K[0]], Y[K[0]]
x1, y1 = X[K[1]], Y[K[1]]
x2, y2 = X[K[2]], Y[K[2]]
# Step 2
J = np.array([[x1 - x0, x2 - x0],
[y1 - y0, y2 - y0]])
invJ = la.inv(J.T)
detJ = la.det(J)
# Step 3
dbasis = np.array([[-1, 1, 0],
[-1, 0, 1]])
# Step 4
dphi = invJ.dot(dbasis)
# Step 5
Aelem = kappa(X[K].mean(), Y[K].mean()) *\
(detJ / 2.0) * (dphi.T).dot(dphi)
# Step 6
belem = f(X[K].mean(), Y[K].mean()) *\
(detJ / 6.0) * np.ones((3,))
# Step 7
AA[ei, :] = Aelem.ravel()
IA[ei, :] = [K[0], K[0], K[0], K[1], K[1], K[1], K[2], K[2], K[2]]
JA[ei, :] = [K[0], K[1], K[2], K[0], K[1], K[2], K[0], K[1], K[2]]
bb[ei, :] = belem.ravel()
ib[ei, :] = [K[0], K[1], K[2]]
jb[ei, :] = 0
The matrices were constructed in COO format, meaning that duplicates were formed. Convert to CSR to contract the duplicate entries. Then convert back to COO for easier manipulation.
A = sparse.coo_matrix((AA.ravel(), (IA.ravel(), JA.ravel())))
A = A.tocsr()
A = A.tocoo()
b = sparse.coo_matrix((bb.ravel(), (ib.ravel(), jb.ravel())))
b = b.tocsr()
b = np.array(b.todense()).ravel()
Next, boundary conditions are applied in this step. First, define a function u0 that is equal to g on the x=0 boundary and
zero elsewhere. To do this, first create two boolean vectors: one
that is true for each vertex on the y=1 boundary (gflag
) and the other that
is true for each vertex on the whole boundary (Dflag
). Then
set u0 appropriately.
Next, we need to set
b←b−Au0,
which is just a matrix-vector product. Finally, we need to set:
Aij=0Aii=1bi=0,
for each boundary node i. The best strategy is to loop over all of
the non-zero entries in A by looking at A.row
,
A.col
, and A.data
in COO format. Check each
row/column index for presence on the boundary and set the values
accordingly.
First flag the locations of the boundary
tol = 1e-12
Dflag = np.logical_or.reduce((abs(X+1) < tol,
abs(X-1) < tol,
abs(Y+1) < tol,
abs(Y-1) < tol))
gflag = abs(Y+1) < tol
ID = np.where(Dflag)[0]
Ig = np.where(gflag)[0]
Then construct u0(x,y)=g(x) for y=1 and zero elsewhere
u0 = np.zeros((nv,))
u0[Ig] = g(X[Ig])
b = b - A * u0
Then mark the diagonal as 1.0 and the off diagonals as 0.0 for each boundary vertex
for k in range(0, len(A.data)):
i = A.row[k]
j = A.col[k]
if Dflag[i] or Dflag[j]:
if i == j:
A.data[k] = 1.0
else:
A.data[k] = 0.0
b[ID] = 0.0
Now solve (and correct from above)
A = A.tocsr()
u = sla.spsolve(A, b)
u = u + u0
And plot
fig = plt.figure(figsize=(8,8))
ax = plt.gca(projection='3d')
ax.plot_trisurf(X, Y, u, triangles=E, cmap=plt.cm.jet, linewidth=0.2)
plt.show()