Repository navigation
Step 5 Store Element Data with Mesh Handle
In this tutorial, we will learn how to associate custom data with the elements of a mesh and how to exchange data over the ghost elements of a parallel partition.
You will find the code to this example in the tutorials/mesh_handle/t8_mesh_step5_element_data.cxx and it creates the executable tutorials/mesh_handle/t8_mesh_step5_element_data.
In the last tutorials we learned how to create a mesh handle mesh and adapt it. In step 4 we also learned about algorithms for partitioning, balancing and creating a ghost layer. In this tutorial, we will start by performing all these operations in one step. After that, we will continue with building a data array and gathering data for the local elements of our process. Further we will exchange the data values of the ghost elements and output the volume data to our .vtu file to visualize it with Paraview.
As already explained in step 4, it is possible to mix the set_* functions together as you wish (see mesh.hxx). We can thus in one step adapt a mesh, balance it, partition it and create a ghost layer. The order in which the set_* functions are called is irrelevant.
Let us execute the example with 4 MPI processes.
mpirun -np 4 ./t8_mesh_step5_element_data
As expected, the resulting mesh looks like the adapted mesh from step 4 before we call the adaption a second time.
With the standard t8_forest, element data was managed entirely by the user. A separate array had to be allocated with enough space for both local and ghost elements, and the user was responsible for keeping the array synchronized with the elements in the forest (see step 5 of the general t8code tutorials).
With the mesh_handle interface we can do this step much simpler. Instead of managing a separate data array, the data becomes part of the mesh itself. After specifying the data type once when creating the mesh, you can directly assign data of this type to each element. Memory management and the association between elements and their data are handled automatically by our mesh.
We first define the type of the data that should be stored for every element. In this example, each element stores its refinement level and volume. These are information that can of course be also accessed via the elements directly but are here stored as element data for demonstration purpose only.
struct data_per_element_type
{
int level;
double volume;
};When creating the mesh, this type is registered as the element data type by adding the corresponding data competences:
using mesh_class = t8_mesh_handle::mesh<t8_mesh_handle::data_element_competences, t8_mesh_handle::data_mesh_competences<data_per_element_type>>;(For a deeper dive into mesh handle competences and competence packs, look into step A of the mesh handle tutorials.)
With this, the mesh now allocates and manages storage for a data vector of type data_per_element_type with one entry per (local) element.
To assign data values, we simply iterate over the elements of the mesh. For each element, we create an instance of our data structure and store it with .set_element_data():
for (auto &elem : mesh) {
data_per_element_type data{
elem.get_level(),
elem.get_volume()
};
elem.set_element_data(data);
}Please also note, that it is possible to transfer an already complete data vector onto the mesh with
mesh.set_element_data(std::move(example_data));;
After all local element data has been initialized, the ghost values can be synchronized with a single function call:
mesh.exchange_ghost_data();
Our mesh knows which data belongs to which element and automatically exchanges the element data associated with ghost elements. With mesh_handle there is no need to allocate additional storage, wrap data into an sc_array or manually determine which entries correspond to ghost elements.
After this exchange is completed, the ghost elements contain the same data as their owning processes.
The data stored on an element can be retrieved directly from the element itself:
auto data = elem.get_element_data();
Or, for a specific value of a specific element:
elem.get_element_data().volume;
This interface works identically for local and ghost elements. Since the mesh owns the data, the user no longer has to distinguish between local and ghost entries.
After creating the mesh and storing elements’ volumes and levels we write the mesh and the volumes of the elements out to .vtu files to visualize everything in Paraview.
In the last steps we just called the function
t8_mesh_handle::write_mesh_to_vtk(mesh, prefix);
to write a mesh as .vtu . But to write our defined user data as well, we need an extended version of this function called write_mesh_to_vtk_ext.
t8code supports writing element based data to .vtu files as long as its stored as doubles or vectors. Each of the data fields to write has to be provided in its own array of length num_local_elements. We support two types:
| VTK variable type | Double per element |
|---|---|
| T8_VTK_SCALAR | 1 |
| T8_VTK_VECTOR | 3 |
Therefore, we need to allocate a new array to store the volumes on their own. This array has one entry per local element.
Double *element_volumes = T8_ALLOC (double, num_elements);
Other than writing user data, write_mesh_to_vtk_ext also offers more control over which properties of the mesh to write:
| Parameter | Description |
|---|---|
| mesh | The mesh to write |
| fileprefix | The prefix of the files |
| num_data | Number of user-defined double valued data fields to write |
| data | Array of t8_vtk_data_filed_t of length num_data providing the user-defined per element data |
| write_treeid | If true, the global tree id of the underlying forest is written to each element |
| write_mpirank | If true, the mpirank is written for each element |
| write_level | If true, the refinement level is written for each element |
| write_element_id | If true, the global element id is written for each element |
| write_ghosts | If true, each process additionally writes its ghost elements |
| write_curved | If true, write the elements as curved element types from vtk |
| do_not_use_API | If true, do not use the VTK API, even if linked and available |
For more documentation take a look into mesh_io.hxx. Note, you can store even more data into the .vtu file. The number of user defined data fields is defined by the parameter num_data. For each user defined data field, we need one t8_vtk_data_field_t variable. In our case, it is only one.
t8_vtk_data_field_t vtk_data;
Next, set the type of this variable. Since we only have one value per element, we can pick T8_VTK_SCALAR:
vtk_data.type = T8_VTK_SCALAR;
Also, the name of the field as should be written to the file.
strcpy (vtk_data.description, "Element volume");
Finally, we copy the elements’ volumes from our mesh to the output array and pass it to write_mesh_to_vtk_ext
We decided to only set set write_treeid, write_mpirank, write_level and write_element_id to true as it would be in the standard function.
In this way, the only difference to write_mesh_to_vtk is the addition of the volume of the elements.
We can now display the volumes of the elements and work with them in Paraview:
General
Step 0 Hello World
Step 1 Creating a coarse mesh
Step 2 Creating a uniform forest
Step 3 Adapting a forest
Step 4 Partition,-Balance,-Ghost
Step 5 Store element data
Step 6 Computing stencils
Step 7 Interpolation
Features
Step 2 Creating a uniform mesh
Step 3 Adapting a mesh
Step 4 Partition, Balance, Ghost
Step 5 Store Element Data
Step A Mesh Competences
Prerequisites & Compilation
t8_time_forest_partition
t8_time_fractal
t8_time_new_refine
t8_time_partition
t8_time_prism
t8_time_set_join_by_vertices
Documentation
Tree Indexing
Element Indexing
Running on JUWELS using Slurm
Overview of the most used API functions
Known issues
Workflow - FreeCAD to t8code
Reproducing Scaling Results
Coding Guidelines
Contribution Workflow
CI Guidelines
Tips
Debugging with gdb
Debugging with valgrind
Development modules
Generating a code coverage report
Test driven development
Testing with GoogleTest
Writing C interface code