MueLu - Level containers

Philosophy

  • MueLu strictly splits data and algorithms.
  • Data is stored in data containers. There is one data container associated with each level. Therefore, the data container class is Level.
  • The algorithms are implemented in Factory classes. Factory here, means, that input data is used and processed to produce output data. A factory can be understood as a data processing unit. It does not store any data itself.

Basics of the Level container

Declare location of include headers


In [1]:
.I /opt/install/do-conf-ep-serial/include/


Out[1]:

Load Trilinos libraries


In [2]:
.L libepetra


Out[2]:


In [3]:
.L libxpetra


Out[3]:


In [4]:
.L libmuelu


Out[4]:


In [5]:
.L libteuchoscore


Out[5]:

Include some standard header files to create a communicator and an Xpetra::Map


In [6]:
#include "Kokkos_DefaultNode.hpp"
#include "Epetra_SerialComm.h"
#include "Teuchos_RCP.hpp"
#include "Teuchos_DefaultSerialComm.hpp"
#include "Xpetra_MapFactory.hpp"
#include "Xpetra_Map.hpp"


Out[6]:

Again, we use standard template parameters:


In [7]:
typedef double SC;
typedef int LO;
typedef int GO;
typedef Kokkos::Compat::KokkosSerialWrapperNode EpetraNode;
typedef EpetraNode NO;


Out[7]:

Let's create a MueLu::Level object:


In [8]:
#include "MueLu_Level.hpp"


Out[8]:


In [9]:
MueLu::Level l;


Out[9]:
(MueLu::Level &) @0x7f27d0fa9018

A MueLu::Level object is basically a data container. You can store (arbitrary) data in the level container given a (unique) name. To store data, you use the Set routine:


In [10]:
l.Set("MyInt", 42);
l.Set("MyDouble", 37.88);


Out[10]:
(void) @0x7ffcffc83ef0

This also works for more complicated objects. Usually, you store RCP pointers to some linear algebra objects, such as matrices, vectors or just Xpetra::Map objects as shown next:


In [11]:
Epetra_SerialComm Comm;
Teuchos::RCP<const Teuchos::Comm<int> > comm = Xpetra::toXpetra(Comm);
Teuchos::RCP<Xpetra::Map<LO,GO,NO>> map = Xpetra::MapFactory<LO,GO,NO>::Build(Xpetra::UseEpetra, 10, 0, comm);


Out[11]:
(Teuchos::RCP<Xpetra::Map<LO, GO, NO> > &) @0x7f27d0fa9150

In [12]:
l.Set("MyMap", map);


Out[12]:
(void) @0x7ffcffc83ef0

The Level class has a print routine, that can be very useful for debugging. It prints a list with all data stored insided and some additional information. Please note, that you have to provide the MueLu::Extreme verbosity flag to print the list.


