Reference documentation for deal.II version 9.4.1
|
This tutorial depends on step-1.
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 .vtu
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(), and 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.
The class GridIn can read many different mesh formats from a file from disk. How this is done is explained in step-5 and can be seen in the function grid_1
in this example, see the code below.
Meshes can be generated from different tools like gmsh, lagrit and cubit. See the documentation of GridIn for more information.
One of the issues you will encounter is that deal.II, at least until version 9.2, can only deal with meshes that only consist of quadrilaterals and hexahedra – tetrahedral meshes were not supported and will likely not be supported with all of the features deal.II offers for quadrilateral and hexahedral meshes for several versions following the 9.3 release that introduced support for simplicial and mixed meshes first. As a consequence, let us not show how to work with the tetgen mesh generator that is widely used but can only generate tetrahedral meshes, but instead illustrate how one can work with Gmsh instead.
Gmsh is the smallest and most quickly set up open source tool we are aware of. It can generate unstructured 2d quad meshes. In 3d, it can extrude 2d meshes to get hexahedral meshes; 3D meshing of unstructured geometry into hexahedra is possible, though there are some issues with the quality of these meshes that imply that these meshes only sometimes work in deal.II.
Gmsh has a graphical user interface, but what it does is in essence only to generate a text file that then drives the actual mesh generation. The graphical user interface does allow querying information about the geometry and mesh, however, and so is useful. But it is useful to understand the text format regardless because it allows automating the mesh generation workflow. These text files, with a .geo
suffix, can contain computations, loops, variables, etc. This format is quite flexible in allowing the description of complex geometries. The mesh is then generated from a surface representation, which is built from a list of line loops, which is built 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 if you want to write it by hand and see the effects in the graphical user interface of gmsh.
This tutorial contains an example .geo
file that describes a box with two circles cut out in the interior and several slits on the sides. For the example.geo
file that you can find in the examples/step-49
directory, Gmsh will show the following view (displaying the boundary indicators as well as the mesh are discussed further down below):
We will go through the basics of Gmsh and show how we can obtain a mesh like this.
It's also worth mentioning this video (which can be found here https://www.youtube.com/watch?v=xL2LmDsDLYw&t=921s) which does a good job of explaining how to use gmsh to create basic shapes and meshes. Let us first go through the documented example.geo
file that describes the geometry.
We will now go through the main parts of example.geo
file that defines the mesh.
In this section we create the points that make up the domain.
The points can either be typed in the .geo
file manually or found via Geometry -> Elementary Entities -> Add -> Point
where you simply enter the coordinates in the prompt or use the mouse to place them. The latter approach yields a list of the following kind:
It is relevant to note that all points in Gmsh are three-dimensional objects. Since we here want to generate a two-dimensional mesh, the points simply have a zero \(z\) coordinate. The fourth number in the curly braces for each point (equal to 1.0 for all of the points above) indicates the desired mesh size in the vicinity of this point. Gmsh's graphical user interfaces writes this into the .geo
file automatically, but it can be omitted and one would probably do that if one were to write this file by hand.
The file contains many more points than just these six. If you look into the file, you will also realize that one does not have to enumerate points consecutively: One can number them in whichever way one wants, which is often useful when constructing complex geometries. In such cases, one might for example want to number all points for one particular part of the geometry starting at zero, and the points for another part at, say, 1000. It does not matter whether all numbers between zero and 1000 are used.
To create lines of the mesh, go to Geometry -> Elementary entities -> Add -> Line
. You do not get a prompt to enter in specific coordinates, rather you simply click a starting point and ending point for each line.
The generated code in the geo file then looks like this:
What appears on the right are pairs of point indices that define a line. As before, the indices of the lines (here 4 to 8) need not be consecutive.
Here are the points and lines that create the two holes in the domain. We start by defining the relevant points for bottom left hole:
Then we use the Circle arc
entity, which is found in the same category as Point
and Lines
. The file reads "Circle" but the difference to note is that this function uses three points to make an arc of a circle, namely the starting point, center, and ending point. The angle of an arc is required to be less than 180 degrees. We therefore split the circle into four arcs, which also allows us to set different boundary IDs. This then looks as follows where point 31 is the common center for all arcs:
We follow the same procedure for the second circle.
This section describes the "Plane Surfaces", i.e., the 2D surfaces for meshing. This can be found in Elementary entities
as Plane Surface
. The purpose of this entity is to specify the domain in 2D. Because we have our mesh split into 4 sections, we must have 4 plane surfaces.
To make a plane surface, simply click Plane surface
and click on all the relevant borders for the mesh. This includes holes or other objects contained in the mesh. Again, since we have 4 different meshes, we will do this 4 times.
The code below is for the top right mesh. Notice we have 2 instances of Curve Loop
, which are constructed from lists of the line segments we have previously built from pairs of two points. The first of the curve loops is the outer boundary of the part of the domain we are describing, whereas the second one is for the hole. This can easily be traced back by following along with the numbers in the braces.
In the description of the first curve loop above, the second line segment (segment 43) is described with a negative sign. This is because we have not paid enough attention when creating the line segment and have specified its two vertices in the wrong order:
The negative sign then ensures that it is considered in the correct orientation, with an order for vertices of 3 -> 36 -> 7 -> 14
.
The other four surfaces are declared in a similar way.
Next we define the physical surface itself. This line is what makes our mesh 2D. The values in the braces on the right hand side are the tags for the Plane Surface
s we declared above. The number in the parentheses of physical surfaces will become the material ID of the mesh as well. For more information about material IDs, check the glossary entry material indicators .
The steps above would be enough to define a two dimensional geometry that can be meshed. In practice, however, one often needs to attach additional information to parts of the geometry. The prototypical example are boundary indicators and material indicators .
To do this, we need to tell Gmsh about all of the "physical entities" and assign them with indices. For example, the physical surface above has been assigned index 2, and so this index "2" will be output as a tag for the cells that make up the surface (i.e., all cells of this mesh) and deal.II will then interpret tags on cells as material ids.
The indices in the list on the right correspond to line segments, with a colon "`:`" denoting a range.
We then assign boundary ID of 1 to the top right circle, where the index 12 corresponds to the circle (a different kind of line segment):
Finally, we assign boundary IDs of 2 and 3 to the top and bottom half of the bottom left hole, respectively. We can see here that we needed to pick two boundaries to make both of the physical curves. Recall this circle is comprised of four circle arcs, so Physical Curve(2) is the top left and top right quarter of the circle, and Physical Curve(3) is the bottom left and bottom right quarter of the circle.
.geo
file contains some "physical lines" and "physical
surfaces", not just for the purposes of material and boundary ids: Without physical lines and surfaces (and volumes in 3d), there is only a collection of geometric objects, but no "domain" that Gmsh can actually define a mesh on.Finally, the .geo
file contains meshing parameters that can all be adjusted in the GMSH GUI. We include these parameters at the bottom of our .geo
file to keep the changes when creating our mesh (with suffix .msh
) file.
To view what each of these do, press Tools -> Options -> Mesh
.
You may change the algorithm used for meshing in 2D algorithm
.
CharacteristicLengthFactor
can be thought of the distance between nodes on the boundaries of the mesh. This sets the initial size of the nodes of the mesh. If we want a finer mesh we want a smaller characteristic length. For this examples purposes we would like to do more refinement in deal.II so we pick a relatively coarse mesh. This can also be adjusted in the Options menu.
SubdivisionAlgorithm
is set to 1 in our file because we want to use "All Quads" for subdivision. In the options menu the default is "None", but we use "All Quads" for the reasons mentioned earlier.
Smoothing
is a post-processing step that iteratively improves mesh quality (for example by moving inner vertices). The number of steps is not crucial in this example. For our example we pick 20, however this value can be adjusted accordingly as it's not the most important step in mesh building.
example.geo
. It's important to note that the .geo
file did not come out as organized as it appears here. It was edited after creating the mesh and geometry to be more readable.Before loading this into deal.II, we need to create the .msh
file. If everything was set up correctly, we can press Mesh -> 2D
. If all this goes well, then we should see something similar to the following output:
It behooves us at this point to also make sure our boundaries were declared correctly. So in the Options menu go to Mesh -> Visibility
. In the dropdown menu labeled "Label type" we may choose a label for our boundaries. We wish to check physical IDs, so we would like to have "Physical tag" picked. Now we check "1D element labels" and we should see the numbers assigned to the specified boundaries.
Finally, we can choose Mesh -> Save
to save the .msh
file, which deal.II's GridIn class can read. You can also generate the .msh
from the .geo
file by running the command
.geo
file is probably the simplest approach), but instead wants to do parametric studies over the geometry for which it is necessary to generate many meshes for geometries that differ in certain parameters. Another case where this is useful is if there is already a CAD geometry for which one only needs a mesh; indeed, this can be done from within deal.II using the Gmsh::create_triangulation_from_boundary_curve() function.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:
regular input mesh | output mesh |
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:
regular input mesh | wall-adapted output mesh |
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:
regular input mesh | perturbed output mesh |
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. (Superconvergence refers to the fact that if a mesh has certain symmetries – for example, if the edges running into a vertex are symmetric to this vertex, and if this is so for all vertices of a cell – that the solution is then often convergent with a higher order than one would have expected from the usual error analysis. In the end, this is a result of the fact that if one were to make a Taylor expansion of the error, the symmetry leads to the fact that the expected next term of the expansion happens to be zero, and the error order is determined by the second next* term. A distorted mesh does not have these symmetries and consequently the error reflects what one will see when solving the equation on any kind of mesh, rather than showing something that is only reflective of a particular situation.)
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:
input mesh 1 | input mesh 2 | merged mesh |
The function 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:
input mesh | top vertices moved 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():
input mesh | extruded output mesh |
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 VTU format that can easily be visualized in Paraview or VisIt.
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).
It's worth noting that it is possible to save manifold ids when using the gmsh api. If we specify
when building deal.II, then DEAL_II_GMSH_WITH_API
gets defined and and we can use GridIn::read_msh()
. More details on the function can be found in its deal.II documentation.
We will be utilizing the SphericalManifold class for the holes. We need to assign manifold IDs for this purpose. As physical IDs from Gmsh are assigned to boundary IDs in deal.II, we will assign manifold IDs based on the boundary IDs loaded from the file.
Here is where we get the boundary IDs made in gmsh, which are in the first coordinate position, and assign them to manifold ids. With our example, we have boundary ID 1 on the top right hole and 2 and 3 for the bottom left hole. We assign both of these boundary IDs 2 because together they make a circle to match the manifold we assign it later.
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 i
th 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 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 should place new points on the interior boundary along the surface of a circle centered at the origin. Fortunately, GridGenerator::hyper_cube_with_cylindrical_hole already attaches a Manifold object to the interior boundary, so we do not need to do anything but refine the mesh (see the results section for a fully worked example where we do attach a Manifold object).
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 an argument that can be called like a function taking a Point and returning a Point. There are different ways of providing such an argument: It could be a pointer to a function; it could be an object of a class that has an operator()
; it could be a lambda function; or it could be anything that is described via a std::function<Point<2>(const Point<2>)>
object.
Decidedly the more modern way is to use a lambda function that takes a Point and returns a Point, and that is what we do in the following:
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 (because results are generally better on meshes with "nicely shaped" cells than on the deformed cells produced by GridTools::distort_random()), 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 superconvergence properties.
Finally, the main function. There isn't much to do here, only to call all the various functions we wrote above.
The program produces a series of .vtu
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, faces, and cells. By default, these new points will be placed at the arithmetic mean of the surrounding points, but this isn't what you want if you need curved boundaries that aren't already adequately resolved by the coarse mesh. For example, for this mesh the central hole is supposed to be round:
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 straight lines. 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 geometry. The way to do this requires three steps:
manifold_id
with a Manifold object. For more information on this see the glossary entry on this topic.manifold_id
. For example, you could get an annular sector with curved cells in Cartesian coordinates (but rectangles in polar coordinates) by doing the following: All functions in the GridGenerator namespace which create a mesh where some cells should be curved also attach the correct Manifold object to the provided Triangulation: i.e., for those functions we get the correct behavior by default. For a hand-generated mesh, however, the situation is much more interesting.
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 cross sections, extruding them into the third direction, and gluing them together. The following code does this, using the techniques previously described:
This creates the following mesh:
This mesh has the right general shape, but the top cells are now polygonal: their edges are no longer along circles and we do not have a very accurate representation of the original geometry. The next step is to teach the top part of the domain that it should be curved. Put another way, all calculations done on the top boundary cells should be done in cylindrical coordinates rather than Cartesian coordinates. We can do this by creating a CylindricalManifold object and associating it with the cells above \(y = 3\). This way, when we refine the cells on top, we will place new points along concentric circles instead of straight lines.
In deal.II we describe all geometries with classes that inherit from Manifold. The default geometry is Cartesian and is implemented in the FlatManifold class. As the name suggests, Manifold and its inheriting classes provide a way to describe curves and curved cells in a general way with ideas and terminology from differential geometry: for example, CylindricalManifold inherits from ChartManifold, which describes a geometry through pull backs and push forwards. In general, one should think that the Triangulation class describes the topology of a domain (in addition, of course, to storing the locations of the vertices) while the Manifold classes describe the geometry of a domain (e.g., whether or not a pair of vertices lie along a circular arc or a straight line). A Triangulation will refine cells by doing computations with the Manifold associated with that cell regardless of whether or not the cell is on the boundary. Put another way: the Manifold classes do not need any information about where the boundary of the Triangulation actually is: it is up to the Triangulation to query the right Manifold for calculations on a cell. Most Manifold functions (e.g., Manifold::get_intermediate_point) know nothing about the domain itself and just assume that the points given to it lie along a geodesic. In this case, with the CylindricalManifold constructed below, the geodesics are arcs along circles orthogonal to the \(z\)-axis centered along the line \((0, 3, z)\).
Since all three top parts of the domain use the same geodesics, we will mark all cells with centers above the \(y = 3\) line as being cylindrical in nature:
With this code, we get a mesh that looks like this:
This change fixes the boundary but creates a new problem: the cells adjacent to the cylinder's axis are badly distorted. We should use Cartesian coordinates for calculations on these central cells to avoid this issue. The cells along the center line all have a face that touches the line \((0, 3, z)\) so, to implement this, we go back and overwrite the manifold_id
s on these cells to be zero (which is the default):
This gives us the following grid:
This gives us a good mesh, where cells at the center of each circle are still Cartesian and cells around the boundary lie along a circle. We can really see the nice detail of the boundary fitted mesh if we refine two more times:
It is often useful to assign different boundary ids to a mesh that is generated in one form or another as described in this tutorial to apply different boundary conditions.
For example, you might want to apply a different boundary condition for the right boundary of the first grid in this program. To do this, iterate over the cells and their faces and identify the correct faces (for example using cell->center()
to query the coordinates of the center of a cell as we do in step-1, or using cell->face(f)->get_boundary_id()
to query the current boundary indicator of the \(f\)th face of the cell). You can then use cell->face(f)->set_boundary_id()
to set the boundary id to something different. You can take a look back at step-1 how iteration over the meshes is done there.
Computations on manifolds, like they are done in step-38, require a surface mesh embedded into a higher dimensional space. While some can be constructed using the GridGenerator namespace or loaded from a file, it is sometimes useful to extract a surface mesh from a volume mesh.
Use the function GridGenerator::extract_boundary_mesh() to extract the surface elements of a mesh. Using the function on a 3d mesh (a Triangulation<3,3>
, for example from grid_4()
), this will return a Triangulation<2,3>
that you can use in step-38. Also try extracting the boundary mesh of a Triangulation<2,2>
.