Basic Components
B2000++ program components, such as elements, materials are implemented as C++ objects. The programming API reference manual describes these objects, although in a typical reference manner, i.e assuming a certain level of knowledge. This document tries to bring the information over to the B2000++ developer more in a tutorial way, explaining code and build.
This chapter explains some of the basic components. Specific components, such as element or property (material) components, are explained in the next chapters.
b2object Component
B2000++ components, such as elements, material models, boundary
conditions, solvers, etc. are instantiated via factory objects. Each
component class registers itself by defining the static member
variable type
. Example (Spring
element b2element_spring.C
). In the class declaration of
ElementStressSpring
:
using type_t = ObjectTypeComplete<
ElementStressSpring,
typename TypedElement<T>::type_t>;
static type_t type;
and outside of class declaration of ElementStressSpring
, in
b2element_spring.C
:
template <typename T>
typename ElementStressSpring<T>::type_t
ElementStressSpring<T>::type(
std::string("SPRING") + (
std::is_same<T, std::csda<double> >::value ? ".CSDA" : ""),
"",
StringList(), element::module,
&TypedElement<T>::type);
template class ElementStressSpring<double>;
template class ElementStressSpring<std::csda<double> >;
If the B2000++ solver does not find "SPRING"
, it will try to load
libb2000.element.SPRING.so.
Implementations of components are all sub-classes of
b2000::Object
. Instances of b2000::ObjectType
are
factory objects which create instances of specific sub-classes of
b2000::Object
and which, in conjunction with
b2000::Module
, allow for the implementation of plugins
without having to recompile B2000++.
The motivation for b2000::Object
and
b2000::ObjectType
is illustrated by means of an example. In
traditional Finite Element codes that are written with procedural
languages such as Fortran or C, element subroutines like
set_nodes()
or get_value()
, i.e computation of first
and second variation, are called within if/else constructs. Example
(pseudo-code):
if (name == "R2") {
element_r2_set_nodes(...);
} else if (name == "Q4") {
element_q4_set_nodes(...);}
or with some switch statement.
While straightforward, this technique requires a lot of code duplication for each set of subroutines, for example:
if (name == "R2") {
element_r2_get_value(...);
} else if (name == "Q4") {
element_q4_get_value(...);
...}
This duplication makes the code hard to maintain. A better approach consists of the object-oriented concept, where functions and data are grouped in polymorphic classes that derive from a common base class. It is the necessary to define if/else constructs only once, for the instantiation of the objects:
Element* e = 0;
if (name == "R2") {
e = new ElementR2();
} else if (name == "Q4") {
e = new ElementQ4();
... }
Thereafter, the polymorphic (virtual) functions can be used regardless of the type:
e->set_nodes(...);
e->get_value(...);
While this approach is more elegant, it does not allow for dynamically adding new elements without having to recompile B2000++.
However, b2000::Object
in conjunction with b2000::ObjectType
allows to register sub-classes of b2000::Object
under a name (and
several aliases), such that no if/else constructs are necessary at
all.
b2000::Object and b2000::ObjectType
Classes whose instances should be created dynamically should be
sub-classes of b2000::Object
and must override the static member
named type
. This static member is derived from
b2000::ObjectType
and serves for registration and as a factory
object for instances of that sub-class.
Using this static type
member, one can, by means of
b2000::ObjectType::get_subtype()
, get the factory object of a
sub-class, and, by means of b2000::ObjectType::new_object()
,
create instances of this sub-class.
Register an Object Sub-class
The implementation of an Object sub-class is explained by means of an
example. Note that it must be done for every sub-class of
b2000::Object
(thus, also for sub-classes of b2000::Element
,
b2000::ElementProperty
, b2000::Solver
, etc.).
The sub-class ElementQ4Stress2DExample
implements a quadrilateral
2D stress element and is derived from b2000::TypedElement<double>
,
which itself is derived from b2000::Element
. To register (under
the name Q4.S.2D.EXAMPLE
):
class ElementQ4Stress2DExample : public b2000::TypedElement<double> {
public:
...
typedef b2000::ObjectTypeComplete<
ElementStress2DExample,
b2000::TypedElement<double>::type_t> type_t;
static type_t type;
};
The static member type
is initialized as follows:
ElementQ4Stress2DExample::type_t
ElementQ4Stress2DExample::type(
"Q4.S.2D.EXAMPLE", // Name of ElementQ4Stress2DExample.
"", // Suffix for name and aliases.
StringList(), // List of aliases (empty in this example).
element::module, // The parent module.
&b2000::TypedElement<double>::type // ObjectType of super class.
Instance of an Object Sub-class
To create and instance of an object sub-class the
b2000::ObjectType::get_subtype()
method is used. This is
demonstrated by means of the following example. The
ElementQ4Stress2DExample
class may be provided by one of the
modules
b2000.element: The name of the shared library is
libb2000++.so
(for builtin element implementations)-b2000.element.Q4.S.2D.EXAMPLE
: The name islibb2000.element.Q4.S.EXAMPLE.so
(for external element implementations)
The ObjectType::get_subtype()
function works for both cases, since
it will attempt to load the shared library
libb2000.element.Q4.S.EXAMPLE.so if Q4.S.EXAMPLE
is not found in
the`` b2000.element`` module.
The following code is a simplification of the Domain implementation:
try {
// Get a pointer to the factory object.
b2000::Element::type_t* eltype = b2000::Element::type.get_subtype(
"Q4.S.2D.EXAMPLE", b2000::element::module);
// Create an instance of ElementQ4Stress2DExample.
b2000::Element* e = eltype->new_object();
assert(dynamic_cast<ElementQ4Stress2DExample*>(e) != 0);
} catch (b2000::KeyError&) {
...
}
b2module Component
A b2000::Module instance represents a shared C++ library or a Python
module. It contains a collection of (name, b2000::ObjectType*)
tuples. The b2000::Module
class is used by`` b2000::ObjectType``
to
To dynamically load shared libraries,
To register newly loaded sub-classes of
b2000::ObjectType
,And to look up sub-classes of
b2000::ObjectType
by their name.
Each module has a parent module, and a module may have one or several
sub-modules. Thus, the set of all modules forms an acyclic directed
graph (a tree). The root of the module tree is the static instance
b2000::b2000_module
.
The Module::get_module()
and the Module::get_object_type()
methods may load a sub-module as a shared library, if that sub-module
has not already been loaded. To this end, the dlopen()
call of
the standard C library is used. During the loading of the shared
library, the static variables are initialized automatically. By means
of this, the (name, b2000::ObjectType*)
tuples are registered in
the Module instance.
The module facility allows to pre-load a given module; the classes contained therein will not be loaded from any other module later. This allows to have different implementations in different shared libraries and to choose the shared library at runtime. Example:
// Obtain a pointer to the B2000::Module instance to use.
b2000::Module* m = &b2000::element; // default
if (module_name != "")
// module_name was given e.g. on the command-line
m = m->get_module(module_name); // load the module
// Use the module instance to create an Element object registered
// under the name R2.S.USER.
try {
// Get a pointer to the factory object.
b2000::Element::type_t* eltype = b2000::Element::type.get_subtype(
"R2.S.USER", m);
// Create an instance of R2.S.USERElementQ4Stress2DExample.
b2000::Element* e = eltype->new_object();
} catch (b2000::KeyError&) {
...
}
A loadable module is created as a shared library. Best practice is to create a new cmake project. For details, please refer to the rod_element and rod_material example components found in the b2programming package.