6.3 Materials, geometries, gravitation and other plugin types

6.3.1 Material models

The material model is responsible for describing the various coefficients in the equations that ASPECT solves. To implement a new material model, you need to overload the aspect::MaterialModel::Interface class and use the ASPECT_REGISTER_MATERIAL_MODEL macro to register your new class. The implementation of the new class should be in namespace aspect::MaterialModel. An example of a material model implemented this way is given in Section 5.4.12.

Specifically, your new class needs to implement the following interface:

    template <int dim> 
    class aspect::MaterialModel::Interface 
    { 
      public: 
        // Physical parameters used in the basic equations 
        virtual void evaluate(const MaterialModelInputs &in, MaterialModelOutputs &out) const=0; 
 
        virtual bool is_compressible () const = 0; 
 
 
        // Reference quantities 
        virtual double reference_viscosity () const = 0; 
 
 
        // Functions used in dealing with run-time parameters 
        static void 
        declare_parameters (ParameterHandler &prm); 
 
        virtual void 
        parse_parameters (ParameterHandler &prm); 
 
 
        // Optional: 
        virtual void initialize (); 
 
        virtual void update (); 
}

The main properties of the material are computed in the function evaluate() that takes a struct of type MaterialModelInputs and is supposed to fill a MaterialModelOutputs structure. For performance reasons this function is handling lookups at an arbitrary number of positions, so for each variable (for example viscosity), a std::vector is returned. The following members of MaterialModelOutputs need to be filled:

struct MaterialModelOutputs 
{ 
          std::vector<double> viscosities; 
          std::vector<double> densities; 
          std::vector<double> thermal_expansion_coefficients; 
          std::vector<double> specific_heat; 
          std::vector<double> thermal_conductivities; 
          std::vector<double> compressibilities; 
}

The variables refer to the coefficients η,Cp,k,ρ in equations (1)–(3), each as a function of temperature, pressure, position, compositional fields and, in the case of the viscosity, the strain rate (all handed in by MaterialModelInputs). Implementations of evaluate() may of course choose to ignore dependencies on any of these arguments.

The remaining functions are used in postprocessing as well as handling run-time parameters. The exact meaning of these member functions is documented in the aspect::MaterialModel::Interface class documentation. Note that some of the functions listed above have a default implementation, as discussed on the documentation page just mentioned.

The function is_compressible returns whether we should consider the material as compressible or not, see Section 2.10.3 on the Boussinesq model. As discussed there, incompressibility as described by this function does not necessarily imply that the density is constant; rather, it may still depend on temperature or pressure. In the current context, compressibility simply means whether we should solve the continuity equation as (ρu) = 0 (compressible Stokes) or as u = 0 (incompressible Stokes).

The purpose of the parameter handling functions has been discussed in the general overview of plugins above.

The functions initialize() and update() can be implemented if desired (the default implementation does nothing) and are useful if the material model has internal state. The function initialize() is called once during the initialization of ASPECT and can be used to allocate memory, initialize state, or read information from an external file. The function update() is called at the beginning of every time step.

Additionally, every material model has a member variable “model_dependence”, declared in the Interface class, which can be accessed from the plugin as “this model_dependence”. This structure describes the nonlinear dependence of the various coefficients on pressure, temperature, composition or strain rate. This information will be used in future versions of ASPECT to implement a fully nonlinear solution scheme based on, for example, a Newton iteration. The initialization of this variable is optional, but only plugins that declare correct dependencies can benefit from these solver types. All packaged material models declare their dependencies in the parse_parameters() function and can be used as a starting point for implementations of new material models.

Older versions of ASPECT used to have individual functions like viscosity() instead of the evaluate() function discussed above. This old interface is no longer supported, restructure your plugin to implement evaluate() instead (even if this function only calls the old functions).

6.3.2 Heating models

The heating model is responsible for describing the various terms in the energy equation (3), using the coefficients provided by the material model. These can be source terms such as radiogenic heat production or shear heating, they can be terms on the left-hand side of the equation, such as part of the latent heating terms, or they can be heating processes related to reactions. Each of these terms is described by a “heating model”, and a simulation can have none, one, or many heating models that are active throughout a simulation, with each heating model usually only implementing the terms for one specific heating process. One can then decide in the input file which heating processes should be included in the computation by providing a list of heating models in the input file.

When the equations are assembled and solved, the heating terms from all heating models used in the computation are added up.

To implement a new heating model, you need to overload the aspect::HeatingModel::Interface class and use the ASPECT_REGISTER_HEATING_MODEL macro to register your new class. The implementation of the new class should be in namespace aspect::HeatingModel.

Specifically, your new class needs to implement the following basic interface:

    template <int dim> 
    class aspect::HeatingModel::Interface 
    { 
      public: 
        // compute heating terms used in the energy equation 
        virtual 
        void 
        evaluate (const MaterialModel::MaterialModelInputs<dim> &material_model_inputs, 
                  const MaterialModel::MaterialModelOutputs<dim> &material_model_outputs, 
                  HeatingModel::HeatingModelOutputs &heating_model_outputs) const; 
 
        // All the following functions are optional: 
        virtual 
        void 
        initialize (); 
 
        virtual 
        void 
        update (); 
 
        // Functions used in dealing with run-time parameters 
        static 
        void 
        declare_parameters (ParameterHandler &prm); 
 
        virtual 
        void 
        parse_parameters (ParameterHandler &prm); 
 
        // Allow the heating model to attach additional material model outputs in case it needs 
        // them to compute the heating terms 
        virtual 
        void 
        create_additional_material_model_outputs(MaterialModel::MaterialModelOutputs<dim> &) const; 
    };

The main properties of the material are computed in the function evaluate() that takes references to MaterialModelInputs and MaterialModelOutputs objects and is supposed to fill the HeatingModelOutputs structure. As in the material model, this function is handling lookups at an arbitrary number of positions, so for each heating term (for example the heating source terms), a std::vector is returned. The following members of HeatingModelOutputs need to be filled:

struct HeatingModelOutputs 
{ 
       std::vector<double> heating_source_terms; 
       std::vector<double> lhs_latent_heat_terms; 
 
       // optional: 
       std::vector<double> rates_of_temperature_change; 
}

Heating source terms are terms on the right-hand side of the equations, such as the adiabatic heating αT u p in equation (3). An example for a left-hand side heating term is the temperature-derivative term ρTΔSX T that is part of latent heat production (see equation (5)).40 Rates of temperature change41 are used when the heating term is related to a reaction process, happening on a faster time scale than the temperature advection. All of these terms can depend on any of the material model inputs or outputs. Implementations of evaluate() may of course choose to ignore dependencies on any of these arguments.

The remaining functions are used in postprocessing as well as handling run-time parameters. The exact meaning of these member functions is documented in the aspect::HeatingModel::Interface class documentation. Note that some of the functions listed above have a default implementation, as discussed on the documentation page just mentioned.

Just like for material models, the functions initialize() and update() can be implemented if desired (the default implementation does nothing) and are useful if the heating model has an internal state. The function initialize() is called once during the initialization of ASPECT and can be used to allocate memory for the heating model, initialize state, or read information from an external file. The function update() is called at the beginning of every time step.

6.3.3 Geometry models

The geometry model is responsible for describing the domain in which we want to solve the equations. A domain is described in deal.II by a coarse mesh and, if necessary, an object that characterizes the boundary. Together, these two suffice to reconstruct any domain by adaptively refining the coarse mesh and placing new nodes generated by refining cells onto the surface described by the boundary object. The geometry model is also responsible for marking different parts of the boundary with different boundary indicators for which one can then, in the input file, select whether these boundaries should be Dirichlet-type (fixed temperature) or Neumann-type (no heat flux) boundaries for the temperature, and what kind of velocity conditions should hold there. In deal.II, a boundary indicator is a number of type types::boundary_id, but since boundaries are hard to remember and get right in input files, geometry models also have a function that provide a map from symbolic names that can be used to describe pieces of the boundary to the corresponding boundary indicators. For example, the simple box geometry model in 2d provides the map {"left" 0, "right" 1, "bottom" 2,"top" 3}, and we have consistently used these symbolic names in the input files used in this manual.

To implement a new geometry model, you need to overload the aspect::GeometryModel::Interface class and use the ASPECT_REGISTER_GEOMETRY_MODEL macro to register your new class. The implementation of the new class should be in namespace aspect::GeometryModel.

Specifically, your new class needs to implement the following basic interface:

    template <int dim> 
    class aspect::GeometryModel::Interface 
    { 
      public: 
        virtual 
        void 
        create_coarse_mesh (parallel::distributed::Triangulation<dim> &coarse_grid) const = 0; 
 
        virtual 
        double 
        length_scale () const = 0; 
 
        virtual 
        double depth(const Point<dim> &position) const = 0; 
 
        virtual 
        Point<dim> representative_point(const double depth) const = 0; 
 
        virtual 
        double maximal_depth() const = 0; 
 
        virtual 
        std::set<types::boundary_id_t> 
        get_used_boundary_indicators () const = 0; 
 
        virtual 
        std::map<std::string,types::boundary_id> 
        get_symbolic_boundary_names_map () const; 
 
        static 
        void 
        declare_parameters (ParameterHandler &prm); 
 
        virtual 
        void 
        parse_parameters (ParameterHandler &prm); 
    };

The kind of information these functions need to provide is extensively discussed in the documentation of this interface class at aspect::GeometryModel::Interface. The purpose of the last two functions has been discussed in the general overview of plugins above.

The create_coarse_mesh function does not only create the actual mesh (i.e., the locations of the vertices of the coarse mesh and how they connect to cells) but it must also set the boundary indicators for all parts of the boundary of the mesh. The deal.II glossary describes the purpose of boundary indicators as follows:

In a Triangulation object, every part of the boundary is associated with a unique number (of type types::boundary_id) that is used to identify which boundary geometry object is responsible to generate new points when the mesh is refined. By convention, this boundary indicator is also often used to determine what kinds of boundary conditions are to be applied to a particular part of a boundary. The boundary is composed of the faces of the cells and, in 3d, the edges of these faces.

By default, all boundary indicators of a mesh are zero, unless you are reading from a mesh file that specifically sets them to something different, or unless you use one of the mesh generation functions in namespace GridGenerator that have a ’colorize’ option. A typical piece of code that sets the boundary indicator on part of the boundary to something else would look like this, here setting the boundary indicator to 42 for all faces located at x = 1:

  for (typename Triangulation<dim>::active_cell_iterator 
         cell = triangulation.begin_active(); 
       cell != triangulation.end(); 
       ++cell) 
    for (unsigned int f=0; f<GeometryInfo<dim>::faces_per_cell; ++f) 
      if (cell->face(f)->at_boundary()) 
        if (cell->face(f)->center()[0] == -1) 
          cell->face(f)->set_boundary_indicator (42); 
  

This calls functions TriaAccessor::set_boundary_indicator. In 3d, it may also be appropriate to call TriaAccessor::set_all_boundary_indicators instead on each of the selected faces. To query the boundary indicator of a particular face or edge, use TriaAccessor::boundary_indicator.

The code above only sets the boundary indicators of a particular part of the boundary, but it does not by itself change the way the Triangulation class treats this boundary for the purposes of mesh refinement. For this, you need to call Triangulation::set_boundary to associate a boundary object with a particular boundary indicator. This allows the Triangulation object to use a different method of finding new points on faces and edges to be refined; the default is to use a StraightBoundary object for all faces and edges. The results section of step-49 has a worked example that shows all of this in action.

The second use of boundary indicators is to describe not only which geometry object to use on a particular boundary but to select a part of the boundary for particular boundary conditions. [...]

Note: Boundary indicators are inherited from mother faces and edges to their children upon mesh refinement. Some more information about boundary indicators is also presented in a section of the documentation of the Triangulation class.

Two comments are in order here. First, if a coarse triangulation’s faces already accurately represent where you want to pose which boundary condition (for example to set temperature values or determine which are no-flow and which are tangential flow boundary conditions), then it is sufficient to set these boundary indicators only once at the beginning of the program since they will be inherited upon mesh refinement to the child faces. Here, at the beginning of the program is equivalent to inside the create_coarse_mesh()) function of the geometry module shown above that generates the coarse mesh.

Secondly, however, if you can only accurately determine which boundary indicator should hold where on a refined mesh – for example because the coarse mesh is the cube [0,L]3 and you want to have a fixed velocity boundary describing an extending slab only for those faces for which z > L Lslab – then you need a way to set the boundary indicator for all boundary faces either to the value representing the slab or the fluid underneath after every mesh refinement step. By doing so, child faces can obtain boundary indicators different from that of their parents. deal.II triangulations support this kind of operations using a so-called post-refinement signal. In essence, what this means is that you can provide a function that will be called by the triangulation immediately after every mesh refinement step.

