The aim of this tutorial is to demonstrate the solution of the axisymmetric equations of linear elasticity in cylindrical polar coordinates.
Acknowledgement:
This implementation of the equations and the documentation were developed jointly with Matthew Russell with financial support to Chris Bertram from the Chiari and Syringomyelia Foundation. |
Theory
Consider a three-dimensional, axisymmetric body (of density , Young's modulus , and Poisson's ratio ), occupying the region whose boundary is . Using cylindrical coordinates , the equations of linear elasticity can be written as
where , is the stress tensor, is the body force and is the displacement field.
Note that, despite the fact that none of the above physical quantities depend on the azimuthal angle , each can have a non-zero component. Also note that variables written with a superscript asterisk are dimensional, and their non-dimensional counterparts will be written without an asterisk. (The coordinate is, by definition, non-dimensional, so it will always be written without an asterisk.)
A boundary traction and boundary displacement are imposed along the boundaries and respectively, where so that
where is the outer unit normal vector.
The constitutive equations relating the stresses to the displacements are
where is the identity tensor and superscript denotes the transpose. In cylindrical coordinates, the matrix representation of the tensor is
and is equal to the trace of this matrix.
We non-dimensionalise the equations, using a problem specific reference length, , and a timescale , and use Young's modulus to non-dimensionalise the body force and the stress tensor:
The non-dimensional form of the axisymmetric linear elasticity equations is then given by
where ,
and the non-dimensional parameter
is the ratio of the elastic body's intrinsic timescale, , to the problem-specific timescale, , that we use for time-dependent problems. The boundary conditions are
We must also specify initial conditions:
Implementation
Within oomph-lib
, the non-dimensional version of the axisymmetric linear elasticity equations (1) combined with the constitutive equations (2) are implemented in the AxisymmetricLinearElasticityEquations
class. This class implements the equations in a way which is general with respect to the specific element geometry. To obtain a fully functioning element class, we must combine the equations class with a specific geometric element class, as discussed in the (Not-So-)Quick Guide. For example, we will combine the AxisymmetricLinearElasticityEquations
class with QElement<2,3>
elements, which are 9-node quadrilateral elements, in our example problem. As usual, the mapping between local and global (Eulerian) coordinates within an element is given by
where ; is the number of nodes in the element. is the -th global (Eulerian) coordinate of the -th node in the element and the are the element's shape functions, which are specific to each type of geometric element.
We store the three components of the displacement vector as nodal data in the order and use the shape functions to interpolate the displacements as
where is the -th displacement component at the -th node in the element, i.e., .
The solution of time dependent problems requires the specification of a TimeStepper
that is capable of approximating second time derivatives. In the example problem below we use the Newmark timestepper.
The test problem
As a test problem we consider forced oscillations of the circular cylinder shown in the sketch below:
Azimuthal cross-section of the geometry.
It is difficult to find non-trivial exact solutions of the governing equations (1), (2), so we manufacture a time-harmonic solution:
This is an exact solution if we set the body force to
where and are the nondimensional Lamé parameters. We impose the displacement along the boundaries according to (4), and impose the traction
along the boundary .
The problem we are solving then consists of equations (1), (2) along with the body force, (5) and boundary traction (6). The initial conditions for the problem are the exact displacement, velocity (and acceleration; see below) according to the solution (4).
Results
The animation below shows the time dependent deformation of the cylinder in the - plane, while the colour contours indicate the azimuthal displacement component. The animation is for , since the time scale is nondimensionalised on the reciprocal of the angular frequency of the oscillations.
Animation (HTML only) of the resulting displacement field.
The next three figures show plots of the radial, axial and azimuthal displacements as functions of at . Note the excellent agreement between the numerical and exact solutions.
Comparison between exact and FE solutions for the r-component of displacement at t = 2.64
Comparison between exact and FE solutions for the z-component of displacement at t = 2.64
Comparison between exact and FE solutions for the theta-component of displacement at t = 2.64
Global parameters and functions
As usual, we define all non-dimensional parameters in a namespace. In this namespace, we also define the body force, the traction to be applied on the boundary , and the exact solution. Note that, consistent with the enumeration of the unknowns, discussed above, the order of the components in the functions that specify the body force and the surface traction is .
{
double Mu =
E/2.0/(1.0+
Nu);
const Vector<double> &x,
const Vector<double> &n,
Vector<double> &result)
{
result[0] = cos(time)*(-6.0*pow(x[0],2)*
Mu*cos(x[1])-
Lambda*(4.0*pow(x[0],2)+pow(x[0],3))*cos(x[1]));
result[1] = cos(time)*(-
Mu*(3.0*pow(x[0],2)-pow(x[0],3))*sin(x[1]));
result[2] = cos(time)*(-
Mu*pow(x[0],2)*(2*pow(x[1],3)));
}
const Vector<double> &x,
Vector<double> &result)
{
result[0] = cos(time)*(
x[0]*(-cos(x[1])*
Mu*(-16.0+x[0]*(x[0]-3.0))+pow(x[0],2)*
Omega_sq)));
result[1] = cos(time)*(
x[0]*sin(x[1])*(
Mu*(-9.0)+
result[2] = cos(time)*(
-x[0]*(8.0*
Mu*pow(x[1],3)+pow(x[0],2)*(pow(x[1],3)*
Omega_sq+6.0*
Mu*x[1])));
}
Namespace for global parameters.
double Rmax
Set up max r coordinate.
double Zmin
Set up min z coordinate.
unsigned Nz
Number of elements in z-direction.
double Nu
Define Poisson's ratio Nu.
double Lz
Length of domain in z-direction.
double Zmax
Set up max z coordinate.
double Lr
Length of domain in r direction.
void boundary_traction(const double &time, const Vector< double > &x, const Vector< double > &n, Vector< double > &result)
The traction function at r=Rmin: (t_r, t_z, t_theta)
void body_force(const double &time, const Vector< double > &x, Vector< double > &result)
The body force function; returns vector of doubles in the order (b_r, b_z, b_theta)
double E
Define the non-dimensional Young's modulus.
double Rmin
Set up min r coordinate.
double Lambda
Lame parameters.
unsigned Nr
Number of elements in r-direction.
double Omega_sq
Square of the frequency of the time dependence.
In addition, the namespace includes the necessary machinery for providing the time dependent equations with their initial data from the exact solution. There are 9 functions, one for each of the components of displacement, velocity and acceleration, and a helper function. For brevity, we list only one of these functions; the others are similar.
double u_r(
const double &time,
const Vector<double> &x)
{
Vector<double> displ(3);
return cos(time)*displ[0];
}
void exact_solution_th(const Vector< double > &x, Vector< double > &u)
Helper function - spatial components of the exact solution in a vector. This is necessary because we ...
double u_r(const double &time, const Vector< double > &x)
Calculate the time dependent form of the r-component of displacement.
The driver code
We start by creating a DocInfo
object that will be used to output the solution, and then build the problem.
int main(
int argc,
char* argv[])
{
CommandLineArgs::setup(argc,argv);
CommandLineArgs::specify_command_line_flag("--validation");
CommandLineArgs::parse_and_assign();
CommandLineArgs::doc_specified_flags();
DocInfo doc_info;
doc_info.set_directory("RESLT");
<QAxisymmetricLinearElasticityElement<3>, Newmark<1> > problem;
Class to validate time harmonic linear elasticity (Fourier decomposed)
int main(int argc, char *argv[])
Driver code.
Next we set up a timestepper and assign the initial conditions.
problem.time_pt()->time()=0.0;
double dt=0;
if(CommandLineArgs::command_line_flag_has_been_set("--validation"))
{
dt=0.1;
}
else
{
dt=0.01;
}
problem.time_pt()->initialise_dt(dt);
problem.set_initial_conditions();
problem.doc_solution(doc_info);
doc_info.number()++;
We calculate the number of timesteps to perform - if we are validating, just do small number of timesteps; otherwise do a full period the time-harmonic oscillation.
unsigned nstep=0;
if(CommandLineArgs::command_line_flag_has_been_set("--validation"))
{
nstep=5;
}
else
{
double t_max=2*MathematicalConstants::Pi;
nstep=unsigned(t_max/dt);
}
Finally we perform a time dependent simulation.
for(unsigned istep=0;istep<nstep;istep++)
{
problem.unsteady_newton_solve(dt);
problem.doc_solution(doc_info);
doc_info.number()++;
}
}
The problem class
The problem class is very simple, and similarly to other problems with Neumann conditions, there are separate meshes for the "bulk" elements and the "face" elements that apply the traction boundary conditions. The function assign_traction_elements()
attaches the traction elements to the appropriate bulk elements.
template<class ELEMENT, class TIMESTEPPER>
{
public:
{
}
private:
};
void actions_before_newton_solve()
Update before solve is empty.
void doc_solution(DocInfo &doc_info)
Doc the solution.
Mesh * Bulk_mesh_pt
Pointer to the bulk mesh.
Mesh * Surface_mesh_pt
Pointer to the mesh of traction elements.
void set_initial_conditions()
Set the initial conditions, either for an impulsive start or with history values for the time stepper...
void actions_before_implicit_timestep()
Actions before implicit timestep.
AxisymmetricLinearElasticityProblem()
Constructor: Pass number of elements in r and z directions, boundary locations and whether we are doi...
void assign_traction_elements()
Allocate traction elements on the bottom surface.
void set_boundary_conditions()
Set the boundary conditions.
void actions_after_newton_solve()
Update after solve is empty.
The problem constructor
The problem constructor creates the mesh objects (which in turn create the elements), pins the appropriate boundary nodes and assigns the boundary conditions according to the functions defined in the Global_Parameters
namespace.
template<class ELEMENT, class TIMESTEPPER>
{
add_time_stepper_pt(new TIMESTEPPER());
Bulk_mesh_pt = new RectangularQuadMesh<ELEMENT>(
time_stepper_pt());
Surface_mesh_pt=new Mesh;
assign_traction_elements();
set_boundary_conditions();
Then the physical parameters are set for each element in the bulk mesh.
unsigned n_el = Bulk_mesh_pt->nelement();
for(unsigned e=0;e<n_el;e++)
{
ELEMENT *el_pt = dynamic_cast<ELEMENT*>(Bulk_mesh_pt->element_pt(e));
}
We then loop over the traction elements and set the applied traction.
unsigned n_traction = Surface_mesh_pt->nelement();
for(unsigned e=0;e<n_traction;e++)
{
AxisymmetricLinearElasticityTractionElement<ELEMENT>*
el_pt =
dynamic_cast<AxisymmetricLinearElasticityTractionElement
<ELEMENT>* >(Surface_mesh_pt->element_pt(e));
}
Finally, we add the two meshes as sub-meshes, build a global mesh from these and assign the equation numbers.
add_sub_mesh(Bulk_mesh_pt);
add_sub_mesh(Surface_mesh_pt);
build_global_mesh();
cout << assign_eqn_numbers() << " equations assigned" << std::endl;
}
The traction elements
We create the face elements that apply the traction to the boundary .
template<class ELEMENT, class TIMESTEPPER>
{
unsigned bound, n_neigh;
bound=3;
n_neigh = Bulk_mesh_pt->nboundary_element(bound);
for(unsigned n=0;n<n_neigh;n++)
{
FiniteElement *traction_element_pt
= new AxisymmetricLinearElasticityTractionElement<ELEMENT>
(Bulk_mesh_pt->boundary_element_pt(bound,n),
Bulk_mesh_pt->face_index_at_boundary(bound,n));
Surface_mesh_pt->add_element_pt(traction_element_pt);
}
}
Initial data
The time integration in this problem is performed using the Newmark scheme which, in addition to the standard initial conditions (3), requires an initial value for the acceleration. Since we will be solving a test case in which the exact solution is known, we can use the exact solution to provide the complete set of initial data required. For the details of the Newmark scheme, see the tutorial on the linear wave equation.
If we're doing an impulsive start, set the displacement, velocity and acceleration to zero, and fill in the time history to be consistent with this.
template<class ELEMENT, class TIMESTEPPER>
{
TIMESTEPPER* timestepper_pt =
dynamic_cast<TIMESTEPPER*>(time_stepper_pt());
bool impulsive_start=false;
if(impulsive_start)
{
unsigned n_node = Bulk_mesh_pt->nnode();
for(unsigned inod=0;inod<n_node;inod++)
{
Node* nod_pt = Bulk_mesh_pt->node_pt(inod);
Vector<double> x(2);
x[0] = nod_pt->x(0);
x[1] = nod_pt->x(1);
nod_pt->set_value(0,0);
nod_pt->set_value(1,0);
nod_pt->set_value(2,0);
timestepper_pt->assign_initial_values_impulsive(nod_pt);
}
}
If we are not doing an impulsive start, we must provide the timestepper with time history values for the displacement, velocity and acceleration. Each component of the these vectors is represented by a function pointer, and in this case, the function pointers return values based on the exact solution.
else
{
Vector<typename TIMESTEPPER::NodeInitialConditionFctPt>
initial_value_fct(3);
Vector<typename TIMESTEPPER::NodeInitialConditionFctPt>
initial_veloc_fct(3);
Vector<typename TIMESTEPPER::NodeInitialConditionFctPt>
initial_accel_fct(3);
double d2_u_z_dt2(const double &time, const Vector< double > &x)
Calculate the time dependent form of the z-component of acceleration.
double d2_u_r_dt2(const double &time, const Vector< double > &x)
Calculate the time dependent form of the r-component of acceleration.
double d_u_theta_dt(const double &time, const Vector< double > &x)
Calculate the time dependent form of the theta-component of velocity.
double d2_u_theta_dt2(const double &time, const Vector< double > &x)
Calculate the time dependent form of the theta-component of acceleration.
double d_u_r_dt(const double &time, const Vector< double > &x)
Calculate the time dependent form of the r-component of velocity.
double d_u_z_dt(const double &time, const Vector< double > &x)
Calculate the time dependent form of the z-component of velocity.
double u_z(const double &time, const Vector< double > &x)
Calculate the time dependent form of the z-component of displacement.
double u_theta(const double &time, const Vector< double > &x)
Calculate the time dependent form of the theta-component of displacement.
Then we loop over all nodes in the bulk mesh and set the initial data values from the exact solution.
unsigned n_node = Bulk_mesh_pt->nnode();
for(unsigned inod=0;inod<n_node;inod++)
{
Node* nod_pt = Bulk_mesh_pt->node_pt(inod);
timestepper_pt->assign_initial_data_values(nod_pt,
initial_value_fct,
initial_veloc_fct,
initial_accel_fct);
}
Post-processing
This member function documents the computed solution to file and calculates the error between the computed solution and the exact solution.
template<class ELEMENT, class TIMESTEPPER>
{
ofstream some_file;
char filename[100];
unsigned npts=10;
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();
sprintf(filename,"%s/exact_soln%i.dat",doc_info.directory().c_str(),
doc_info.number());
some_file.open(filename);
Bulk_mesh_pt->output_fct(some_file,npts,time_pt()->time(),
some_file.close();
double error=0.0;
double norm=0.0;
sprintf(filename,"%s/error%i.dat",doc_info.directory().c_str(),
doc_info.number());
some_file.open(filename);
Bulk_mesh_pt->compute_error(some_file,
time_pt()->time(),
error,norm);
some_file.close();
cout << "\nNorm of error: " << sqrt(error) << std::endl;
cout << "Norm of solution: " << sqrt(norm) << std::endl << std::endl;
cout << std::endl;
}
void exact_solution(const double &time, const Vector< double > &x, Vector< double > &u)
The exact solution in a vector: 0: u_r, 1: u_z, 2: u_theta and their 1st and 2nd derivs.
Comments
- Given that we non-dimensionalised all stresses on Young's modulus it seems odd that we provide the option to specify a non-dimensional Young's modulus via the member function
AxisymmetricLinearElasticity::youngs_modulus_pt()
. The explanation for this is that this function specifies the ratio of the material's actual Young's modulus to the Young's modulus used in the non-dimensionalisation of the equations. The capability to specify such ratios is important in problems where the elastic body is made of multiple materials with different constitutive properties. If the body is made of a single, homogeneous material, the specification of the non-dimensional Young's modulus is not required – it defaults to 1.0.
Exercises
- Try setting the boolean flag
impulsive_start
to true
in the AxisymmetricLinearElasticityProblem::set_initial_conditions
function and compare the system's evolution to that obtained when a "smooth" start from the exact solution is performed.
- Omit the specification of the Young's modulus and verify that the default value gives the same solution.
- Confirm that the assignment of the history values for the Newmark timestepper in
AxisymmetricLinearElasticityProblem::set_initial_conditions
sets the correct initial values for the displacement, velocity and acceleration. (Hint: the relevant code is already contained in the driver code, but was omitted in the code listings shown above.)
Source files for this tutorial
PDF file
A pdf version of this document is available.