An introductory example¶
This file contains an introductory example to define and traverse a simple quadrilateral
mesh. The code for this example can be found in
axom/src/axom/slam/examples/UserDocs.cpp
.
We first import the unified Slam header, which includes all necessary files for working with slam:
#include "axom/slam.hpp"
Note
All code in slam is in the axom::slam
namespace.
For convenience, we add the following namespace declaration to our example to allow us
to directly use the slam
namespace:
namespace slam = axom::slam;
Type aliases and variables¶
We begin by defining some type aliases for the Sets, Relations and Maps in our mesh. These type aliases would typically be found in a configuration file or in class header files.
We use the following types throughout this example:
using ArrayIndir = slam::policies::CArrayIndirection<PosType, ElemType>;
Sets¶
Our mesh is defined in terms of two sets: Vertices and Elements, whose entities are
referenced by integer-valued indices. Since both sets use a contiguous range of indices
starting from 0, we use slam::PositionSet
to represent them.
We define the following type aliases:
using PosType = slam::DefaultPositionType;
using ElemType = slam::DefaultElementType;
using VertSet = slam::PositionSet<PosType, ElemType>;
using ElemSet = slam::PositionSet<PosType, ElemType>;
and declare them as:
VertSet verts; // The set of vertices in the mesh
ElemSet elems; // The set of elements in the mesh
For other available set types, see Set.
Relations¶
We also have relations describing the incidences between the mesh vertices and elements.
The element-to-vertex boundary relation encodes the indices of the vertices in the
boundary of each element. Since this is a quad mesh and there are always four vertices in
the boundary of a quadrilateral, we use a ConstantCardinality
policy with a
CompileTimeStride
set to 4 for this StaticRelation
.
// Type aliases for element-to-vertex boundary relation
enum
{
VertsPerElem = 4
};
using CTStride = slam::policies::CompileTimeStride<PosType, VertsPerElem>;
using ConstCard = slam::policies::ConstantCardinality<PosType, CTStride>;
using ElemToVertRelation =
slam::StaticRelation<PosType, ElemType, ConstCard, ArrayIndir, ElemSet, VertSet>;
The vertex-to-element coboundary relation encodes the indices of all elements incident
in each of the vertices. Since the cardinality of this relation changes for different
vertices, we use a VariableCardinality
policy for this StaticRelation
.
// Type aliases for vertex-to-element coboundary relation
using VarCard = slam::policies::VariableCardinality<PosType, ArrayIndir>;
using VertToElemRelation =
slam::StaticRelation<PosType, ElemType, VarCard, ArrayIndir, VertSet, ElemSet>;
We declare them as:
ElemToVertRelation bdry; // Boundary relation from elements to vertices
VertToElemRelation cobdry; // Coboundary relation from vertices to elements
For other available set types, see Relation.
Maps¶
Finally, we have some maps that attach data to our sets.
The following defines a type alias for the positions of the mesh vertices.
It is templated on a point type (Point2
) that handles simple operations on 2D points.
using BaseSet = slam::Set<PosType, ElemType>;
using ScalarMap = slam::Map<Point2, BaseSet>;
using PointMap = slam::Map<Point2, BaseSet>;
using VertPositions = PointMap;
It is declared as:
VertPositions position; // vertex position
Constructing the mesh¶
This example uses a very simple fixed mesh, which is assumed to not change after it has been initialized.
Sets¶
The sets are created using a constructor that takes the number of elements.
verts = VertSet(11); // Construct vertex set with 11 vertices
elems = ElemSet(5); // Construct the element set with 5 elements
The values of the vertex indices range from 0
to verts.size()-1
(and similarly
for elems
).
Note
All sets, relations and maps in Slam have internal validity checks using
the isValid()
function:
SLIC_ASSERT_MSG(verts.isValid(), "Vertex set is not valid.");
SLIC_ASSERT_MSG(elems.isValid(), "Elment set is not valid.");
Relations¶
The relations are constructed by binding their associated sets and arrays of data to the
relation instance. In this example, we use an internal helper class RelationBuilder
.
We construct the boundary relation by attaching its two sets (elems
for its
fromSet
and verts
for its toSet
) and an array of indices for the relation’s
data.
// construct boundary relation from elements to vertices
using RelationBuilder = ElemToVertRelation::RelationBuilder;
bdry = RelationBuilder().fromSet(&elems).toSet(&verts).indices(
RelationBuilder::IndicesSetBuilder()
.size(static_cast<int>(evInds.size()))
.data(evInds.data()));
The Coboundary relation requires an additional array of offsets (begins
) to indicate
the starting index in the relation for each vertex:
// construct coboundary relation from vertices to elements
using RelationBuilder = VertToElemRelation::RelationBuilder;
cobdry = RelationBuilder()
.fromSet(&verts)
.toSet(&elems)
.begins(RelationBuilder::BeginsSetBuilder()
.size(verts.size())
.data(veBegins.data()))
.indices(RelationBuilder::IndicesSetBuilder()
.size(static_cast<int>(veInds.size()))
.data(veInds.data()));
Since these are static relations, we used data that was constructed elsewhere.
Note that these relations are lightweight wrappers over the underlying data – no data is
copied. To iteratively build the relations, we would use the DynamicConstantRelation
and DynamicVariableRelation
classes.
See Simplifying mesh setup for more details about Slam’s Builder
classes for sets,
relations and maps.
Maps¶
We define the positions of the mesh vertices as a Map
on the verts
set.
For this example, we set the first vertex to lie at the origin, and the remaining
vertices line within an annulus around the unit circle.
// construct the position map on the vertices
position = VertPositions(&verts);
// first vertex is at origin
position[0] = Point2(0., 0.);
// remaining vertices lie within annulus around unit disk
// in cw order, starting at angleOffset
constexpr double rInner = 0.8;
constexpr double rOuter = 1.2;
constexpr double angleOffset = 0.75;
const double N = verts.size() - 1;
for(int i = 1; i < verts.size(); ++i)
{
const double angle = -(i - 1) / N * 2 * M_PI + angleOffset;
const double mag = axom::utilities::random_real(rInner, rOuter);
position[i] = Point2(mag * std::cos(angle), mag * std::sin(angle));
}
Traversing the mesh¶
Now that we’ve constructed the mesh, we can start traversing the mesh connectivity and attaching more fields.
Computing a derived field¶
Our first traversal loops through the vertices and computes a derived field on the position map. For each vertex, we compute its distance to the origin.
// Create a Map of scalars over the vertices
ScalarMap distances(&verts);
for(int i = 0; i < distances.size(); ++i) // <-- Map::size()
{
auto vID = verts[i]; // <-- Set::operator[]
const Point2& pt = position[vID]; // <-- Map::operator[]
distances[i] = std::sqrt(pt[0] * pt[0] // <-- Map::operator[]
+ pt[1] * pt[1]);
}
Computing element centroids¶
Our next example uses element-to-vertex boundary relation to compute the centroids of each element as the average of its vertex positions.
// Create a Map of Point2 over the mesh elements
using ElemCentroidMap = PointMap;
ElemCentroidMap centroid = ElemCentroidMap(&elems);
// for each element...
for(int eID = 0; eID < elems.size(); ++eID) // <-- Set::size()
{
Point2 ctr;
auto elVerts = bdry[eID]; // <-- Relation::operator[]
// find average position of incident vertices
for(int i = 0; i < elVerts.size(); ++i) // <-- Set::size()
{
auto vID = elVerts[i]; // <-- Set::operator[]
ctr += position[vID]; // <-- Map::operator[]
}
ctr /= elVerts.size(); // <-- Set::size())
centroid[eID] = ctr; // <-- Map::operator[]
}
Perhaps the most interesting line here is when we call the relation’s subscript
operator (bdry[eID]
). This function takes an element index
(eID
) and returns the set of vertices that are incident in this element.
As such, we can use all functions in the Set API on this return type, e.g. size()
and the subscript operator.
Outputting mesh to disk¶
As a final example, we highlight several different ways to iterate through the
mesh’s Sets, Relations and Maps as we output the mesh to disk (in the vtk
format).
This is a longer example, but the callouts
(left-aligned comments of the form // <-- message
) point to
different iteration patterns.
std::ofstream meshfile;
meshfile.open("quadMesh.vtk");
std::ostream_iterator<PosType> out_it(meshfile, " ");
// write header
meshfile << "# vtk DataFile Version 3.0\n"
<< "vtk output\n"
<< "ASCII\n"
<< "DATASET UNSTRUCTURED_GRID\n\n"
<< "POINTS " << verts.size() << " double\n";
// write positions
for(auto pos : position) // <-- Uses range-based for on position map
{
meshfile << pos[0] << " " << pos[1] << " 0\n";
}
// write elem-to-vert boundary relation
meshfile << "\nCELLS " << elems.size() << " " << 5 * elems.size();
for(auto e : elems) // <-- uses range-based for on element set
{
meshfile << "\n4 ";
std::copy(bdry.begin(e), // <-- uses relation's iterators
bdry.end(e),
out_it);
}
// write element types ( 9 == VKT_QUAD )
meshfile << "\n\nCELL_TYPES " << elems.size() << "\n";
for(int i = 0; i < elems.size(); ++i)
{
meshfile << "9 ";
}
// write element ids
meshfile << "\n\nCELL_DATA " << elems.size() << "\nSCALARS cellIds int 1"
<< "\nLOOKUP_TABLE default \n";
for(int i = 0; i < elems.size(); ++i)
{
meshfile << elems[i] << " "; // <-- uses size() and operator[] on set
}
// write vertex ids
meshfile << "\n\nPOINT_DATA " << verts.size() << "\nSCALARS vertIds int 1"
<< "\nLOOKUP_TABLE default \n";
for(int i = 0; i < verts.size(); ++i)
{
meshfile << verts[i] << " ";
}
meshfile << "\n";