FEM Higher Order¶
import numpy as np
import scipy.sparse as spr
from scipy.sparse.linalg import spsolve
import plotly.offline as plotly
import plotly.graph_objects as go
import plotly.figure_factory as ff
# For static visualization
import plotly.io as pio
pio.renderers.default = "svg"
# For interactive visualization
#pio.renderers.defaut = "notebook"
As in the linear case, the domain is \(\Omega = [0, 1]\), which we discretize with \(n_{el}\) segments (or elements) \(s_i\).
Now we also create the high-order nodes.
Note that we append them at the end.
#domain
omega = np.array([0, 1])
#number of bases and elements
n_el = 10
#Regular nodes, as before
s = np.linspace(omega[0], omega[1], num=n_el+1)
# s = np.cumsum(np.random.rand(n_elements+1))
s = (s-s[0])/(s[-1]-s[0])
# we now pick the order
order = 3
nodes = s
#more bases
n_bases = n_el + 1 + n_el*(order-1)
#create the nodes for the plots
for e in range(n_el):
#For every segment, we create order + 1 new high-order nodes
tmp = np.linspace(s[e], s[e+1], num=order+1)
#exclude the first and last since they already exists
tmp = tmp[1:-1]
#and append at the end of nodes
nodes = np.append(nodes, tmp)
Plot, in orange the high-order nodes, in blue the linear nodes.
go.Figure(data=[
go.Scatter(x=s, y=np.zeros(s.shape), mode='lines+markers'),
go.Scatter(x=nodes[n_el+1:], y=np.zeros(nodes.shape), mode='markers')
])
Local bases¶
As in the linear case, we define the reference element \(\hat s= [0, 1]\), a segment of unit length.
On each element we now have
order+1
(e.g., 2 for linear, 3 for quadratic) non-zero local bases.
We define their “piece” on \(\hat s\).
It is important to respect the order of the nodes: the first 2 bases are always for the endpoints, and the others are ordered left to right.
Definition of linear bases, same as before
def hat_phi_1_0(x):
return 1-x
def hat_phi_1_1(x):
return x
Definition of quadratic bases
def hat_phi_2_0(x):
return 2*(x-0.5)*(x-1)
def hat_phi_2_1(x):
return 2*(x-0)*(x-0.5)
def hat_phi_2_2(x):
return -4*(x-0.5)**2+1
Definition of cubic bases
def hat_phi_3_0(x):
return -9/2*(x-1/3)*(x-2/3)*(x-1)
def hat_phi_3_1(x):
return 9/2*(x-0)*(x-1/3)*(x-2/3)
def hat_phi_3_2(x):
return 27/2*(x-0)*(x-2/3)*(x-1)
def hat_phi_3_3(x):
return -27/2*(x-0)*(x-1/3)*(x-1)
Utility function to return the list of functions
def hat_phis(order):
if order == 1:
return [hat_phi_1_0, hat_phi_1_1]
elif order == 2:
return [hat_phi_2_0, hat_phi_2_1, hat_phi_2_2]
elif order == 3:
return [hat_phi_3_0, hat_phi_3_1, hat_phi_3_2, hat_phi_3_3]
We can now plot the
order+1
local bases, same code as before.
Note that the first two bases correspond to the end-points, and the others are ordered.
x = np.linspace(0, 1)
data = []
tmp = hat_phis(order)
for o in range(order+1):
data.append(go.Scatter(x=x, y=tmp[o](x), mode='lines', name="$\hat\phi_{}$".format(o)))
go.Figure(data=data)
We use
sympy
to compute the gradients of the local bases.
import sympy as sp
xsym = sp.Symbol('x')
def grad_hat_phis(order):
#For linear we need to get the correct size
if order == 1:
return [lambda x : -np.ones(x.shape), lambda x : np.ones(x.shape)]
res = []
tmp = hat_phis(order)
for fun in tmp:
res.append(sp.lambdify(xsym, fun(xsym).diff(xsym)))
return res
Plotting gradients
x = np.linspace(0, 1)
data = []
tmp = grad_hat_phis(order)
for o in range(order+1):
data.append(go.Scatter(x=x, y=tmp[o](x), mode='lines', name="$\hat\phi_{}$".format(o)))
go.Figure(data=data)
Basis construction¶
This code is exacly as before.
The only difficulty is the local to global mapping:
- the first 2 nodes are always the same
\[ g_e^0 = e \qquad\mathrm{and}\qquad g_e^1=g+1 \] - the others are
\[ g_e^i = n_{el} + e (\mathrm{order}-1) + i. \]
elements = []
for e in range(n_el):
el = {}
el["n_bases"] = order+1
#2 bases
el["phi"] = hat_phis(order)
el["grad_phi"] = grad_hat_phis(order)
#local to global mapping
high_order_nodes = list(range(n_el + 1 + e*(order-1), n_el + e*(order-1) + order))
el["loc_2_glob"] = [e, e+1] + high_order_nodes
#geometric mapping
el["gmapping"] = lambda x, e=e : s[e] + x*(s[e+1]-s[e])
el["grad_gmapping"] = lambda x : (s[e+1]-s[e])
elements.append(el)
We define a function to interpolate the vector \(\vec{u}\) using the local to global, geometric mapping, and local bases to interpolate the data, as before.
def interpolate(u):
uinterp = np.array([])
x = np.array([])
xhat = np.linspace(0, 1)
for e in range(n_el):
el = elements[e]
uloc = np.zeros(xhat.shape)
for i in range(el["n_bases"]):
glob_node = el["loc_2_glob"][i]
loc_base = el["phi"][i]
uloc += u[glob_node] * loc_base(xhat)
uinterp = np.append(uinterp, uloc)
x = np.append(x, el["gmapping"](xhat))
return x, uinterp
We can generate a random vector \(\vec{u}\) and use the previous function. This will interpolate all nodes.
u = np.random.rand(n_bases)
x, uinterp = interpolate(u)
go.Figure(data=[
go.Scatter(x=x, y=uinterp, mode='lines'),
go.Scatter(x=nodes, y=u, mode='markers'),
])
Assembly¶
We are now ready the assemble the global stiffness matrix, which is exacly as before.
import quadpy
scheme = quadpy.line_segment.gauss_patterson(5)
rows = []
cols = []
vals = []
for e in range(n_el):
el = elements[e]
for i in range(el["n_bases"]):
for j in range(el["n_bases"]):
val = scheme.integrate(
lambda x:
el["grad_phi"][i](x) * el["grad_phi"][j](x) / el["grad_gmapping"](x),
[0.0, 1.0])
rows.append(el["loc_2_glob"][i])
cols.append(el["loc_2_glob"][j])
vals.append(val)
rows = np.array(rows)
cols = np.array(cols)
vals = np.array(vals)
L = spr.coo_matrix((vals, (rows, cols)))
L = spr.csr_matrix(L)
We set the rows
0
and n_el
to identity for the boundary conditions.
for bc in [0, n_el]:
_, nnz = L[bc,:].nonzero()
for j in nnz:
if j != bc:
L[bc, j] = 0.0
L[bc, bc] = 1.0
We set the right-hand side to zero, and set the two boundary conditions to 1 and 4.
f = np.zeros((n_bases, 1))
f[0] = 1
f[n_el] = 4
We now solve \(L\vec{u}=f\) for \(\vec{u}\).
u = spsolve(L, f)
We now plot the solution. We expect a line, independently of
order
!
x, uinterp = interpolate(u)
go.Figure(data=[
go.Scatter(x=x, y=uinterp, mode='lines', name="solution"),
go.Scatter(x=nodes, y=u, mode='markers', name="$u$"),
])
Mass Matrix¶
This is exactly as before!
rows = []
cols = []
vals = []
for e in range(n_el):
el = elements[e]
for i in range(el["n_bases"]):
for j in range(el["n_bases"]):
val = scheme.integrate(
lambda x:
el["phi"][i](x) * el["phi"][j](x) * el["grad_gmapping"](x),
[0.0, 1.0])
rows.append(el["loc_2_glob"][i])
cols.append(el["loc_2_glob"][j])
vals.append(val)
rows = np.array(rows)
cols = np.array(cols)
vals = np.array(vals)
M = spr.coo_matrix((vals, (rows, cols)))
M = spr.csr_matrix(M)
Now we set \(\vec{f}=4\) and zero boundary conditions.
f = 4*np.ones((n_bases, 1))
f = M*f
f[0] = 0
f[n_el] = 0
We now solve \(L\vec{u}=M\vec{f}\) for \(\vec{u}\)
u = spsolve(L, f)
x, uinterp = interpolate(u)
go.Figure(data=[
go.Scatter(x=x, y=uinterp, mode='lines', name="solution"),
go.Scatter(x=nodes, y=u, mode='markers', name="$u$"),
])