Using Fields

Fields store useful simulation quantities. MultiMat supports defining fields on the mesh and material subsets of the mesh. This section describes how to create fields and access their data.

Adding a Field

The addField() method adds a field to a MultiMat object. The method accepts arguments that indicate the mapping, layout, and sparsity for the supplied data, which are given using an axom::ArrayView. The data given in the view are copied into new memory managed by MultiMat.

The field mapping argument indicates the space where the data live: the mesh cells, the materials, or the cells/material regions defined over the mesh. The data layout argument indicates how the data are organized with respect to cells and materials. For data that have 1 value per cell, pass PER_CELL. For data that have 1 value per material (ignoring how many cells use the material), pass PER_MAT. For data that have a unique value per material within a cell, pass PER_CELL_MAT. For PER_CELL_MAT data, it is important to know the data layout. Pass CELL_DOM if all of the material values for a cell are sequential neighbors in memory; otherwise pass MAT_DOM.

Sparsity layout indicates whether the data array contains the maximum number of values (numMaterials * numCells for DENSE fields) or whether it instead contains only the subset of elements where materials are defined. The length of SPARSE fields is determined by the number of true values in the Cell-Mesh Relation (CMR).

  constexpr int ncells = 9;
  constexpr int nmats = 3;
  constexpr int nComponents = 1;
  // Add PER_CELL field.
  double perCellData[] = {1., 2., 3., 4., 5., 6., 7., 8., 9.};
  axom::ArrayView<double> perCellAV(perCellData, ncells);
  mm.addField("perCell",
              axom::multimat::FieldMapping::PER_CELL,
              axom::multimat::DataLayout::CELL_DOM,
              axom::multimat::SparsityLayout::DENSE,
              perCellAV,
              nComponents);

  // Add PER_MAT field.
  double perMatData[] = {1., 2., 3.};
  axom::ArrayView<double> perMatAV(perMatData, nmats);
  mm.addField("perMat",
              axom::multimat::FieldMapping::PER_MAT,
              axom::multimat::DataLayout::MAT_DOM,
              axom::multimat::SparsityLayout::DENSE,
              perMatAV,
              nComponents);

  // Add PER_CELL_MAT DENSE field. 0's where there is no material.
  double perCellMatDense[ncells][nmats] = {
    {0.55,  0.45,  0.},   // cell 0
    {0.425, 0.425, 0.15}, // cell 1
    {0.3,   0.4,   0.3},  // cell 2
    {0.425, 0.425, 0.15}, // cell 3
    {0.,    0.2,   0.8},  // cell 4
    {0.,    0.,    1.},   // cell 5
    {0.3,   0.4,   0.3},  // cell 6
    {0.,    0.,    1.},   // cell 7
    {0.,    0.,    1.}    // cell 8
  };
  axom::ArrayView<double> perCellMatDenseAV(&perCellMatDense[0][0], ncells * nmats);
  mm.addField("perCellMatDense",
              axom::multimat::FieldMapping::PER_CELL_MAT,
              axom::multimat::DataLayout::CELL_DOM,
              axom::multimat::SparsityLayout::DENSE,
              perCellMatDenseAV,
              nComponents);

  // Add PER_CELL_MAT SPARSE field. 0's do not need to appear.
  double perCellMatSparse[] = {
    0.55,  0.45,        // cell 0
    0.425, 0.425, 0.15, // cell 1
    0.3,   0.4,   0.3,  // cell 2
    0.425, 0.425, 0.15, // cell 3
           0.2,   0.8,  // cell 4
                  1.,   // cell 5
    0.3,   0.4,   0.3,  // cell 6
                  1.,   // cell 7
                  1.    // cell 8
  };
  axom::ArrayView<double> perCellMatSparseAV(perCellMatSparse,
                                             sizeof(perCellMatSparse) / sizeof(double));
  mm.addField("perCellMatSparse",
              axom::multimat::FieldMapping::PER_CELL_MAT,
              axom::multimat::DataLayout::CELL_DOM,
              axom::multimat::SparsityLayout::SPARSE,
              perCellMatSparseAV,
              nComponents);

Multi-Component Data

MultiMat can store fields with multiple components (vector data) by passing a non-unity stride in the last argument when adding a field. Multi-component data are arranged in memory as a contiguous block where the components of the first element (cell or material) exist sequentially in memory, followed immediately by the components for the next element, and so on.

  constexpr int nComponents = 2;
  double data[] = {0., 0.,  // cell 0 x,y components
                   1., 1.,  // cell 1 x,y components
                   2., 4.,  // cell 2 x,y components
                   3., 9.,  // cell 3 x,y components
                   4., 16., // cell 4 x,y components
                   5., 25., // cell 5 x,y components
                   6., 36., // cell 6 x,y components
                   7., 49., // cell 7 x,y components
                   8., 64.  // cell 8 x,y components
                  };
  axom::ArrayView<double> dataAV(data, sizeof(data) / sizeof(double));
  mm.addField("perCellMC",
              axom::multimat::FieldMapping::PER_CELL,
              axom::multimat::DataLayout::CELL_DOM,
              axom::multimat::SparsityLayout::DENSE,
              dataAV,
              nComponents);

