# (launch:thebe)=
# # Defining a finite element ({term}`FE`)
#
# The finite element method is a way of representing a function $u$ in a function space $V$, given $u$
# satisfies a certain partial differential equation.
#
# In this tutorial, we will keep to function spaces $V\subset H^{1}(\Omega)$,
#
# $$
# H^1(\Omega):=\{u\in L^2(\Omega) \text{ and } \nabla u\in L^2(\Omega)\}.
# $$
#
# Next, we need a finite subset of functions in $H^1(\Omega)$, and we call this the finite element space.
# This means that any function in $V$ can be represented as a linear combination of these basis functions
#
# $$u\in V \Leftrightarrow u(x) = \sum_{i=0}^{N-1} u_i \phi_i(x),$$
# where $N$ are the number of basis functions, $\phi_i$ is the $i$-th basis function and $u_i$ are the coefficients.
#
#
# ## Formal definition of a finite element
# A finite element is often described{cite}`ciarlet2002` as a triplet $(R, \mathcal{V}, \mathcal{L})$ where
# - $R\subset \mathbb{R}^n$ is the reference element (often polygon or polyhedron).
# - $\mathcal{\mathcal{V}}$ is a finite-dimensional polynomial space on $R$ with dimension $n$.
# - $\mathcal{L}=\{l_0, \dots, l_{n-1}\}$ is the basis of the dual space
# $\mathcal{V}^*:=\{f:\mathcal{V}\rightarrow\mathbb{R}\vert f \text{ is linear}\}.$
#
# One often associates each $l_i$ with a sub-entity of $R$, i.e. a vertex, an edge, a face or the cell itself.
#
# ## Example: A second order Lagrange space on a quadrilateral
#
# ```{sidebar}
#
# 
# Illustration of dof positioning on a quadrilateral
# (from DefElement, CC BY 4.0).
#
# ```
#
# - $R$ is a quadrilateral with vertices $(0,0)$, $(1,0)$, $(0,1)$ and $(1,1)$.
# - $\mathcal{V}=\mathrm{span}\{1, y, y^2, x, x^2, xy, xy^2, x^2, x^2y, x^2y^2\}$.
# - The basis of the dual space is
#
# $$
# \begin{align*}
# l_0&: v \mapsto v(0,0)\\
# l_1&: v \mapsto v(1,0)\\
# l_2&: v \mapsto v(0,1)\\
# l_3&: v \mapsto v(1,1)\\
# l_4&: v \mapsto v(0.5,0)\\
# l_5&: v \mapsto v(0, 0.5)\\
# l_6&: v \mapsto v(1, 0.5)\\
# l_7&: v \mapsto v(0.5, 1)\\
# l_8&: v \mapsto v(0.5, 0.5)\\
# \end{align*}
# $$
#
# ## Determining the underlying basis functions $\phi_i$
#
# The basis functions are determined by
#
# $$l_i(\phi_j)=\delta_{ij}=\begin{cases}1 \qquad i=j\\ 0 \qquad i \neq j\end{cases}$$
#
# For the example above this gives us the following basis functions
#
#
#
#
#
#
#
#
#
#
#
#
#
# The basis functions of a second order Lagrange space on a quadrilateral
# (from DefElement, CC BY 4.0).
#
#
#
# ### Uniqueness of basis functions
# This means that we can use different basis functions to span the same polynomial spaces $\mathcal{V}$
# by choosing different dual basis functions $l_i$.
#
# An example of this is choosing different positioning of the dofs (non-equispaced) for Lagrange elements.
# See [FEniCS: Variants of Lagrange Elements](https://docs.fenicsproject.org/dolfinx/v0.9.0/python/demos/demo_lagrange_variants.html)
# for more information.
#
# An algorithmic approach for determining the basis functions based of the dual space is for instance given at
# [Finite elements - analysis and implementation
# (Imperial College London)](https://finite-element.github.io/L2_fespaces.html#vandermonde-matrix-and-unisolvence).
#
# # Creating a finite element in FEniCSx
#
# There is a large variety of finite elements: [List of finite elements](https://defelement.com/elements/index.html).
#
# However, in today's lecture, we will focus on the [Lagrange elements](https://defelement.com/elements/lagrange.html).
#
# We start by considering the basis functions of a first order Lagrange element on a triangle:
#
#
#
#
# 
# First order Lagrange basis functions
#
#
# To symbolically represent this element, we use `basix.ufl.element`,
# that creates a representation of a finite element using {term}`Basix`,
# which in turn can be used in the {term}`UFL`.
# +
import numpy as np
import basix.ufl
element = basix.ufl.element("Lagrange", "triangle", 1)
# -
# The three arguments we provide to Basix are the name of the element family, the reference
# cell we would like to use and the degree of the polynomial space.
#
# Next, we will evaluate the basis functions at a certain set of `points` in the reference triangle.
# We call this {term}`tabulation` and we use the `tabulate` method of the element object.
# The two input arguments are:
# 1) The number of spatial derivatives of the basis functions we want to compute.
# 2) A set of input points to evaluate the basis functions at as
# a numpy array of shape `(num_points, reference_cell_dimension)`.
#
# In this case, we want to compute the basis functions themselves, so we set the first argument to 0.
# +
points = np.array([[0.0, 0.1], [0.3, 0.2]])
values = element.tabulate(0, points)
# -
# + tags = ["remove-input"]
print(f"{values=}")
# -
# We can get the first order derivatives of the basis functions by setting the first argument to 1.
# Observe that the output we get from this command also includes the 0th order derivatives.
# Thus we note that the output has the shape `(num_spatial_derivatives+1, num_points, num_basis_functions)`
# +
values = element.tabulate(1, points)
# -
# + tags = ["remove-input"]
print(values)
# -
# ## Visualizing the basis functions
# Next, we create a short script for visualizing the basis functions on the reference element.
# We will for simplicity only consider first order derivatives and triangular or quadrilateral cells.
import matplotlib.pyplot as plt
def plot_basis_functions(element, M: int):
"""Plot the basis functions and the first order derivatives sampled at
`M` points along each direction of an edge of the reference element.
:param element: The basis element
:param M: Number of points
:return: The matplotlib instances for a plot of the basis functions
"""
# We use basix to sample points (uniformly) in the reference cell
points = basix.create_lattice(element.cell_type, M - 1, basix.LatticeType.equispaced, exterior=True)
# We evaluate the basis function and derivatives at the points
values = element.tabulate(1, points)
# Determine the layout of the plots
num_basis_functions = values.shape[2]
num_columns = values.shape[0]
derivative_dir = ["x", "y"]
figs = [
plt.subplots(1, num_columns, layout="tight", subplot_kw={"projection": "3d"})
for i in range(num_basis_functions)
]
colors = plt.rcParams["axes.prop_cycle"]()
for i in range(num_basis_functions):
_, axs = figs[i]
[(ax.set_xlabel("x"), ax.set_ylabel("y")) for ax in axs.flat]
for j in range(num_columns):
ax = axs[j]
ax.scatter(points[:, 0], points[:, 1], values[j, :, i], color=next(colors)["color"])
if j > 0:
ax.set_title(
r"$\partial\phi_{i}/\partial {x_j}$".format(i="{" + f"{i}" + "}", x_j=derivative_dir[j - 1])
)
else:
ax.set_title(r"$\phi_{i}$".format(i="{" + f"{i}" + "}"))
return figs
# ## Basis functions sampled at random points
fig = plot_basis_functions(element, 15)
# We also illustrate the procedure on a second order Lagrange element on a quadrilateral, as discussed above.
second_order_element = basix.ufl.element("Lagrange", "quadrilateral", 2, basix.LagrangeVariant.gll_warped)
fig = plot_basis_functions(second_order_element, 12)
# ## Lower precision tabulation
#
# In some cases, one might want to use a lower accuracy for tabulation of basis functions to speed up computations.
# This can be changed in basix by adding `dtype=np.float32` to the element constructor.
low_precision_element = basix.ufl.element("Lagrange", "triangle", 1, dtype=np.float32)
points_low_precision = points.astype(np.float32)
basis_values = low_precision_element.tabulate(0, points_low_precision)
print(f"{basis_values=}\n {basis_values.dtype=}")
# We observe that elements that are close to zero is now an order of magnitude larger than its `np.float64` counterpart.
# ## Exercises
#
# Basix allows for a large variety of extra options to tweak your finite elements, see for instance
# [Variants of Lagrange elements](https://docs.fenicsproject.org/dolfinx/v0.9.0/python/demos/demo_lagrange_variants.html)
# for how to choose the node spacing in a Lagrange element.
# Using the plotting script above, try to plot basis functions of a high order quadrilateral element with different Lagrange variants.
# - Do you observe the same phenomenon on quadrilaterals as on intervals?
# - What about triangles?
#
# ```{admonition} Tip
# :class: dropdown tip
# Try to increase the plotting resolution to 40.
# ```
# ## References
# ```{bibliography}
# :filter: cited and ({"src/finite_element_method/finite_element"} >= docnames)
# ```