The moving-boundary Navier-Stokes problem discussed in this document is a "warm-up" problem for the classical fluid-structure interaction problem of flow in a 2D collapsible channel. Here we compute the flow through a 2D channel in which part of one wall is replaced by a moving "membrane" whose motion is prescribed. In another example, we will demonstrate how easy it is to extend the driver code for the current problem to a fluid-structure interaction problem in which the "membrane" is represented by an elastic beam that deforms in response to the fluid traction.
We note that the (FSI version of the) problem considered here was analysed in much more detail in
-
Jensen, O.E. & Heil, M. (2003) High-frequency self-excited oscillations in a collapsible-channel flow. Journal of Fluid Mechanics 481 235-268. (pdf preprint) (abstract)
where a detailed discussion (and an asymptotic analysis) of the flow-structures described below may be found.
The problem
Finite-Reynolds-number flow in a 2D channel with an oscillating wall . The figure below shows a sketch of the problem: Flow is driven by a prescribed pressure drop through a 2D channel of width and total length The upstream and downstream lengths of the channel are rigid, whereas the upper wall in the central section performs a prescribed oscillation. The shape of the moving segment is parametrised by a Lagrangian coordinate, , so that the position vector to the moving wall is given by .
Sketch of the problem.
We scale all lengths on the channel width, , use the average velocity through the undeformed channel, , to scale the velocities, and use to non-dimensionalise time. (As usual, we employ asterisks distinguish dimensional parameters from their non-dimensional equivalents.)
The flow is then governed by the unsteady Navier-Stokes equations
and the continuity equation
with , subject to the following boundary and initial conditions:
|
We consider a wall motion that deforms the initially "flush" wall into a parabolic shape. We denote the non-dimensional
amplitude of the oscillation by
and its period by
, and parametrise the position vector to a point on the wall by the Lagrangian coordinate
as
where the "ramp" function
is used to facilitate the start-up of the simulation from the initial condition of steady Poiseuille flow.
provides a "smooth" startup of the wall motion during the first period of the oscillation.
The results
The figure below shows a snapshot, taken from the animation of the computational results. The first four figures show (from top left to bottom right) "carpet plots" of the axial and transverse velocities, the axial component of the perturbation velocity
, and the pressure distribution. The 2D contour plot at the bottom of the figure shows a contour plot of the pressure and a few instantaneous streamlines.
Snapshot from the animation of the flow field for Re = Re St = 50, T=0.45, A=0.01.
The figures illustrate the flow structures identified in Jensen & Heil's (2003) asymptotic analysis of 2D channel flows that are driven by high-frequency, small-amplitude oscillations of a finite section of one of their walls: The flow consists of oscillatory axial "sloshing flows", superimposed on the mean Poiseuille flow that is driven by the applied pressure drop. During phases when the wall moves inwards (outwards) the flow generated by the moving wall decelerates (accelerates) the flow in the upstream region as the wall "injects" fluid into ("sucks" fluid out of) the domain. Conversely, in the downstream region the flow generated by the wall adds to the pressure-driven mean flow during phases when the wall moves inwards. This is shown most clearly in the plot of the axial velocity perturbation. In the plots shown above, the wall moves outwards and the axial perturbation velocity is positive (i.e. in the direction of the pressure-driven mean flow) in the upstream region, and negative in the downstream region. This is also shown in the time-traces of the velocities at two control points in the in- and outflow cross-sections, shown in the figure below:
Time-trace of the axial velocities at two control points in the upstream and downstream cross-sections, and the vertical position of a control point on the wall. (Re = Re St = 50, T=0.45, A=0.01.)
Finally, we comment that the plot of the perturbation velocities confirms the two-layer structure of the sloshing flows predicted in the asymptotic analysis. The sloshing flow comprises a blunt core flow region in which the flow is dominated by inertial effects while thin Stokes layers develop near the wall. Within these layers, the fluid viscosity reduces the axial velocity to zero. The carpet plot of the pressure shows that the pressure distribution is dominated by the variations induced by the oscillatory sloshing flows. For a detailed discussion of the flow structure we refer to Jensen & Heil (2003).
Overview of the driver code
Overall, the driver code is very similar to codes developed for other moving-boundary Navier-Stokes problems, such as the driver code used to simulate the flow inside an oscillating ellipse. The present code is slightly lengthier because of the traction boundary conditions which we impose by attaching traction elements to the upstream end of the mesh. (Refer to the traction-driven Rayleigh problem for a more detailed discussion of this technique.) Also, as discussed in another example, the traction elements must be removed/re-attached before/after every mesh adaptation.
The domain is discretised by the
CollapsibleChannelMesh
which employs the CollapsibleChannelDomain
to provide a MacroElement
- based representation of the deforming domain in terms of the GeomObject
that describes the motion of the "collapsible" section of the wall (boundary 3). The sketch below illustrates the topology and the mesh deformation: As the wall deforms, the
boundaries of the MacroElements
in the "collapsible" part of the Domain
follow the wall motion.
Sketch of the CollapsibleChannelDomain/Mesh.
The no-slip boundary conditions on the moving wall are applied as in the oscillating ellipse problem, by executing the function FSI_functions::apply_no_slip_on_moving_wall(...)
in Problem::actions_before_implicit_timestep()
for all nodes that are located on boundary 3.
The following sections provide a complete annotated listing of the driver code. Most functions should already be familiar from previous examples and you may want to skip straight to the Comments and Exercises.
The moving wall
As usual, we represent the moving wall as a GeomObject
and define its shape by implementing the pure virtual function GeomObject::position(...)
. The arguments to the constructor specify the Eulerian coordinates of wall's left end, its undeformed length, the amplitude of the oscillations,
, and the period of the oscillations
. We also pass the pointer to a Time
object to the constructor and store it in a private data member, to allow the position(...)
functions to access the current value of the continuous time. The amplitude of the wall motion, and the period of its oscillations are stored as private data members, accessible via suitable member functions.
class OscillatingWall : public GeomObject
{
public:
OscillatingWall(const double& h, const double& x_left, const double& l,
const double& a, const double& period, Time* time_pt) :
GeomObject(1,2), H(h), Length(l), X_left(x_left), A(a), B(0.0), T(period),
Time_pt(time_pt)
{}
~OscillatingWall(){}
double& amplitude(){return A;}
double& period(){return T;}
Since the GeomObject
represents a moving (i.e. time-dependent) boundary, we implement both versions of the GeomObject::position(...)
function: The "unsteady" version computes the position vector at the t
-th previous timestep.
void position(const unsigned& t, const Vector<double>&zeta,
Vector<double>& r) const
{
using namespace MathematicalConstants;
double ramp=1.0;
if (Time_pt->time(t)<T)
{
ramp=0.5*(1.0-cos(Pi*Time_pt->time(t)/T));
}
r[0] = zeta[0]+X_left
-B*A*sin(2.0*3.14159*zeta[0]/Length)*
sin(2.0*Pi*(Time_pt->time(t))/T)*ramp;
r[1] = H+A*((Length-zeta[0])*zeta[0])/pow(0.5*Length,2)*
sin(2.0*Pi*(Time_pt->time(t))/T)*ramp;
}
The version without additional argument computes the position vector at the present time:
void position(const Vector<double>&zeta, Vector<double>& r) const
{
position (0, zeta, r);
}
Finally, here are the various private data members:
unsigned ngeom_data() const {return 0;}
private:
double H;
double Length;
double X_left;
double A;
double B;
double T;
Time* Time_pt;
};
[Note: We note that the OscillatingWall
class allows the wall shape to be slightly more complicated than required by (5). If the parameter B
is set to a non-zero value, the material points on the wall also undergo some horizontal displacement. We will use this capability in one of the exercises in section Comments and Exercises.]
Namespace for the "global" physical variables
As usual, we define the problem parameters in a namespace and assign default values that can be overwritten if required.
namespace Global_Physical_Variables
{
double Re=50.0;
double ReSt=50.0;
double P_up=0.0;
We also implement the function that defines the prescribed (axial) traction at the inflow boundary.
void prescribed_traction(const double& t,
const Vector<double>& x,
const Vector<double>& n,
Vector<double>& traction)
{
traction.resize(2);
traction[0]=P_up;
traction[1]=0.0;
}
}
The driver code
As with most previous time-dependent codes, we use command line arguments to indicate if the code is run during oomph-lib's
self-test. If any command line arguments are specified, we use a coarser discretisation and perform fewer timesteps.
After storing the command line arguments, we choose the number of elements in the mesh, set the lengths of the domain and choose the amplitude and period of the oscillation. The parameter values are chosen such that the wall motion resembles that in the FSI simulations shown in Fig. 5 of Jensen & Heil (2003).
int main(int argc, char* argv[])
{
CommandLineArgs::setup(argc,argv);
unsigned coarsening_factor=1;
if (CommandLineArgs::Argc>1)
{
coarsening_factor=4;
}
unsigned nup=20/coarsening_factor;
unsigned ncollapsible=40/coarsening_factor;
unsigned ndown=40/coarsening_factor;
unsigned ny=16/coarsening_factor;
double lup=5.0;
double lcollapsible=10.0;
double ldown=10.0;
double ly=1.0;
double amplitude=1.0e-2;
double period=0.45;
We set the (non-dimensional) upstream pressure to
, so that in the absence of any wall oscillation, the steady flow through the channel is Poiseuille flow,
; see Comments and Exercises.
Global_Physical_Variables::P_up=12.0*(lup+lcollapsible+ldown);
Next, we specify the output directory and build the problem with refineable 2D Crouzeix Raviart Elements.
DocInfo doc_info;
doc_info.set_directory("RESLT");
ofstream trace_file;
char filename[100];
sprintf(filename,"%s/trace.dat",doc_info.directory().c_str());
trace_file.open(filename);
CollapsibleChannelProblem<RefineableQCrouzeixRaviartElement<2> >
problem(nup, ncollapsible, ndown, ny,
lup, lcollapsible, ldown, ly,
amplitude,period);
Next we set up the time-stepping parameters for a simulation of three periods of the oscillation, performed with 40 timesteps per period. Fewer timesteps are performed if the code is run in self-test mode.
unsigned nsteps_per_period=40;
unsigned nperiod=3;
unsigned nstep=nsteps_per_period*nperiod;
if (CommandLineArgs::Argc>1)
{
nstep=3;
}
double dt=period/double(nsteps_per_period);
double t_min=0.0;
We initialise the timestepper and set the initial conditions before documenting the initial condition.
problem.time_pt()->time()=t_min;
problem.initialise_dt(dt);
problem.set_initial_condition();
problem.doc_solution(doc_info, trace_file);
doc_info.number()++;
Next we set the error targets for the adaptive mesh refinement; a smaller target error is used when the code is run in self-test mode to ensure that some mesh refinement is performed during the first few timesteps.
problem.bulk_mesh_pt()->max_permitted_error()=1.0e-3;
problem.bulk_mesh_pt()->min_permitted_error()=1.0e-5;
if (CommandLineArgs::Argc>1)
{
problem.bulk_mesh_pt()->max_permitted_error()=1.0e-4;
problem.bulk_mesh_pt()->min_permitted_error()=1.0e-6;
}
The timestepping loop itself is identical to that used in other time-dependent driver codes with adaptive mesh refinement. During the first timestep, an arbitrary number of spatial adaptations may be performed, as the initial condition can be re-assigned on the refined mesh. (This is indicated by setting the boolean flag first
to true
when calling the spatially adaptive, unsteady Newton solver.) During subsequent timesteps the need to interpolate the history values onto the refined mesh limits the benefits of repeated mesh adaptations and we limit the number of spatial adaptations per timestep to 1.
bool first=true;
unsigned max_adapt=10;
for (unsigned istep=0;istep<nstep;istep++)
{
problem.unsteady_newton_solve(dt, max_adapt, first);
problem.doc_solution(doc_info, trace_file);
doc_info.number()++;
first=false;
max_adapt=1;
}
trace_file.close();
}
The problem class
As usual, we template the problem class by the element type and provide an access functions to the "bulk" Navier-Stokes mesh.
template <class ELEMENT>
class CollapsibleChannelProblem : public Problem
{
public :
CollapsibleChannelProblem(const unsigned& nup,
const unsigned& ncollapsible,
const unsigned& ndown,
const unsigned& ny,
const double& lup,
const double& lcollapsible,
const double& ldown,
const double& ly,
const double& amplitude,
const double& period);
~CollapsibleChannelProblem() {}
RefineableCollapsibleChannelMesh<ELEMENT>* bulk_mesh_pt()
{
return dynamic_cast<RefineableCollapsibleChannelMesh<ELEMENT>*>
(Bulk_mesh_pt);
}
No action is needed before or after solving, so the pure virtual functions Problem::actions_before_newton_solve()
and Problem::actions_after_newton_solve()
can remain empty.
void actions_before_newton_solve(){}
void actions_after_newton_solve(){}
We will use the function Problem::actions_before_implicit_timestep()
to update the no-slip boundary conditions on the moving wall before each timestep and employ Problem::actions_before_adapt()
and Problem::actions_after_adapt()
to wipe and rebuild the mesh of prescribed traction elements each time a mesh adaptation is performed. The functions Problem::doc_solution(...)
and Problem::set_initial_condition()
will do what they say...
void actions_before_implicit_timestep();
void actions_before_adapt();
void actions_after_adapt();
void set_initial_condition();
void doc_solution(DocInfo& doc_info, ofstream& trace_file);
The private helper functions create_traction_elements(...)
and delete_traction_elements()
attach and remove the traction elements from the upstream boundary of the "bulk" Navier-Stokes mesh.
private :
void create_traction_elements(const unsigned &b,
Mesh* const &bulk_mesh_pt,
Mesh* const &surface_mesh_pt);
void delete_traction_elements(Mesh* const &surface_mesh_pt);
The private member data contains the geometric parameters as well as the pointer to the GeomObject
that describes the moving wall.
unsigned Nup;
unsigned Ncollapsible;
unsigned Ndown;
unsigned Ny;
double Lup;
double Lcollapsible;
double Ldown;
double Ly;
OscillatingWall* Wall_pt;
Further private member data includes pointers to the "bulk" mesh and the surface mesh that contains the traction elements, and pointers to control nodes in the in- and outflow cross-sections.
RefineableCollapsibleChannelMesh<ELEMENT>* Bulk_mesh_pt;
Mesh* Surface_mesh_pt;
Node* Left_node_pt;
Node* Right_node_pt;
};
The problem constructor
The arguments passed to the problem constructor specify the number of elements and lengths of the various parts of the channel, as well as the amplitude and period of the wall oscillations.
We store the parameters in the problem's private member data and increase the maximum permitted residual for the Newton iteration.
template <class ELEMENT>
CollapsibleChannelProblem<ELEMENT>::CollapsibleChannelProblem(
const unsigned& nup,
const unsigned& ncollapsible,
const unsigned& ndown,
const unsigned& ny,
const double& lup,
const double& lcollapsible,
const double& ldown,
const double& ly,
const double& amplitude,
const double& period)
{
Nup=nup;
Ncollapsible=ncollapsible;
Ndown=ndown;
Ny=ny;
Lup=lup;
Lcollapsible=lcollapsible;
Ldown=ldown;
Ly=ly;
Problem::Max_residuals=10000;
We continue by building the BDF<2>
timestepper and pass a pointer to it to the Problem
add_time_stepper_pt(new BDF<2>);
Next, we create the GeomObject
that represents the oscillating wall, and pass a pointer to it to the constructor of the CollapsibleChannelMesh
Wall_pt=new OscillatingWall(height, x_left, length, amplitude, period,
time_pt());
Bulk_mesh_pt = new RefineableCollapsibleChannelMesh<ELEMENT>(
nup, ncollapsible, ndown, ny,
lup, lcollapsible, ldown, ly,
Wall_pt,
time_stepper_pt());
We create a second mesh to store the applied traction elements and attach them to the inflow boundary (boundary 5) of the "bulk" fluid mesh, using the function create_traction_elements(...)
. Both submeshes are then combined into a global mesh.
Surface_mesh_pt = new Mesh;
create_traction_elements(5,Bulk_mesh_pt,Surface_mesh_pt);
add_sub_mesh(Bulk_mesh_pt);
add_sub_mesh(Surface_mesh_pt);
build_global_mesh();
We create the spatial error estimator for the fluid mesh and loop over the various elements to set the pointers to the relevant physical parameters, first for the Navier-Stokes elements in the bulk mesh,
Z2ErrorEstimator* error_estimator_pt=new Z2ErrorEstimator;
dynamic_cast<RefineableCollapsibleChannelMesh<ELEMENT>*>
(Bulk_mesh_pt)->spatial_error_estimator_pt()=error_estimator_pt;
unsigned n_element=Bulk_mesh_pt->nelement();
for(unsigned e=0;e<n_element;e++)
{
ELEMENT* el_pt = dynamic_cast<ELEMENT*>(Bulk_mesh_pt->element_pt(e));
el_pt->re_pt() = &Global_Physical_Variables::Re;
el_pt->re_st_pt() = &Global_Physical_Variables::ReSt;
}
and then for the applied traction elements in the surface mesh:
unsigned n_el=Surface_mesh_pt->nelement();
for(unsigned e=0;e<n_el;e++)
{
NavierStokesTractionElement<ELEMENT> *el_pt =
dynamic_cast< NavierStokesTractionElement<ELEMENT>*>(
Surface_mesh_pt->element_pt(e));
el_pt->traction_fct_pt() = &Global_Physical_Variables::prescribed_traction;
}
We apply the boundary conditions and pin the velocity on the relevant mesh boundaries:
- both axial and transverse velocities are pinned along the bottom and the top boundaries (boundaries 0, 2, 3 and 4).
- the transverse velocities are pinned along the in- and outflow boundaries (boundaries 1 and 5).
unsigned ibound=0;
unsigned num_nod= bulk_mesh_pt()->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
for(unsigned i=0;i<2;i++)
{
bulk_mesh_pt()->boundary_node_pt(ibound, inod)->pin(i);
}
}
for(unsigned ib=2;ib<5;ib++)
{
num_nod= bulk_mesh_pt()->nboundary_node(ib);
for (unsigned inod=0;inod<num_nod;inod++)
{
for(unsigned i=0;i<2;i++)
{
bulk_mesh_pt()->boundary_node_pt(ib, inod)->pin(i);
}
}
}
ibound=1;
num_nod= bulk_mesh_pt()->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
bulk_mesh_pt()->boundary_node_pt(ibound, inod)->pin(1);
}
ibound=5;
num_nod= bulk_mesh_pt()->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
bulk_mesh_pt()->boundary_node_pt(ibound, inod)->pin(1);
}
We select two control nodes on the inflow and outflow boundaries to document the velocities.
ibound=5;
num_nod= bulk_mesh_pt()->nboundary_node(ibound);
unsigned control_nod=num_nod/2;
Left_node_pt= bulk_mesh_pt()->boundary_node_pt(ibound, control_nod);
ibound=1;
num_nod= bulk_mesh_pt()->nboundary_node(ibound);
control_nod=num_nod/2;
Right_node_pt= bulk_mesh_pt()->boundary_node_pt(ibound, control_nod);
Finally, we set up the equation numbering scheme.
cout <<"Number of equations: " << assign_eqn_numbers() << std::endl;
}
Post processing
The function doc_solution(...)
documents the results, and records the time-trace of the axial velocities at the two control nodes and the position of the midpoint on the oscillating wall.
template <class ELEMENT>
void CollapsibleChannelProblem<ELEMENT>:: doc_solution(DocInfo& doc_info,
ofstream& trace_file)
{
ofstream some_file;
char filename[100];
unsigned npts;
npts=5;
sprintf(filename,"%s/soln%i.dat",doc_info.directory().c_str(),
doc_info.number());
some_file.open(filename);
bulk_mesh_pt()->output(some_file,npts);
some_file.close();
Vector<double> zeta(1);
zeta[0]=0.5*Lcollapsible;
Vector<double> wall_point(2);
Wall_pt->position(zeta,wall_point);
trace_file << time_pt()->time() << " "
<< wall_point[1] << " "
<< Left_node_pt->value(0) << " "
<< Right_node_pt->value(0) << " "
<< std::endl;
sprintf(filename,"%s/wall%i.dat",doc_info.directory().c_str(),
doc_info.number());
some_file.open(filename);
unsigned nplot=100;
for (unsigned i=0;i<nplot;i++)
{
zeta[0]=double(i)/double(nplot-1)*Lcollapsible;
Wall_pt->position(zeta,wall_point);
some_file << wall_point[0] << " "
<< wall_point[1] << std::endl;
}
some_file.close();
}
Creation of the traction elements
The creation of the applied traction elements follows the usual pattern, explained in detail elsewhere: We loop over the elements in the fluid mesh that are adjacent to the specified mesh boundary, and build the corresponding traction elements, which are added to the surface mesh.
template <class ELEMENT>
void CollapsibleChannelProblem<ELEMENT>::create_traction_elements(
const unsigned &b, Mesh* const &bulk_mesh_pt, Mesh* const &surface_mesh_pt)
{
unsigned n_element = bulk_mesh_pt->nboundary_element(b);
for(unsigned e=0;e<n_element;e++)
{
ELEMENT* bulk_elem_pt = dynamic_cast<ELEMENT*>
(bulk_mesh_pt->boundary_element_pt(b,e));
int face_index = bulk_mesh_pt->face_index_at_boundary(b,e);
NavierStokesTractionElement<ELEMENT>* traction_element_pt =
new NavierStokesTractionElement<ELEMENT>(bulk_elem_pt,face_index);
surface_mesh_pt->add_element_pt(traction_element_pt);
}
}
Delete the traction elements
Since the "bulk" elements that the applied traction elements are attached to may disappear during mesh adaptation, we delete all traction elements before the adaptation and re-attach them afterwards. The deletion is performed by the following member function. Note that the surface mesh that contains the traction elements is not deleted, as this would also delete the associated nodes which are shared with the corresponding bulk elements.
template<class ELEMENT>
void CollapsibleChannelProblem<ELEMENT>::
delete_traction_elements(Mesh* const &surface_mesh_pt)
{
unsigned n_element = surface_mesh_pt->nelement();
for(unsigned e=0;e<n_element;e++)
{
delete surface_mesh_pt->element_pt(e);
}
surface_mesh_pt->flush_element_and_node_storage();
}
Apply the initial conditions
Initial conditions are applied as usual. We start by confirming that the timestepper is a member of the BDF
family and therefore operates on history values that represent the solution at previous timesteps. We assign the previous nodal positions and velocities at all nodes, assuming that for
the wall is at rest and the flow field is given by steady Poiseuille flow.
template <class ELEMENT>
void CollapsibleChannelProblem<ELEMENT>::set_initial_condition()
{
if (time_stepper_pt()->type()!="BDF")
{
std::ostringstream error_stream;
error_stream
<< "Timestepper has to be from the BDF family!\n"
<< "You have specified a timestepper from the "
<< time_stepper_pt()->type() << " family" << std::endl;
throw OomphLibError(error_stream.str(),
OOMPH_CURRENT_FUNCTION,
OOMPH_EXCEPTION_LOCATION);
}
bulk_mesh_pt()->node_update();
unsigned num_nod = bulk_mesh_pt()->nnode();
for (unsigned n=0;n<num_nod;n++)
{
Vector<double> x(2);
x[0]=bulk_mesh_pt()->node_pt(n)->x(0);
x[1]=bulk_mesh_pt()->node_pt(n)->x(1);
bulk_mesh_pt()->node_pt(n)->set_value(0,6.0*(x[1]/Ly)*(1.0-(x[1]/Ly)));
bulk_mesh_pt()->node_pt(n)->set_value(1,0.0);
}
bulk_mesh_pt()->assign_initial_values_impulsive();
}
Actions before the timestep
Before each timestep, we update the nodal positions in the fluid mesh, and apply the no-slip condition to each node on mesh boundary 3.
template <class ELEMENT>
void CollapsibleChannelProblem<ELEMENT>::actions_before_implicit_timestep()
{
bulk_mesh_pt()->node_update();
unsigned ibound=3;
unsigned num_nod=bulk_mesh_pt()->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
Node* node_pt=bulk_mesh_pt()->boundary_node_pt(ibound,inod);
FSI_functions::apply_no_slip_on_moving_wall(node_pt);
}
}
Actions before the mesh adaptation
As discussed above, we delete the applied traction elements before performing any mesh adaptation and then rebuild the global mesh.
template<class ELEMENT>
void CollapsibleChannelProblem<ELEMENT>::actions_before_adapt()
{
delete_traction_elements(Surface_mesh_pt);
rebuild_global_mesh();
}
Actions before the mesh adaptation
Once the mesh has been adapted, we (re-)create the prescribed traction elements and rebuild the global mesh. We also have to pass the pointers to prescribed traction function to the newly created traction elements.
template<class ELEMENT>
void CollapsibleChannelProblem<ELEMENT>::actions_after_adapt()
{
create_traction_elements(5,Bulk_mesh_pt,Surface_mesh_pt);
rebuild_global_mesh();
unsigned n_element=Surface_mesh_pt->nelement();
for(unsigned e=0;e<n_element;e++)
{
NavierStokesTractionElement<ELEMENT> *el_pt =
dynamic_cast<NavierStokesTractionElement<ELEMENT>*>(
Surface_mesh_pt->element_pt(e));
el_pt->traction_fct_pt() = &Global_Physical_Variables::prescribed_traction;
}
}
Comments and Exercises
- Check the non-dimensionalisation of the governing equations and confirm that a (non-dimensional) upstream pressure
is required to drive the steady Poiseuille flow specified by (3) through the static, undeformed channel. Use this to "validate" (well, "plausibility-check", anyway...) the code by setting the amplitude of the wall oscillation to zero.
- Double the upstream pressure while keeping the amplitude of the wall oscillation at zero and confirm that the flow accelerates until it (asymptotically) approaches Poiseuille flow with twice the initial flowrate as