The way to do this is by writing a function that sets boundary indicators and that will be called by the Triangulation class. The triangulation does not provide a pointer to itself to the function being called, nor any other information, so the trick is to get this information into the function. C++ provides a nice mechanism for this that is best explained using an example:

    #include <deal.II/base/std_cxx1x/bind.h> 
 
    template <int dim> 
    void set_boundary_indicators (parallel::distributed::Triangulation<dim> &triangulation) 
    { 
      ... set boundary indicators on the triangulation object ... 
    } 
 
    template <int dim> 
    void 
    MyGeometry<dim>:: 
    create_coarse_mesh (parallel::distributed::Triangulation<dim> &coarse_grid) const 
    { 
      ... create the coarse mesh ... 
 
      coarse_grid.signals.post_refinement.connect 
        (std_cxx1x::bind (&set_boundary_indicators<dim>, 
                          std_cxx1x::ref(coarse_grid))); 
 
    }

What the call to std_cxx1x::bind does is to produce an object that can be called like a function with no arguments. It does so by taking the address of a function that does, in fact, take an argument but permanently fix this one argument to a reference to the coarse grid triangulation. After each refinement step, the triangulation will then call the object so created which will in turn call set_boundary_indicators<dim> with the reference to the coarse grid as argument.

This approach can be generalized. In the example above, we have used a global function that will be called. However, sometimes it is necessary that this function is in fact a member function of the class that generates the mesh, for example because it needs to access run-time parameters. This can be achieved as follows: assuming the set_boundary_indicators() function has been declared as a (non-static, but possibly private) member function of the MyGeometry class, then the following will work:

    #include <deal.II/base/std_cxx1x/bind.h> 
 
    template <int dim> 
    void 
    MyGeometry<dim>:: 
    set_boundary_indicators (parallel::distributed::Triangulation<dim> &triangulation) const 
    { 
      ... set boundary indicators on the triangulation object ... 
    } 
 
    template <int dim> 
    void 
    MyGeometry<dim>:: 
    create_coarse_mesh (parallel::distributed::Triangulation<dim> &coarse_grid) const 
    { 
      ... create the coarse mesh ... 
 
      coarse_grid.signals.post_refinement.connect 
        (std_cxx1x::bind (&MyGeometry<dim>::set_boundary_indicators, 
                          std_cxx1x::cref(*this), 
                          std_cxx1x::ref(coarse_grid))); 
    }

Here, like any other member function, set_boundary_indicators implicitly takes a pointer or reference to the object it belongs to as first argument. std::bind again creates an object that can be called like a global function with no arguments, and this object in turn calls set_boundary_indicators with a pointer to the current object and a reference to the triangulation to work on. Note that because the create_coarse_mesh function is declared as const, it is necessary that the set_boundary_indicators function is also declared const.

Note: For reasons that have to do with the way the parallel::distributed::Triangulation is implemented, functions that have been attached to the post-refinement signal of the triangulation are called more than once, sometimes several times, every time the triangulation is actually refined.

6.3.4 Gravity models

The gravity model is responsible for describing the magnitude and direction of the gravity vector at each point inside the domain. To implement a new gravity model, you need to overload the aspect::GravityModel::Interface class and use the ASPECT_REGISTER_GRAVITY_MODEL macro to register your new class. The implementation of the new class should be in namespace aspect::GravityModel.

Specifically, your new class needs to implement the following basic interface:

    template <int dim> 
    class aspect::GravityModel::Interface 
    { 
      public: 
        virtual 
        Tensor<1,dim> 
        gravity_vector (const Point<dim> &position) const = 0; 
 
        virtual 
        void 
        update (); 
 
        static 
        void 
        declare_parameters (ParameterHandler &prm); 
 
        virtual 
        void 
        parse_parameters (ParameterHandler &prm); 
    };

The kind of information these functions need to provide is discussed in the documentation of this interface class at aspect::GravityModel::Interface. The first needs to return a gravity vector at a given position, whereas the second is called at the beginning of each time step, for example to allow a model to update itself based on the current time or the solution of the previous time step. The purpose of the last two functions has been discussed in the general overview of plugins above.

6.3.5 Initial conditions

