Following the numerous 2D problems discussed in earlier examples we now demonstrate that the solution of 3D problems is just as easy. For this purpose we discuss the adaptive solution of the 3D Poisson problem
We choose a source function and boundary conditions for which
is the exact solution. Here where
is the vector of the spatial coordinates, and the vectors
and
are constants. For large values of the constant
the solution varies rapidly across the plane through
whose normal is given by
.
Here are some plots of the exact and computed solutions for
,
, and
at various levels of mesh refinement. Note that the plot of the exact solution was produced by setting the nodal values to the exact solution, obtained by evaluating (3) at the nodal positions. The elements' basis functions were then used to interpolate between the nodal values. On the coarse meshes, the interpolation between the "exact" nodal values is clearly inadequate to resolve the rapid variation of the solution.
Plot of the solution
Global parameters and functions
Following our usual practice, we use a namespace, TanhSolnForPoisson
, to define the source function, the exact solution and various problem parameters.
{
void get_exact_u(
const Vector<double>& x, Vector<double>& u)
{
u[0] = tanh(Alpha*((x[0]-X_0)*N_x/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[1]-Y_0)*
N_y/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[2]-Z_0)*
N_z/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)));
}
{
u = tanh(Alpha*((x[0]-X_0)*N_x/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[1]-Y_0)*
N_y/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[2]-Z_0)*
N_z/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)));
}
void get_source(
const Vector<double>& x,
double& source)
{
double s1,s2,s3,s4;
s1 = -2.0*tanh(Alpha*((x[0]-X_0)*N_x/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[1]-
Y_0)*N_y/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[2]-Z_0)*N_z/sqrt(N_x*N_x+N_y*N_y+N_z*
N_z)))*(1.0-pow(tanh(Alpha*((x[0]-X_0)*N_x/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[1]-
Y_0)*N_y/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[2]-Z_0)*N_z/sqrt(N_x*N_x+N_y*N_y+N_z*
N_z))),2.0))*Alpha*Alpha*N_x*N_x/(N_x*N_x+N_y*N_y+N_z*N_z);
s3 = -2.0*tanh(Alpha*((x[0]-X_0)*N_x/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[1]-
Y_0)*N_y/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[2]-Z_0)*N_z/sqrt(N_x*N_x+N_y*N_y+N_z*
N_z)))*(1.0-pow(tanh(Alpha*((x[0]-X_0)*N_x/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[1]-
Y_0)*N_y/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[2]-Z_0)*N_z/sqrt(N_x*N_x+N_y*N_y+N_z*
N_z))),2.0))*Alpha*Alpha*N_y*N_y/(N_x*N_x+N_y*N_y+N_z*N_z);
s4 = -2.0*tanh(Alpha*((x[0]-X_0)*N_x/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[1]-
Y_0)*N_y/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[2]-Z_0)*N_z/sqrt(N_x*N_x+N_y*N_y+N_z*
N_z)))*(1.0-pow(tanh(Alpha*((x[0]-X_0)*N_x/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[1]-
Y_0)*N_y/sqrt(N_x*N_x+N_y*N_y+N_z*N_z)+(x[2]-Z_0)*N_z/sqrt(N_x*N_x+N_y*N_y+N_z*
N_z))),2.0))*Alpha*Alpha*N_z*N_z/(N_x*N_x+N_y*N_y+N_z*N_z);
s2 = s3+s4;
source = s1+s2;
}
}
The driver code
The driver code solves the 3D Poisson problem with full spatial adaptivity – a fairly time-consuming process. To minimise the run-times when the code is executed during oomph-lib's
self-tests, we use command line arguments to optionally limit the number of adaptive refinements. If the code is run with a(ny) command line arguments, only a single adaptive refinement is performed; otherwise up to four levels of refinement are permitted. oomph-lib
provides storage for the command line arguments in the namespace CommandLineArgs
to make them accessible to other parts of the code.
Otherwise the driver code is very similar to that used in the corresponding 2D Poisson problems: We construct the problem, passing the pointer to the source function. Next, we create a DocInfo
object to specify the output directory, and execute the global self-test to assert that the problem has been set up correctly. Next we solve the problem on the coarse initial mesh (comprising four 27-node brick elements) and then adapt the problem based on the elemental error estimates, until the maximum number of adaptations has been reached or until the adaptation ceases to changes the mesh.
int main(
int argc,
char *argv[])
{
CommandLineArgs::setup(argc,argv);
DocInfo doc_info;
doc_info.set_directory("RESLT");
doc_info.number()=0;
cout << "Self test: " << problem.self_test() << std::endl;
problem.newton_solve();
problem.doc_solution(doc_info);
doc_info.number()++;
unsigned max_solve;
if (CommandLineArgs::Argc>1)
{
max_solve=1;
cout << "Only doing one adaptation for validation" << std::endl;
}
else
{
max_solve=4;
}
for (unsigned isolve=0;isolve<max_solve;isolve++)
{
problem.adapt();
if ((problem.mesh_pt()->nrefined() !=0)||
(problem.mesh_pt()->nunrefined()!=0))
{
problem.newton_solve();
}
else
{
cout << "Mesh wasn't adapted --> we'll stop here" << std::endl;
break;
}
problem.doc_solution(doc_info);
doc_info.number()++;
}
}
The problem class
The problem class has the usual structure – the only difference to the corresponding 2D codes is that the assignment of the boundary conditions in actions_before_newton_solve()
now involves thee nodal coordinates rather than two.
template<class ELEMENT>
{
public:
PoissonEquations<3>::PoissonSourceFctPt source_fct_pt);
RefineableEighthSphereMesh<ELEMENT>*
mesh_pt()
{
return dynamic_cast<RefineableEighthSphereMesh<ELEMENT>*>(Problem::mesh_pt());
}
{
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);
double u;
Vector<double> x(3);
x[0]=nod_pt->x(0);
x[1]=nod_pt->x(1);
x[2]=nod_pt->x(2);
nod_pt->set_value(0,u);
}
}
}
private:
};
[See the discussion of the 1D Poisson problem for a more detailed discussion of the function type PoissonEquations<3>::PoissonSourceFctPt.]
The Problem constructor
In the Problem
constructor, we set the "steepness parameter"
to a large value and create the mesh for a a sphere of radius 5. Next, we create the error estimator and pass it to the adaptive mesh.
template<class ELEMENT>
PoissonEquations<3>::PoissonSourceFctPt source_fct_pt) :
Source_fct_pt(source_fct_pt)
{
double radius=5.0;
Problem::mesh_pt() = new RefineableEighthSphereMesh<ELEMENT>(radius);
Z2ErrorEstimator* error_estimator_pt=new Z2ErrorEstimator;
mesh_pt()->spatial_error_estimator_pt()=error_estimator_pt;
We adjust the targets for the mesh adaptation so that the single mesh adaptation performed during a validation run produces a non-uniform refinement pattern. (The error targets for this case were determined by trial and error.) The tighter error tolerances specified otherwise are appropriate to properly resolve the solution, as shown in the animated gif files at the beginning of this document.
if (CommandLineArgs::Argc>1)
{
mesh_pt()->max_permitted_error()=0.7;
mesh_pt()->min_permitted_error()=0.5;
}
else
{
mesh_pt()->max_permitted_error()=0.01;
mesh_pt()->min_permitted_error()=0.001;
}
Next, we assign the boundary conditions. In the present problem all boundaries are Dirichlet boundaries, therefore we loop over all nodes on all boundaries and pin their values. If only a subset of the mesh boundaries were of Dirichlet type, only the nodes on those boundaries would have to be pinned. "Usually" the numbering of the mesh boundaries is (or at least should be!) documented in the mesh constructor but it can also be obtained from the function Mesh::output_boundaries
(...) whose use is illustrated here.
ofstream some_file;
some_file.open("boundaries.dat");
mesh_pt()->output_boundaries(some_file);
some_file.close();
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++)
{
mesh_pt()->boundary_node_pt(ibound,inod)->pin(0);
}
}
Finally we loop over all elements to assign the source function pointer, and then call the generic Problem::assign_eqn_numbers()
routine to set up 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));
el_pt->source_fct_pt() = Source_fct_pt;
}
cout <<"Number of equations: " << assign_eqn_numbers() << std::endl;
}
Post-processing
The function doc_solution
(...) writes the FE solution and the corresponding exact solution, defined in TanhSolnForPoisson::get_exact_u
(...) to disk. The DocInfo
object specifies the output directory and the label for the file names. [See the discussion of the 1D Poisson problem for a more detailed discussion of the generic Mesh
member functions Mesh::output
(...), Mesh::output_fct
(...) and Mesh::compute_error
(...)].
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();
sprintf(filename,"%s/exact_soln%i.dat",doc_info.directory().c_str(),
doc_info.number());
some_file.open(filename);
some_file.close();
double error,norm;
sprintf(filename,"%s/error%i.dat",doc_info.directory().c_str(),
doc_info.number());
some_file.open(filename);
error,norm);
some_file.close();
cout << "error: " << sqrt(error) << std::endl;
cout << "norm : " << sqrt(norm) << std::endl << std::endl;
}
Source files for this tutorial
PDF file
A pdf version of this document is available.