- The flow field has the largest velocity gradients in the thin Stokes layers near the wall, causing the automatic mesh adaptation procedure to refine the mesh pre-dominantly in these regions. To facilitate the resolution of such layers the
CollapsibleChannelDomain
and CollapsibleChannelMesh
allow the specification of a mapping [0,1] -> [0,1] that redistributes the nodal points in the vertical direction so that the elements near the wall become more squashed. By default the "boundary-layer squash function" is the identity but it may be overloaded by specifying a function pointer to an alternative function. The driver code already includes a demonstration of this capability which may be activated by compiling the driver code with -DUSE_BL_SQUASH_FCT
. This activates the code segment #ifdef USE_BL_SQUASH_FCT
Bulk_mesh_pt->node_update();
#endif
double squash_fct(const double &s)
Mapping [0,1] -> [0,1] that re-distributes nodal points across the channel width.
in the Problem constructor. The "squash function" used for this example is defined in the following namespace:
{
{
double y=s;
{
}
{
}
else
{
}
return y;
}
}
Namespace to define the mapping [0,1] -> [0,1] that re-distributes nodal points across the channel wi...
double Delta
Boundary layer width.
double Fract_in_BL
Fraction of points in boundary layer.
With this function 50% of the nodal points in the vertical direction are located within two boundary-layer regions which occupy 2 x 10% of the channel's width. The figure below shows the element shapes for a (coarse) initial mesh that is used in the validation run, with and without the boundary-layer squashing function:
Coarse initial meshes with and without the boundary-layer squash function.
Confirm that if this "squashing function" is applied to the mesh that is used during the non-self-test runs (this mesh has 16 x larger number of elements than the meshes shown above), the quality of the computed solution improves so much that no subsequent mesh adaptation is required.
- The flow structures observed during the small-amplitude oscillations (shown in the animation at the beginning of this document) are in perfect agreement with the structures predicted by Jensen & Heil's (2003) asymptotic analysis. As an exercise, increase the amplitude of the wall oscillation (to
, say) to confirm that the flow-structure predicted by the theory (which is strictly applicable only for small-amplitude
oscillations) also provides an excellent description of of the system's behaviour during large-amplitude oscillations with more complicated wall motions.
For instance, the figure below shows a snapshot of the the animation of the computational results for an oscillation in which the wall undergoes a more complicated motion, described by
for
. For these parameter values, the wall performs a large-amplitude oscillation in the course of which material particles are not only displaced vertically but also in the horizontal direction. Nevertheless, the flow generated by the moving wall may be described as arising from the superposition of Poiseuille flow and an axial sloshing motion, the latter obviously having a much larger amplitude than in the previous case. The
animation of the flow field shows that more complex local flow features develop briefly whenever the flow separates from the wall. However, the appearance of these features does not change the macroscopic behaviour of the flow.
Flow field for a large-amplitude wall motion. Re=ReSt=50; A=B=0.5; T=0.45.
Source files for this tutorial
PDF file
A pdf version of this document is available.