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

  1. b2000.element: The name of the shared library is libb2000++.so (for builtin element implementations)-

  2. b2000.element.Q4.S.2D.EXAMPLE: The name is libb2000.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.