NiHu
2.0
|
This page explains a bit about how NiHu works. The topic discussed here is not about Boundary Elements but programming. We explain how static polymorphism and c++ template metaprogramming is applied to efficiently incorporate inhomogeneous meshes into the NiHu toolbox.
Evaluating the Weighted Residual approach means integrating a kernel \(K(x_0,x)\) over a set of elements \(E\) extended with some shape functions \(N(x)\):
\[ R = \int_{E}K(x_0,x) N(x) dx \]
Obviously, when programming a BEM, we want to write our integration routine generally, so that our code remains capable to handle as many different element and kernel types as possible.
A straightforward C++ solution to this problem is dynamic polymorphism implemented with virtual functions, abstract base classes and heterogeneous collections. Using this technique, the integration routine receives pointers to abstract kernel and element interface classes as input, and performs integration by invoking the functionalities of the abstract interfaces. The specific behaviour of different subproblems are implemented in specialised derived classes of the abstract base.
The main advantage of dynamic polymorphism is that it can easily and transparently manage heterogeneous problems, for example BEM problems where the mesh contains different kind of elements. However, dynamic polymorphism has some important drawbacks too:
These drawbacks can add up to a significant decrease of overall performance.
Static polymorphism, on the contrary, means that we still implement our problem generally, but keep in mind that only one single specific instance of the general problem will be compiled and run at once. As dispatch is done compile time, invoking different special instances of the same abstract problem is managed by compiling and calling the different specialisations separately.
In C++ static polymorphism can be implemented using class templates and function templates. In the example below we demonstrate its application with a simple class template that integrates an arbitrary kernel over an arbitrary element:
When reading the above code, the following observations should be made:
weighted_integral
is implemented in terms of a general Kernel
and Elem
class, but it is going to be instantiated for a single specific Kernel
and Elem
type.integrate
function's arguments and return type are specific to the template parameters Elem
and Kernel
. This means that different specialisations can have different interface, and the function can be inlined and optimised.integrate
is "computed" by a metafunction product_type
.The class template can be instantiated for any Elem
and Kernel
type (that provide an appropriate eval
function, get_shape
function and quadrature iterators). For example, specialisation with a Helmholtz kernel and triangle elements can be invoked by compiling the code segment below:
What if we need to incorporate quad elements into our program? We need to rewrite the code as
It is obvious that integrating tria and quad elements can only be done separately, one element type after the other. This is the price paid for better performance.
What if we need to evaluate double integrals (Galerkin BEM) with arbitrary test and trial shape functions, five different element types and three different kernels? We have to repeat our simple traversing code segment \(5^2\times3=75\) times! This is the point where template metaprogramming becomes inevitable.
Metaprogramming is writing programs that write programs. Template Metaprogramming is using C++ templates to write programs that write programs. In the following we will apply template metaprogramming to code generation.
To generalise the above example, we need two things:
std::vector<tria_1_elem>
member, a std::vector<quad_1_elem>
member and further vectors of other element types.weighted_integral
for each element type and call the integrate
function for each element of the element type's vector container.The first task is accomplished by an inheritance trick. Our container class has to be derived from all the different std::vector<>
containers:
The above pattern is easily implemented using a series of classes inheriting from at most two bases
The recursive implementation of this pattern is the following:
And the instantiation for a given vector of element types is as follows:
This is the technique how NiHu stores inhomogeneous meshes. For more information check out the class documentation of class NiHu::mesh and its inhomogeneous container member NiHu::mesh::m_elements.
Now that we have our inhomogeneous container mesh
consisting of homogeneous std::vector
-s, we can rewrite our code segment that integrates over all the element types
This pattern can be generalised using NiHu's tmp::call_each
code generating metafunction. call_each
is used to instantiate a class template for each entry of a specified type vector, and call the instantiated class' nested functor called type
. The application is demonstrated as follows: