Menu Home

Dealing with volumic curved elements

curved_quad

Dealing with volumic curved elements

The maths

To be improved :

The mapping

Consider a reference volumic element $\hat{\Omega}$ or order $n$ and the $n$ associated shape functions $\hat{\lambda}_i$. We restrict this explanation to elements of order $n$ with $n$ nodes $\hat{x}_i$ (excluding Hermite elements for instance).

Now considering a local volumic element of order $n$ and its $n$ nodes $x_i$. The reference to local element mapping read : $$x = F_{rl}(\hat{x}) = \sum_{i=1}^n \hat{\lambda}_i(\hat{x}) x_i.$$

So the shape functions in the reference element and the nodes of the local element give the mapping.

Integration formula

Using Finite Element Method, one has to compute integrals in the local element : $\int_\Omega f(x) dx$. To compute an integral on an element, we often use numerical integration by applying quadrature rules: $$\int_\Omega f(x) dx = \sum_{i \in N} \omega_i f(\xi_i),$$ where $\omega_i$ and $\xi_i$ are the $N$ weights and nodes of the selected quadrature rule.

Now, the quadrature rules are not known for every type of geometrical elements, they are actually often determined for simple geometrical elements called reference element : the $[-1,1]$ segment, a right triangle, a square…

To compute an integral on a arbitrary element, we have to apply a transformation to this local element to obtain the reference element and apply the quadrature rule. To do so, we need this very important formula: $$\int_\Omega f(x) dx = \int_\hat{\Omega} |J_{rl}(\hat{x})| \left(f \circ F_{rl}\right)(\hat{x}) d\hat{x},$$ where $F_{rl}$ is the reference to local mapping, i.e $F_{rl}(\hat{x}) = s$, and $J_{rl}$ is the determinant of the jacobian matrix of this mapping.

Summary

To use a curved volumic element, we only need to know a reference element and the associated shape functions. Then the mapping is directly obtained and the integration formula can be applied.

The code

Import necessary modules: LinearAlgebra, ForwardDiff for automatic differentiation, Cubature to apply quadrature on ‘multi-dimensionnal’ elements (otherwise in 1D there is FastGaussQuadrature).

In [1]:
using LinearAlgebra
using ForwardDiff
using Cubature
using Plots

We overload the sum and product operator of two functions (in order for ForwardDiff to apply without harm):

In [2]:
Base.:+(f::Function, g::Function) = x -> f(x) + g(x)
Base.:*(f::Function, g::Function) = x -> f(x) * g(x)

We create an alias for the sum of functions, just for fun:

In [3]:
const Σ = sum;

Now we define two structures for nodes and elements. These structures could be parametrized by the size of the dimensionnal space ($\mathbb{R}^2$, $\mathbb{R}^3$…), but for the sake of clarity everything will be in $\mathbb{R}^2$ here. A Node is simply an array containing the coordinates. An Element is an ordered list of nodes. The chosen order is arbitrary, here we will choose gmsh convention (more details below).

In [4]:
struct Node
    x :: Array{Float64, 1}
end

struct Element{npts}
  nodes :: NTuple{npts, Node}
end

A convenient function (specially to plot) is immediatly declared to convert an Element into a matrices containing all its nodes coordinates:

In [5]:
function element2mat(element)
  mat = zeros(2, length(element.nodes))
  for i = 1 : length(element.nodes)
    mat[:,i] = element.nodes[i].x
  end
  return mat
end;

We can now specify two particular entities : the quadrilateral of order 2, defined by its nine nodes; and the reference quadrilateral (=square) of order 2. The order of the nodes is conform to gmsh convention : the four nodes of the square, the four mid-nodes and the center. The choice of $[-1,1] \times [-1,1]$ is arbitrary, the only thing that matter is that the shape-functions (see below) are built on these nodes.

