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()