This document provides an overview of
and
The document is part of a series of tutorials that discuss how to modify existing serial driver codes so that the Problem
object can be distributed across multiple processors.
In the original (serial) driver code for Turek & Hron's FSI benchmark problem we only adapted the fluid mesh. Before discussing how to modify the code to refine the fluid and solid meshes simultaneously, we provide a brief reminder of the procedure used to discretise fluid-structure interaction problems that involve fluid and solid domains of equal spatial dimension (e.g. a 2D fluid domain interacting with a 2D solid domain) when using algebraic node update methods to adjust the position of the nodes in the fluid mesh.
We refer to another tutorial for a discussion of FSI problems involving the interaction of fluids with (lower-dimensional) shell and beam structures.
The figure below shows a sketch of a simple(r) fluid-structure interaction problem involving fluid and solid domains that meet along a single mesh boundary. We assume that the fluid mesh uses an algebraic node update function to adjust the position of its nodes in response to changes in the domain boundary, represented by the GeomObject
shown in magenta. (You may wish to consult another tutorial for a reminder of how oomph-lib's
algebraic node update methods work).
In an FSI problem, the fluid mesh's free boundary is a boundary of the solid mesh, i.e. the boundary along which the fluid exerts a traction onto the solid. Within oomph-lib
, the fluid traction is applied to the solid domain by attaching FSISolidTractionElements
to the faces of the "bulk" solid elements adjacent to the FSI boundary. (In the above sketch the FSISolidTractionElements
are shown in blue.) The deformation of the fluid and solid meshes is coupled by using the MeshAsGeomObject
formed from the FSISolidTractionElements
as the GeomObject
that defines the moving boundary of the fluid mesh. (In sketch above, this is indicated by the magenta arrow.)
If the solid mesh is not adapted, the adaptation for the fluid mesh is straightforward and proceeds fully automatically as described elsewhere. In particular, the node update data for newly-created fluid nodes is created automatically by a call to the AlgebraicMesh::update_node_update(...)
function during the adaptation. This function obtains the required information about the boundary by using the MeshAsGeomObject
built from the FSISolidTractionElements
.
If the solid mesh is also adapted, then the existing FSISolidTractionElements
must (at some point) be deleted and new ones must be attached to the adapted "bulk" solid mesh. In all other problems, this is done by deleting the FSISolidTractionElements
in Problem::actions_before_adapt()
and attaching new ones in Problem::actions_after_adapt()
; see, e.g. the tutorial on the solution of a Poisson problem with flux boundary conditions. However, in the present problem this is not possible because, once the FSISolidTractionElements
have been deleted, the MeshAsGeomObject
can no longer be used to represent the shape and position of the FSI boundary, which would cause the adaptation of the fluid mesh to fail.
To avoid this problem, we adopt the following strategy:
Problem's
collection of sub-meshes, we add the fluid mesh before the solid mesh. (This happens to be what was done already in the original driver code.) Usually, the order in which sub-meshes are added to the Problem
is irrelevant. Here the order does matter because we will exploit the fact that the sub-meshes are adapted individually, in the order in which they were added to the Problem
. FSISolidTractionElements
are not deleted in Problem::actions_before_adapt()
and remain attached to the "bulk" solid elements throughout the "bulk" mesh adaptation procedure. When the fluid mesh is adapted, the appropriate MeshAsGeomObject
is, therefore, still fully-functional (and refers to the boundary as represented by the solid domain before the "bulk" solid mesh is adapted). FSISolidTractionElements
into "dangling" elements. (This occurs whenever a FSISolidTractionElements
is attached to a "bulk" solid elements that disappears during the adaptation, e.g. by being refined.) Problem::actions_after_adapt()
we delete the existing FSISolidTractionElements
and immediately (re-)attach new ones. Now, the MeshAsGeomObject
that represents the FSI boundary is broken because it still refers to the just deleted FSISolidTractionElements
. MeshAsGeomObject
from the newly-created FSISolidTractionElements
, and update the fluid mesh's pointer to this new GeomObject
that describes the boundary shape. AlgebraicMesh::update_node_update(...)
function for all nodes in the fluid mesh to ensure that their node update data refers to the new FSISolidTractionElements
. FSISolidTractionElements
via a call to FSI_functions::setup_fluid_load_info_for_solid_elements(...)
, etc.) remain the same as in the previous version of the code.In the present example, there are two "bulk" meshes corresponding to the fluid and solid domains and three "surface" meshes of traction elements. The traction elements are FaceElements
created from the "bulk" fluid elements and should be deleted before the problem is distributed, see the tutorial on applying flux boundary conditions in a Poisson problem for more details. In the previous example involving the interaction of a 2D fluid domain with a 1D beam structure there were only two meshes: a "bulk" fluid mesh and a "surface" solid mesh. In that problem all elements in the 1D mesh of FSIHermiteBeamElements
were retained on all processors as halo elements by using the function Mesh::keep_all_elements_as_halos()
. The same methodology could be used here, but it would be extremely wasteful to retain all the solid elements in the "bulk" solid mesh because only the elements next to the FSI boundary are required. Instead, we use a more fine-grained method of retaining elements via the function GeneralisedElement::must_be_kept_as_halo()
.
Most of the driver code is identical to the original serial version discussed in another tutorial. We therefore only discuss those parts of the code that have to be changed to allow (i) the simultaneous adaptation of the fluid and solid meshes, and (ii) the problem distribution.
As usual in a parallel driver code, the only addition to the main()
function is the inclusion of calls to MPI_Helpers::init()
, MPI_Helpers::finalize()
, and the Problem::distribute()
functions.
The only additions to the serial version of the problem class are the functions actions_before_distribute()
and actions_after_distribute()
, and the helper function delete_fsi_traction_elements()
, discussed below.
To facilitate the deletion and re-creation of the FSISolidTractionElements
before and after the adaptation (and distribution) we provide a new helper function delete_fsi_traction_elements()
which complements the already-existing create_fsi_traction_elements()
function:
As discussed above, we must ensure that the "bulk" solid elements adjacent to the FSI boundary are retained on all processors. Hence, the actions_before_distribute()
function starts with a loop over the FSISolidTractionElements
within which we use the function GeneralisedElement::must_be_kept_as_halo()
to indicate that the associated bulk elements must be retained.
Next, we flush all the meshes from the problem's collection of sub-meshes and add only the "bulk" fluid and solid meshes (in that order!). The FaceElements
do not need to be distributed, because they will be re-created in actions_after_distribute()
.
Following the problem distribution, we delete the old FSISolidTractionElements
and then (re-)attach new ones, which will be created as halo elements where necessary.
We complete the build of the FSISolidTractionElements
by passing the FSI parameter and the boundary number in the bulk mesh. The relevant code is identical to the serial version and we omit its listing here.
Next, we create new MeshAsGeomObjects
from the newly-created FSISolidTractionElements
and pass them to the (algebraic) fluid mesh:
The MeshAsGeomObjects
have changed, so we must call the update_node_update()
function again for each node in the fluid mesh:
Now we add the FSI traction meshes back to the problem and rebuild the global mesh.
Finally, we re-set the fluid load on the solid elements by calling FSI_functions::setup_fluid_load_info_for_solid_elements(...)
before re-assigning the auxiliary node update function that imposes the no-slip condition for all fluid nodes on the FSI boundaries. [Recall that the (re-)assignment of the auxiliary node-update function must be performed after the call to FSI_functions::setup_fluid_load_info_for_solid_elements(...)
.]
The remainder of the function identifies which processors contain the fluid control node whose velocities we document in the trace file.
The actions_after_adapt()
function is very similar to actions_after_distribute()
function, so we omit is listing here. The only significant differences are that (i) the redundant fluid and solid pressures are (re)-pinned; (ii) the identification of the fluid control node does not need to be setup; and (iii) the traction meshes were never removed from the problem, so do not need to be added back in.
As with the other parallel driver codes, the main modification to the post-processing function is the addition of the processor number to all output files. Furthermore, we only write the trace file on the processors that contain the fluid control node. In the interest of brevity we omit the listing of the modified function.
The figure below illustrates the distribution of the problem across four processors, represented by the four colours, with the fluid elements outlined in black and the solid elements outlined in white.
Zooming in near the "flag" shows how both fluid and solid meshes are refined and distributed independently:
When employing load balancing in this problem, we modify the time-stepping loop to perform the procedure after each timestep:
The function Problem::build_mesh()
must be supplied by the user if they wish to use the load balancing capability. Thus, in this driver code, we move all the required code to build the entire global mesh into this function, and call it from within the problem constructor:
The build_mesh()
function itself contains all the relevant code from within the previous parallel driver code's problem constructor.
In this example, all that is required for the actions_after_load_balance()
function is the addition of the unpin-repin procedure from the actions_after_adapt()
function to the appropriate part of the actions_after_distribute()
function, since all the other functionality is already identical. The actions_before_load_balance()
function is identical to the actions_before_distribute()
function.
A pdf version of this document is available.