Allocators

MultiMat supports allocating data through allocators. There are 2 separate allocators. The “Slam” allocator allocates data for internal data structures. The field allocator is used to allocate field bulk data, which is useful to override when writing GPU algorithms. Both allocators can be set at once using the setAllocatorID() method.

  • setAllocatorID()

  • setSlamAllocatorID()

  • setFieldAllocatorID()

External Field Data

MultiMat normally allocates its own memory for fields, however fields that point to externally-allocated memory can also be added using the addExternalField() method. This method has the same arguments as the addField() method, except that the the supplied view is used as the field’s actual data instead of being used to initialize additional memory. The addExternalField() method is useful when MultiMat is managing fields allocated and initialized externally, such as through Sidre, Conduit, MFEM, etc.

Removing a Field

Removing a field is done by calling the removeField() method on the MultiMat object and passing the name of the field to be removed. MultiMat will remove the field from its list of fields and deallocate memory, as needed. For external fields, deallocating the field’s bulk data is the responsibility of the caller.

mm.removeField("myField");

Introspection

The MultiMat object provides methods that permit host codes to determine the number of fields, their names, and their properties. The getNumberOfFields() method returns the number of fields. The getFieldName() method takes a field index and returns the name of the field. The getFieldIdx() method returns the field index for a given field name.

  // Print the field names in the MultiMat object mm.
  for(int i = 0; i < mm.getNumberOfFields(); i++)
  {
    // Get field properties
    auto name = mm.getFieldName(i);
    auto mapping = mm.getFieldMapping(i);
    auto layout = mm.getFieldDataLayout(i);
    auto sparsity = mm.getFieldSparsityLayout(i);
    auto dataType = mm.getFieldDataType(i);

    std::cout << name << ":"
              << "\n\tmapping: " << mapping << "\n\tlayout: " << layout
              << "\n\tsparsity: " << sparsity << "\n\tdataType: " << dataType << "\n";
  }
  std::cout << "Volfrac index: " << mm.getFieldIdx("Volfrac") << std::endl;

Accessing Field Data

Accessing fields and their data is best done when the field’s properties are known. The field mapping determines the mesh subset where the field is defined. Fields with either PER_CELL or PER_MAT mappings are defined along one dimension of the numMaterials * numCells grid so they are “1D” fields. Fields with PER_CELL_MAT field mapping are defined using both axes of the numMaterials * numCells grid so they are “2D” fields.

MultiMat provides separate field access functions for 1D/2D fields. In addition, there are specific 2D methods to access fields according to whether their data are dense or sparse. Each of these templated methods returns a field object specific to the field’s stored data and layout. The field object is used to read/write the field’s data.

  • get1dField()

  • get2dField()

  • getDense2dField()

  • getSparse2dField()

Indexing Sets

The data layout for a 2D field determines how it should be traversed. The MultiMat object provides methods that access the Cell-Material Relation (CMR) and return indexing sets that are useful for specific materials or cells. For example, if data use a CELL_DOM layout then all of the material values for a cell are contiguous in memory, even though a given cell might not use all possible materials. To write loops over sparse data that focus on only the valid cell-material pairs from the CMR, the getIndexingSetOfCell() and getIndexingSetOfMat() methods can be called.

  // CELL_DOM data (iterate over cells then materials)
  const std::string fieldName("perCellMatSparse");
  auto f = mm.getSparse2dField<double>(fieldName);
  std::cout << "Field: " << fieldName << std::endl;
  for(int i = 0; i < mm.getNumberOfCells(); i++)
  {
    std::cout << "\tcell " << i << " values: ";
    for(const auto &idx : mm.getIndexingSetOfCell(i, axom::multimat::SparsityLayout::SPARSE))
    {
      std::cout << f[idx] << ", ";
    }
    std::cout << "\n";
  }

1D Fields

1D Fields are those with a field mapping of PER_CELL or PER_MAT. 1D fields can be retrieved from MultiMat using the get1dField() method, which returns an object that can access the field data. The get1dField() method takes a template argument for the type of data stored in the field so if double-precision data are stored in MultiMat then get1dField<double>() should be called to access the field.

  // Sum all values in the field.
  double sum = 0.;
  auto f = mm.get1dField<double>("perCell");
  for(int i = 0; i < mm.getNumberOfCells(); i++)
  {
    sum += f[i];
  }

1D fields can store multi-component values as well, which adds a small amount of complexity. The field provides a numComp() method that returns the number of components. A component for a given cell is retrieved using the 2-argument call operator() by passing the cell index and then the desired component index.

  double sum = 0.;
  auto f = mm.get1dField<double>("perCellMC");
  for(int i = 0; i < mm.getNumberOfCells(); i++)
  {
    for(int comp = 0; comp < f.numComp(); comp++)
    {
      sum += f(i, comp);
    }
  }

