Steady Convection Rolls: contours of temperature and element boundaries for a two-dimensional domain heated from below at Ra = 1800
We study convection of an incompressible Newtonian fluid heated from below in a two-dimensional domain of height : the Bénard problem. The lower wall is maintained at a temperature and the upper wall is maintained at a temperature , where . The theory is described the non-refineable version of the problem.
In this example, we solve the same physical problem, but using refineable elements. As an alternative to the time-stepping procedure adopted previously, we perturb the trivial steady-state solution and re-solve the steady equations to obtain the steady symmetry-broken solution. In what follows, we shall only describe those parts of the code that differ from the non-refineable version.
The driver code
We start by setting the direction of gravity, and constructing the problem using the new RefineableBuoyantQCrouzeixRaviartElements, described below.
{
RefineableBuoyantQCrouzeixRaviartElement<2> > problem;
/////////////////////////////////////////////////////////////////// /////////////////////////////////...
Vector< double > Direction_of_gravity(2)
Gravity vector.
int main()
Driver code for 2D Boussinesq convection problem with adaptivity.
As discussed in the previous example, a small perturbation is required to force the solution from the trivial steady state of zero velocity and linear temperature variation. Therefore, we add a small perturbation to the vertical velocity on the upper wall before solving the steady problem, allowing for up to two levels of adaptive mesh refinement.
problem.enable_imperfection();
problem.newton_solve(2);
problem.doc_solution();
Having forced the solution into a non-trivial symmetry-broken state, we switch off the perturbation and re-solve the problem, allowing for another two levels of adaptive refinement. The Newton solver now converges to the unperturbed but symmetry-broken solution shown above.
problem.disable_imperfection();
problem.newton_solve(2);
problem.doc_solution();
}
The problem class
The problem class contains the constructor and (empty) destructor, the usual action functions, and an access function to the specific mesh used in this problem.
template<class ELEMENT>
{
public:
RectangularQuadMesh<ELEMENT>*
mesh_pt()
{
return dynamic_cast<RectangularQuadMesh<ELEMENT>*>(
Problem::mesh_pt());
}
void actions_after_newton_solve()
Update the problem after solve (empty)
~RefineableConvectionProblem()
Destructor. Empty.
void actions_before_newton_solve()
Update the problem specs before solve:
RectangularQuadMesh< ELEMENT > * mesh_pt()
Overloaded version of the problem's access function to the mesh. Recasts the pointer to the base Mesh...
RefineableConvectionProblem()
Constructor.
No specific action is required before the adaptation but following the mesh adaptation exactly one pressure degree of freedom must be pinned in the problem. (Since the domain is enclosed the pressure is only determined up to an arbitrary constant.) The pressure degree of freedom that was pinned before the adaptation may have disappeared during the adaptation, therefore the constraint must be re-applied. However, we unpin all pressure degrees of freedom first to ensure that we do not accidentally pin two pressure degrees of freedom.
void actions_before_adapt() {}
void actions_after_adapt()
{
RefineableNavierStokesEquations<2>::
unpin_all_pressure_dofs(mesh_pt()->element_pt());
fix_pressure(0,0,0.0);
}
void fix_pressure(const unsigned &e, const unsigned &pdof,
const double &pvalue)
{
dynamic_cast<ELEMENT*>(mesh_pt()->element_pt(e))->
fix_pressure(pdof,pvalue);
}
The remaining member functions provide access to the boolean flag that controls the application of the imperfection, and document the solution:
void enable_imperfection() {Imperfect = true;}
void disable_imperfection() {Imperfect = false;}
void doc_solution();
private:
DocInfo Doc_info;
bool Imperfect;
};
The constructor
We pass the element type as a template parameter to the problem constructor, which has no arguments. The constructor builds a coarse initial RefineableRectangularQuadMesh
, using elements and allocates a spatial error estimator that is attached to the mesh.
template<class ELEMENT>
{
Doc_info.set_directory("RESLT");
unsigned n_x=9;
unsigned n_y=8;
double l_x=3.0;
double l_y=1.0;
RefineableRectangularQuadMesh<ELEMENT>* cast_mesh_pt =
new RefineableRectangularQuadMesh<ELEMENT>(n_x,n_y,l_x,l_y);
Problem::mesh_pt() = cast_mesh_pt;
cast_mesh_pt->spatial_error_estimator_pt()=new Z2ErrorEstimator;
cast_mesh_pt->max_permitted_error()=0.5e-3;
cast_mesh_pt->min_permitted_error()=0.5e-4;
Next, the boundary constraints are imposed. We pin all velocities and the temperature on the top and bottom walls and pin only the horizontal velocity on the sidewalls. As discussed above, a single pressure value must be pinned to ensure a unique solution.
unsigned num_bound = mesh_pt()->nboundary();
for(unsigned ibound=0;ibound<num_bound;ibound++)
{
unsigned val_max=3;
if((ibound==1) || (ibound==3)) {val_max=1;}
unsigned num_nod= mesh_pt()->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
for(unsigned j=0;j<val_max;j++)
{
mesh_pt()->boundary_node_pt(ibound,inod)->pin(j);
}
}
}
fix_pressure(0,0,0.0);
We complete the build of the elements by setting the pointers to the physical parameters and finally assign the equation numbers
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));
}
cout <<"Number of equations: " << assign_eqn_numbers() << endl;
}
double Rayleigh
Rayleigh number, set to be greater than the threshold for linear instability.
double Inverse_Prandtl
1/Prandtl number
double Peclet
Peclet number (identically one from our non-dimensionalisation)
The function fix_pressure(...)
This function is a simple wrapper to the element's fix_pressure(...)
function.
void fix_pressure(const unsigned &e, const unsigned &pdof,
const double &pvalue)
{
dynamic_cast<ELEMENT*>(mesh_pt()->element_pt(e))->
fix_pressure(pdof,pvalue);
}
The function actions_before_newton_solve(...)
The function is used to set the specific values of the Dirichlet boundary conditions. If the boolean flag Imperfect
is true then a small mass-conserving imperfection is added to the velocity boundary condition on the top wall.
template<class ELEMENT>
{
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++)
{
Node* nod_pt=mesh_pt()->boundary_node_pt(ibound,inod);
unsigned vel_max=2;
if((ibound==1) || (ibound==3)) {vel_max = 1;}
for(unsigned j=0;j<vel_max;j++) {nod_pt->set_value(j,0.0);}
if(ibound==2)
{
nod_pt->set_value(2,-0.5);
if(Imperfect)
{
double x = nod_pt->x(0);
double value = sin(2.0*3.141592654*x/3.0);
nod_pt->set_value(1,value);
}
}
if(ibound==0) {nod_pt->set_value(2,0.5);}
}
}
}
The function doc_solution(...)
This function writes the complete velocity, pressure and temperature fields to a file in the output directory specified in the DocInfo
object.
template<class ELEMENT>
{
ofstream some_file;
char filename[100];
unsigned 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();
Doc_info.number()++;
}
void doc_solution()
Doc the solution.
Creating the new RefineableBouyantQCrouzeixRaviartElement class
As in the non-refineable version of the problem we create the refineable element RefineableBuoyantQCrouzeixRaviartElement
by multiple inheritance from the RefineableQCrouzeixRaviartElement
and RefineableQAdvectionDiffusionElement:
template<unsigned DIM>
class RefineableBuoyantQCrouzeixRaviartElement
: public virtual RefineableQAdvectionDiffusionElement<DIM, 3>,
public virtual RefineableQCrouzeixRaviartElement<DIM>
{
Many of the additional member functions required in the combined multi-physics element are identical to those in the non-refineable version:
- the access function to (the pointer to the) Rayleigh number,
ra_pt()
,
- the output functions,
output(...)
,
- the function
required_n_value(...)
which specifies the number of values required at each node,
- the function
u_index_adv_diff(...)
which specifies the index at which the temperature is stored within the elements' Nodes
,
- the function
get_wind_adv_diff(...)
which specifies the "wind" in the advection diffusion equations in terms of the Navier-Stokes velocities,
- the function
get_body_force_nst(...)
which specifies the body force in the Navier-Stokes equations in terms of the temperature.
- the two "fill in" function are implemented as in the non-refineable element. The function
fill_in_contribution_to_residuals(...)
concatenates the contributions from the two underlying elements; the function fill_in_contribution_to_jacobian(...)
computes the coupled elemental Jacobian matrix by finite-differencing.
We shall only discuss those additional functions that are required in the refineable version of the combined multi-physics element.
Both constituent elements are derived from the ElementWithZ2ErrorEstimator
base class and each element provides its own definition of the "Z2-flux" that is used by the Z2 error estimator to compute elemental error estimates. We must decide on a single error estimator for the combined element. We could base the error estimation entirely on the fluid flow, or the temperature field, but instead we shall choose our single error estimate to be the maximum of the fluid and temperature error estimates. The functions required by the Z2ErrorEstimator
are overloaded to return all the flux terms associated with both the velocity and temperature fields, with the velocity field terms stored first.
unsigned nrecovery_order()
{
return RefineableQCrouzeixRaviartElement<DIM>::nrecovery_order();
}
unsigned num_Z2_flux_terms()
{
return (
RefineableQCrouzeixRaviartElement<DIM>::num_Z2_flux_terms() +
RefineableQAdvectionDiffusionElement<DIM, 3>::num_Z2_flux_terms());
}
void get_Z2_flux(const Vector<double>& s, Vector<double>& flux)
{
unsigned n_fluid_flux =
RefineableQCrouzeixRaviartElement<DIM>::num_Z2_flux_terms();
RefineableQCrouzeixRaviartElement<DIM>::get_Z2_flux(s, flux);
unsigned n_temp_flux =
RefineableQAdvectionDiffusionElement<DIM, 3>::num_Z2_flux_terms();
Vector<double> temp_flux(n_temp_flux);
RefineableQAdvectionDiffusionElement<DIM, 3>::get_Z2_flux(s, temp_flux);
for (unsigned i = 0; i < n_temp_flux; i++)
{
flux[n_fluid_flux + i] = temp_flux[i];
}
}
The default behaviour of the Z2 error estimator is to combine all components of the flux vector into a single compound flux. In the present multi-physics element, we instead define two compound fluxes: one corresponding to the combined temperature fluxes and the other to the combined velocity field fluxes. The flux components associated with each compound flux must specified by overloading the function get_Z2_compound_flux_indices
which returns a vector of the same length as the number of flux components, containing the index of the compound flux to which the flux component contributes.
unsigned ncompound_fluxes()
{
return 2;
}
void get_Z2_compound_flux_indices(Vector<unsigned>& flux_index)
{
unsigned n_fluid_flux =
RefineableQCrouzeixRaviartElement<DIM>::num_Z2_flux_terms();
unsigned n_temp_flux =
RefineableQAdvectionDiffusionElement<DIM, 3>::num_Z2_flux_terms();
for (unsigned i = 0; i < n_fluid_flux; i++)
{
flux_index[i] = 0;
}
for (unsigned i = 0; i < n_temp_flux; i++)
{
flux_index[n_fluid_flux + i] = 1;
}
}
The Z2ErrorEstimator
calculates the error estimates for each compound flux. The individual error estimates are then combined to a single error estimate by the function Z2ErrorEstimator::get_combined_error_estimate()
. By default the single error estimate is chosen to be the maximum of all calculated error estimates. Alternative user-defined functions can be specified via the function pointer Z2ErrorEstimator::CombinedErrorEstimateFctPt&
combined_error_fct_pt()
, see Comments for a more detailed discussion of this aspect.
The vertex nodes are defined by the underlying geometric element, but require a final overload to prevent ambiguities:
unsigned nvertex_node() const
{
return QElement<DIM, 3>::nvertex_node();
}
Node* vertex_node_pt(const unsigned& j) const
{
return QElement<DIM, 3>::vertex_node_pt(j);
}
The number of continuously interpolated values is DIM
+
1
: DIM
velocity components and one temperature.
unsigned ncont_interpolated_values() const
{
return DIM + 1;
}
The two versions of the get_interpolated_values(...)
function must return the continuously interpolated variables at a specified position within the element:
void get_interpolated_values(const Vector<double>& s,
Vector<double>& values)
{
Vector<double> nst_values;
RefineableQCrouzeixRaviartElement<DIM>::get_interpolated_values(
s, nst_values);
Vector<double> advection_values;
RefineableQAdvectionDiffusionElement<DIM, 3>::get_interpolated_values(
s, advection_values);
for (unsigned i = 0; i < DIM; i++)
{
values.push_back(nst_values[i]);
}
values.push_back(advection_values[0]);
}
void get_interpolated_values(const unsigned& t,
const Vector<double>& s,
Vector<double>& values)
{
Vector<double> nst_values;
RefineableQCrouzeixRaviartElement<DIM>::get_interpolated_values(
t, s, nst_values);
Vector<double> advection_values;
RefineableQAdvectionDiffusionElement<DIM, 3>::get_interpolated_values(
s, advection_values);
for (unsigned i = 0; i < DIM; i++)
{
values.push_back(nst_values[i]);
}
values.push_back(advection_values[0]);
}
Finally, the setup and build functions must call the build functions of the two constituent elements. In addition, the pointer to the Rayleigh number must be passed to the sons after refinement.
void further_setup_hanging_nodes()
{
RefineableQCrouzeixRaviartElement<DIM>::further_setup_hanging_nodes();
RefineableQAdvectionDiffusionElement<DIM,
3>::further_setup_hanging_nodes();
}
void rebuild_from_sons(Mesh*& mesh_pt)
{
RefineableQAdvectionDiffusionElement<DIM, 3>::rebuild_from_sons(mesh_pt);
RefineableQCrouzeixRaviartElement<DIM>::rebuild_from_sons(mesh_pt);
}
void further_build()
{
RefineableQCrouzeixRaviartElement<DIM>::further_build();
RefineableQAdvectionDiffusionElement<DIM, 3>::further_build();
RefineableBuoyantQCrouzeixRaviartElement<DIM>* cast_father_element_pt =
dynamic_cast<RefineableBuoyantQCrouzeixRaviartElement<DIM>*>(
this->father_element_pt());
this->Ra_pt = cast_father_element_pt->ra_pt();
}
Comments and Exercises
Comments
- Error estimation for multi-physics elements
The error estimation for the combined multi-physics element is performed with the Z2ErrorEstimator
which computes an error estimate based Zienkiewicz and Zhu's flux recovery technique, using the elemental "Z2 flux" defined in the pure virtual function get_Z2_flux(...)
. The two constituent elements already provide their own implementation of this function:
- In the
RefineableQAdvectionDiffusionElement
the temperature gradient is used as the flux.
- In the
RefineableQCrouzeixRaviartElement
the flux is defined by the components of the fluid's rate of strain tensor.
It is not obvious what combination of these flux terms should be used to compute the error estimates for the combined multi-physics element. The most general option would be to combine the two flux vectors, possibly using a weighting factor to control the relative importance of the various components.
In the example above, we chose an error estimate that was the maximum value of the individual error estimates for the Navier-Stokes flux and the temperature flux. Refinement will be performed if either of the single-physics error estimates are above the chosen thresholds. It is also possible to base the error estimation entirely on the Navier-Stokes fluxes; an appropriate choice for problems in which the variations in the velocity field are expected to be much more rapid than those in the temperature field. Alternatively,
the error estimation could be based exclusively on the temperature field; a choice that would be appropriate for problems with thin thermal boundary layers. The different error estimates can all be specified by user-defined functions that combine the vector of compound-flux error estimates into a single number. For example, the function double navier_stokes_flux_error(const Vector<double> &errors)
{return errors[0];}
specifies that the combined error estimate is the first of the compound error estimates — the error estimate for the Navier–Stokes equations.
In the current example both fields vary very smoothly, and as a result spatial adaptivity is not really required in this problem. This is why we set a very narrow range of target errors – if the default targets are used, oomph-lib
refines the mesh uniformly.
Exercises
- Confirm that for a Rayleigh number of the system is stable, i.e. it returns to the trivial state, when the perturbation to the vertical velocity on the upper wall is switched off.
- Re-write the multi-physics elements so that the temperature is stored before the fluid velocities. Confirm that the solution is unchanged in this case.
- Try using
RefineableQTaylorHoodElements
as the "fluid" element part of the multi-physics elements.
- Change the error estimate to be based entirely on the error in the Navier–Stokes fluxes by using a user-defined function. Is there any difference in the refinement pattern?
Source files for this tutorial
PDF file
A pdf version of this document is available.