In [6]:
const Quad2 = Element{9}
const quadRef = Quad2((Node([-1., -1.]), Node([1., -1.]), Node([1., 1.]), Node([-1., 1.]), Node([0., -1]), Node([1., 0.]),
Node([0., 1.]), Node([-1., 0.]), Node([0., 0.])));

Let’s plot the nodes and edges of our reference element:

In [7]:
refmat = element2mat(quadRef)
scatter(refmat[1,:], refmat[2,:], label = "Reference element", aspect_ratio = :equal, legend=:topleft)
plot!(push!(refmat[1,1:4], refmat[1,1]), push!(refmat[2,1:4], refmat[2,1]), label = "")
Out[7]:

Now the nine shape functions associated to the reference element or order 2. These are Lagrange $x,y$ polynomials who cancel in every nodes but one, where their value is 1. Important detail here : because we will apply automatic differentiation on a combination of these functions, we use a vector variable x instead of a couple x,y.

In [8]:
function shape_functions(element::Quad2)
  λ₁(x)    =  1/4 * x[1] * x[2] * (1 - x[1]) * (1 - x[2])
  λ₂(x)    = -1/4 * x[1] * x[2] * (1 + x[1]) * (1 - x[2])
  λ₃(x)    =  1/4 * x[1] * x[2] * (1 + x[1]) * (1 + x[2])
  λ₄(x)    = -1/4 * x[1] * x[2] * (1 - x[1]) * (1 + x[2])
  λ₁₂(x)   = -1/2 * (1 + x[1]) * (1 - x[1]) * x[2] * (1 - x[2])
  λ₂₃(x)   =  1/2 * x[1] * (1 + x[1]) * (1 - x[2]) * (1 + x[2])
  λ₃₄(x)   =  1/2 * (1 - x[1]) * (1 + x[1]) * x[2] * (1 + x[2])
  λ₄₁(x)   = -1/2 * x[1] * (1 - x[1]) * (1 - x[2]) * (1 + x[2])
  λ₁₂₃₄(x) =  (1 + x[1]) * (1 - x[1]) * (1 - x[2]) * (1 + x[2])
  return λ₁, λ₂, λ₃, λ₄, λ₁₂, λ₂₃, λ₃₄, λ₄₁, λ₁₂₃₄
end;

With these shape functions, the reference element to local element mapping can be easily defined. Indeed this mapping is: $$ \begin{pmatrix} x \\ y \end{pmatrix} = F_{rl}(\hat{x}, \hat{y}) = \sum_{i=1}^n \hat{\lambda}_i(\hat{x}_i, \hat{y}_i) \begin{pmatrix} x_i\\ y_i \end{pmatrix}$$ where $n$ is the number of nodes.

The building the mapping only consists in gathering the shape functions and computing the sum weighted by the coordinates of the local element:

In [9]:
function ref2loc(element::Quad2)
  # Get the shape functions for this element
  λ = shape_functions(element)

  # Weight each function by the corresponding node
  fns = []
  for i = 1 : length(element.nodes)
    push!(fns, x -> element.nodes[i].x .* λ[i](x))
  end

  # Return the sum of these functions
  return Σ(fns)
end;

Let’s illustrate our mapping. First we define 9 nodes of our curved element. This element represent a portion of annulus defined by $1 \leq r \leq 2$ and $0 \leq \theta \leq \pi / 4$. This can be obtained with gmsh : define your geometry, then ask for a second order mesh and read the output nodes.

In [10]:
p₁    = Node([1., 0.])
p₂    = Node([2., 0.])
p₃    = Node([(2), (2)])
p₄    = Node([(2)/2, (2)/2])
p₁₂   = Node([1.5, 0.])
p₂₃   = Node([1.847759064007178, 0.7653668671815615])
p₃₄   = Node([1.060660171779187, 1.060660171779187])
p₄₁   = Node([0.923879532003589, 0.3826834335907808])
p₁₂₃₄ = Node([1.385819298004377, 0.5740251503858539]);

Then we can build a quadrilateral of order 2 and retrieve the mapping:

In [11]:
quad = Quad2((p₁, p₂, p₃, p₄, p₁₂, p₂₃, p₃₄, p₄₁, p₁₂₃₄))
F = ref2loc(quad);

Now plot the local element using the mapping and the nodes. We draw the edges of the local element by applying the mapping on the edge of the reference element. We also « translate » the local element to improve the plot.

In [12]:
mat = element2mat(quad)
n = 100
tₓ = 1. # For translation

# First 'horizontal' edges
x = range(-1., stop = 1., length = n)
xymin = zeros(2, n); xymax = zeros(2, n) 
for i = 1 : n
  xymin[:,i] = F([x[i], -1.])
  xymax[:,i] = F([x[i],  1.])
end
plot!(xymin[1,:] .+ tₓ, xymin[2,:], label = "")
plot!(xymax[1,:] .+ tₓ, xymax[2,:], label = "")

# Then 'vertical' edges
y = range(-1., stop = 1., length = n)
xymin = zeros(2, n); xymax = zeros(2, n) 
for i = 1 : n
  xymin[:,i] = F([-1., y[i]])
  xymax[:,i] = F([ 1., y[i]])
end
plot!(xymin[1,:] .+ tₓ, xymin[2,:], label = "")
plot!(xymax[1,:] .+ tₓ, xymax[2,:], label = "")

# Finally, the nodes
scatter!(mat[1,:] .+ tₓ, mat[2,:], label = "Curved element")
Out[12]:

Now that we have the mapping, we juste need a function to compute an integral in the local element : first we apply the local to reference element transformation (see formula in the first part of this document) and then we compute the integral in the reference element with a quadrature (using the Cubature package):

In [13]:
function (element::Quad2, g, F, J)
  f = J * (g  F)
  #(val, err) = hcubature(f, xmin = [-1., -1.], xmax = [1., 1.])
  (val, err) = hcubature(f, [-1., -1.], [1., 1.]) # [-1,-1] and [1,1] are the bounds of the reference element
  return val
end;

Here we chose to provide the integral function with the mapping and the jacobian, but it could have been retrieved from the element. This jacobian is simply computed from the mapping $F_{rl}$ using automatic differentiation with the ForwardDiff package (see below).

Let’s play with this function. First, what is the area of our reference element?

In [14]:
F = ref2loc(quadRef)
jacob = x -> ForwardDiff.jacobian(F, x)
J = x -> abs(det(jacob(x)))
g(x) = 1.
@show (quadRef, g, F, J);
∫(quadRef, g, F, J) = 3.999999999999999

What is the area of the annulus portion? Analytically, the calculus is $\mathrm{A} = \int_{\theta = 0}^{\pi/4} \int_{r=1}^2 r dr d\theta = 3 \pi / 8 $. In cartesian coordinates, it is just the integral of the $x \rightarrow 1$ function.

In [15]:
F = ref2loc(quad)
jacob = x -> ForwardDiff.jacobian(F, x)
J = x -> abs(det(jacob(x)))
g(x) = 1.
@show (quad, g, F, J), 3π / 8;
(∫(quad, g, F, J), (3π) / 8) = (1.1771803388670854, 1.1780972450961724)

Note that the two results are close but not exactly the same : this is likely due to the approximation of the annulus section by a second order quadrilateral.

And finally, let’s integrate a more complex function, such as $(x,y) \rightarrow \sqrt{x^2 + y^2}$. Analytically, we have : $\int_\Omega \sqrt{x^2 + y^2} dxdy = \int_{\theta = 0}^{\pi/4} \int_{r=1}^2 r^2 dr d\theta = 7\pi / 4$.

In [16]:
F = ref2loc(quad)
jacob = x -> ForwardDiff.jacobian(F, x)
J = x -> abs(det(jacob(x)))
g(x) = (x[1]^2 + x[2]^2)
@show (quad, g, F, J), 7π / 12;
(∫(quad, g, F, J), (7π) / 12) = (1.8304568583586573, 1.832595714594046)