Reference documentation for deal.II version Git ab1cc5b 2017-03-24 06:23:49 -0600
|Table of contents|
This tutorial is an extension to step-1 and demonstrates several ways to obtain more involved meshes than the ones shown there.
Generating complex geometries is a challenging task, especially in three space dimensions. We will discuss several ways to do this, but this list is not exhaustive. Additionally, there is not one approach that fits all problems.
This example program shows some of ways to create and modify meshes for computations and outputs them as
.eps files in much the same way as we do in step-1. No other computations or adaptive refinements are done; the idea is that you can use the techniques used here as building blocks in other, more involved simulators. Please note that the example program does not show all the ways to generate meshes that are discussed in this introduction.
When you use adaptive mesh refinement, you definitely want the initial mesh to be as coarse as possible. The reason is that you can make it as fine as you want using adaptive refinement as long as you have memory and CPU time available. However, this requires that you don't waste mesh cells in parts of the domain where they don't pay off. As a consequence, you don't want to start with a mesh that is too fine to start with, because that takes up a good part of your cell budget already, and because you can't coarsen away cells that are in the initial mesh.
That said, your mesh needs to capture the given geometry adequately.
There are several ways to create an initial mesh. Meshes can be modified or combined in many ways as discussed later on.
The easiest way to generate meshes is to use the functions in namespace GridGenerator, as already discussed in step-1. There are many different helper functions available, including GridGenerator::hyper_cube(), GridGenerator::hyper_shell(), GridGenerator::hyper_ball(), GridGenerator::hyper_cube_with_cylindrical_hole(), ...
If there is no good fit in the GridGenerator namespace for what you want to do, you can always create a Triangulation in your program "by hand". For that, you need a list of vertices with their coordinates and a list of cells referencing those vertices. You can find an example in the function create_coarse_grid in step-14. All the functions in GridGenerator are implemented in this fashion.
We are happy to accept more functions to be added to GridGenerator. So, if you end up writing a function that might be useful for a larger audience, please contribute it.
Meshes can be generated from different tools like gmsh, lagrit and cubit. See the documentation of GridIn for more information. The problem is that deal.II needs meshes that only consist of quads and hexas – tetrahedral meshes won't work (this means tools like tetgen can not be used directly).
We will describe a possible workflow using Gmsh. Gmsh is the smallest and most quickly set up open source tool we are aware of. It can generate unstructured 2d quad meshes, but in 3d it can only extrude 2d meshes to get hexahedral meshes. 3D meshing of unstructured geometry into hexahedra is not supported at the time of writing this tutorial (early 2013).
In gmsh, a mesh is described in a text based
.geo file, that can contain computations, loops, variables, etc. It is very flexible. The mesh is generated from a surface representation, which is build from a list of line loops, which is build from a list of lines, which are in turn built from points. The
.geo script can be written and edited by hand or it can be generated automatically by creating objects graphically inside gmsh. In many cases it is best to combine both approaches. The file can be easily reloaded by pressing "reload" under the "Geometry" tab.
This tutorial contains an example
.geo file, that describes a box with two objects cut out in the interior. This is how
untitled.geo looks like in gmsh (displaying the boundary indicators as well as the mesh discussed further down below):
You might want to open the
untitled.geo file in a text editor (it is located in the same directory as the
step-49.cc source file) to see how it is structured. You can see how the boundary of the domain is composed of a number of lines and how later on we combine several lines into "physical lines" (or "physical surfaces") that list the logical lines' numbers. "Physical" object are the ones that carry information about the boundary indicator (see this glossary entry).
deal.II's GridIn class can read the
.msh format written by gmsh and that contains a mesh created for the geometry described by the
.geo file. You generate the
.msh from the
.geo by running the commands
on the command line, or by clicking "Mesh" and then "2D" inside Gmsh after loading the file. Now this is the mesh read from the
.msh file and saved again by deal.II as an image (see the
grid_1 function of the current program):
After acquiring one (or several) meshes in the ways described above, there are many ways to manipulate them before using them in a finite element computation.
The GridTools namespace contains a collection of small functions to transform a given mesh in various ways. The usage of the functions GridTools::shift, GridTools::rotate, GridTools::scale is fairly obvious, so we won't discuss those functions here.
The function GridTools::transform allows you to transform the vertices of a given mesh using a smooth function. An example of its use is also given in the results section of step-38 but let us show a simpler example here: In the function
grid_5() of the current program, we perturb the y coordinate of a mesh with a sine curve:
Similarly, we can transform a regularly refined unit square to a wall-adapted mesh in y direction using the formula \((x,y) \mapsto (x,\tanh(2*y)/\tanh(2))\). This is done in
grid_6() of this tutorial:
Finally, the function GridTools::distort_random allows you to move vertices in the mesh (optionally ignoring boundary nodes) by a random amount. This is demonstrated in
grid_7() and the result is as follows:
This function is primarily intended to negate some of the superconvergence effects one gets when studying convergence on regular meshes, as well as to suppress some optimizations in deal.II that can exploit the fact that cells are similar in shape. In practice, it is of course always better to work with a sequence of unstructured meshes (see possible extensions at the end of the this section).
The function GridGenerator::merge_triangulations() allows you to merge two given Triangulation objects into a single one. For this to work, the vertices of the shared edge or face have to match exactly. Lining up the two meshes can be achieved using GridTools::shift and GridTools::scale. In the function
grid_2() of this tutorial, we merge a square with a round hole (generated with GridGenerator::hyper_cube_with_cylindrical_hole()) and a rectangle (generated with GridGenerator::subdivided_hyper_rectangle()). The function GridGenerator::subdivided_hyper_rectangle() allows you to specify the number of repetitions and the positions of the corners, so there is no need to shift the triangulation manually here. You should inspect the mesh graphically to make sure that cells line up correctly and no unpaired nodes exist in the merged Triangulation.
These are the input meshes and the output mesh:
grid_3() demonstrates the ability to pick individual vertices and move them around in an existing mesh. Note that this has the potential to produce degenerate or inverted cells and you shouldn't expect anything useful to come of using such meshes. Here, we create a box with a cylindrical hole that is not exactly centered by moving the top vertices upwards:
For the exact way how this is done, see the code below.
If you need a 3d mesh that can be created by extruding a given 2d mesh (that can be created in any of the ways given above), you can use the function GridGenerator::extrude_triangulation(). See the
grid_4() function in this tutorial for an example. Note that for this particular case, the given result could also be achieved using the 3d version of GridGenerator::hyper_cube_with_cylindrical_hole(). The main usage is a 2d mesh, generated for example with gmsh, that is read in from a
.msh file as described above. This is the output from grid_4():
Creating a coarse mesh using the methods discussed above is only the first step. When you have it, it will typically serve as the basis for further mesh refinement. This is not difficult — in fact, there is nothing else to do — if your geometry consists of only straight faces. However, this is often not the case if you have a more complex geometry and more steps than just creating the mesh are necessary. We will go over some of these steps in the results section below.
This tutorial program is odd in the sense that, unlike for most other steps, the introduction already provides most of the information on how to use the various strategies to generate meshes. Consequently, there is little that remains to be commented on here, and we intersperse the code with relatively little text. In essence, the code here simply provides a reference implementation of what has already been described in the introduction.
The following function generates some output for any of the meshes we will be generating in the remainder of this program. In particular, it generates the following information:
Finally, the function outputs the mesh in encapsulated postscript (EPS) format that can easily be visualized in the same way as was done in step-1.
Next loop over all faces of all cells and find how often each boundary indicator is used (recall that if you access an element of a std::map object that doesn't exist, it is implicitly created and default initialized – to zero, in the current case – before we then increment it):
Finally, produce a graphical representation of the mesh to an output file :
In this first example, we show how to load the mesh for which we have discussed in the introduction how to generate it. This follows the same pattern as used in step-5 to load a mesh, although there it was written in a different file format (UCD instead of MSH).
Here, we first create two triangulations and then merge them into one. As discussed in the introduction, it is important to ensure that the vertices at the common interface are located at the same coordinates.
In this function, we move vertices of a mesh. This is simpler than one usually expects: if you ask a cell using
cell->vertex(i) for the coordinates of its
ith vertex, it doesn't just provide the location of this vertex but in fact a reference to the location where these coordinates are stored. We can then modify the value stored there.
So this is what we do in the first part of this function: We create a square of geometry \([-1,1]^2\) with a circular hole with radius 0.25 located at the origin. We then loop over all cells and all vertices and if a vertex has a \(y\) coordinate equal to one, we move it upward by 0.5.
Note that this sort of procedure does not usually work this way because one will typically encounter the same vertices multiple times and may move them more than once. It works here because we select the vertices we want to use based on their geometric location, and a vertex moved once will fail this test in the future. A more general approach to this problem would have been to keep a std::set of of those vertex indices that we have already moved (which we can obtain using
cell->vertex_index(i) and only move those vertices whose index isn't in the set yet.
In the second step we will refine the mesh twice. To do this correctly, we have to associate a geometry object with the boundary of the hole; since the boundary of the hole has boundary indicator 1 (see the documentation of the function that generates the mesh), we need to create an object that describes a spherical manifold (i.e., a hyper ball) with appropriate center and assign it to the triangulation. Notice that the function that generates the triangulation sets the boundary indicators of the inner mesh, but leaves unchanged the manifold indicator. We copy the boundary indicator to the manifold indicators in order for the object to be refined accordingly. We can then refine twice:
The mesh so generated is then passed to the function that generates output. In a final step we remove the boundary object again so that it is no longer in use by the triangulation when it is destroyed (the boundary object is destroyed first in this function since it was declared after the triangulation).
There is one snag to doing things as shown above: If one moves the nodes on the boundary as shown here, one often ends up with cells in the interior that are badly distorted since the interior nodes were not moved around. This is not that much of a problem in the current case since the mesh did not contain any internal nodes when the nodes were moved – it was the coarse mesh and it so happened that all vertices are at the boundary. It's also the case that the movement we had here was, compared to the average cell size not overly dramatic. Nevertheless, sometimes one does want to move vertices by a significant distance, and in that case one needs to move internal nodes as well. One way to do that automatically is to call the function GridTools::laplace_transform that takes a set of transformed vertex coordinates and moves all of the other vertices in such a way that the resulting mesh has, in some sense, a small distortion.
This example takes the initial grid from the previous function and simply extrudes it into the third space dimension:
This and the next example first create a mesh and then transform it by moving every node of the mesh according to a function that takes a point and returns a mapped point. In this case, we transform \((x,y) \mapsto (x,y+\sin(\pi x/5))\).
GridTools::transform takes a triangulation and any kind of object that can be called like a function as arguments. This function-like argument can be simply the address of a function as in the current case, or an object that has an
operator() as in the next example, or for example a
std::function<Point<2>(const Point<2>)> object one can get via
std::bind in more complex cases.
In this second example of transforming points from an original to a new mesh, we will use the mapping \((x,y) \mapsto (x,\tanh(2y)/\tanh(2))\). To make things more interesting, rather than doing so in a single function as in the previous example, we here create an object with an
operator() that will be called by GridTools::transform. Of course, this object may in reality be much more complex: the object may have member variables that play a role in computing the new locations of vertices.
In this last example, we create a mesh and then distort its (interior) vertices by a random perturbation. This is not something you want to do for production computations, but it is a useful tool for testing discretizations and codes to make sure they don't work just by accident because the mesh happens to be uniformly structured and supporting super-convergence properties.
Finally, the main function. There isn't much to do here, only to call the subfunctions.
The program produces a series of
.eps files of the triangulations. The methods are discussed above.
As mentioned in the introduction, creating a coarse mesh using the methods discussed here is only the first step. In order to refine a mesh, the Triangulation needs to know where to put new vertices on the mid-points of edges and faces. By default, these new points will be placed at the centers of the old edge but this isn't what you want if you need curved boundaries that aren't already adequately resolved by the coarse mesh. Several of the meshes shown in the introduction section fall into this category. For example, for this mesh the central hole is supposed to be round:
On the other hand, if you simply refine it, the Triangulation class can not know whether you wanted the hole to be round or to be an octagon. The default is to place new points along existing edges. After two mesh refinement steps, this would yield the following mesh, which is not what we wanted:
What needs to happen is that you tell the triangulation that you in fact want to use a curved boundary. The way to do this requires three steps:
To illustrate this process in more detail, let us consider an example created by Yuhan Zhou as part of a 2013 semester project at Texas A&M University. The goal was to generate (and use) a geometry that describes a microstructured electric device. In a CAD program, the geometry looks like this:
In the following, we will walk you through the entire process of creating a mesh for this geometry, including a number of common pitfalls by showing the things that can go wrong.
The first step in getting there was to create a coarse mesh, which was done by creating a 2d coarse mesh for each of the two cross section, extruding them into the third direction, and gluing them together. The following code does this, using the techniques previously described:
With this code, you get a mesh that looks like this:
The next step is to teach each of the top surfaces that they should be curved. We can do this by creating CylinderBoundary objects that describe this. A first attempt looks like this:
With this code, we get a mesh that looks like this:
This is clearly not correct: The new vertices that have been entered at mid-edge and mid-face points are not where they should have been. Upon some reflection, it turns out that while the radii of the cylinders are correct, the axes of the two cylinder objects should not have been along coordinate axes but shifted. This can be corrected by creating them as follows, the two points given as arguments indicating the direction and a point on the axis:
This yields an improvement, though it is still not quite correct:
Looking closely at this mesh, we realize that the new points on mid-face vertices are where they should be, though the new vertices inserted at mid-edge points are in the wrong place (you see this by comparing the picture with the one of the coarse mesh). What is happening is that we are only telling the triangulation to use these geometry objects for the faces but not for the adjacent edges as well. This is easily fixed by using the function TriaAccessor::set_all_boundary_ids() instead of TriaAccessor::set_boundary_id() used above. With this change, the grid now looks like this:
This is already better. However, something is still going wrong on the front left face. On second look, we can also see that the faces where the geometry widens have been refined at the bottom, that there is one transition face that looks wrong because it has a triangle rather than a quadrilateral, and that finally the transition faces in the cylindrical region appear not to have been refined at all in radial direction.
This due to the fact that we have (erroneously) marked all boundary faces between \(0\le z\le 2.5\) with the boundary indicator for the small cylinder and similarly for the other regions. This condition includes the faces parallel to the x-y plane. To fix it, we need to exclude faces whose center points have \(z\) values equal to (or at least close to, since we should not compare for equality in floating point arithmetic) 0, 2.5, 5 or 7.5. This replacement code does the trick:
With this, we finally get a mesh that looks good:
We can then refine the mesh two more times to see in more detail what happens to the curved part of the boundary: