Postdoc at Umeå University, Sweden
It is always exciting to explore whether a particular package can be
extended to implement different methods with some of the low-level API
available within the package. So, I wanted to answer the question -
Can Gridap.jl
can be used to implement the virtual element
method? Virtual element methods can be seen as a generalization of the
finite element method on arbitrary shaped polygon. At the time of
writing this, Gridap.jl
supports simplices and hypercubes to build
the discretization. However, it is possible to use the underlying
machinery to implement the lowest order virtual element method using
Gridap.jl
. In this post, I will be briefly discussing the
implementation.
Let us take the following example problem available in
the README.md
of the
repository. I won’t be
posting the code here, but I strongly recommend that you take a look
at the repository (linked above). But before that, I will talk about
some of the assumption behind this implementation:
SparseMatrixAssembler
available for the FESpace
routine to
virtual elements. This can be done using the multiple dispatch
feature available in Julia.Gridap.jl
via the ∫
method. Higher order polygons require
simplexification which is again beyond the scope of this
implementation.FEM
formulation. Virtual element method has a different weak
formulation which includes stability terms. This will be managed
differently as we will see in the next section. However, if being
implemented as a package, I presume that VEM weak formulation
requires special treatment.To build a lowest order VE space for a discrete model, we use the new data structure
P1ConformingVESpace(::DiscreteModel, ::Function; kwargs...):
model::DiscreteModel
Π∇
stability_term
linear_fespace::FESpace # FESpace(model, ReferenceFE(lagrangian, Float64, 1); kwargs...)
stab_coeff::Function
For the lowest order order case, the virtual element degrees of freedom is identical
to the lowest order Lagrange finite element space. So for
geometrical purposes, we can associate a linear FESpace
to the new
conforming VESpace
. In addition to that we have two new data
structures Π∇, stability_term
for the purpose of (cellwise) VEM
projectors. You can find the details of the construction
here. These
terms depend entirely on the geometry of the element and can be
computed while building the space.
Once we construct the VESpace
, we can construct the stiffness
matrices (local) and the load vector (local) using these functions,
cell-wise:
function _generate_mat_contribs(a::Function, V::VESpace, acd, ind)
area, centroid, diameter = acd[end]
stab_coeff = get_stab_coeff(V)
:
# Some magic here ... #
:
coeff_matrix = stab_coeff(centroid)
projector'*H*projector + (coeff_matrix)*stability_term # Final term
end
Here the coeff_matrix
is a diagonal matrix of the stability
function evaluated at the element centroid. This defines the scaling
of the stability term. In the example, for the weak formulation
a(u,v) = ∫(K*∇(u)⋅∇(v) + u*v)Qₕ
the diffusion function K
is used to define the scaling of the
term, since the weak form scales ~ K
. Also, we multiply the
integrand in the weak form with the cell quadrature Qₕ =
CellQuadrature(Ω, 4)
rather than the measure dΩ = Measure(Ω,
4)
as done conventionally in Gridap. This is because we obtain the
cell-wise values as a LazyArray
rather than a Dict
and it was
easy to access the cell-wise contribution. This will be done using
dΩ
in the future.
The load vector similarly can be obtained using the method
function _generate_vec_contribs(l::Function, V::VESpace, acd, ind)
a, c, d = acd[ind]
:
# More magic ... #
:
projector'*Fⱼ
end
where we don’t have the stability term. The magic
here is that
Gridap
can be used to compute the action of the bilinear form
a(u,v)
on the scaled monomials. This is because the quadrature
rules on the elements are well defined and direct numerical
integration is possible (Do take a look at how the implementation
works in the code
here
and
here). However,
note that this fails for arbitrary order polygons. The idea is then
to subdivide the polygons into triangles and dispatch ∫
appropriately (Future work).
To obtain the cell-wise contributions, we map the above functions to
each element in the mesh: We do this using the lazy_map
functionality.
matcontribs = lazy_map(i -> _generate_mat_contribs(a, trial, acd, i), 1:num_cells(mesh))
loadcontribs = lazy_map(i -> _generate_vec_contribs(l, trial, acd, i), 1:num_cells(mesh))
We then use the low-level assembly routine: SparseMatrixAssembler
available in Gridap, but with the extra dispatch
function SparseMatrixAssembler(trial::VESpace, test::VESpace)
SparseMatrixAssembler(trial.linear_fespace, test.linear_fespace)
end
# Assemble the matrices
σₖ = get_cell_dof_ids(trial.linear_fespace)
matdata = ([matcontribs],[σₖ],[σₖ])
vecdata = ([loadcontribs], [σₖ])
assem = SparseMatrixAssembler(trial, test)
matrix = assemble_matrix(assem, matdata)
vector = assemble_vector(assem, vecdata)
We then solve the problem and interpolate the raw vector (VEM
solution) to the underlying FESpace
, since we cannot express the
virtual element basis explicitly. We perform a very basic
convergence analysis to see if the numbers obey the error estimate
|| u - uₕ ||₀ ≤ Ch²
We run the rate of convergence script ooc_vem_example.jl
to check:
julia> err
5×1 Matrix{Float64}:
0.040594555904984765
0.010306757323680199
0.002586435110433769
0.0006472156606070523
0.00016184181015375544
julia> rate = log.(err[1:end-1] ./ err[2:end])./log.(2)
4-element Vector{Float64}:
1.9776957536320143
1.994553606200162
1.9986465750207745
1.9996621557003598
And thus we are able to verify the rate of convergence result. This shows that the raw vector we obtain is the virtual element solution!
tags: vem - gridap © 2019- Balaje Kalyanaraman. Hosted on Github pages. Based on the Minimal Theme by orderedlist