The purpose of this tutorial is to demonstrate the adaptive solution of solid mechanics problems using unstructured meshes generated by oomph-lib's
inline unstructured mesh generation procedures. The use of these methods for solid mechanics problems required no additional effort on the part of the user and the setup is essentially the same as that described for unstructured solid mechanics without mesh adaptation. Lagrangian coordinates are projected between meshes in the same way as all other field variables, Eulerian coordinates and history values, see the description in another tutorial.
The solid mechanics problem described here can be regarded as a sub-problem for the
unstructured adaptive fluid–structure interaction tutorial. In addition, we can use the problem to assess the errors incurred when projecting the solution between different meshes.
The problem
An elastic bar is fixed at the base and loaded by a constant pressure on its left-hand side. The pressure load is increased and then decreased so that at the end of the simulation the bar should return to its undeformed position. The strain energy in the final configuration is a measure of the projection error because if there were no projection at all it would be exactly zero (or certainly zero to less than machine precision).
Sketch of the problem.
Results
The animation shown below illustrates the solid's deformation and illustrates the adaptation of the mesh as the load changes.
Plot of the deformation.
The initial strain energy is , and the strain energy in the final configuration after the external pressure has been reset to zero, but the mesh has been adapted, is . The strain energy at the maximum deflection is .
Global Physical Variables
We define the various physical variables in a global namespace. We define Poisson's ratio and prepare a pointer to a constitutive equation.
{
//////////////////////////////////////////////////////////////////// ////////////////////////////////...
ConstitutiveLaw * Constitutive_law_pt
Pointer to constitutive law.
double Nu
Poisson's ratio.
Next we define the pressure load to be applied at the left-hand boundary,
const Vector<double> &n, Vector<double> &traction)
{
unsigned dim = traction.size();
for(unsigned i=0;i<dim;i++)
{
}
}
}
void constant_pressure(const Vector< double > &xi, const Vector< double > &x, const Vector< double > &n, Vector< double > &traction)
Constant pressure load. The arguments to this function are imposed on us by the SolidTractionElements...
double P
Uniform pressure.
The driver code
The driver code consists of essentially the same code repeated for three different formulations of solid mechanics: (i) (compressible) displacement only; (ii) (compressible) displacement-pressure; and (iii) incompressible displacement-pressure. We shall describe the code only for the first formulation.
Initially, we specify an output directory and instantiate a constitutive equation. (Recall that the single-argument constructor to the GeneralisedHookean
constitutive law implies that all stresses are non-dimensionalised on Young's modulus ).
int main(
int argc,
char **argv)
{
DocInfo doc_info;
doc_info.set_directory("RESLT");
int main(int argc, char **argv)
Demonstrate how to solve an unstructured solid problem.
We then open an output file for the strain energy, create the Problem
object using a displacement formulation of the equations and output the initial configuration.
{
std::ofstream strain("RESLT/s_energy.dat");
std::cout << "Running with pure displacement formulation\n";
doc_info.number()++;
Unstructured solid problem.
void doc_solution(DocInfo &doc_info)
Doc the solution.
Finally, we perform the parameter study by slowly increasing and then reducing the pressure on the left-hand boundary. Note that one round of mesh adaptation is specified for every Newton solve.
double pressure_increment=0.1e-2;
unsigned nstep=5;
for (unsigned istep=0;istep<nstep;istep++)
{
problem.newton_solve(1);
std::cout << "Strain energy is " << strain_energy << "\n";
doc_info.number()++;
if(istep==2) {pressure_increment *= -1.0;}
}
strain.close();
}
double get_strain_energy()
Calculate the strain energy.
The Problem class
The Problem
class has the obvious member functions as well as a function to set whether the material is incompressible and a function to compute the strain energy of the elastic body. The class provides storage for the two sub-meshes: the bulk mesh of 2D solid elements and the mesh of 1D traction elements that will be attached to the left-hand boundary. In addition, storage is provided for the polygon that represents the initial outer boundary of the solid body and a boolean flag that is used to specify whether the material is incompressible or not.
template<class ELEMENT>
{
public:
private:
};
UnstructuredSolidProblem()
Constructor:
~UnstructuredSolidProblem()
Destructor (empty)
TriangleMeshPolygon * Outer_boundary_polyline_pt
Triangle mesh polygon for outer boundary.
SolidMesh * Traction_mesh_pt
Pointer to mesh of traction elements.
void actions_before_adapt()
Remove Traction Mesh.
bool Incompressible
Boolean flag used in an incompressible problem.
void set_incompressible()
Set the problem to be incompressible.
void actions_after_adapt()
Add on the traction elements after adaptation.
RefineableSolidTriangleMesh< ELEMENT > * Solid_mesh_pt
Bulk mesh.
The Problem constructor
We begin by building the closed, piecewise linear boundary of the undeformed solid body , The boundaries are labelled anticlockwise with boundary with the left-hand boundary being boundary 0, see the sketch above. This process is a simplified version of the construction used in another tutorial.
template<class ELEMENT>
Incompressible(false)
{
Vector<TriangleMeshCurveSection*> boundary_segment_pt(4);
Vector<Vector<double> > bound_seg(2);
for(unsigned i=0;i<2;i++) {bound_seg[i].resize(2);}
bound_seg[0][0]=0.0;
bound_seg[0][1]=0.0;
bound_seg[1][0]=0.0;
bound_seg[1][1]=5.0;
unsigned bound_id = 0;
boundary_segment_pt[0] = new TriangleMeshPolyLine(bound_seg,bound_id);
bound_seg[0][0]=0.0;
bound_seg[0][1]=5.0;
bound_seg[1][0]=1.0;
bound_seg[1][1]=5.0;
bound_id = 1;
boundary_segment_pt[1] = new TriangleMeshPolyLine(bound_seg,bound_id);
bound_seg[0][0]=1.0;
bound_seg[0][1]=5.0;
bound_seg[1][0]=1.0;
bound_seg[1][1]=0.0;
bound_id = 2;
boundary_segment_pt[2] = new TriangleMeshPolyLine(bound_seg,bound_id);
bound_seg[0][0]=1.0;
bound_seg[0][1]=0.0;
bound_seg[1][0]=0.0;
bound_seg[1][1]=0.0;
bound_id = 3;
boundary_segment_pt[3] = new TriangleMeshPolyLine(bound_seg,bound_id);
Outer_boundary_polyline_pt = new TriangleMeshPolygon(boundary_segment_pt);
double uniform_element_area=0.2;
TriangleMeshClosedCurve* closed_curve_pt=Outer_boundary_polyline_pt;
TriangleMeshParameters triangle_mesh_parameters(
closed_curve_pt);
triangle_mesh_parameters.element_area() =
uniform_element_area;
Solid_mesh_pt =
new RefineableSolidTriangleMesh<ELEMENT>(
triangle_mesh_parameters);
We next construct an error estimator and specify the target errors and element sizes.
Solid_mesh_pt->disable_iterative_solver_for_projection();
Z2ErrorEstimator* error_estimator_pt=new Z2ErrorEstimator;
Solid_mesh_pt->spatial_error_estimator_pt()=error_estimator_pt;
Solid_mesh_pt->max_permitted_error()=0.0001;
Solid_mesh_pt->min_permitted_error()=0.001;
Solid_mesh_pt->max_element_size()=0.2;
Solid_mesh_pt->min_element_size()=0.001;
We output the boundaries, construct an empty traction mesh and combine the bulk and traction meshes into a global mesh.
this->Solid_mesh_pt->output_boundaries("boundaries.dat");
Traction_mesh_pt=new SolidMesh;
add_sub_mesh(Solid_mesh_pt);
add_sub_mesh(Traction_mesh_pt);
build_global_mesh();
Finally we call actions_after_adapt()
, which constructs the traction elements, sets the boundary conditions and completes the build of the elements, and then we assign the equation numbers
this->actions_after_adapt();
cout <<"Number of equations: " << assign_eqn_numbers() << std::endl;
}
Actions before adaptation
The actions_before_adapt()
function simply deletes the traction elements and clears the storage in the face mesh.
template<class ELEMENT>
{
unsigned n_element = Traction_mesh_pt->nelement();
for(unsigned e=0;e<n_element;e++) {delete Traction_mesh_pt->element_pt(e);}
Traction_mesh_pt->flush_element_and_node_storage();
}
Actions before adaptation
The function actions_after_adapt()
first builds the traction elements adjacent to the left-hand boundary (boundary 0) and rebuilds the global mesh. The constant_pressure()
load function is passed to each of the traction elements.
template<class ELEMENT>
{
unsigned b=0;
unsigned n_element = Solid_mesh_pt->nboundary_element(b);
for(unsigned e=0;e<n_element;e++)
{
ELEMENT* bulk_elem_pt = dynamic_cast<ELEMENT*>(
Solid_mesh_pt->boundary_element_pt(b,e));
int face_index = Solid_mesh_pt->face_index_at_boundary(b,e);
SolidTractionElement<ELEMENT> *el_pt =
new SolidTractionElement<ELEMENT>(bulk_elem_pt,face_index);
Traction_mesh_pt->add_element_pt(el_pt);
}
this->rebuild_global_mesh();
Next, the boundary conditions of a fixed base (boundary 3) are set. These must be reset every time after an adaptation because completely new nodes are generated.
unsigned ibound=3;
unsigned num_nod= mesh_pt()->nboundary_node(ibound);
for (unsigned inod=0;inod<num_nod;inod++)
{
SolidNode* nod_pt=Solid_mesh_pt->boundary_node_pt(ibound,inod);
for (unsigned i=0;i<2;i++) {nod_pt->pin_position(i);}
}
Finally, the constitutive law and, if required, incompressibility flag are passed to the bulk (solid) elements. Again, this must be performed after every adaptation because a completely new mesh is generated.
n_element = Solid_mesh_pt->nelement();
for(unsigned e=0;e<n_element;e++)
{
ELEMENT *el_pt = dynamic_cast<ELEMENT*>(Solid_mesh_pt->element_pt(e));
el_pt->constitutive_law_pt() =
if(Incompressible)
{
dynamic_cast<TPVDElementWithContinuousPressure<2>*>(el_pt)
->set_incompressible();
}
}
}
Computation of the strain energy
The strain energy is computed by looping over all elements in the bulk mesh and adding their contributions to the potential (strain) energy.
template<class ELEMENT>
{
double strain_energy=0.0;
const unsigned n_element = Solid_mesh_pt->nelement();
for(unsigned e=0;e<n_element;e++)
{
ELEMENT *el_pt = dynamic_cast<ELEMENT*>(Solid_mesh_pt->element_pt(e));
double pot_en, kin_en;
el_pt->get_energy(pot_en,kin_en);
strain_energy += pot_en;
}
return strain_energy;
}
Post-processing
The post-processing routine outputs the deformed domain shape and the applied traction. In the spirit of continuing paranoia we also document the domain boundaries. It is exactly the same as in the related non-adaptive unstructured solid tutorial.
Comments and Exercises
Exercises
- Examine the changes in strain energy under variations in mesh refinement tolerances and number of intermediate steps between the undeformed and maximally deformed states.
- What happens if the Lagrangian coordinates are reset after every adaptation? Why?
- Modify the problem so that compression is from the upper surface, rather than the left-hand side. What happens when the material is incompressible?
Source files for this tutorial
PDF file
A pdf version of this document is available.