Skip to content

Commit

Permalink
GeometryMapping and FunctionValues as sub-components for FEValues (Fe…
Browse files Browse the repository at this point in the history
…rrite-FEM#764)

This patch should be non-breaking for documented usage of FEValues. The main changes are:

* Introduce FunctionValues and GeometryMapping as sub-components for use in FEValues

* Use FunctionValues and GeometryMapping in CellValues and FaceValues

* FaceValues have a vector of these types, one for each face. 

* Prepare for generalized mapping by having the cell::AbstractCell as (optional) input to reinit

* Allow keyword arguments to CellValues, FaceValues, and PointValues to optionally update the gradient

* Replace PointValuesInternal with a PointValues that doesn't update jacobians and gradients

* Update both geometric and function shape values and gradients during changes of quadrature points in reinit of PointValues

Documentation changes: 

* Describe the theory behind the mapping of finite elements

* Show how a SimpleCellValues object can be created to explain via code how this mapping is done in a simple case

* Devdocs for FEValues
  • Loading branch information
KnutAM authored Dec 6, 2023
1 parent 2dfcaf9 commit 2d21665
Show file tree
Hide file tree
Showing 30 changed files with 1,194 additions and 636 deletions.
6 changes: 3 additions & 3 deletions benchmark/helper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,17 @@ function _generalized_ritz_galerkin_assemble_interfaces(dh::Ferrite.AbstractDofH
end

# Minimal Petrov-Galerkin type local assembly loop. We assume that both function spaces share the same integration rule. Test is applied from the left.
function _generalized_petrov_galerkin_assemble_local_matrix(grid::Ferrite.AbstractGrid, cellvalues_shape::CellValues{<: Ferrite.InterpolationByDim{dim}}, f_shape, cellvalues_test::CellValues{<: Ferrite.InterpolationByDim{dim}}, f_test, op) where {dim}
function _generalized_petrov_galerkin_assemble_local_matrix(grid::Ferrite.AbstractGrid, cellvalues_shape::CellValues, f_shape, cellvalues_test::CellValues, f_test, op)
n_basefuncs_shape = getnbasefunctions(cellvalues_shape)
n_basefuncs_test = getnbasefunctions(cellvalues_test)
Ke = zeros(n_basefuncs_test, n_basefuncs_shape)

#implicit assumption: Same geometry!
X_shape = zeros(Vec{dim,Float64}, Ferrite.getngeobasefunctions(cellvalues_shape))
X_shape = zeros(get_coordinate_type(grid), Ferrite.getngeobasefunctions(cellvalues_shape))
getcoordinates!(X_shape, grid, 1)
reinit!(cellvalues_shape, X_shape)

X_test = zeros(Vec{dim,Float64}, Ferrite.getngeobasefunctions(cellvalues_test))
X_test = zeros(get_coordinate_type(grid), Ferrite.getngeobasefunctions(cellvalues_test))
getcoordinates!(X_test, grid, 1)
reinit!(cellvalues_test, X_test)

Expand Down
130 changes: 73 additions & 57 deletions docs/Manifest.toml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Optim = "429524aa-4258-5aef-a3af-852621145aeb"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
Tensors = "48a634ad-e948-5137-8d70-aa71f2a747f4"
TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
UnPack = "3a884ed6-31ef-47d7-9d2a-63182c4928ed"
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ bibtex_plugin = CitationBibliography(
"Topic guides" => [
"Topic guide overview" => "topics/index.md",
"topics/fe_intro.md",
"topics/FEValues.md",
"topics/degrees_of_freedom.md",
"topics/assembly.md",
"topics/boundary_conditions.md",
Expand Down
13 changes: 11 additions & 2 deletions docs/src/assets/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,17 @@ @article{Scroggs2022
@phdthesis{Cenanovic2017,
author={Mirza Cenanovic},
title={Finite element methods for surface problems},
school={J\o{}pk\o{}ping University, School of Engineering},
year=2017
school={Jönköping University, School of Engineering},
year={2017},
}
@misc{Kirby2017,
title={A general approach to transforming finite elements},
author={Robert C. Kirby},
year={2017},
eprint={1706.09017},
doi={10.48550/arXiv.1706.09017},
archivePrefix={arXiv},
primaryClass={math.NA},
}
@article{SimMie:1992:act,
title = {Associative coupled thermoplasticity at finite strains: Formulation, numerical analysis and implementation},
Expand Down
39 changes: 39 additions & 0 deletions docs/src/devdocs/FEValues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# [FEValues](@id devdocs-fevalues)

## Type definitions
* `AbstractValues`
* `AbstractCellValues`
* [`CellValues`](@ref)
* `AbstractFaceValues`
* [`FaceValues`](@ref)
* [`PointValues`](@ref)
* `PointValuesInternal` (Optimized version of PointValues)

## Internal types
```@docs
Ferrite.GeometryMapping
Ferrite.MappingValues
Ferrite.FunctionValues
```

## Custom FEValues
Custom FEValues, `fe_v::AbstractValues`, should normally implement the [`reinit!`](@ref) method. Subtypes of `AbstractValues` have default implementations for some functions, but require some lower-level access functions, specifically

* [`function_value`](@ref), requires
* [`shape_value`](@ref)
* [`getnquadpoints`](@ref)
* [`getnbasefunctions`](@ref)
* [`function_gradient`](@ref), [`function_divergence`](@ref), [`function_symmetric_gradient`](@ref), and [`function_curl`](@ref) requires
* [`shape_gradient`](@ref)
* [`getnquadpoints`](@ref)
* [`getnbasefunctions`](@ref)
* [`spatial_coordinate`](@ref), requires
* [`geometric_value`](@ref)
* [`getngeobasefunctions`](@ref)
* [`getnquadpoints`](@ref)


### Array bounds
* Asking for the `n`th quadrature point must be inside array bounds if `1 <= n <= getnquadpoints(fe_v)`. (`checkquadpoint` can, alternatively, be dispatched to check that `n` is inbounds.)
* Asking for the `i`th shape value or gradient must be inside array bounds if `1 <= i <= getnbasefunctions(fe_v)`
* Asking for the `i`th geometric value must be inside array bounds if `1 <= i <= getngeobasefunctions(fe_v)`
2 changes: 1 addition & 1 deletion docs/src/devdocs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ developing the library.

```@contents
Depth = 1
Pages = ["reference_cells.md", "interpolations.md", "elements.md", "dofhandler.md", "performance.md"]
Pages = ["reference_cells.md", "interpolations.md", "elements.md", "FEValues.md", "dofhandler.md", "performance.md"]
```
1 change: 1 addition & 0 deletions docs/src/devdocs/interpolations.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Ferrite.getnbasefunctions(::Interpolation)
Ferrite.reference_coordinates(::Interpolation)
Ferrite.is_discontinuous(::Interpolation)
Ferrite.adjust_dofs_during_distribution(::Interpolation)
Ferrite.mapping_type
```

for all entities which exist on that reference element. The dof functions default to having no
Expand Down
2 changes: 1 addition & 1 deletion docs/src/literate-howto/postprocessing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ include("../tutorials/heat_equation.jl");
# Next we define a function that computes the heat flux for each integration point in the domain.
# Fourier's law is adopted, where the conductivity tensor is assumed to be isotropic with unit
# conductivity ``\lambda = 1 ⇒ q = - \nabla u``, where ``u`` is the temperature.
function compute_heat_fluxes(cellvalues::CellValues{<:ScalarInterpolation}, dh::DofHandler, a::AbstractVector{T}) where T
function compute_heat_fluxes(cellvalues::CellValues, dh::DofHandler, a::AbstractVector{T}) where T

n = getnbasefunctions(cellvalues)
cell_dofs = zeros(Int, n)
Expand Down
6 changes: 3 additions & 3 deletions docs/src/literate-tutorials/incompressible_elasticity.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ end
# use a `PseudoBlockArray` from `BlockArrays.jl`.

function doassemble(
cellvalues_u::CellValues{<:VectorInterpolation},
cellvalues_p::CellValues{<:ScalarInterpolation},
facevalues_u::FaceValues{<:VectorInterpolation},
cellvalues_u::CellValues,
cellvalues_p::CellValues,
facevalues_u::FaceValues,
K::SparseMatrixCSC, grid::Grid, dh::DofHandler, mp::LinearElasticity
)
f = zeros(ndofs(dh))
Expand Down
8 changes: 5 additions & 3 deletions docs/src/literate-tutorials/linear_shell.jl
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ end;
# ##### Main element routine
# Below is the main routine that calculates the stiffness matrix of the shell element.
# Since it is a so called degenerate shell element, the code is similar to that for an standard continuum element.
shape_reference_gradient(cv::CellValues, q_point, i) = cv.fun_values.dNdξ[i, q_point]

function integrate_shell!(ke, cv, qr_ooplane, X, data)
nnodes = getnbasefunctions(cv)
ndofs = nnodes*5
Expand All @@ -281,9 +283,9 @@ function integrate_shell!(ke, cv, qr_ooplane, X, data)
ef1, ef2, ef3 = fiber_coordsys(p)

for iqp in 1:getnquadpoints(cv)
N = cv.N[:,iqp]
dNdξ = cv.dNdξ[:,iqp]
dNdx = cv.dNdx[:,iqp]
N = [shape_value(cv, iqp, i) for i in 1:nnodes]
dNdξ = [shape_reference_gradient(cv, iqp, i) for i in 1:nnodes]
dNdx = [shape_gradient(cv, iqp, i) for i in 1:nnodes]

for oqp in 1:length(qr_ooplane.weights)
ζ = qr_ooplane.points[oqp][1]
Expand Down
29 changes: 18 additions & 11 deletions docs/src/reference/fevalues.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,27 @@ DocTestSetup = :(using Ferrite)

# FEValues

## [CellValues](@id reference-cellvalues)
## Main types
[`CellValues`](@ref) and [`FaceValues`](@ref) are the most common
subtypes of `Ferrite.AbstractValues`. For more details about how
these work, please see the related [topic guide](@ref fevalues_topicguide).

```@docs
CellValues
FaceValues
```

## Applicable functions
The following functions are applicable to both `CellValues`
and `FaceValues`.

```@docs
reinit!
getnquadpoints(::CellValues)
getnquadpoints
getdetJdV
shape_value
shape_gradient
shape_value(::Ferrite.AbstractValues, ::Int, ::Int)
shape_gradient(::Ferrite.AbstractValues, ::Int, ::Int)
shape_symmetric_gradient
shape_divergence
Expand All @@ -25,15 +36,11 @@ function_divergence
spatial_coordinate
```

## [FaceValues](@id reference-facevalues)

All of the methods for [`CellValues`](@ref) apply for `FaceValues` as well.
In addition, there are some methods that are unique for `FaecValues`:
In addition, there are some methods that are unique for `FaceValues`.

```@docs
FaceValues
getcurrentface
getnquadpoints(::FaceValues)
Ferrite.getcurrentface
getnormal
```

## [InterfaceValues](@id reference-interfacevalues)
Expand Down
127 changes: 127 additions & 0 deletions docs/src/topics/FEValues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# [FEValues](@id fevalues_topicguide)
A key type of object in `Ferrite.jl` is the so-called `FEValues`, where the most common ones are `CellValues` and `FaceValues`. These objects are used inside the element routines and are used to query the integration weights, shape function values and gradients, and much more; see [`CellValues`](@ref) and [`FaceValues`](@ref). For these values to be correct, it is necessary to reinitialize these for the current cell by using the [`reinit!`](@ref) function. This function maps the values from the reference cell to the actual cell, a process described in detail below, see [Mapping of finite elements](@ref mapping-theory). After that, we show an implementation of a [`SimpleCellValues`](@ref SimpleCellValues) type to illustrate how `CellValues` work for the most standard case, excluding the generalizations and optimization that complicates the actual code.

## [Mapping of finite elements](@id mapping_theory)
The shape functions and gradients stored in an `FEValues` object, are reinitialized for each cell by calling the `reinit!` function.
The main part of this calculation, considers how to map the values and derivatives of the shape functions,
defined on the reference cell, to the actual cell.

The geometric mapping of a finite element from the reference coordinates to the real coordinates is shown in the following illustration.

![mapping_figure](https://raw.githubusercontent.com/Ferrite-FEM/Ferrite.jl/gh-pages/assets/fe_mapping.svg)

This mapping is given by the geometric shape functions, $\hat{N}_i^g(\boldsymbol{\xi})$, such that
```math
\begin{align*}
\boldsymbol{x}(\boldsymbol{\xi}) =& \sum_{\alpha=1}^N \hat{\boldsymbol{x}}_\alpha \hat{N}_\alpha^g(\boldsymbol{\xi}) \\
\boldsymbol{J} :=& \frac{\mathrm{d}\boldsymbol{x}}{\mathrm{d}\boldsymbol{\xi}} = \sum_{\alpha=1}^N \hat{\boldsymbol{x}}_\alpha \otimes \frac{\mathrm{d} \hat{N}_\alpha^g}{\mathrm{d}\boldsymbol{\xi}}\\
\boldsymbol{\mathcal{H}} :=&
\frac{\mathrm{d} \boldsymbol{J}}{\mathrm{d} \boldsymbol{\xi}} = \sum_{\alpha=1}^N \hat{\boldsymbol{x}}_\alpha \otimes \frac{\mathrm{d}^2 \hat{N}^g_\alpha}{\mathrm{d} \boldsymbol{\xi}^2}
\end{align*}
```
where the defined $\boldsymbol{J}$ is the jacobian of the mapping, and in some cases we will also need the corresponding hessian, $\boldsymbol{\mathcal{H}}$ (3rd order tensor).

We require that the mapping from reference coordinates to real coordinates is [diffeomorphic](https://en.wikipedia.org/wiki/Diffeomorphism), meaning that we can express $\boldsymbol{x} = \boldsymbol{x}(\boldsymbol{\xi}(\boldsymbol{x}))$, such that
```math
\begin{align*}
\frac{\mathrm{d}\boldsymbol{x}}{\mathrm{d}\boldsymbol{x}} = \boldsymbol{I} &= \frac{\mathrm{d}\boldsymbol{x}}{\mathrm{d}\boldsymbol{\xi}} \cdot \frac{\mathrm{d}\boldsymbol{\xi}}{\mathrm{d}\boldsymbol{x}}
\quad\Rightarrow\quad
\frac{\mathrm{d}\boldsymbol{\xi}}{\mathrm{d}\boldsymbol{x}} = \left[\frac{\mathrm{d}\boldsymbol{x}}{\mathrm{d}\boldsymbol{\xi}}\right]^{-1} = \boldsymbol{J}^{-1}
\end{align*}
```
Depending on the function interpolation, we may want different types of mappings to conserve certain properties of the fields. This results in the different mapping types described below.

### Identity mapping
`Ferrite.IdentityMapping`

For scalar fields, we always use scalar base functions. For tensorial fields (non-scalar, e.g. vector-fields), the base functions can be constructed from scalar base functions, by using e.g. `VectorizedInterpolation`. From the perspective of the mapping, however, each component is mapped as an individual scalar base function. And for scalar base functions, we only require that the value of the base function is invariant to the element shape (real coordinate), and only depends on the reference coordinate, i.e.
```math
\begin{align*}
N(\boldsymbol{x}) &= \hat{N}(\boldsymbol{\xi}(\boldsymbol{x}))\nonumber \\
\mathrm{grad}(N(\boldsymbol{x})) &= \frac{\mathrm{d}\hat{N}}{\mathrm{d}\boldsymbol{\xi}} \cdot \boldsymbol{J}^{-1}
\end{align*}
```

### Covariant Piola mapping, H(curl)
`Ferrite.CovariantPiolaMapping`

The covariant Piola mapping of a vectorial base function preserves the tangential components. For the value, the mapping is defined as
```math
\begin{align*}
\boldsymbol{N}(\boldsymbol{x}) = \boldsymbol{J}^{-\mathrm{T}} \cdot \hat{\boldsymbol{N}}(\boldsymbol{\xi}(\boldsymbol{x}))
\end{align*}
```
which yields the gradient,
```math
\begin{align*}
\mathrm{grad}(\boldsymbol{N}(\boldsymbol{x})) &= \boldsymbol{J}^{-T} \cdot \frac{\mathrm{d} \hat{\boldsymbol{N}}}{\mathrm{d} \boldsymbol{\xi}} \cdot \boldsymbol{J}^{-1} - \boldsymbol{J}^{-T} \cdot \left[\hat{\boldsymbol{N}}(\boldsymbol{\xi}(\boldsymbol{x}))\cdot \boldsymbol{J}^{-1} \cdot \boldsymbol{\mathcal{H}}\cdot \boldsymbol{J}^{-1}\right]
\end{align*}
```

!!! details "Derivation"
Expressing the gradient, $\mathrm{grad}(\boldsymbol{N})$, in index notation,
```math
\begin{align*}
\frac{\mathrm{d} N_i}{\mathrm{d} x_j} &= \frac{\mathrm{d}}{\mathrm{d} x_j} \left[J^{-\mathrm{T}}_{ik} \hat{N}_k\right] = \frac{\mathrm{d} J^{-\mathrm{T}}_{ik}}{\mathrm{d} x_j} \hat{N}_k + J^{-\mathrm{T}}_{ik} \frac{\mathrm{d} \hat{N}_k}{\mathrm{d} \xi_l} J_{lj}^{-1}
\end{align*}
```

Except for a few elements, $\boldsymbol{J}$ varies as a function of $\boldsymbol{x}$. The derivative can be calculated as
```math
\begin{align*}
\frac{\mathrm{d} J^{-\mathrm{T}}_{ik}}{\mathrm{d} x_j} &= \frac{\mathrm{d} J^{-\mathrm{T}}_{ik}}{\mathrm{d} J_{mn}} \frac{\mathrm{d} J_{mn}}{\mathrm{d} x_j} = - J_{km}^{-1} J_{in}^{-T} \frac{\mathrm{d} J_{mn}}{\mathrm{d} x_j} \nonumber \\
\frac{\mathrm{d} J_{mn}}{\mathrm{d} x_j} &= \mathcal{H}_{mno} J_{oj}^{-1}
\end{align*}
```

### Contravariant Piola mapping, H(div)
`Ferrite.ContravariantPiolaMapping`

The covariant Piola mapping of a vectorial base function preserves the normal components. For the value, the mapping is defined as
```math
\begin{align*}
\boldsymbol{N}(\boldsymbol{x}) = \frac{\boldsymbol{J}}{\det(\boldsymbol{J})} \cdot \hat{\boldsymbol{N}}(\boldsymbol{\xi}(\boldsymbol{x}))
\end{align*}
```
This gives the gradient
```math
\begin{align*}
\mathrm{grad}(\boldsymbol{N}(\boldsymbol{x})) = [\boldsymbol{\mathcal{H}}\cdot\boldsymbol{J}^{-1}] : \frac{[\boldsymbol{I} \underline{\otimes} \boldsymbol{I}] \cdot \hat{\boldsymbol{N}}}{\det(\boldsymbol{J})}
- \left[\frac{\boldsymbol{J} \cdot \hat{\boldsymbol{N}}}{\det(\boldsymbol{J})}\right] \otimes \left[\boldsymbol{J}^{-T} : \boldsymbol{\mathcal{H}} \cdot \boldsymbol{J}^{-1}\right]
+ \boldsymbol{J} \cdot \frac{\mathrm{d} \hat{\boldsymbol{N}}}{\mathrm{d} \boldsymbol{\xi}} \cdot \frac{\boldsymbol{J}^{-1}}{\det(\boldsymbol{J})}
\end{align*}
```

!!! details "Derivation"
Expressing the gradient, $\mathrm{grad}(\boldsymbol{N})$, in index notation,
```math
\begin{align*}
\frac{\mathrm{d} N_i}{\mathrm{d} x_j} &= \frac{\mathrm{d}}{\mathrm{d} x_j} \left[\frac{J_{ik}}{\det(\boldsymbol{J})} \hat{N}_k\right] =\nonumber\\
&= \frac{\mathrm{d} J_{ik}}{\mathrm{d} x_j} \frac{\hat{N}_k}{\det(\boldsymbol{J})}
- \frac{\mathrm{d} \det(\boldsymbol{J})}{\mathrm{d} x_j} \frac{J_{ik} \hat{N}_k}{\det(\boldsymbol{J})^2}
+ \frac{J_{ik}}{\det(\boldsymbol{J})} \frac{\mathrm{d} \hat{N}_k}{\mathrm{d} \xi_l} J_{lj}^{-1} \\
&= \mathcal{H}_{ikl} J^{-1}_{lj} \frac{\hat{N}_k}{\det(\boldsymbol{J})}
- J^{-T}_{mn} \mathcal{H}_{mnl} J^{-1}_{lj} \frac{J_{ik} \hat{N}_k}{\det(\boldsymbol{J})}
+ \frac{J_{ik}}{\det(\boldsymbol{J})} \frac{\mathrm{d} \hat{N}_k}{\mathrm{d} \xi_l} J_{lj}^{-1}
\end{align*}
```

## [Walkthrough: Creating `SimpleCellValues`](@id SimpleCellValues)
In the following, we walk through how to create a `SimpleCellValues` type which
works similar to `Ferrite.jl`'s `CellValues`, but is not performance optimized and not as general. The main purpose is to explain how the `CellValues` works for the standard case of `IdentityMapping` described above.
Please note that several internal functions are used, and these may change without a major version increment. Please see the [Developer documentation](@ref) for their documentation.

```@eval
# Include the example here, but modify the Literate output to suit being embedded
using Literate, Markdown
base_name = "SimpleCellValues_literate"
Literate.markdown(string(base_name, ".jl"); name = base_name, execute = true, credit = false, documenter=false)
content = read(string(base_name, ".md"), String)
rm(string(base_name, ".md"))
rm(string(base_name, ".jl"))
Markdown.parse(content)
```

## Further reading
* [defelement.com](https://defelement.com/ciarlet.html#Mapping+finite+elements)
* Kirby (2017) [Kirby2017](@cite)
Loading

0 comments on commit 2d21665

Please sign in to comment.