Reference documentation for deal.II version 9.1.1
|
Table of contents | |
---|---|
In step-18, we saw a need to write output files in an intermediate format: in a parallel program, it doesn't scale well if all processors participate in computing a result, and then only a single processor generates the graphical output. Rather, each of them should generate output for its share of the domain, and later on merge all these output files into a single one.
Thus was the beginning of step-19: it is the program that reads a number of files written in intermediate format, and merges and converts them into the final format that one would like to use for visualization. It can also be used for the following purpose: if you are unsure at the time of a computation what graphics program you would like to use, write your results in intermediate format; it can later be converted, using the present program, to any other format you may want.
While this in itself was not interesting enough to make a tutorial program, we have used the opportunity to introduce one class that has proven to be extremely help- and useful in real application programs, but had not been covered by any of the previous tutorial programs: the ParameterHandler
class. This class is used in applications that want to have some of their behavior determined at run time, using input files. For example, one may want to specify the geometry, or specifics of the equation to be solved, at run time. Other typical parameters are the number of nonlinear iterations, the name of output files, or the names of input files specifying material properties or boundary conditions.
Working with such parameter files is not rocket science. However, it is rather tedious to write the parsers for such files, in particular if they should be extensible, be able to group parameters into subsections, perform some error checks such as that parameters can have only certain kinds of values (for example, it should only be allowed to have integer values in an input file for parameters that denote a number of iteration), and similar requirements. The ParameterHandler
class allows for all this: an application program will declare the parameters it expects (or call a function in the library that declares a number of parameters for you), the ParameterHandler
class then reads an input file with all these parameters, and the application program can then get their values back from this class.
In order to perform these three steps, the ParameterHandler
offers three sets of functions: first, the ParameterHandler::declare_entry
function is used to declare the existence of a named parameter in the present section of the input file (one can enter and leave subsections in a parameter file just like you would navigate through a directory tree in a file system, with the functions ParameterHandler::enter_subsection
and ParameterHandler::leave_subsection
taking on the roles of the commands cd dir
and cd ..
; the only difference being that if you enter a subsection that has never been visited before, it is created: it isn't necessary to "create" subsections explicitly). When declaring a parameter, one has to specify its name and default value, in case the parameter isn't later listed explicitly in the parameter file. In addition to that, there are optional arguments indicating a pattern that a parameter has to satisfy, such as being an integer (see the discussion above), and a help text that might later give an explanation of what the parameter stands for.
Once all parameters have been declared, parameters can be read, using the ParameterHandler::parse_input
family of functions. There are versions of this function that can read from a file stream, that take a file name, or that simply take a string and parse it. When reading parameters, the class makes sure that only parameters are listed in the input that have been declared before, and that the values of parameters satisfy the pattern that has been given to describe the kind of values a parameter can have. Input that uses undeclared parameters or values for parameters that do not conform to the pattern are rejected by raising an exception.
A typical input file will look like this:
Note that subsections can be nested.
Finally, the application program can get the values of declared parameters back by traversing the subsections of the parameter tree and using the ParameterHandler::get
and related functions. The ParameterHandler::get
simply returns the value of a parameter as a string, whereas ParameterHandler::get_integer
, ParameterHandler::get_double
, and ParameterHandler::get_bool
already convert them to the indicated type.
Using the ParameterHandler
class therefore provides for a pretty flexible mechanism to handle all sorts of moderately complex input files without much effort on the side of the application programmer. We will use this to provide all sorts of options to the step-19 program in order to convert from intermediate file format to any other graphical file format.
The rest of the story is probably best told by looking at the source of step-19 itself. Let us, however, end this introduction by pointing the reader at the extensive class documentation of the ParameterHandler
class for more information on specific details of that class.
As usual, we start with include files. This program is content with really few of these – we only need two files from the library (one for input and output of graphical data, one for parameter handling), and a few C++ standard headers:
As mentioned in the first few tutorial programs, all names in deal.II are declared in a namespace dealii
. To make using these function and class names simpler, we import the entire content of that namespace into the global scope. As done for all previous programs already, we'll also place everything we do here into a namespace of its own:
Before we start with the actual program, let us declare a few global variables that will be used to hold the parameters this program is going to use. Usually, global variables are frowned upon for a good reason, but since we have such a short program here that does only a single thing, we may stray from our usual line and make these variables global, rather than passing them around to all functions or encapsulating them into a class.
The variables we have are: first, an object that will hold parameters of operation, such as output format (unless given on the command line); second, the names of input and output files; and third, the format in which the output is to be written:
All the stuff this program does can be done from here on. As described in the introduction, what we have to do is declare what values the parameter file can have, parse the command line, read the input files, then write the output. We will do this in this order of operation, but before that let us declare a function that prints a message about how this program is to be used; the function first prints a general message, and then goes on to list the parameters that are allowed in the parameter file (the ParameterHandler
class has a function to do exactly this; see the results section for what it prints):
The second function is used to declare the parameters this program accepts from the input file. While we don't actually take many parameters from the input file except for, possibly, the output file name and format, we nevertheless want to show how to work with parameter files.
In short, the ParameterHandler
class works as follows: one declares the entries of parameters that can be given in input files together, and later on one can read an input file in which these parameters are set to their values. If a parameter is not listed in the input file, the default value specified in the declaration of that parameter is used. After that, the program can query the values assigned to certain parameters from the ParameterHandler
object.
Declaring parameters can be done using the ParameterHandler::declare_entry
function. It's arguments are the name of a parameter, a default value (given as a string, even if the parameter is numeric in nature, and thirdly an object that describes constraints on values that may be passed to this parameter. In the example below, we use an object of type Patterns::Anything
to denote that there are no constraints on file names (this is, of course, not true – the operating system does have constraints, but from an application standpoint, almost all names are valid). In other cases, one may, for example, use Patterns::Integer
to make sure that only parameters are accepted that can be interpreted as integer values (it is also possible to specify bounds for integer values, and all values outside this range are rejected), Patterns::Double
for floating point values, classes that make sure that the given parameter value is a comma separated list of things, etc. Take a look at the Patterns
namespace to see what is possible.
The fourth argument to declare_entry
is a help string that can be printed to document what this parameter is meant to be used for and other information you may consider important when declaring this parameter. The default value of this fourth argument is the empty string.
I always wanted to have an example program describing the ParameterHandler
class, because it is so particularly useful. It would have been useful in a number of previous example programs (for example, in order to let the tolerance for linear solvers, or the number of refinement steps be determined by a run-time parameter, rather than hard-coding them into the program), but it turned out that trying to explain this class there would have overloaded them with things that would have distracted from the main purpose. However, while writing this program, I realized that there aren't all that many parameters this program can usefully ask for, or better, it turned out: declaring and querying these parameters was already done centralized in one place of the library, namely the DataOutInterface
class that handles exactly this – managing parameters for input and output.
So the second function call in this function is to let the DataOutInterface
declare a good number of parameters that control everything from the output format to what kind of output should be generated if output is written in a specific graphical format. For example, when writing data in encapsulated postscript (EPS) format, the result is just a 2d projection, not data that can be viewed and rotated with a viewer. Therefore, one has to choose the viewing angle and a number of other options up front, when output is generated, rather than playing around with them later on. The call to DataOutInterface::declare_parameters
declares entries that allow to specify them in the parameter input file during run-time. If the parameter file does not contain entries for them, defaults are taken.
As a final note: DataOutInterface
is a template, because it is usually used to write output for a specific space dimension. However, this program is supposed to be used for all dimensions at the same time, so we don't know at compile time what the right dimension is when specifying the template parameter. Fortunately, declaring parameters is something that is space dimension independent, so we can just pick one arbitrarily. We pick 1
, but it could have been any other number as well.
Since everything that this program can usefully request in terms of input parameters is already handled by now, let us nevertheless show how to use input parameters in other circumstances. First, parameters are like files in a directory tree: they can be in the top-level directory, but you can also group them into subdirectories to make it easier to find them or to be able to use the same parameter name in different contexts.
Let us first declare a dummy parameter in the top-level section; we assume that it will denote the number of iterations, and that useful numbers of iterations that a user should be able to specify are in the range 1...1000, with a default value of 42:
Next, let us declare a sub-section (the equivalent to a subdirectory). When entered, all following parameter declarations will be within this subsection. To also visually group these declarations with the subsection name, I like to use curly braces to force my editor to indent everything that goes into this sub-section by one level of indentation. In this sub-section, we shall have two entries, one that takes a Boolean parameter and one that takes a selection list of values, separated by the '|' character:
After this, we have left the subsection again. You should have gotten the idea by now how one can nest subsections to separate parameters. There are a number of other possible patterns describing possible values of parameters; in all cases, if you try to pass a parameter to the program that does not match the expectations of the pattern, it will reject the parameter file and ask you to fix it. After all, it does not make much sense if you had an entry that contained the entry "red" for the parameter "Generate output".
Our next task is to see what information has been provided on the command line. First, we need to be sure that there is at least one parameter: an input file. The format and the output file can be specified in the parameter file, but the list of input files can't, so at least one parameter needs to be there. Together with the name of the program (the zeroth parameter), argc
must therefore be at least 2. If this is not the case, we print an error message and exit:
Next, collect all parameters in a list that will be somewhat simpler to handle than the argc
/argv
mechanism. We omit the name of the executable at the zeroth index:
Then process all these parameters. If the parameter is -p
, then there must be a parameter file following (which we should then read), in case of -x
it is the name of an output format. Finally, for -o
it is the name of the output file. In all cases, once we've treated a parameter, we remove it from the list of parameters:
Now read the input file :
Both the output file name as well as the format can be specified on the command line. We have therefore given them global variables that hold their values, but they can also be set in the parameter file. We therefore need to extract them from the parameter file here, because they may be overridden by later command line parameters:
Finally, let us note that if we were interested in the values of the parameters declared above in the dummy subsection, we would write something like this to extract the value of the Boolean flag (the prm.get
function returns the value of a parameter as a string, whereas the prm.get_X
functions return a value already converted to a different type):
We would assign the result to a variable, of course, but don't here in order not to generate an unused variable that the compiler might warn about.
Alas, let's move on to handling of output formats:
Otherwise, this is not a parameter that starts with a known minus sequence, and we should consider it to be the name of an input file. Let us therefore add this file to the list of input files:
Next check a few things and create errors if the checks fail. Firstly, there must be at least one input file
Now that we have all the information, we need to read all the input files, merge them, and generate a single output file. This, after all, was the motivation, borne from the necessity encountered in the step-18 tutorial program, to write this program in the first place.
So what we do first is to declare an object into which we will merge the data from all the input file, and read in the first file through a stream. Note that every time we open a file, we use the AssertThrow
macro to check whether the file is really readable – if it isn't then this will trigger an exception and corresponding output will be generated from the exception handler in main()
:
For all the other input files, we read their data into an intermediate object, and then merge that into the first object declared above:
Once we have this, let us open an output stream, and parse what we got as the name of the output format into an identifier. Fortunately, the DataOutBase
class has a function that does this parsing for us, i.e. it knows about all the presently supported output formats and makes sure that they can be specified in the parameter file or on the command line. Note that this ensures that if the library acquires the ability to output in other output formats, this program will be able to make use of this ability without having to be changed!
Finally, write the merged data to the output:
The function above takes template parameters relating to the space dimension of the output, and the dimension of the objects to be output. (For example, when outputting whole cells, these two dimensions are the same, but the intermediate files may contain only data pertaining to the faces of cells, in which case the first parameter will be one less than the space dimension.)
The problem is: at compile time, we of course don't know the dimensions used in the input files. We have to plan for all cases, therefore. This is a little clumsy, since we need to specify the dimensions statically at compile time, even though we will only know about them at run time.
So here is what we do: from the first input file, we determine (using a function in DataOutBase
that exists for this purpose) these dimensions. We then have a series of switches that dispatch, statically, to the do_convert
functions with different template arguments. Not pretty, but works. Apart from this, the function does nothing – except making sure that it covered the dimensions for which it was called, using the AssertThrow
macro at places in the code that shouldn't be reached:
Finally, the main program. There is not much more to do than to make sure parameters are declared, the command line is parsed (which includes reading parameter files), and finally making sure the input files are read and output is generated. Everything else just has to do with handling exceptions and making sure that appropriate output is generated if one is thrown.
With all that above, here is first what we get if we just run the program without any parameters at all:
That's a lot of output for such a little program, but then that's also a lot of output formats that deal.II supports. You will realize that the output consists of first entries in the top-level section (sorted alphabetically), then a sorted list of subsections. Most of the parameters have been declared by the DataOutBase
class, but there are also the dummy entries and sections we have added in the declare_parameters()
function, along with their default values and documentations.
Let us try to run this program on a set of input files generated by a modified step-18 run on 32 nodes of a cluster. The computation was rather big, with more than 350,000 cells and some 1.2M unknowns. That makes for 32 rather big intermediate files that we will try to merge using the present program. Here is the list of files, totaling some 245MB of data:
So let's see what happens if we attempt to merge all these files into a single one:
So in roughly two minutes we have merged 240MB of data. Counting reading and writing, that averages a throughput of 3.8MB per second, not so bad.
If visualized, the output looks very much like that shown for step-18. But that's not quite as important for the moment, rather we are interested in showing how to use the parameter file. To this end, remember that if no parameter file is given, or if it is empty, all the default values listed above are used. However, whatever we specify in the parameter file is used, unless overridden again by parameters found later on the command line.
For example, let us use a simple parameter file named solution-0005.prm
that contains only one line:
If we run step-19 with it again, we obtain this (for simplicity, and because we don't want to visualize 240MB of data anyway, we only convert the one, the twelfth, intermediate file to gnuplot format):
We can then visualize this one file with gnuplot, obtaining something like this:
That's not particularly exciting, but the file we're looking at has only one 32nd of the entire domain anyway, so we can't expect much.
In more complicated situations, we would use parameter files that set more of the values to non-default values. A file for which this is the case could look like this, generating output for the OpenDX visualization program:
If one wanted to, one could write comments into the file using the same format as used above in the help text, i.e. everything on a line following a hashmark (#
) is considered a comment.
If one runs step-19 with this input file, this is what is going to happen:
Ah, right: valid values for the iteration parameter needed to be within the range [1...1000]. We would fix that, then go back to run the program with correct parameters.
This program should have given some insight into the input parameter file handling that deal.II provides. The ParameterHandler
class has a few more goodies beyond what has been shown in this program, for those who want to use this class, it would be useful to read the documentation of that class to get the full picture.