In this example we consider the flow in a 2D channel past a cylinder with an attached elastic "flag". This is the FSI benchmark problem proposed by Turek & Hron,
"Proposal for Numerical Benchmarking of Fluid-Structure Interaction between an Elastic Object and a Laminar Incompressible Flow", S. Turek & J. Hron, pp. 371-385. In:
"Fluid-Structure Interaction" Springer Lecture Notes in Computational Science and Engineering 53. Ed. H.-J. Bungartz & M. Schaefer. Springer Verlag 2006. The problem combines the two single-physics problems of
This is our first example problem that involves the coupling between a fluid and "proper" solid (rather than beam structure) and also includes both fluid and wall inertia.
The problem presented here was used as one of the test cases for oomph-lib's
FSI preconditioner; see
Heil, M., Hazel, A.L. & Boyle, J. (2008): Solvers for large-displacement fluid-structure interaction problems: Segregated vs. monolithic approaches. Computational Mechanics.
In this tutorial we concentrate on the problem formulation. The application of the preconditioner is discussed elsewhere – the required source code is contained in the driver code.
The Problem
The figure below shows a sketch of the problem: A 2D channel of height and length conveys fluid of density and dynamic viscosity and contains a cylinder of diameter , centred at to which a linearly elastic "flag" of thickness and length is attached. Steady Poiseuille flow with average velocity is imposed at the left end of the channel while we assume the outflow to be parallel and axially traction-free. We model the flag as a linearly elastic Hookean solid with elastic modulus , density and Poisson's ratio
Sketch of the problem in dimensional variables.
We non-dimensionalise all length and coordinates on the diameter of the cylinder, , the velocities on the mean velocity, , and the fluid pressure on the viscous scale. To facilitate comparisons with Turek & Hron's dimensional benchmark data (particularly for the period of the self-excited oscillations), we use a timescale of to non-dimensionalise time. The fluid flow is then governed by the non-dimensional Navier-Stokes equations
where and , and
subject to parabolic inflow
at the inflow cross-section; parallel, axially-traction-free outflow at the outlet; and no-slip on the stationary channel walls and the surface of the cylinder, . The no-slip condition on the moving flag is
where are Lagrangian coordinates parametrising the three faces of the flag.
We describe the deformation of the elastic flag by the non-dimensional position vector which is determined by the principle of virtual displacements
where all solid stresses and tractions have been non-dimensionalised on Young's modulus, ; see the Solid Mechanics Tutorial for details. The solid mechanics timescale ratio (the ratio of the timescale chosen to non-dimensionalise time, to the intrinsic timescale of the solid) can be expressed in terms of the Reynolds and Strouhal numbers, the density ratio, and the FSI interaction parameter as
Here is a sketch of the non-dimensional version of the problem:
Sketch of the fluid problem in dimensionless variables, showing the Lagrangian coordinates that parametrise the three faces of the flag.
Parameter values for the benchmark problems
The (dimensional) parameter values given in Turek & Hron's benchmark correspond to the following non-dimensional parameters:
Geometry
- Cylinder diameter
- Centre of cylinder
- Channel length
- Channel width
- Thickness of the undeformed flag
- Right end of undeformed flag
Non-dimensional parameters
The three FSI test cases correspond to the following non-dimensional parameters:
Results
The test cases FSI2 and FSI3 are the most interesting because the system develops large-amplitude self-excited oscillations
FSI2
Following an initial transient period the system settles into large-amplitude self-excited oscillations during which the oscillating flag generates a regular vortex pattern that is advected along the channel. This is illustrated in the figure below which shows a snapshot of the flow field (pressure contours and instantaneous streamlines) at
Snapshot of the flow field (instantaneous streamlines and pressure contours)
The constantly adapted mesh contains and average of 65,000 degrees of freedom. A relatively large timestep of – corresponding to about 50 timesteps per period of the oscillation – was used in this computation. With this discretisation the system settles into oscillations with a period of and an amplitude of the tip-displacement of
Time trace of the tip displacement.
FSI3
The figures below shows the corresponding results for the case FSI3 in which the fluid and solid densities are equal and the Reynolds number twice as large as in the FSI2 case. The system performs oscillations of much higher frequency and smaller amplitude. This is illustrated in the figure below which shows a snapshot of the flow field (pressure contours and instantaneous streamlines) at
Snapshot of the flow field (instantaneous streamlines and pressure contours)
This computation was performed with a timestep of and resulted in oscillations with a period of and an amplitude of the tip-displacement of
The increase in frequency and Reynolds number leads to the development of thinner boundary and shear layers which require a finer spatial resolution, involving an average of 84,000 degrees of freedom.
Time trace of the tip displacement.
Overview of the driver code
Since the driver code is somewhat lengthy we start by providing a brief overview of the main steps in the Problem
construction:
- We start by discretising the flag with 2D solid elements, as in the corresponding single-physics solid mechanics example.
- Next we attach
FSISolidTractionElements
to the three solid mesh boundaries that are exposed to the fluid traction. These elements are used to compute and impose the fluid traction onto the solid elements, using the flow field from the adjacent fluid elements.
- We now combine the three sets of
FSISolidTractionElements
into three individual (sub-)meshes and convert these to GeomObjects
, using the MeshAsGeomObject
class.
- The
GeomObject
representation of the three surface meshes is then passed to the constructor of the fluid mesh. The algebraic node-update methodology provided in the AlgebraicMesh
base class is used to update its nodal positions in response to the motion of its bounding GeomObjects
.
- Finally, we use the helper function
FSI_functions::setup_fluid_load_info_for_solid_elements(...)
to set up the fluid-structure interaction – this function determines which fluid elements are adjacent to the Gauss points in the FSISolidTractionElements
that apply the fluid traction to the solid.
- Done!
Parameter values for the benchmark problems
As usual, We use a namespace to define the (many) global parameters, using default assignments for the FSI1 test case.
{
double Centre_x
x position of centre of cylinder
double Radius
Radius of cylinder.
double Nu
Poisson's ratio.
double Lambda_sq
Timescale ratio for solid (dependent parameter assigned in set_parameters())
double Density_ratio
Density ratio (solid to fluid; default assignment for FSI1 test case)
double Q
FSI parameter (default assignment for FSI1 test case)
double ReSt
Product of Reynolds and Strouhal numbers (default assignment for FSI1 test case)
string Case_ID
Default case ID.
double Re
Reynolds number (default assignment for FSI1 test case)
bool Ignore_fluid_loading
Ignore fluid (default assignment for FSI1 test case)
ConstitutiveLaw * Constitutive_law_pt
Pointer to constitutive law.
double St
Strouhal number (default assignment for FSI1 test case)
double Centre_y
y position of centre of cylinder
We also include a gravitational body force for the solid. (This is only used for the solid mechanics test cases, CSM1 and CSM2, which will not be discussed here.)
const Vector<double> &xi,
Vector<double> &b)
{
b[0]=0.0;
}
void gravity(const double &time, const Vector< double > &xi, Vector< double > &b)
Non-dimensional gravity as body force.
double Gravity
Non-dim gravity (default assignment for FSI1 test case)
The domain geometry and flow field are fairly complex and it is difficult to construct a good initial guess for the Newton iteration. To ensure its convergence at the beginning of the simulation we therefore employ the method suggested by Turek & Hron: We start the flow from rest and ramp up the inflow profile from zero to its maximum value. The parameters for the time-dependent increase in the influx are defined here:
double flux(
const double& t)
{
{
0.5*(1.0-cos(MathematicalConstants::Pi*t/
Ramp_period));
}
else
{
}
}
double Max_flux
Max. flux.
double flux(const double &t)
Flux increases between Min_flux and Max_flux over period Ramp_period.
double Min_flux
Min. flux.
double Ramp_period
Period for ramping up in flux.
Finally, we provide a helper function that assigns the parameters for the various test cases, depending on their ID ("FSI1", "FSI2", "FSI3", "CSM1" or "CSM2"). Here is the assignment for the case FSI1:
{
if (case_id=="FSI1")
{
}
void set_parameters(const string &case_id)
Set parameters for the various test cases.
In the interest of brevity we omit the listings of the assignments for the other cases. Finally, we select the length of the time interval over which the influx is ramped up from zero to its maximum value to be equal to 20 timesteps, create a constitutive equation for the solid, and document the parameter values used in the simulation:
oomph_info << std::endl;
oomph_info << "-------------------------------------------"
<< std::endl;
oomph_info << "Case: " << case_id << std::endl;
oomph_info <<
"Re = " <<
Re << std::endl;
oomph_info <<
"St = " <<
St << std::endl;
oomph_info <<
"ReSt = " <<
ReSt << std::endl;
oomph_info <<
"Q = " <<
Q << std::endl;
oomph_info <<
"Dt = " <<
Dt << std::endl;
oomph_info <<
"Ramp_period = " <<
Ramp_period << std::endl;
oomph_info <<
"Max_flux = " <<
Max_flux << std::endl;
oomph_info <<
"Lambda_sq = " <<
Lambda_sq << std::endl;
oomph_info <<
"Gravity = " <<
Gravity << std::endl;
oomph_info << "-------------------------------------------"
<< std::endl << std::endl;
}
}
The driver code
The driver code has the usual structure, though in this case we use the command line arguments to indicate which case (FSI1, FSI2, FSI3, CSM1 or CSM2) to run. The absence of a command line argument is interpreted as the code being run as part of oomph-lib's
self-test procedure in which case we perform a computation with the parameter values for case FSI1 and perform only a few timesteps.
int main(
int argc,
char* argv[])
{
CommandLineArgs::setup(argc,argv);
string case_id="FSI1";
if (CommandLineArgs::Argc==1)
{
oomph_info << "No command line arguments; running self-test FSI1"
<< std::endl;
}
else if (CommandLineArgs::Argc==2)
{
case_id=CommandLineArgs::Argv[1];
}
else
{
oomph_info << "Wrong number of command line arguments" << std::endl;
oomph_info << "Enter none (for default) or one (namely the case id"
<< std::endl;
oomph_info << "which should be one of: FSI1, FSI2, FSI3, CSM1"
<< std::endl;
}
std::cout << "Running case " << case_id << std::endl;
int main(int argc, char *argv[])
Driver.
We set up the global parameter values, create a DocInfo
object and trace file to record the output, and build the problem.
DocInfo doc_info;
ofstream trace_file;
doc_info.set_directory("RESLT");
trace_file.open("RESLT/trace.dat");
double length=25.0;
double height=4.1;
RefineableQPVDElement<2,3> > problem(length, height);
//////////////////////////////////////////////////////////////////// ////////////////////////////////...
Next, we choose the number of timesteps (using a smaller number for a validation run, and for the case FSI1 in which the system rapidly approaches a steady state) and initialise the time-stepping for an impulsive start from the zero flow solution.
unsigned nstep=4000;
{
std::cout << "Reducing number of steps for FSI1 " << std::endl;
nstep=400;
}
if (CommandLineArgs::Argc==1)
{
std::cout << "Reducing number of steps for validation " << std::endl;
nstep=2;
}
problem.initialise_dt(dt);
problem.assign_initial_values_impulsive(dt);
Finally, we document the initial condition and start the time-stepping procedure, setting the first
flag to false
because we have not specified an analytical expression for the initial conditions that could be re-assigned after the mesh adaptation when computing the first timestep.
problem.doc_solution(doc_info,trace_file);
doc_info.number()++;
bool first = false;
unsigned max_adapt=1;
for(unsigned i=0;i<nstep;i++)
{
problem.unsteady_newton_solve(dt,max_adapt,first);
problem.doc_solution(doc_info,trace_file);
doc_info.number()++;
}
trace_file.close();
}
The Problem class
The Problem
class contains the usual member functions, such as access functions to the various meshes. Because the nodal positions are updated by an algebraic node-update procedure, the function actions_before_newton_convergence_check()
is employed to update the nodal positions in response to changes in the (solid) variables during the Newton iteration. The function actions_before_implicit_timestep()
is used to adjust the influx during the start-up period.
template< class FLUID_ELEMENT,class SOLID_ELEMENT >
{
public:
RefineableAlgebraicCylinderWithFlagMesh<FLUID_ELEMENT>*
fluid_mesh_pt()
ElasticRefineableRectangularQuadMesh<SOLID_ELEMENT>*&
solid_mesh_pt()
private:
ElasticRefineableRectangularQuadMesh<SOLID_ELEMENT>*
Solid_mesh_pt;
RefineableAlgebraicCylinderWithFlagMesh<FLUID_ELEMENT>*
Fluid_mesh_pt;
};
Vector< SolidMesh * > Traction_mesh_pt
Vector of pointers to mesh of FSI traction elements.
RefineableAlgebraicCylinderWithFlagMesh< FLUID_ELEMENT > * fluid_mesh_pt()
Access function for the fluid mesh.
RefineableAlgebraicCylinderWithFlagMesh< FLUID_ELEMENT > * Fluid_mesh_pt
Pointer to fluid mesh.
ElasticRefineableRectangularQuadMesh< SOLID_ELEMENT > * Solid_mesh_pt
Pointer to solid mesh.
double Domain_height
Overall height of domain.
void doc_solution(DocInfo &doc_info, ofstream &trace_file)
Doc the solution.
void actions_after_adapt()
Actions after adapt: Re-setup the fsi lookup scheme.
Node * Fluid_control_node_pt
Pointer to fluid control node.
void actions_before_implicit_timestep()
Update the time-dependent influx.
TurekProblem(const double &length, const double &height)
Constructor: Pass length and height of domain.
void actions_before_newton_solve()
Update function (empty)
ElasticRefineableRectangularQuadMesh< SOLID_ELEMENT > *& solid_mesh_pt()
Access function for the solid mesh.
SolidMesh *& traction_mesh_pt(const unsigned &i)
Access function for the i-th mesh of FSI traction elements.
void actions_before_newton_convergence_check()
Update the (dependent) fluid node positions following the update of the solid variables before perfor...
SolidMesh * Combined_traction_mesh_pt
Combined mesh of traction elements – only used for documentation.
void create_fsi_traction_elements()
Create FSI traction elements.
Node * Solid_control_node_pt
Pointer to solid control node.
void actions_after_newton_solve()
Update function (empty)
double Domain_length
Overall length of domain.
The problem constructor
We start by building the solid mesh, using an initial discretisation with 20 x 2 elements in the x- and y-directions. (The length of the flag is determined such that it emanates from its intersection with the cylinder and ends at x=6; The origin
vector shifts the "lower left" vertex of the solid mesh so that its centreline is aligned with the cylinder.)
template< class FLUID_ELEMENT,class SOLID_ELEMENT >
const double &height) : Domain_height(height),
Domain_length(length)
{
Max_newton_iterations=20;
Max_residuals=1.0e4;
unsigned n_x=20;
unsigned n_y=2;
Newmark<2>* flag_time_stepper_pt=new Newmark<2>;
add_time_stepper_pt(flag_time_stepper_pt);
Vector<double> origin(2);
double l_x=6.0-origin[0];
solid_mesh_pt() = new ElasticRefineableRectangularQuadMesh<SOLID_ELEMENT>(
n_x,n_y,l_x,l_y,origin,flag_time_stepper_pt);
We create an error estimator for the solid mesh and identify a control node at the tip of the flag to track its motion.
Z2ErrorEstimator* solid_error_estimator_pt=new Z2ErrorEstimator;
solid_mesh_pt()->spatial_error_estimator_pt()=solid_error_estimator_pt;
FiniteElement* el_pt=solid_mesh_pt()->finite_element_pt(n_x*n_y/2-1);
unsigned nnod=el_pt->nnode();
Solid_control_node_pt=el_pt->node_pt(nnod-1);
std::cout << "Coordinates of solid control point "
<< Solid_control_node_pt->x(0) << " "
<< Solid_control_node_pt->x(1) << " " << std::endl;
Finally, we perform one uniform mesh refinement and disable any further mesh adaptation.
solid_mesh_pt()->refine_uniformly();
solid_mesh_pt()->disable_adaptation();
Next, we attach FSISolidTractionElements
to the boundaries of the solid mesh that are exposed to the fluid. We complete their build by specifying which boundary of the bulk mesh they are attached to, as this information is required when setting up the fluid-structure interaction; see Further comments and exercises.
Traction_mesh_pt.resize(3);
Traction_mesh_pt[0]=new SolidMesh;
Traction_mesh_pt[1]=new SolidMesh;
Traction_mesh_pt[2]=new SolidMesh;
create_fsi_traction_elements();
for (unsigned bound=0;bound<3;bound++)
{
unsigned n_face_element = Traction_mesh_pt[bound]->nelement();
for(unsigned e=0;e<n_face_element;e++)
{
FSISolidTractionElement<SOLID_ELEMENT,2>* elem_pt=
dynamic_cast<FSISolidTractionElement<SOLID_ELEMENT,2>*>
(Traction_mesh_pt[bound]->element_pt(e));
elem_pt->set_boundary_number_in_bulk_mesh(bound);
}
}
Finally, we create GeomObject
representations of the three surface meshes of FSISolidTractionElements
. We will use these to represent the curvilinear, moving boundaries of the fluid mesh.
MeshAsGeomObject*
bottom_flag_pt=
new MeshAsGeomObject
(Traction_mesh_pt[0]);
MeshAsGeomObject* tip_flag_pt=
new MeshAsGeomObject
(Traction_mesh_pt[1]);
MeshAsGeomObject* top_flag_pt=
new MeshAsGeomObject
(Traction_mesh_pt[2]);
The final mesh to be built is the fluid mesh whose constructor requires pointers to the four GeomObjects
that represent the cylinder and three fluid-loaded faces of the flag, respectively. We represent the cylinder by a Circle
object:
We build the mesh and identify a control node (a node at the upstream face of the cylinder), before creating an error estimator and performing one uniform mesh refinement.
BDF<2>* fluid_time_stepper_pt=new BDF<2>;
add_time_stepper_pt(fluid_time_stepper_pt);
Fluid_mesh_pt=
new RefineableAlgebraicCylinderWithFlagMesh<FLUID_ELEMENT>
(cylinder_pt,
top_flag_pt,
bottom_flag_pt,
tip_flag_pt,
length, height,
fluid_time_stepper_pt);
Fluid_control_node_pt=Fluid_mesh_pt->node_pt(5);
Z2ErrorEstimator* fluid_error_estimator_pt=new Z2ErrorEstimator;
fluid_mesh_pt()->spatial_error_estimator_pt()=fluid_error_estimator_pt;
Fluid_mesh_pt->refine_uniformly();
We now add the various meshes to the Problem's
collection of sub-meshes and combine them to a global mesh
add_sub_mesh(solid_mesh_pt());
for (unsigned i=0;i<3;i++)
{
add_sub_mesh(traction_mesh_pt(i));
}
add_sub_mesh(fluid_mesh_pt());
build_global_mesh();
The application of boundary conditions for the solid are straightforward: All displacements of the flag's left end (mesh boundary 3) are suppressed; the other faces are free. Strictly speaking, the pinning of the redundant solid pressure nodes is superfluous since the RefineableQPVDElement
used for the discretisation of the flag employ a displacement-based formulation, but it is good practise to perform this step anyway to "future-proof" the code for the use of other element types.
unsigned n_side = mesh_pt()->nboundary_node(3);
for(unsigned i=0;i<n_side;i++)
{
solid_mesh_pt()->boundary_node_pt(3,i)->pin_position(0);
solid_mesh_pt()->boundary_node_pt(3,i)->pin_position(1);
}
PVDEquationsBase<2>::pin_redundant_nodal_solid_pressures(
solid_mesh_pt()->element_pt());
The fluid has Dirichlet boundary conditions (prescribed velocity) everywhere apart from the outflow where only the horizontal velocity is unknown.
unsigned num_bound = fluid_mesh_pt()->nboundary();
for(unsigned ibound=0;ibound<num_bound;ibound++)
{
unsigned num_nod= fluid_mesh_pt()->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
if (ibound != 1)
{
fluid_mesh_pt()->boundary_node_pt(ibound,inod)->pin(0);
fluid_mesh_pt()->boundary_node_pt(ibound,inod)->pin(1);
}
else
{
fluid_mesh_pt()->boundary_node_pt(ibound,inod)->pin(1);
}
}
}
RefineableNavierStokesEquations<2>::
pin_redundant_nodal_pressures(fluid_mesh_pt()->element_pt());
We impose a parabolic inflow profile with the current value of the influx at the inlet (fluid mesh boundary 3).
double t=0.0;
unsigned ibound=3;
unsigned num_nod= Fluid_mesh_pt->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
double ycoord = Fluid_mesh_pt->boundary_node_pt(ibound,inod)->x(1);
double uy = ampl*6.0*ycoord/Domain_height*(1.0-ycoord/Domain_height);
Fluid_mesh_pt->boundary_node_pt(ibound,inod)->set_value(0,uy);
Fluid_mesh_pt->boundary_node_pt(ibound,inod)->set_value(1,0.0);
}
We complete the build of the solid elements by passing them the pointer to the constitutive equation, the gravity vector and the timescale ratio:
unsigned n_element =solid_mesh_pt()->nelement();
for(unsigned i=0;i<n_element;i++)
{
SOLID_ELEMENT *el_pt = dynamic_cast<SOLID_ELEMENT*>(
solid_mesh_pt()->element_pt(i));
el_pt->constitutive_law_pt() =
}
The fluid elements require pointers to the Reynolds and Womersley (product of Reynolds and Strouhal) numbers:
unsigned nelem=fluid_mesh_pt()->nelement();
for (unsigned e=0;e<nelem;e++)
{
FLUID_ELEMENT* el_pt = dynamic_cast<FLUID_ELEMENT*>
(fluid_mesh_pt()->element_pt(e));
}
Setting up the fluid-structure interaction is done from "both" sides" of the fluid-solid interface: First we ensure that the no-slip condition is automatically applied to all fluid nodes that are located on the three faces of the flag (mesh boundaries 5, 6 and 7). This is done by passing the function pointer to the
FSI_functions::apply_no_slip_on_moving_wall()
function to the relevant fluid nodes (recall that the auxiliary node update functions are automatically executed whenever the position of a node is updated by the algebraic node update). Since the no-slip condition (1) involves the Strouhal number (which, in the current problem, is not equal to the default value of FSI_functions::Strouhal_for_no_slip=1.0
), we overwrite the default assignment with the actual Strouhal number in the problem.
{
for(unsigned ibound=5;ibound<8;ibound++ )
{
unsigned num_nod= Fluid_mesh_pt->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
Fluid_mesh_pt->boundary_node_pt(ibound, inod)->
set_auxiliary_node_update_fct_pt(
FSI_functions::apply_no_slip_on_moving_wall);
}
}
Next, we set up the lookup schemes required by the FSISolidTractionElements
to establish which fluid elements affect the traction onto the solid:
FSI_functions::setup_fluid_load_info_for_solid_elements<FLUID_ELEMENT,2>
(this,5,Fluid_mesh_pt,Traction_mesh_pt[0]);
FSI_functions::setup_fluid_load_info_for_solid_elements<FLUID_ELEMENT,2>
(this,6,Fluid_mesh_pt,Traction_mesh_pt[2]);
FSI_functions::setup_fluid_load_info_for_solid_elements<FLUID_ELEMENT,2>
(this,7,Fluid_mesh_pt,Traction_mesh_pt[1]);
}
All interactions have now been specified and we conclude by assigning the equation numbers
cout << assign_eqn_numbers() << std::endl;
}
Create traction elements
This is a helper function that attaches FSISolidTractionElement
to the solid elements that are exposed to the fluid traction. We store the elements in three distinct sub-meshes – one for each face. (Yet another mesh, pointed to by Combined_traction_mesh_pt
, is created for post-processing purposes.)
template<class FLUID_ELEMENT,class SOLID_ELEMENT >
{
std::set<SolidNode*> all_nodes;
for (unsigned b=0;b<3;b++)
{
unsigned n_element = solid_mesh_pt()->nboundary_element(b);
for(unsigned e=0;e<n_element;e++)
{
SOLID_ELEMENT* bulk_elem_pt = dynamic_cast<SOLID_ELEMENT*>(
solid_mesh_pt()->boundary_element_pt(b,e));
int face_index = solid_mesh_pt()->face_index_at_boundary(b,e);
Traction_mesh_pt[b]->add_element_pt(
new FSISolidTractionElement<SOLID_ELEMENT,2>(bulk_elem_pt,face_index));
}
unsigned nnod=solid_mesh_pt()->nboundary_node(b);
for (unsigned j=0;j<nnod;j++)
{
all_nodes.insert(solid_mesh_pt()->boundary_node_pt(b,j));
}
}
Combined_traction_mesh_pt=new SolidMesh(Traction_mesh_pt);
for (std::set<SolidNode*>::iterator it=all_nodes.begin();
it!=all_nodes.end();it++)
{
Combined_traction_mesh_pt->add_node_pt(*it);
}
}
Actions before Newton convergence check
The algebraic node-update procedure updates the positions in response to changes in the solid displacements but this is not done automatically when the Newton solver updates the solid mechanics degrees of freedom. We therefore force a node-update before the Newton convergence check.
template <class FLUID_ELEMENT,class SOLID_ELEMENT>
{
fluid_mesh_pt()->node_update();
}
Actions before the timestep
Before each timestep we update the inflow profile for all fluid nodes on mesh boundary 3.
template <class FLUID_ELEMENT,class SOLID_ELEMENT>
{
double t=time_pt()->time();
unsigned ibound=3;
unsigned num_nod= Fluid_mesh_pt->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
double ycoord = Fluid_mesh_pt->boundary_node_pt(ibound,inod)->x(1);
double uy = ampl*6.0*ycoord/Domain_height*(1.0-ycoord/Domain_height);
Fluid_mesh_pt->boundary_node_pt(ibound,inod)->set_value(0,uy);
Fluid_mesh_pt->boundary_node_pt(ibound,inod)->set_value(1,0.0);
}
}
Actions after adapt
After each adaptation, we unpin and re-pin all redundant pressures degrees of freedom. This is necessary because their "redundant-ness" may have been altered by changes in the refinement pattern; see another tutorial for details. We ensure the automatic application of the no-slip condition on fluid nodes that are located on the faces of the flag, and re-setup the FSI lookup scheme that tells FSISolidTractionElements
which fluid elements are located next to their Gauss points.
template<class FLUID_ELEMENT,class SOLID_ELEMENT >
{
RefineableNavierStokesEquations<2>::
unpin_all_pressure_dofs(fluid_mesh_pt()->element_pt());
RefineableNavierStokesEquations<2>::
pin_redundant_nodal_pressures(fluid_mesh_pt()->element_pt());
PVDEquationsBase<2>::
unpin_all_solid_pressure_dofs(solid_mesh_pt()->element_pt());
PVDEquationsBase<2>::pin_redundant_nodal_solid_pressures(
solid_mesh_pt()->element_pt());
{
for(unsigned ibound=5;ibound<8;ibound++ )
{
unsigned num_nod= Fluid_mesh_pt->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
Fluid_mesh_pt->boundary_node_pt(ibound, inod)->
set_auxiliary_node_update_fct_pt(
FSI_functions::apply_no_slip_on_moving_wall);
}
}
FSI_functions::setup_fluid_load_info_for_solid_elements<FLUID_ELEMENT,2>
(this,5,Fluid_mesh_pt,Traction_mesh_pt[0]);
FSI_functions::setup_fluid_load_info_for_solid_elements<FLUID_ELEMENT,2>
(this,6,Fluid_mesh_pt,Traction_mesh_pt[2]);
FSI_functions::setup_fluid_load_info_for_solid_elements<FLUID_ELEMENT,2>
(this,7,Fluid_mesh_pt,Traction_mesh_pt[1]);
}
}
Post-processing
The function doc_solution(...)
produces the output for the fluid, solid and traction meshes and writes selected data to the trace file.
template<class FLUID_ELEMENT,class SOLID_ELEMENT >
DocInfo& doc_info, ofstream& trace_file)
{
ofstream some_file;
char filename[100];
unsigned n_plot = 5;
sprintf(filename,"%s/solid_soln%i.dat",doc_info.directory().c_str(),
doc_info.number());
some_file.open(filename);
solid_mesh_pt()->output(some_file,n_plot);
some_file.close();
sprintf(filename,"%s/soln%i.dat",doc_info.directory().c_str(),
doc_info.number());
some_file.open(filename);
fluid_mesh_pt()->output(some_file,n_plot);
some_file.close();
sprintf(filename,"%s/traction%i.dat",doc_info.directory().c_str(),
doc_info.number());
some_file.open(filename);
for(unsigned i=0;i<3;i++)
{
unsigned n_element = Traction_mesh_pt[i]->nelement();
for(unsigned e=0;e<n_element;e++)
{
FSISolidTractionElement<SOLID_ELEMENT,2>* el_pt =
dynamic_cast<FSISolidTractionElement<SOLID_ELEMENT,2>* > (
Traction_mesh_pt[i]->element_pt(e) );
el_pt->output(some_file,5);
}
}
some_file.close();
trace_file << time_pt()->time() << " "
<< Solid_control_node_pt->x(0) << " "
<< Solid_control_node_pt->x(1) << " "
<< Fluid_control_node_pt->value(2) << " "
<< std::endl;
cout << "Doced solution for step "
<< doc_info.number()
<< std::endl << std::endl << std::endl;
}
Further comments and exercises
- When completing the build of the
FSISolidTractionElements
(the elements that apply the fluid traction to the solid elements that are exposed to the fluid) we specified the number of the solid mesh boundary they are located on, using
elem_pt->set_boundary_number_in_bulk_mesh(bound);
This information is required when setting up the fluid-structure interaction because the MeshAsGeomObject
representation of the mesh of FSISolidTractionElements
is parametrised by the boundary coordinate in the solid mesh. Explore the details of the implementation by commenting out the relevant line of code and use the debugger to find out how and where the code fails. Note: Since this step is somewhat subtle and therefore easily forgotten, the FSISolidTractionElements
issue an explicit warning if the bulk boundary number has not been set – but only if the the library is compiled in PARANOID mode.
- When comparing our results against those in Turek & Hron's benchmark, we only focused on the period and amplitude of the fully-developed self-excited oscillations. The benchmark data also provides data on the time-dependent variations of the drag and lift coefficients. Design suitable
FaceElements
(to be attached to the faces of the Navier-Stokes elements that are adjacent to the flag or the cylinder) to compute these quantities. The NavierStokesSurfacePowerElements
should provide a good basis for these.
Acknowledgements
- This code was originally developed by Stefan Kollmannsberger and his students Iason Papaioannou and
Orkun Oezkan Doenmez. It was completed by Floraine Cordier.
Source files for this tutorial
PDF file
A pdf version of this document is available.