In [13]:
l.print(std::cout, MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  MyDouble              NoFactory           0      User   double           37.8800                             
  MyInt                 NoFactory           0      User   int              42                                  
  MyMap                 NoFactory           0      User   Map              available                           
Out[13]:
(void) @0x7ffcffc83ef0

Routines like IsAvailable allow you to check programmatically at runtime whether a variable is stored in the Level container:


In [14]:
l.IsAvailable("MyMap");


Out[14]:
(bool) true

To access data, you can use the Get data. It is templated on the data type.


In [15]:
Teuchos::RCP<Xpetra::Map<LO,GO,NO>> myMap = l.Get<Teuchos::RCP<Xpetra::Map<LO,GO,NO>>>("MyMap");


Out[15]:
(Teuchos::RCP<Xpetra::Map<LO, GO, NO> > &) @0x7f27d0fa9168

In [16]:
std::cout << *myMap << std::endl;


 
 Number of Global Entries = 10
 Maximum of all GIDs      = 9
 Minimum of all GIDs      = 0
 Index Base               = 0
 

Out[16]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f27c8cee700

Data, which is stored on the level class using the Set routine is not automatically freed after the Get call, as one can easily verify:


In [17]:
l.print(std::cout, MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  MyDouble              NoFactory           0      User   double           37.8800                             
  MyInt                 NoFactory           0      User   int              42                                  
  MyMap                 NoFactory           0      User   Map              available                           
Out[17]:
(void) @0x7ffcffc83ef0

As you can see, the variable myMap is still contained in the list of data.

You can explicitly remove data from the container by using the following Delete call.


In [18]:
l.Delete("MyMap",MueLu::NoFactory::get());


Out[18]:
(void) @0x7ffcffc83ef0

Printing out the content of l proofs that MyMap is not stored in the container any more.


In [19]:
l.print(std::cout, MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  MyDouble              NoFactory           0      User   double           37.8800                             
  MyInt                 NoFactory           0      User   int              42                                  
Out[19]:
(void) @0x7ffcffc83ef0

You may have noticed that we needed the MueLu::NoFactory::get() parameter in the Delete call. This leads us to the next step, discussing data dependencies and finally the Factory concept in MueLu.

What is MueLu::NoFactory?

MueLu::NoFactory means that the associated data has not been created by a so-called Factory. In order to automatically manage and resolve data dependencies MueLu uses the so-called Factories. Factory classes contain the algorithms: they require some algorithm-specific input data, process the input data and produce some output data. In contrast: the Level class is a pure container class, storing and providing data that is used by the Factory classes.

Please note: the naming Factory class might be misleading. It is not a Factory class in the sense of the Factory pattern to create new objects as it is used, e.g., in Xpetra (see, e.g., Xpetra::MapFactory).

Long in short: MueLu strictly separates data and algorithms. The data is stored in the Level container classes whereas data is processed in the Factory classes. The Factory classes get data from the Level classes and store newly generated data in the Level classes.

How can we declare data dependencies?

Factory-generated data versus user data

To demonstrate the principle, let's define the most trivial Factory class, one can think of:


In [20]:
#include "MueLu_SingleLevelFactoryBase.hpp"
using Teuchos::RCP;
using MueLu::Level;
class DummyFact : public MueLu::SingleLevelFactoryBase { 
    virtual void DeclareInput(Level &currentLevel) const {};
    virtual void Build(Level & currentLevel) const {};
};


Out[20]:

For now, the meaning of the empty routines DeclareInput and Build is not important.

Next, we create two instances, that is, two different factories.


In [21]:
DummyFact f1;
DummyFact f2;


Out[21]:
(DummyFact &) @0x7f27d0fa92c0

Important is, that each factory has a unique memory adress, e.g.


In [22]:
std::cout << "Memory adress of factory1: " << &f1 << std::endl;
std::cout << "Memory adress of factory2: " << &f2 << std::endl;


Memory adress of factory1: 0x7f27d0fa9180
Memory adress of factory2: 0x7f27d0fa92c0
Out[22]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f27c8cee700

The keypoint is to understand, that data in MueLu is uniquely described by the name and the generating factory. The generating factory itself is identified and referred to by its memory adress.

This allows to store different data using the same variable name but a different factory.

Let's give an example:


In [23]:
l.Set("MyInt", 46, &f1);
l.Set("MyInt", 47, &f2);


******* WARNING *******
Level::Set: unable to store "MyInt" generated by factory 0x7f27d0fa9180 on level -1, as it has not been requested and no keep flags were set for it

******* WARNING *******
Level::Set: unable to store "MyInt" generated by factory 0x7f27d0fa92c0 on level -1, as it has not been requested and no keep flags were set for it
Out[23]:
(void) @0x7ffcffc83ef0

In [25]:
l.print(std::cout,MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  MyDouble              NoFactory           0      User   double           37.8800                             
  MyInt                 NoFactory           0      User   int              42                                  
  MyInt                 0x7f27d0fa9180      1      No     unknown          not available   0xc2550f0           
Out[25]:
(void) @0x7ffcffc83ef0

Obviously, storing data failed. In fact, data generated by a factory is only stored, if it has requested before.

So, let's request the data named MyInt generated by f1:


In [24]:
l.Request("MyInt", &f1);
l.print(std::cout,MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  MyDouble              NoFactory           0      User   double           37.8800                             
  MyInt                 NoFactory           0      User   int              42                                  
  MyInt                 0x7f27d0fa9180      1      No     unknown          not available   0xc2550f0           
Out[24]:
(void) @0x7ffcffc83ef0

The printout shows a variable MyInt generated by NoFactory from the beginning (with data $42$). There is a second MyInt generated by the adress associated with f1. The variable is listed to be requested, but there is no data stored (data not available). Now, let's try to set the data:


In [26]:
l.Set("MyInt", 46, &f1);
l.print(std::cout,MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  MyDouble              NoFactory           0      User   double           37.8800                             
  MyInt                 NoFactory           0      User   int              42                                  
  MyInt                 0x7f27d0fa9180      1      No     int              46              0xc2550f0           
Out[26]:
(void) @0x7ffcffc83ef0

That worked! Now we have two different variables MyInt. One generated by NoFactory (which corresponds to the user) with the value $42$ and one generated by f1 with the value $46$.

Ok. Let's store another MyInt generated by f2, just to test it:


In [27]:
l.Request("MyInt", &f2);
l.Set("MyInt", 47, &f2);
l.print(std::cout,MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  MyDouble              NoFactory           0      User   double           37.8800                             
  MyInt                 NoFactory           0      User   int              42                                  
  MyInt                 0x7f27d0fa9180      1      No     int              46              0xc2550f0           
  MyInt                 0x7f27d0fa92c0      1      No     int              47              0xc2550f0           
Out[27]:
(void) @0x7ffcffc83ef0

In [28]:
int ret = l.Get<int>("MyInt", &f2);
int ret2 = l.Get<int>("MyInt", &f2);
int ret3 = l.Get<int>("MyInt", &f2);
l.print(std::cout, MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  MyDouble              NoFactory           0      User   double           37.8800                             
  MyInt                 NoFactory           0      User   int              42                                  
  MyInt                 0x7f27d0fa9180      1      No     int              46              0xc2550f0           
  MyInt                 0x7f27d0fa92c0      1      No     int              47              0xc2550f0           
Out[28]:
(void) @0x7ffcffc83ef0

As you can see, we can access the data several times and it is not automatically freed. We can use Release to decrement the request counter. If it goes to zero, the data is deleted from the Level container. If it was a RCP pointer that runs out of scope, the underlying data is also automatically freed.


In [29]:
l.Release("MyInt", &f2);
l.print(std::cout, MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  MyDouble              NoFactory           0      User   double           37.8800                             
  MyInt                 NoFactory           0      User   int              42                                  
  MyInt                 0x7f27d0fa9180      1      No     int              46              0xc2550f0           
Out[29]:
(void) @0x7ffcffc83ef0

Long in short: the Request call increases the request counter, whereas the Release call decreases the request counter. If the request counter is zero the data is removed from the level container. Only data, which has been requested before (i.e., its request counter is larger than zero) can be stored.

The only exception is user data (generated by NoFactory). It can always be stored. User data has to be deleted using the Delete call.

To clear all data from the level class the following commands would be necessary:


In [30]:
l.Release("MyInt", &f1); // Release the data gen. by f1
l.Delete("MyInt", MueLu::NoFactory::get()); // Remove the 42 from l
l.Delete("MyDouble", MueLu::NoFactory::get());
l.print(std::cout, MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
Out[30]:
(void) @0x7ffcffc83ef0

The level container now should be empty.

Factory dependencies

The naive approach

Let's implement a first non-trivial factory. That is, we have to provide a implementation for the DeclareInput and the Build routine. The following factory shall take a variable InputNumber of type int as input and store the square root of it in the level container using the variable name SquareRoot.

In the DeclareInput routine we have to declare all necessary input data. In our case this is the InputNumber variable, which is supposed to be provided by the user. In the Build routine we obtain the input data from the Level class, build the square root and store the result on the Level class.

A naive implementation could look like follows:


In [31]:
class SquareRootFact : public MueLu::SingleLevelFactoryBase { 
    public:
    virtual void DeclareInput(MueLu::Level &currentLevel) const {
        currentLevel.DeclareInput("InputNumber", MueLu::NoFactory::get(), this);
    };
    virtual void Build(MueLu::Level & currentLevel) const {
        // extract data from level
        double userInput = currentLevel.Get<int>("InputNumber");
        // do calculation, process data
        double result = sqrt(userInput);
        // store result in level container
        currentLevel.Set("SquareRoot", result);
    };
};


Out[31]:

Let's create an instance of the class:


In [32]:
SquareRootFact sqFact;


Out[32]:
(SquareRootFact &) @0x7f27d0fa9418

The Level class has a Request routine, which recursively makes sure that all necessary input data for the given factory is properly requested in the Level container:


In [33]:
l.Request(sqFact);
l.print(std::cout, MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  InputNumber           NoFactory           1      No     unknown          not available   0x7f27d0fa9418      
Out[33]:
(void) @0x7ffcffc83ef0

In our case above call is not really necessary (but it does not really hurt either), since we only need user-provided data as input (NoFactory).

Please note: we never call the DeclareInput routine of the Factory class directly (unless you are an absolute expert), since it contains some sophisticated logic to recursively resolve data dependencies. You always should think from the level perspective and call the member functions provided by the Level class.

Of course, we must not forget to set the input data:


In [34]:
l.Set("InputNumber", 25);


Out[34]:
(void) @0x7ffcffc83ef0

Then, we can call the build routine and check the output


In [35]:
sqFact.Build(l);
l.print(std::cout,MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  InputNumber           NoFactory           1      User   int              25              0x7f27d0fa9418      
  SquareRoot            NoFactory           0      User   double           5.0000                              
Out[35]:
(void) @0x7ffcffc83ef0

It seems to work, but let's find a more elegant solution.


In [36]:
l.ExpertClear();


Out[36]:
(void) @0x7ffcffc83ef0

Use data dependencies between factories

Usually, input data for a factory is provided by another factory (rather than the user). So, just for demonstration purposes, let's introduce a new factory DataFactory2 which only stores a number in the level container. The implementation would be like this:


In [37]:
class DataFactory2 : public MueLu::SingleLevelFactoryBase { 
    public:
    virtual void DeclareInput(MueLu::Level &currentLevel) const {};
    virtual void Build(MueLu::Level & currentLevel) const {
        currentLevel.Set("Number", 36.0, this);
    };
};


Out[37]:

Please note the this pointer as third argument in the Set call. It means that the concrete instance of DataFactory2 generated the variable Number, that is stored in the Level container.

Next, we create a SquareRootFact2 factory, which expects the input variable Number from a factory which actually provides the data variable Number. In the DeclareInput call as well as in the Get call in the Build routine, we use the GetFactory("Number") routine, which basically returns the factory which is responsible to produce the variable Number. In our case this will be an instance of DataFactory2. We will see later how we declare the data dependency.


In [38]:
class SquareRootFact2 : public MueLu::SingleLevelFactoryBase { 
    public:
    virtual void DeclareInput(MueLu::Level &currentLevel) const {
        currentLevel.DeclareInput("Number", GetFactory("Number").get(), this);
    };
    virtual void Build(MueLu::Level & currentLevel) const {
        // extract data from level
        double userInput = currentLevel.Get<double>("Number", GetFactory("Number").get());
        //double userInput = Get<double>(currentLevel,"Number");
        // do calculation, process data
        double result = sqrt(userInput);
        // store result in level container
        currentLevel.Set("SquareRoot", result, this);
    };
};


Out[38]:

We create an instance of both the DataFactory2 and SquareRootFact2 classes.


In [39]:
DataFactory2 datFact;
SquareRootFact2 sqFact2;


Out[39]:
(SquareRootFact2 &) @0x7f27d0fa96a0

Then, we declare the data dependencies between these two factories. In our case datFact is responsible to provide variable Number as input for factory sqFact2. In other words: sqFact2 requires Number generated by datFact. That is, we set datFact as the generating factory for Number in sqFact2. This is done by the SetFactory routine:


In [40]:
sqFact2.SetFactory("Number", Teuchos::rcpFromRef(datFact));


Out[40]:
(void) @0x7ffcffc83ef0

To calculate the square root of Number we then request the result on the level class with the command


In [41]:
l.Request("SquareRoot", &sqFact2);


Out[41]:
(void) @0x7ffcffc83ef0

In [42]:
l.print(std::cout,MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  Number                0x7f27d0fa9560      1      No     unknown          not available   0x7f27d0fa96a0      
  SquareRoot            0x7f27d0fa96a0      1      No     unknown          not available   0xc2550f0           
Out[42]:
(void) @0x7ffcffc83ef0

As one can see, the Request call for SquareRoot automatically triggered an Request for Number, since it is prerequisite to calculate the result.

After all requests have (automatically) been resolved in a recursive fashion we are ready for the result.


In [43]:
double sqres = l.Get<double>("SquareRoot", &sqFact2);


Out[43]:
(double) 6.000000

Again, we do not call the Build routines directly, but just ask for the result. The Build routines are then automatically recursively called to produce the requested result.

Note, you have to call Request("SquareRoot",...) before the Get("SquareRoot",...) call. Data, which has not been requested first, won't be calculated and nothing would happen.

Next, we can look at the content of the level container. Obviously, it only contains the SquareRoot variable with the result $6$. The input data Number was only necessary to calculate the variable SquareRoot. No other factory has requested Number. Therefore, Number has automatically been freed and removed from the container.

The result SquareRoot is stored on the level class as we've requested it by our Request call.


In [44]:
l.print(std::cout,MueLu::Extreme);


LevelID = -1
  data name             gen. factory addr.  req    keep   type             data            req'd by            
  --------------------  ------------------  -----  -----  ---------------  --------------  --------------------
  SquareRoot            0x7f27d0fa96a0      1      No     double           6.0000          0xc2550f0           
Out[44]:
(void) @0x7ffcffc83ef0

Of course, the Build routines are only called if we try to get data from the level container, which is not available, yet. Once it has been calculated, we can extract it as often we want (as long as we do not release the data) without the data being recalculated.


In [45]:
double sqres2 = l.Get<double>("SquareRoot", &sqFact2);


Out[45]:
(double) 6.000000

A slightly more elegant implementation

The Get and Set calls can be written a little bit more elegant using the Get and Set functions of the Factory class. In the following example, the commented lines are equivalent with the new commands below.


In [46]:
class DataFactory3 : public MueLu::SingleLevelFactoryBase { 
    public:
    virtual void DeclareInput(MueLu::Level &currentLevel) const {};
    virtual void Build(MueLu::Level & currentLevel) const {
        //currentLevel.Set("Number", 36.0, this);
        Set<double>(currentLevel, "Number", 36.0);
    };
};

class SquareRootFact3 : public MueLu::SingleLevelFactoryBase { 
    public:
    virtual void DeclareInput(MueLu::Level &currentLevel) const {
        //currentLevel.DeclareInput("Number", GetFactory("Number").get(), this);
        Input(currentLevel,"Number");
    };
    virtual void Build(MueLu::Level & currentLevel) const {
        // extract data from level
        //double userInput = currentLevel.Get<double>("Number", GetFactory("Number").get());
        double userInput = Get<double>(currentLevel,"Number");
        // do calculation, process data
        double result = sqrt(userInput);
        // store result in level container
        //currentLevel.Set("SquareRoot", result, this);
        Set<double>(currentLevel,"SquareRoot", result);
    };
};


Out[46]:

The following list might help to understand the different syntax of the Level and Factory calls. Note, that Factory internally always uses GetFactory to determine the associated generating factory for the data variables.

Level class Factory class
Level::DeclareInput("VariableName", FactoryPtr, GeneratingFactoryPtr) Factory::Input(Level, "VariableName")
Level::Get("VariableName", FactoryPtr) Factory::Get(Level, "VariableName")
Level::Set("VariableName", FactoryPtr) Factory::Set(Level, "VariableName")

In [47]:
DataFactory3 datFact3;
SquareRootFact3 sqFact3;
sqFact3.SetFactory("Number", Teuchos::rcpFromRef(datFact3));
l.Request("SquareRoot", &sqFact3);
l.Get<double>("SquareRoot", &sqFact3);


Out[47]:
(double) 6.000000

In [ ]: