In this example we shall demonstrate the spatially adaptive solution of the steady 3D Navier-Stokes equations using the problem of developing pipe flow.
The example problem
Results for Taylor-Hood elements
The figure below shows the results computed with oomph-lib's
3x3x3-node 3D adaptive Taylor-Hood elements for the parameters
,
and
. The large exponent
imposes a very blunt inflow profile, which creates a thin boundary layer near the wall. Diffusion of vorticity into the centre of the tube smooths the velocity profile which ultimately approaches a parabolic Poiseuille profile. If you are viewing these results online you will be able to see how successive mesh adaptations refine the mesh – predominantly near the entry region where the large velocity gradients in the boundary layer require a fine spatial discretisation. Note also that on the coarsest mesh, even the (imposed) inflow profile is represented very poorly.
Contour plot of the axial velocity distribution for Re=100. Flow is from right to left.
Axial velocity profiles in equally-spaced cross-sections along the tube for Re=100. Flow is from left to right.
Global parameters and functions
The problem only contains one global parameter, the Reynolds number, which we define in a namespace, as usual.
{
}
Namespace for physical parameters.
double Re
Reynolds number.
The driver code
Since the 3D computations can take a long time, and since all demo codes are executed during oomph-lib's
self-test procedures, we allow the code to operate in two modes:
- By default, we specify error targets for which the code refines the mesh near the inflow region, and allow up to five successive mesh adaptations. The code is executed in this mode if the executable is run without any command line arguments.
- If the code is run during the self-test procedure (indicated by specifying some random command line argument), we only perform one level of adaptation to speed up the self-test. However, because the original mesh is very coarse, the first mesh adaptation refines all elements in the mesh (cf. the animation of the adaptive mesh refinement shown above), so that no hanging nodes are created – not a good test-case for a validation run! Therefore adjust the error targets so that the first (and only) mesh adaption only refines a few elements and therefore creates a few hanging nodes.
The main code therefore starts by storing the command line arguments and setting the adaptation targets accordingly:
int main(
int argc,
char* argv[])
{
CommandLineArgs::setup(argc,argv);
unsigned max_adapt;
double max_error_target,min_error_target;
if (CommandLineArgs::Argc==1)
{
max_adapt=5;
max_error_target=0.005;
min_error_target=0.0005;
}
else
{
max_adapt=1;
max_error_target=0.02;
min_error_target=0.002;
}
int main(int argc, char *argv[])
///////////////////////////////////////////////////////////////////// ///////////////////////////////...
We then create a DocInfo
object to specify the labels for the output files, and solve the problem, first with Taylor-Hood and then with Crouzeix-Raviart elements, writing the results from the two discretisations to different directories:
DocInfo doc_info;
{
doc_info.set_directory("RESLT_TH");
doc_info.number()=0;
problem(doc_info,min_error_target,max_error_target);
cout << " Doing Taylor-Hood elements " << std::endl;
problem.doc_solution();
problem.newton_solve(max_adapt);
}
{
doc_info.set_directory("RESLT_CR");
doc_info.number()=0;
problem(doc_info,min_error_target,max_error_target);
cout << " Doing Crouzeix-Raviart elements " << std::endl;
problem.newton_solve(max_adapt);
}
}
Entry flow problem in quarter tube domain.
The problem class
The problem class is very similar to the ones used in the 2D examples. We pass the DocInfo
object and the target errors to the Problem
constructor.
template<class ELEMENT>
{
public:
const double& max_error_target);
EntryFlowProblem(DocInfo &doc_info, const double &min_error_target, const double &max_error_target)
Constructor: Pass DocInfo object and target errors.
~EntryFlowProblem()
Destructor (empty)
The function Problem::actions_after_newton_solve()
is used to document the solutions computed at various levels of mesh refinement:
void actions_after_newton_solve()
{
doc_solution();
Doc_info.number()++;
}
The function Problem::actions_before_newton_solve()
is discussed below, and, as in all adaptive Navier-Stokes computations, we use the function Problem::actions_after_adapt()
to pin any redundant pressure degrees of freedom; see another tutorial for details.
void actions_before_newton_solve();
void actions_after_adapt()
{
RefineableNavierStokesEquations<3>::
pin_redundant_nodal_pressures(mesh_pt()->element_pt());
}
Finally, we have the usual doc_solution()
function and include an access function to the mesh. The private member data Alpha
determines the bluntness of the inflow profile.
void doc_solution();
RefineableQuarterTubeMesh<ELEMENT>* mesh_pt()
{
return dynamic_cast<RefineableQuarterTubeMesh<ELEMENT>*>(Problem::mesh_pt());
}
private:
int Alpha;
DocInfo Doc_info;
};
The constructor
We start by building the adaptive mesh for the quarter tube domain. As for most meshes with curvilinear boundaries, the RefineablequarterTubeMesh
expects the curved boundary to be represented by a GeomObject
. We therefore create an EllipticalTube
with unit half axes, i.e. a unit cylinder and a pass a pointer to the GeomObject
to the mesh constructor. The "ends" of the curvilinear boundary (in terms of the maximum and minimum values of the Lagrangian coordinates that parametrise the shape of the GeomObject
) are such that it represents a quarter of a cylindrical tube of length
.
template<class ELEMENT>
const double& min_error_target,
const double& max_error_target)
: Doc_info(doc_info)
{
double radius=1.0;
GeomObject* Wall_pt=new EllipticalTube(radius,radius);
Vector<double> xi_lo(2);
xi_lo[0]=0.0;
xi_lo[1]=0.0;
Vector<double> xi_hi(2);
xi_hi[0]=7.0;
xi_hi[1]=2.0*atan(1.0);
unsigned nlayer=6;
double frac_mid=0.5;
Problem::mesh_pt()=
new RefineableQuarterTubeMesh<ELEMENT>(Wall_pt,xi_lo,frac_mid,xi_hi,nlayer);
Next, we build an error estimator and specify the target errors for the mesh adaptation:
Z2ErrorEstimator* error_estimator_pt=new Z2ErrorEstimator;
mesh_pt()->spatial_error_estimator_pt()=error_estimator_pt;
mesh_pt()->max_permitted_error()=max_error_target;
mesh_pt()->min_permitted_error()=min_error_target;
Now we have to apply boundary conditions on the various mesh boundaries. [Reminder: If the numbering of the mesh boundaries is not apparent from its documentation (as it should be!), you can use the function Mesh::output_boundaries(...)
to output them in a tecplot-readable form.
ofstream some_file;
char filename[100];
sprintf(filename,"boundaries.dat");
some_file.open(filename);
mesh_pt()->output_boundaries(some_file);
some_file.close();
If the mesh has N boundaries, the output file will contain N different zones, each containing the
coordinates of the nodes on the boundary.]
For the RefineableQuarterTubeMesh
, the boundaries are numbered as follows:
- Boundary 0: "Inflow" cross section; located along the line parametrised by
on the GeomObject
that specifies the wall.
- Boundary 1: Plane

- Boundary 2: Plane

- Boundary 3: The curved wall
- Boundary 4: "Outflow" cross section; located along the line parametrised by
on the GeomObject
that specifies the wall.
Plot of the mesh boundaries.
We apply the following boundary conditions:
- Boundary 0: (
) pin all three velocities.
- Boundary 1: (
) pin
.
- Boundary 2: (
) pin
.
- Boundary 3: (
) pin all three velocities.
- Boundary 4: (
) pin
and
.
unsigned num_bound = mesh_pt()->nboundary();
for(unsigned ibound=0;ibound<num_bound;ibound++)
{
unsigned num_nod= mesh_pt()->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
if(ibound!=1) mesh_pt()->boundary_node_pt(ibound,inod)->pin(1);
if(ibound!=2) mesh_pt()->boundary_node_pt(ibound,inod)->pin(0);
if((ibound==0) || (ibound==3))
{
mesh_pt()->boundary_node_pt(ibound,inod)->pin(2);
}
}
}
Now we assign the re_pt()
for each element and pin the redundant nodal pressures (see another tutorial for details).
unsigned n_element = mesh_pt()->nelement();
for(unsigned i=0;i<n_element;i++)
{
ELEMENT* el_pt = dynamic_cast<ELEMENT*>(mesh_pt()->element_pt(i));
}
RefineableNavierStokesEquations<3>::
pin_redundant_nodal_pressures(mesh_pt()->element_pt());
We provide an initial guess for the velocity field by initialising all velocity components with their Poiseuille flow values.
unsigned n_nod=mesh_pt()->nnode();
for (unsigned j=0;j<n_nod;j++)
{
Node* node_pt=mesh_pt()->node_pt(j);
double x=node_pt->x(0);
double y=node_pt->x(1);
double r=sqrt(x*x+y*y );
node_pt->set_value(0,0.0);
node_pt->set_value(1,0.0);
node_pt->set_value(2,(1.0-r*r));
}
Finally, we set the value of Alpha
, the exponent that specifies the bluntness of the inflow profile and assign the equation numbers.
Alpha=20;
cout <<"Number of equations: " << assign_eqn_numbers() << std::endl;
}
Actions before solve
We use the function Problem::actions_before_newton_solve()
to re-assign the inflow boundary conditions before every solve. In the present problem this is an essential step because the blunt inflow profile (4) cannot be represented accurately on the initial coarse mesh (see the animation of the axial velocity profiles at the beginning of this document). As discussed in the example that illustrates the use of spatial adaptivity for time-dependent problems, oomph-lib
automatically (i) applies the correct boundary conditions for newly created nodes that are located on the mesh boundaries and (ii) assigns the nodal values at such nodes by interpolation from the previously computed solution. This procedure is adequate on boundaries where homogeneous boundary conditions are applied, e.g. on the curved wall, the symmetry and the outflow boundaries. However, on the inflow boundary, the interpolation from the FE representation of the blunt velocity profile (imposed on the coarse initial mesh) onto the refined mesh, does not yield a more accurate representation of the prescribed inflow profile. It is therefore necessary to re-assign the nodal values on this boundary after every adaptation, i.e. before every solve.
template<class ELEMENT>
{
unsigned ibound=0;
unsigned num_nod= mesh_pt()->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
double x=mesh_pt()->boundary_node_pt(ibound,inod)->x(0);
double y=mesh_pt()->boundary_node_pt(ibound,inod)->x(1);
double r=sqrt(x*x+y*y);
mesh_pt()->boundary_node_pt(ibound,inod)->
set_value(2,(1.0-pow(r,Alpha)));
}
void actions_before_newton_solve()
Update the problem specs before solve.
Post processing
This function remains exactly the same as in the 2D examples.
template<class ELEMENT>
{
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);
mesh_pt()->output(some_file,npts);
some_file.close();
}
void doc_solution()
Doc the solution.
Comments and exercises
- Suppress the reassignment of the prescribed inflow profile in
Problem::actions_before_newton_solve()
to confirm that this step is essential if the computation is to converge to the exact solution.
- Suppress the specification of the parabolic (Poiseuille) velocity profile as the initial guess for the velocity field in the problem constructor to confirm that the assignment of a "good" initial guess for the solution is essential for the convergence of the Newton method. [Hint: You can simply comment out the initialisation of the velocities – they then retain their default initial values of 0.0. When you re-run the code, the Newton iteration will "die" immediately with an error message stating that the maximum residual exceeds the default threshold of 10.0, stored in the protected data member
Problem::Max_residuals
. Try increasing the value of this threshold in the Problem constructor. Is this sufficient to make the Newton method converge?]
Source files for this tutorial
PDF file
A pdf version of this document is available.