In many previous examples we illustrated how oomph-lib's
Domain
and MacroElement
objects facilitate the generation of refineable (and non-refineable) meshes in domains with moving, curvilinear boundaries. Consult, for instance:
The two main features of the MacroElement/Domain
- based node-update are that, once the curvilinear domain is represented by a Domain
object
Mesh::node_update()
.The availability of a "black-box" node-update procedure is very convenient but in some applications it may be desirable (or even necessary) to provide a customised node-update function, either because the mesh deformation generated by the "black-box" procedure is not appropriate or because it is not efficient. The latter problem arises particularly in fluid-structure interaction problems.
In this tutorial we shall demonstrate an alternative node-update technique, based on oomph-lib's
AlgebraicNode
, AlgebraicElement
, and AlgebraicMesh
classes. The key feature of this approach is that it allows "each node to update its own
position". This is in contrast to the Domain/MacroElement-based
approach in which we can only update the nodal positions of all nodes in the mesh simultaneously – not a particularly sparse operation! [We note that the Node
and FiniteElement
base classes provide the virtual functions Node::node_update()
and FiniteElement::node_update()
. These functions are intended to be used for node-by-node or element-by-element node-updates but in their default implementations they are empty. Hence no node-update is performed unless these functions are overloaded in derived classes such as AlgebraicNode
and AlgebraicElement
.]
The idea behind the algebraic node-updates is simple: The AlgebraicMesh
class (the base class for meshes containing AlgebraicElements
and AlgebraicNodes
) contains the pure virtual function
which must be implemented in every specific AlgebraicMesh
. Its task is to update the position of the node specified by the pointer-valued argument.
The specific implementation of the node-update operation is obviously problem-dependent but it is easy to illustrate the general procedure by considering the collapsible channel mesh sketched below:
The upper figure shows the mesh in the undeformed domain in which wall shape is parametrised by the intrinsic (Lagrangian) coordinate as In this configuration each node is located at a certain fraction along the vertical lines across the channel. For instance, the -th node (drawn in blue) is located at a fraction of along the straight line that connects reference point on the bottom wall to reference point on the flexible upper wall. We note that reference point may be identified by its Lagrangian coordinate on the wall.
The lower figure shows a sketch of the deformed domain and illustrates a possible algebraic node-update strategy: Given the new wall shape, described by , we position each node on the straight line that connects its reference point on the lower wall to the reference point on the deformable upper wall.
To perform this node-update procedure for a specific node, we generally have to store
GeomObject(s)
that define the curvilinear domain boundaries. GeomObjects
. AlgebraicMesh
that implements the node-update procedure.Since the node-update data is node-specific, we provide storage for it in the AlgebraicNode
class – a class that is derived from the Node
class. The node-update function itself is shared by many nodes in the mesh and is implemented in AlgebraicMesh::algebraic_node_update(...)
. This function extracts the node-update parameters, , and and the pointer to the GeomObject
that parametrises the wall, wall_pt
, say, from the AlgebraicNode
passed to it. With these parameters, the position of reference point is given by while the coordinates of reference point may be obtained from a call to wall_pt->position(...)
. The nodal position may then be updated via
To use the algebraic node-update capabilities of an existing AlgebraicMesh
, all Nodes
must be replaced by AlgebraicNodes
to allow the storage of the node-specific node-update parameters. Recall that within oomph-lib
Nodes
are usually created by the elements, using the function FiniteElement::construct_node(...)
whose argument specifies the local node number of the newly created Node
within the element that creates it. (The specification of the local node number is required to allow the FiniteElement::construct_node(...)
function to create a Node
with the appropriate number of values and history values. For instance, in 2D Taylor-Hood Navier-Stokes elements, the elements' vertex nodes have to store three values, representing the two velocity components and the pressure, whereas all other nodes only require storage for the two velocity components.)
To ensure that the FiniteElement::construct_node(...)
function creates AlgebraicNodes
rather than "ordinary" Nodes
, we provide the templated wrapper class
which overloads the FiniteElement::construct_node(...)
function so that it creates AlgebraicNodes
instead. In most other respects, the "wrapped" element behaves exactly as the underlying ELEMENT
itself. To use an existing AlgebraicMesh
with a certain element type, QTaylorHoodElement<2>
, say, we simply "upgrade" the element to an AlgebraicElement
by specifying the element type as AlgebraicElement<QTaylorHoodElement<2>
>.
The changes to an existing driver code that performs the node update by the default Domain/MacroElement
methodology are therefore completely trivial. You may wish to compare the driver code collapsible_channel.cc, discussed in an earlier example, to the driver code collapsible_channel_algebraic.cc in which the fluid domain is discretised by the MyAlgebraicCollapsibleChannelMesh
, discussed below.
To illustrate how to create a new AlgebraicMesh
, we will now discuss the implementation of the MyAlgebraicCollapsibleChannelMesh
– an AlgebraicMesh
- version of the CollapsibleChannelMesh
, used in the earlier example.
We construct the mesh by multiple inheritance from the AlgebraicMesh
base class, and the already-existing CollapsibleChannelMesh
. The constructor first calls the constructor of the underlying CollapsibleChannelMesh
(thus "recycling" the basic mesh generation process, i.e. the generation of nodes and elements etc.) and then adds the algebraic node-update information by calling the function setup_algebraic_node_update()
.
We declare the interface for the pure virtual function algebraic_node_update(...)
, to be discussed below, and implement a second pure virtual function, AlgebraicMesh::update_node_update()
, that may be used to update reference values following a mesh adaptation. Since the current mesh is not refineable, we leave this function empty and refer to another example for a more detailed discussion of its role.
The protected member function setup_algebraic_node_update()
will be discussed below.
When the function setup_algebraic_node_update()
is called, the constructor of the underlying CollapsibleChannelMesh
will already have created the mesh's elements and nodes, and the nodes will be located at their initial positions in the undeformed domain.
To set up the algebraic node-update data, we start by extracting the lengths of the upstream rigid section, , and the length of the collapsible segment, , from the CollapsibleChannelDomain:
Next, we loop over all AlgebraicNodes
in the mesh and determine their current positions:
If the j
-th node is located in the "collapsible" section of the mesh we determine its reference coordinate, , on the wall and determine the coordinates of its reference point from the GeomObject
that represents the moving wall.
Just to be on the safe side, we check that the wall is actually in its undeformed configuration, as assumed.
Next, we package the data required for the node-update operations (the pointers to GeomObject(s)
and the reference values) into vectors. In the present example, the node-update operation only involves a single GeomObject:
We have three reference values: The - coordinate of point ,
as well as the fractional height, ,
and the reference coordinate, , along the wall:
The vectors of reference values and geometric objects are then passed to the node, together with the pointer to the mesh (this
) that implements the node-update function.
The function algebraic_node_update(...)
reverses the setup process: It extracts the node-update data from the AlgebraicNode
and updates its position at the t
-th previous time-level:
We start by extracting the vectors of reference values and GeomObjects
involved in this node's node-update, using the functions AlgebraicNode::vector_ref_value()
which returns a Vector of reference values, and the function
AlgebraicNode::vector_geom_object_pt()
which returns a Vector of pointers to GeomObjects
.
Next, we translate these into local variables,
and obtain the current wall position from the wall GeomObject
.
Finally, we update the nodal position:
Done!
Node::node_update()
function by AlgebraicNode::node_update()
allows each node to "update its
own position". The AlgebraicMesh
and AlgebraicElement
classes provide their own re-implementation of the node_update()
functions in the Mesh
and FiniteElement
classes, and perform the node-updates by executing the AlgebraicNode::node_update()
function of their constituent nodes. AlgebraicNode
constructor provides default assignments for the node-update data. In particular, the pointer to the Mesh that performs the node update is initialised by a pointer to the (static instantiation of a) DummyMesh
whose AlgebraicMesh::algebraic_node_update(...)
and AlgebraicMesh::update_node_update(...)
functions are empty. This ensures that nodes for which these default assignments are not overwritten stay at their original position when the node udate is performed. This implementation provides a sensible default behaviour for the node update. AlgebraicNode::add_node_update_info(...)
function, using its alternative interface AlgebraicMesh::algebraic_node_update(...)
function for a mesh that contains multiple node-update functions, the ID of the node-update function associated with a particular node can be obtained from AlgebraicNode::node_update_fct_id()
, allowing the node-update function to take the appropriate action. (As an example, consider the implementation of the algebraic node-update function for the RefineableAlgebraicFishMesh
in which different node-update functions are used to update the position of nodes in the fish's "body" and in its "tail".) If the AlgebraicNode::add_node_update_info(...)
function is called without specifying an ID, a default ID of 0 is assigned. "add_..."
in the AlgebraicNode::add_node_update_info(...)
function, AlgebraicNodes
may be associated with multiple node-update functions, though the different node-update functions associated with a node must, of course, give the same result. This may be verified by calling the AlgebraicNode::self_test()
function for all nodes in an AlgebraicMesh
. AlgebraicMeshes
(discussed in more detail below), AlgebraicNodes
must be associated with all possible node-update functions to ensure that the reference values for newly created nodes are determined correctly during the adaptive mesh refinement. AlgebraicMeshes
may be created by multiple inheritance from a suitable RefineableMesh
base class (e.g. the RefineableQuadMesh
class), and setting up the required tree representation of the coarse initial mesh, exactly as for "ordinary" meshes (see the tutorial "How to create simple refineable meshes" for details). As an example, here is the class definition for a refineable version of the MyAlgebraicCollapsibleChannelMesh
discussed above: AlgebraicNodes
are determined by interpolation from the node's "father element". Furthermore, it is assumed GeomObjects
are involved in the node-update function for the newly created node. AlgebraicMesh::update_node_update(...)
function which is executed automatically whenever a refineable AlgebraicMesh
is adapted. AlgebraicNode::node_update(...)
function is called for a hanging node, it first updates the position of its master nodes (using their own node-update functions) and then updates its own constrained position accordingly. GeomObjects
that are involved in an AlgebraicNode's
node update, may depend on unknowns in the overall problem. Such unknowns constitute a GeomObject's
geometric Data
which may be obtained from its member function GeomObject::geom_data_pt(...)
. AlgebraicElements
use finite differencing to include the shape derivatives (i.e. the derivatives of the residuals of the underlying (wrapped) element with respect to the geometric Data
involved in the element's node update) into the element's Jacobian matrix. MyAlgebraicCollapsibleChannelMesh
and its refineable equivalent are slightly simplified versions of the meshes in oomph-lib's
src/meshes
directory. These meshes contain a number of additional sanity checks that we omitted here for the sake of brevity. Furthermore, these meshes can be used with GeomObjects
that comprise "sub-"GeomObjects
, a feature that is essential in problems with proper fluid-structure interaction. We will discuss this in another example.MyAlgebraicCollapsibleChannelMesh
, you will notice that the mesh has an additional constructor that allows the specification of the "boundary layer squash function" first introduced in the original CollapsibleChannelMesh
. Explain why in the AlgebraicMesh
version of this mesh, the function pointer to the "boundary layer
squash function" can only be specified via the constructor – the access function is deliberately broken.A pdf version of this document is available.