The initial conditions model is responsible for describing the initial temperature distribution throughout the domain. It essentially has to provide a function that for each point can return the initial temperature. Note that the model (1)–(3) does not require initial values for the pressure or velocity. However, if coefficients are nonlinear, one can significantly reduce the number of initial nonlinear iterations if a good guess for them is available; consequently, ASPECT initializes the pressure with the adiabatically computed hydrostatic pressure, and a zero velocity. Neither of these two has to be provided by the objects considered in this section.

To implement a new initial conditions model, you need to overload the aspect::InitialConditions::Interface class and use the ASPECT_REGISTER_INITIAL_CONDITIONS macro to register your new class. The implementation of the new class should be in namespace aspect::InitialConditions.

Specifically, your new class needs to implement the following basic interface:

    template <int dim> 
    class aspect::InitialConditions::Interface 
    { 
      public: 
        void 
        initialize (const GeometryModel::Interface<dim>       &geometry_model, 
                    const BoundaryTemperature::Interface<dim> &boundary_temperature, 
                    const AdiabaticConditions<dim>            &adiabatic_conditions); 
 
        virtual 
        double 
        initial_temperature (const Point<dim> &position) const = 0; 
 
        static 
        void 
        declare_parameters (ParameterHandler &prm); 
 
        virtual 
        void 
        parse_parameters (ParameterHandler &prm); 
    };

The meaning of the first class should be clear. The purpose of the last two functions has been discussed in the general overview of plugins above.

6.3.6 Prescribed velocity boundary conditions

Most of the time, one chooses relatively simple boundary values for the velocity: either a zero boundary velocity, a tangential flow model in which the tangential velocity is unspecified but the normal velocity is zero at the boundary, or one in which all components of the velocity are unspecified (i.e., for example, an outflow or inflow condition where the total stress in the fluid is assumed to be zero). However, sometimes we want to choose a velocity model in which the velocity on the boundary equals some prescribed value. A typical example is one in which plate velocities are known, for example their current values or historical reconstructions. In that case, one needs a model in which one needs to be able to evaluate the velocity at individual points at the boundary. This can be implemented via plugins.

To implement a new boundary velocity model, you need to overload the aspect::VelocityBoundaryConditions::Interface class and use the ASPECT_REGISTER_VELOCITY_BOUNDARY_CONDITIONS macro to register your new class. The implementation of the new class should be in namespace aspect::VelocityBoundaryConditions.

Specifically, your new class needs to implement the following basic interface:

    template <int dim> 
    class aspect::VelocityBoundaryConditions::Interface 
    { 
      public: 
        virtual 
        Tensor<1,dim> 
        boundary_velocity (const Point<dim> &position) const = 0; 
 
        virtual 
        void 
        initialize (const GeometryModel::Interface<dim> &geometry_model); 
 
        virtual 
        void 
        update (); 
 
        static 
        void 
        declare_parameters (ParameterHandler &prm); 
 
        virtual 
        void 
        parse_parameters (ParameterHandler &prm); 
    };

The first of these functions needs to provide the velocity at the given point. The next two are other member functions that can (but need not) be overloaded if a model wants to do initialization steps at the beginning of the program or at the beginning of each time step. Examples are models that need to call an external program to obtain plate velocities for the current time, or from historical records, in which case it is far cheaper to do so only once at the beginning of the time step than for every boundary point separately. See, for example, the aspect::VelocityBoundaryConditions::GPlates class.

The remaining functions are obvious, and are also discussed in the documentation of this interface class at aspect::VelocityBoundaryConditions::Interface. The purpose of the last two functions has been discussed in the general overview of plugins above.

6.3.7 Temperature boundary conditions

The boundary conditions are responsible for describing the temperature values at those parts of the boundary at which the temperature is fixed (see Section 6.3.3 for how it is determined which parts of the boundary this applies to).

To implement a new boundary conditions model, you need to overload the aspect::BoundaryTemperature::Interface class and use the ASPECT_REGISTER_BOUNDARY_TEMPERATURE_MODEL macro to register your new class. The implementation of the new class should be in namespace aspect::BoundaryTemperature.

Specifically, your new class needs to implement the following basic interface:

    template <int dim> 
    class aspect::BoundaryTemperature::Interface 
    { 
      public: 
        virtual 
        double 
        temperature (const GeometryModel::Interface<dim> &geometry_model, 
                     const unsigned int                   boundary_indicator, 
                     const Point<dim>                    &location) const = 0; 
 
        virtual 
        double minimal_temperature () const = 0; 
 
        virtual 
        double maximal_temperature () const = 0; 
 
        static 
        void 
        declare_parameters (ParameterHandler &prm); 
 
        virtual 
        void 
        parse_parameters (ParameterHandler &prm); 
    };