2D Fields

2D fields are those with a PER_CELL_MAT field mapping. Since the fields can vary over materials and cells (in either order) and they can be dense or sparse, there are multiple ways to iterate over the field data.

Fields can be iterated using access patterns suitable for DENSE sparsity, even when the data may be SPARSE. The field’s findValue() function is useful in this case since it allows 2 indices to be passed in addition to a component index. The first index is the cell number for CELL_DOM fields, making the second index the material number. For MAT_DOM fields, the order is reversed. This approach to locating the data is general and can be used to traverse the data in the opposite order of the native data layout, if desired. However, each call to findValue() includes a short search and the method can return nullptr if no valid cell/material pair is located for the field.

    auto map = mm.get2dField<double>("CellMat Array");

    for(int i = 0; i < mm.getNumberOfCells(); i++)
    {
      for(int m = 0; m < mm.getNumberOfMaterials(); m++)
      {
        for(int c = 0; c < map.numComp(); ++c)
        {
          double* valptr = map.findValue(i, m, c);
          // ^ contains a hidden for-loop for sparse layouts, O(row_size) time
          if(valptr)
          {
            sum += *valptr;
          }
        }
      }
    }

For algorithms where sparse data traversal is desired, the MultiMat indexing sets can be used directly as an alternative to dense traversal patterns. Cells are iterated first in this example since the field has a CELL_DOM data layout. The materials for the current cell are queried are used to to compute an index into the field data.

      auto map = mm.get2dField<double>("CellMat Array");

      for(int i = 0; i < mm.getNumberOfCells(); i++)
      {
        //the materials (by id) in this cell
        MultiMat::IdSet setOfMaterialsInThisCell = mm.getMatInCell(i);
        //the indices into the maps
        MultiMat::IndexSet indexSet = mm.getIndexingSetOfCell(i, sparsity);

        SLIC_ASSERT(setOfMaterialsInThisCell.size() == indexSet.size());
        for(int j = 0; j < indexSet.size(); j++)
        {
          //int idx = setOfMaterialsInThisCell.at(j); //mat_id
          for(int comp = 0; comp < map.numComp(); ++comp)
          {
            double val = map[indexSet[j] * map.numComp() + comp];  //<-----

            //if 1dMap is used, this is also valid
            //SLIC_ASSERT(val == map(indexSet[j], comp));

            sum += val;
          }
        }
      }

MultiMat 2D fields provide iterators as a means for writing simpler code. The iterators can be used to write sparse data traversal algorithms without the added complexity of using index sets.

    auto map2d = mm.get2dField<double>("CellMat Array");
    for(int i = 0; i < mm.getNumberOfCells() /*map2d.firstSetSize()*/; i++)
    {
      for(auto iter = map2d.set_begin(i); iter != map2d.set_end(i); iter++)
      {
        // int idx = iter.index(); get the index
        for(int comp = 0; comp < map2d.numComp(); ++comp)
        {
          sum += iter(comp);                            //<----
          SLIC_ASSERT(iter(comp) == iter.value(comp));  //value()
        }
        SLIC_ASSERT(iter(0) == (*iter)[0]);  //2 ways to get the first component
        SLIC_ASSERT(iter(0) == iter.value(0));
      }
    }

Flat iterators can be used to traverse all data in a field. This is useful when computing values over the field and the algorithm does not need to know the cell or material associated with the data.

    auto map2d = mm.get2dField<double>("CellMat Array");
    for(auto iter = map2d.set_begin(); iter != map2d.set_end(); ++iter)
    {
      //get the indices
      //int cell_id = iter.firstIndex();
      //int mat_id = iter.secondIndex();
      for(int comp = 0; comp < map2d.numComp(); ++comp)
      {
        double val = iter(comp);               //<----
        SLIC_ASSERT(val == iter.value(comp));  //another way to get the value
        sum += val;
      }
    }

Conversions

MultiMat can store fields with various data mappings, layouts, and sparsity values. This allows for great flexibility in how fields are represented in memory. MultiMat provides conversion routines that allow fields to be converted internally between various representations.

Field conversion can be done for a variety of reasons. Perhaps fields are converted from sparse to dense to expose them to an external library that needs dense data. Or, perhaps dense fields are retrieved from I/O routines and then made sparse in MultiMat during simulation execution. Other times, depending on the needs of an algorithm, it can make sense to transpose the data, changing CELL_DOM to MAT_DOM or vice-versa. The following methods perform these data conversions and they take a field index as an argument.

Convert field sparsity:

  • convertFieldToSparse()

  • convertFieldToDense()

Convert field data layout:

  • transposeField()

  • convertFieldToMatDom()

  • convertFieldToCellDom()