The first of these functions needs to provide the fixed temperature at the given point. The geometry model and the boundary indicator of the particular piece of boundary on which the point is located is also given as a hint in determining where this point may be located; this may, for example, be used to determine if a point is on the inner or outer boundary of a spherical shell. The remaining functions are obvious, and are also discussed in the documentation of this interface class at aspect::BoundaryTemperature::Interface. The purpose of the last two functions has been discussed in the general overview of plugins above.

6.3.8 Postprocessors: Evaluating the solution after each time step

Postprocessors are arguably the most complex and powerful of the plugins available in ASPECT since they do not only passively provide any information but can actually compute quantities derived from the solution. They are executed once at the end of each time step and, unlike all the other plugins discussed above, there can be an arbitrary number of active postprocessors in the same program (for the plugins discussed in previous sections it was clear that there is always exactly one material model, geometry model, etc.).

Motivation. The original motivation for postprocessors is that the goal of a simulation is of course not the simulation itself, but that we want to do something with the solution. Examples for already existing postprocessors are:

Since writing this text, there may have been other additions as well.

However, postprocessors can be more powerful than this. For example, while the ones listed above are by and large stateless, i.e., they do not carry information from one invocation at one timestep to the next invocation,42 there is nothing that prohibits postprocessors from doing so. For example, the following ideas would fit nicely into the postprocessor framework:

In all of these cases, the essential limitation is that postprocessors are passive, i.e., that they do not affect the simulation but only observe it.

The statistics file. Postprocessors fall into two categories: ones that produce lots of output every time they run (e.g., the visualization postprocessor), and ones that only produce one, two, or in any case a small and fixed number of often numerical results (e.g., the postprocessors computing velocity, temperature, or heat flux statistics). While the former are on their own in implementing how they want to store their data to disk, there is a mechanism in place that allows the latter class of postprocessors to store their data into a central file that is updated at the end of each time step, after all postprocessors are run.

To this end, the function that executes each of the postprocessors is given a reference to a dealii::TableHandler object that allows to store data in named columns, with one row for each time step. This table is then stored in the statistics file in the directory designated for output in the input parameter file. It allows for easy visualization of trends over all time steps. To see how to put data into this statistics object, take a look at the existing postprocessor objects.

Note that the data deposited into the statistics object need not be numeric in type, though it often is. An example of text-based entries in this table is the visualization class that stores the name of the graphical output file written in a particular time step.

Implementing a postprocessor. Ultimately, implementing a new postprocessor is no different than any of the other plugins. Specifically, you’ll have to write a class that overloads the aspect::Postprocess::Interface base class and use the ASPECT_REGISTER_POSTPROCESSOR macro to register your new class. The implementation of the new class should be in namespace aspect::Postprocess.

In reality, however, implementing new postprocessors is often more difficult. Primarily, this difficulty results from two facts:

Given these comments, the interface a postprocessor class has to implement is rather basic:

    template <int dim> 
    class aspect::Postprocess::Interface 
    { 
      public: 
        virtual 
        std::pair<std::string,std::string> 
        execute (TableHandler &statistics) = 0; 
 
        virtual 
        void 
        save (std::map<std::string, std::string> &status_strings) const; 
 
        virtual 
        void 
        load (const std::map<std::string, std::string> &status_strings); 
 
        static 
        void 
        declare_parameters (ParameterHandler &prm); 
 
        virtual 
        void 
        parse_parameters (ParameterHandler &prm); 
    };

The purpose of these functions is described in detail in the documentation of the aspect::Postprocess::Interface class. While the first one is responsible for evaluating the solution at the end of a time step, the save/load functions are used in checkpointing the program and restarting it at a previously saved point during the simulation. The first of these functions therefore needs to store the status of the object as a string under a unique key in the database described by the argument, while the latter function restores the same state as before by looking up the status string under the same key. The default implementation of these functions is to do nothing; postprocessors that do have non-static member variables that contain a state need to overload these functions.

There are numerous postprocessors already implemented. If you want to implement a new one, it would be helpful to look at the existing ones to see how they implement their functionality.

Postprocessors and checkpoint/restart. Postprocessors have save() and load() functions that are used to write the data a postprocessor has into a checkpoint file, and to load it again upon restart. This is important since many postprocessors store some state – say, a temporal average over all the time steps seen so far, or the number of the last graphical output file generated so that we know how the next one needs to be numbered.

The typical case is that this state is the same across all processors of a parallel computation. Consequently, what ASPECT writes into the checkpoint file is only the state obtained from the postprocessors on processor 0 of a parallel computation. On restart, all processors read from the same file and the postprocessors on all processors will be initialized by what the same postprocessor on processor 0 wrote.

There are situations where postprocessors do in fact store complementary information on different processors. At the time of writing this, one example is the postprocessor that supports advecting passive particles along the velocity field: on every processor, it handles only those particles that lie inside the part of the domain that is owned by this MPI rank. The serialization approach outlined above can not work in this case, for obvious reasons. In cases like this, one needs to implement the save() and load() differently than usual: one needs to put all variables that are common across processors into the maps of string as usual, but one then also needs to save all state that is different across processors, from all processors. There are two ways: If the amount of data is small, you can use MPI communications to send the state of all processors to processor zero, and have processor zero store it in the result so that it gets written into the checkpoint file; in the load() function, you will then have to identify which part of the text written by processor 0 is relevant to the current processor. Or, if your postprocessor stores a large amount of data, you may want to open a restart file specifically for this postprocessor, use MPI I/O or other ways to write into it, and do the reverse operation in load().

Note that this approach requires that ASPECT actually calls the save() function on all processors. This in fact happens – though ASPECT also discards the result on all but processor zero.

6.3.9 Visualization postprocessors

As mentioned in the previous section, one of the postprocessors that are already implemented in ASPECT is the aspect::Postprocess::Visualization class that takes the solution and outputs it as a collection of files that can then be visualized graphically, see Section 4.4. The question is which variables to output: the solution of the basic equations we solve here is characterized by the velocity, pressure and temperature; on the other hand, we are frequently interested in derived, spatially and temporally variable quantities such as the viscosity for the actual pressure, temperature and strain rate at a given location, or seismic wave speeds.

ASPECT already implements a good number of such derived quantities that one may want to visualize. On the other hand, always outputting all of them would yield very large output files, and would furthermore not scale very well as the list continues to grow. Consequently, as with the postprocessors described in the previous section, what can be computed is implemented in a number of plugins and what is computed is selected in the input parameter file (see Section ??).

Defining visualization postprocessors works in much the same way as for the other plugins discussed in this section. Specifically, an implementation of such a plugin needs to be a class that derives from interface classes, should by convention be in namespace aspect::Postprocess::VisualizationPostprocessors, and is registered using a macro, here called ASPECT_REGISTER_VISUALIZATION_POSTPROCESSOR. Like the postprocessor plugins, visualization postprocessors can derive from class aspect::Postprocess::SimulatorAccess if they need to know specifics of the simulation such as access to the material models and to get access to the introspection facility outlined in Section 6.1. A typical example is the plugin that produces the viscosity as a spatially variable field by evaluating the viscosity function of the material model using the pressure, temperature and location of each visualization point (implemented in the aspect::Postprocess::VisualizationPostprocessors::Viscosity class). On the other hand, a hypothetical plugin that simply outputs the norm of the strain rate ε(u) : ε(u) would not need access to anything but the solution vector (which the plugin’s main function is given as an argument) and consequently is not derived from the aspect::Postprocess::SimulatorAccess class.43

Visualization plugins can come in two flavors:

If all of this sounds confusing, we recommend consulting the implementation of the various visualization plugins that already exist in the ASPECT sources, and using them as a template.

6.3.10 Mesh refinement criteria

Despite research since the mid-1980s, it isn’t completely clear how to refine meshes for complex situations like the ones modeled by ASPECT. The basic problem is that mesh refinement criteria either can refine based on some variable such as the temperature, the pressure, the velocity, or a compositional field, but that oftentimes this by itself is not quite what one wants. For example, we know that Earth has discontinuities, e.g., at 440km and 610km depth. In these places, densities and other material properties suddenly change. Their resolution in computation models is important as we know that they affect convection patterns. At the same time, there is only a small effect on the primary variables in a computation – maybe a jump in the second or third derivative, for example, but not a discontinuity that would be clear to see. As a consequence, automatic refinement criteria do not always refine these interfaces as well as necessary.

To alleviate this, ASPECT has plugins for mesh refinement. Through the parameters in Section ??, one can select when to refine but also which refinement criteria should be used and how they should be combined if multiple refinement criteria are selected. Furthermore, through the usual plugin mechanism, one can extend the list of available mesh refinement criteria (see the parameter “Strategy” in Section ??). Each such plugin is responsible for producing a vector of values (one per active cell on the current processor, though only those values for cells that the current processor owns are used) with an indicator of how badly this cell needs to be refined: large values mean that the cell should be refined, small values that the cell may be coarsened away.

To implement a new mesh refinement criterion, you need to overload the aspect::MeshRefinement::Interface class and use the ASPECT_REGISTER_MESH_REFINEMENT_CRITERION macro to register your new class. The implementation of the new class should be in namespace aspect::MeshRefinement.

Specifically, your new class needs to implement the following basic interface:

    template <int dim> 
    class aspect::MeshRefinement::Interface 
    { 
      public: 
        virtual 
        void 
        execute (Vector<float> &error_indicators) const = 0; 
 
        static 
        void 
        declare_parameters (ParameterHandler &prm); 
 
        virtual 
        void 
        parse_parameters (ParameterHandler &prm); 
    };

The first of these functions computes the set of refinement criteria (one per cell) and returns it in the given argument. Typical examples can be found in the existing implementations in the source/mesh_refinement directory. As usual, your termination criterion implementation will likely need to be derived from the SimulatorAccess to get access to the current state of the simulation.

The remaining functions are obvious, and are also discussed in the documentation of this interface class at aspect::MeshRefinement::Interface. The purpose of the last two functions has been discussed in the general overview of plugins above.

6.3.11 Criteria for terminating a simulation

ASPECT allows for different ways of terminating a simulation. For example, the simulation may have reached a final time specified in the input file. However, it also allows for ways to terminate a simulation when it has reached a steady state (or, rather, some criterion determines that it is close enough to steady state), or by an external action such as placing a specially named file in the output directory. The criteria determining termination of a simulation are all implemented in plugins. The parameters describing these criteria are listed in Section ??.

To implement a termination criterion, you need to overload the aspect::TerminationCriteria::Interface class and use the ASPECT_REGISTER_TERMINATION_CRITERION macro to register your new class. The implementation of the new class should be in namespace aspect::TerminationCriteria.

Specifically, your new class needs to implement the following basic interface:

    template <int dim> 
    class aspect::TerminationCriteria::Interface 
    { 
      public: 
        virtual 
        bool 
        execute () const = 0; 
 
        static 
        void 
        declare_parameters (ParameterHandler &prm); 
 
        virtual 
        void 
        parse_parameters (ParameterHandler &prm); 
    };

The first of these functions returns a value that indicates whether the simulation should be terminated. Typical examples can be found in the existing implementations in the source/termination_criteria directory. As usual, your termination criterion implementation will likely need to be derived from the SimulatorAccess to get access to the current state of the simulation.

The remaining functions are obvious, and are also discussed in the documentation of this interface class at aspect::TerminationCriteria::Interface. The purpose of the last two functions has been discussed in the general overview of plugins above.

40Whether a term should go on the left or right hand side of the equation is, in some sense, a choice one can make. Putting a term onto the right hand side makes it an explicit term as far as time stepping is concerned, and so may imply a time step restriction if its dynamics are too fast. On the other hand, it does not introduce a nonlinearity if it depends on more than just a multiple of the temperature (such as the term αT u p). In practice, whether one wants to put a specific term on one side or the other may be a judgment call based on experience with numerical methods.

41Or, more correctly: Rates of thermal energy change.

42This is not entirely true. The visualization plugin keeps track of how many output files it has already generated, so that they can be numbered consecutively.

43The actual plugin aspect::Postprocess::VisualizationPostprocessors::StrainRate only computes ε(u ) : ε(u ) in the incompressible case. In the compressible case, it computes [ε(u ) 1 3(trε(u))I] : [ε(u) 1 3(trε(u))I] instead. To test whether the model is compressible or not, the plugin needs access to the material model object, which the class gains by deriving from aspect::Postprocess::SimulatorAccess and then calling this->get_material_model().is_compressible().