BUMP Utilities¶
The BUMP component contains several useful building blocks for writing algorithms for Blueprint meshes.
Structured as classes with an
execute()methodOften templated on execution space and views
Classes are used so algorithms can contain state such as view objects and so various algorithm stages or utility functions can be written as helper methods. Classes also allow algorithms to be modified using inheritance and overriding methods. Finally, classes allow the BUMP component to provide specialized versions of templated algorithms.
Blueprint meshes consist of various parts such as coordsets, topologies, fields, and matsets. These constructs are organized as various paths within a root Conduit node. Elements such as strings and scalar values can be stored as usual within a Conduit node in host memory. Conduit nodes that contain bulk data such as coordinate or field data should point to memory blocks that are valid for the execution environment in which algorithms will run. To achieve this, entire Conduit node hierarchies can be copied to the proper memory space or they can be constructed by forcing Conduit to allocate memory in the proper memory space.
NOTE: Some code examples use a utils:: namespace prefix as shorthand for axom::bump::utilities.
Copying Blueprint Data¶
If a conduit::Node containing Blueprint data is not in a memory space appropriate
for the target execution space, the data can be copied to a suitable memory space using
the axom::bump::utilities::copy<ExecSpace>() function. The target execution
space (and thus its memory space) is specified using the copy function’s ExecSpace
template argument. The copy function copies the source conduit::Node to
the destination conduit::Node, making sure to use the appropriate Axom allocator for
non-string bulk arrays (e.g. arrays of ints, floats, doubles, etc.). Data small enough to
fit in a conduit::Node and strings are left in the host memory space, which lets
algorithms on the host side query them. For data that have been moved to the device,
their sizes and data types can still be queried using normal Conduit mechanisms such
as the conduit::Node::dtype() method.
conduit::Node hostMesh, deviceMesh, hostMesh2;
// host->device
axom::bump::utilities::copy<axom::HIP_EXEC<256>>(deviceMesh, hostMesh);
// device->host
axom::bump::utilities::copy<axom::SEQ_EXEC>(hostMesh2, deviceMesh);
ClipField¶
The axom::bump::extraction::ClipField class intersects all the zones in the input Blueprint
mesh with an implicit surface where the selected input field equals zero and produces a new
Blueprint mesh based on the selected zone fragments produced by the intersection. This can be thought
of as an isosurface algorithm but with a volumetric output mesh where the mesh is either inside or
outside of the selected isovalue. The ClipField class has multiple template arguments to
select the execution space, the type of topology view, the type of coordset view, and the
type of intersector used to determine intersections. The default intersection uses an isosurface-
based intersection method, though other intersectors could be created to perform plane
or sphere intersections.
// Make views for the device mesh.
conduit::Node &n_x = deviceMesh.fetch_existing("coordsets/coords/values/x");
conduit::Node &n_y = deviceMesh.fetch_existing("coordsets/coords/values/y");
conduit::Node &n_z = deviceMesh.fetch_existing("coordsets/coords/values/z");
axom::ArrayView<float> xView(static_cast<float *>(n_x.data_ptr()),
n_x.dtype().number_of_elements());
axom::ArrayView<float> yView(static_cast<float *>(n_y.data_ptr()),
n_y.dtype().number_of_elements());
axom::ArrayView<float> zView(static_cast<float *>(n_z.data_ptr()),
n_z.dtype().number_of_elements());
CoordsetView coordsetView(xView, yView, zView);
conduit::Node &n_conn = deviceMesh.fetch_existing("topologies/topo/elements/connectivity");
axom::ArrayView<int> connView(static_cast<int *>(n_conn.data_ptr()),
n_conn.dtype().number_of_elements());
TopoView topoView(connView);
// Clip the data
conduit::Node deviceClipMesh, options;
axom::bump::extraction::ClipField<ExecSpace, TopoView, CoordsetView> clipper(topoView,
coordsetView);
options["field"] = "distance";
options["value"] = 0.;
options["inside"] = 1;
options["outside"] = 1;
clipper.execute(deviceMesh, options, deviceClipMesh);
CoordsetBlender¶
The axom::bump::CoordsetBlender class takes a BlendData and makes
a new explicit coordset where each new point corresponds to one blend group. A “BlendData” is
an object that groups several array views that describe a set of blend groups. Each blend group
is formed from a list of node ids and weight values. A new coordinate is formed by looking
up the points in the blend group in the source coordset and multiplying them by their weights
and summing them together to produce the new point for the output coordset. Classes such as
ClipField use CoordsetBlender to make new coordsets that contain points that were a
combination of multiple points in the input coordset.
axom::bump::CoordsetBlender<ExecSpace, CoordsetView, axom::bump::SelectSubsetPolicy> cb;
cb.execute(blend, m_coordsetView, n_coordset, n_newCoordset, getAllocatorID());
CoordsetSlicer¶
The axom::bump::CoordsetSlicer class takes SliceData and makes a
new explicit coordset where each point corresponds to a single index from the node indices
stored in SliceData. This class can be used to select a subset of a coordset, reorder nodes
in a coordset, or repeat nodes in a coordset.
axom::bump::CoordsetSlicer<ExecSpace, CoordsetView> cs(m_coordsetView);
cs.setAllocatorID(getAllocatorID());
n_newCoordset.reset();
cs.execute(nodeSlice, n_coordset, n_newCoordset);
CutField¶
The axom::bump::extraction::CutField class intersects all the zones in the input Blueprint
mesh with an implicit surface where the selected input field equals zero. This is an isosurface
algorithm, though it could be used with other intersection policies. The default algorithm
produces a new Blueprint mesh containing topologically 2D or 1D zone fragments, based on the
input mesh’s dimension. The CutField class has multiple template arguments to
select the execution space, the type of topology view, the type of coordset view, and the
type of intersector used to determine intersections. The default intersection uses an isosurface-
based intersection method, though other intersectors could be created to perform plane
or sphere intersections.
// Wrap the data in views.
auto coordsetView = axom::bump::views::make_rectilinear_coordset<conduit::float64, NDIMS>::view(
deviceMesh["coordsets/coords"]);
using CoordsetView = decltype(coordsetView);
auto topologyView =
axom::bump::views::make_rectilinear_topology<NDIMS>::view(deviceMesh["topologies/mesh"]);
using TopologyView = decltype(topologyView);
conduit::Node hostOptions;
hostOptions["field"] = "braid";
hostOptions["value"] = 1.;
conduit::Node deviceOptions, deviceResult;
utils::copy<ExecSpace>(deviceOptions, hostOptions);
axom::bump::extraction::CutField<ExecSpace, TopologyView, CoordsetView> iso(topologyView,
coordsetView);
iso.execute(deviceMesh, deviceOptions, deviceResult);
ExtractZones¶
The axom::bump::ExtractZones class takes a list of selected zone ids and extracts
a new mesh from a source mesh that includes only the selected zones. There is a derived class
ExtractZonesAndMatset that also extracts a matset, if present.
ExtractZones<ExecSpace, TopologyView, CoordsetView>::execute(selectedZonesView,
n_input,
n_options,
n_output);
ExtrudeMesh¶
The axom::bump::ExtrudeMesh class extrudes a 2D Blueprint mesh composed
of triangles and quad shapes (polygons are not yet supported) and produces 3D zones repeated some
number of times in the Z direction. Fields and matsets are also extruded.
// Make new VFs via mapper.
const int coarseNodesInZ = 4;
using SrcExtruder = bump::ExtrudeMesh<ExecSpace, SrcTopologyView, SrcCoordsetView>;
SrcExtruder srcExt(srcTopo, srcCoordset);
conduit::Node n_opts;
n_opts["nz"] = coarseNodesInZ;
n_opts["z0"] = 0.;
n_opts["z1"] = 3.;
n_opts["topologyName"] = "postmir";
n_opts["outputTopologyName"] = "epm"; // epm = "Extruded Post MIR"
n_opts["outputCoordsetName"] = "epm_coords";
n_opts["outputMatsetName"] = "epm_matset";
srcExt.execute(n_dev, n_opts, n_dev);
FieldBlender¶
The axom::bump::FieldBlender class is similar to the CoordsetBlender
class, except that it operates on a field instead of coordsets. The class is used to create a
new field that includes values derived from multiple weighted source values.
FieldSlicer¶
The axom::bump::FieldSlicer class selects specific indices from a
field and makes a new field.
std::vector<axom::IndexType> indices {0, 1, 2, 7, 8, 9};
axom::Array<axom::IndexType> sliceIndices(indices.size(),
indices.size(),
axom::execution_space<ExecSpace>::allocatorID());
axom::copy(sliceIndices.data(), indices.data(), sizeof(axom::IndexType) * indices.size());
bump::SliceData slice;
slice.m_indicesView = sliceIndices.view();
conduit::Node slicedMesh;
bump::FieldSlicer<ExecSpace> fs;
fs.execute(slice, deviceMesh["fields/scalar"], slicedMesh["fields/scalar"]);
fs.execute(slice, deviceMesh["fields/vector"], slicedMesh["fields/vector"]);
MakePointMesh¶
The axom::bump::MakePointMesh class generates a point at the center
of each zone (or selected set of zones) in an input topology and generates a new unstructured
topology consisting of points located at those zone centers.
// Make a point mesh of the selected zones.
bump::MakePointMesh<ExecSpace, TopologyView, CoordsetView> pm(m_topologyView, m_coordsetView);
pm.setAllocatorID(getAllocatorID());
pm.execute(cleanZones, n_topology, n_coordset, n_options, n_cleanOutput);
MakePolyhedralTopology¶
The axom::bump::MakePolyhedralTopology class transforms an
input topology from its native form to an unstructured polyhedral topology. The output
topology uses the same coordset as the input topology. The faces produced from each zone
in the source topology will not be unique. The MergePolyhedralFaces class can be used
to merge polyhedral faces so they are unique.
// Run the algorithm
const conduit::Node &n_input = deviceMesh["topologies/mesh"];
conduit::Node &n_output = deviceMesh["topologies/polymesh"];
if(type == "uniform")
{
auto topologyView = views::make_uniform_topology<3>::view(n_input);
using TopologyView = decltype(topologyView);
using ConnectivityType = typename TopologyView::ConnectivityType;
bump::MakePolyhedralTopology<ExecSpace, TopologyView> mp(topologyView);
mp.execute(n_input, n_output);
bump::MergePolyhedralFaces<ExecSpace, ConnectivityType>::execute(n_output);
}
MakeUnstructured¶
The axom::bump::MakeUnstructured class takes a structured topology
and creates a new unstructured topology. This class does not need views to wrap the input
structured topology.
conduit::Node deviceResult;
bump::MakeUnstructured<ExecSpace> uns;
uns.execute(deviceMesh["topologies/mesh"], deviceMesh["coordsets/coords"], "mesh", deviceResult);
MakeZoneCenters¶
The axom::bump::MakeZoneCenters class takes an input Blueprint
topology and produces a new element-associated Blueprint vector field that contains the zone
centers. The number of components in the vector will match the number of components for the
topology’s coordset. The zone center is computed as the average of the node coordinates used
in the zone. Likewise, the type (e.g. float, double) used to compute and represent the zone
centers will match the type of the values that define the coordset.
bump::MakeZoneCenters<ExecSpace, TopologyView, CoordsetView> zc(m_topologyView, m_coordsetView);
conduit::Node n_zcfield;
zc.setAllocatorID(getAllocatorID());
zc.execute(n_topo, n_coordset, n_zcfield);
MakeZoneVolumes¶
The axom::bump::MakeZoneVolumes class takes an input Blueprint
topology and produces a new element-associated Blueprint vector field that contains the zone
volumes for 3D, or areas for 2D.
MatsetSlicer¶
The axom::bump::MatsetSlicer class is similar to the FieldSlicer
class except it slices matsets instead of fields. The same SliceData can be passed to
MatsetSlicer to pull out and assemble a new matset data for a specific list of zones.
MatsetSlicer<ExecSpace, MatsetView> ms(m_matsetView);
ms.setAllocatorID(this->getAllocatorID());
SliceData zSlice;
zSlice.m_indicesView = selectedZonesView;
ms.execute(zSlice, n_matset, n_newMatset);
MergeCoordsetPoints¶
The axom::bump::MergeCoordsetPoints class merges duplicate
coordinates in an input coordset, within a given tolerance.
The tolerance is passed via an options node with a key value called “tolerance”. Points
are merged by first rounding off extra precision in a temporary point copy that is used to
make a hashed name for the point. Points within the tolerance get the same hashed
name and points are made unique using the Unique class.
The selected point for each unique name is copied into the coordset, overwriting the old
coordset values. The number of points in the coordset will change if any merging is done.
This means that any topologies that reference the coordset will need to be updated.
The class passes out a selectedIds array containing indices from the original coordset
that are used in the new coordset. This information can be used with FieldSlicer to
slice/update vertex fields. An old2new array is also returned, which is a
map to convert old node indices to new indices in the updated coordset. This map can be
used to update connectivity node numbers.
namespace utils = axom::bump::utilities;
axom::Array<axom::IndexType> old2new;
auto newCoordsetView =
axom::bump::views::make_explicit_coordset<CoordType, CoordsetView::dimension()>::view(
n_coordset);
using NewCoordsetView = decltype(newCoordsetView);
axom::bump::MergeCoordsetPoints<ExecSpace, NewCoordsetView> mcp(newCoordsetView);
conduit::Node n_mcp_options;
n_mcp_options["tolerance"] = point_tolerance;
const bool merged = mcp.execute(n_coordset, n_mcp_options, selectedIds, old2new);
MergeMeshes¶
The axom::bump::MergeMeshes class merges data for coordsets,
topology, and fields from multiple input meshes into a new combined mesh. The class also
supports renaming nodes using a map that converts a local mesh’s node ids to the final
output node numbering, enabling meshes to be merged such that some nodes get combined.
A derived class can also merge matsets.
std::vector<bump::MeshInput> inputs(2);
inputs[0].m_input = deviceMesh.fetch_ptr("domain0000");
inputs[1].m_input = deviceMesh.fetch_ptr("domain0001");
inputs[1].m_nodeMapView = deviceNodeMap.view();
inputs[1].m_nodeSliceView = deviceNodeSlice.view();
MergePolyhedralFaces¶
The axom::bump::MergePolyhedralFaces class takes an input
Blueprint topology, which may have duplicated faces, and makes the face definitions
in the subelements unique and rewrites the subelement and element connectivity. For
faces to be merged successfully, the faces must reference the same coordinate indices
in the coordset. The MergePolyhedralFaces class modifies the Conduit node that
contains the input polyhedral topology.
// Run the algorithm
const conduit::Node &n_input = deviceMesh["topologies/mesh"];
conduit::Node &n_output = deviceMesh["topologies/polymesh"];
if(type == "uniform")
{
auto topologyView = views::make_uniform_topology<3>::view(n_input);
using TopologyView = decltype(topologyView);
using ConnectivityType = typename TopologyView::ConnectivityType;
bump::MakePolyhedralTopology<ExecSpace, TopologyView> mp(topologyView);
mp.execute(n_input, n_output);
bump::MergePolyhedralFaces<ExecSpace, ConnectivityType>::execute(n_output);
}
NodeToZoneRelationBuilder¶
The axom::bump::NodeToZoneRelationBuilder class creates a Blueprint
O2M (one to many) relation that relates node numbers to the zones that contain them. This mapping
is akin to inverting the normal mesh connectivity which is a map of zones to node ids. The O2M
relation is useful for recentering data from the zones to the nodes.
const conduit::Node &deviceTopo = deviceMesh["topologies/mesh"];
const conduit::Node &deviceCoordset = deviceMesh["coordsets/coords"];
// Run the algorithm on the device
conduit::Node deviceRelation;
bump::NodeToZoneRelationBuilder<ExecSpace> n2z;
n2z.execute(deviceTopo, deviceCoordset, deviceRelation);
PlaneSlice¶
The axom::bump::extraction::PlaneSlice class slices input Blueprint geometry using
a slice plane and produces a new Blueprint output mesh. This algorithm is a close cousin
to axom::bump::extraction::CutField, except that it uses a plane intersector that
accepts “origin” and “normal” parameters to specify the slice plane.
// Encode the plane in the options.
conduit::Node hostOptions;
hostOptions["topology"] = "mesh";
hostOptions["normal"].set(it->second.getNormal().data(), NDIMS);
auto origin = it->second.getNormal() * it->second.getOffset();
hostOptions["origin"].set(origin.data(), NDIMS);
conduit::Node deviceOptions, deviceResult;
utils::copy<ExecSpace>(deviceOptions, hostOptions);
axom::bump::extraction::PlaneSlice<ExecSpace, TopologyView, CoordsetView> slice(topologyView,
coordsetView);
slice.execute(deviceMesh, deviceOptions, deviceResult);
PrimalAdaptor¶
The axom::bump::PrimalAdaptor class takes a topology view and a
coordset view and makes it possible to retrieve a zone as a shape from Axom’s Primal
component. For example, the PrimalAdaptor class can wrap a topology view that contains 2D
shapes such as triangles, quads, polygons and allow them to be accessed as an
axom::primal::Polygon. For 3D, Primal shapes are returned for meshes that contain
tetrahedra, hexahedra, or polyhedra. For unstructured meshes that contain pyramids or
wedges, or mixed shapes, a VariableShape is returned that allows those shapes to be
represented using one or more primal shapes.
When the class is instantiated with
axom::bump::views::UnstructuredTopologyPolyhedralView as its topology view, the getShape()
method will normally return axom::primal::Polyhedron. Converting between Blueprint polyhedron
zones and Axom Polyhedron objects is sometimes overkill so the PrimalAdaptor class can
also expose polyhedra as a special PolyhedralFaces representation that represents the
polyhedron as a collection of axom::primal::Plane objects. This mode is selected by
instantiating PrimalAdaptor with the makeFaces template parameter set to true.
// Get the zone as a primal shape and compute area or volume, as needed.
using ShapeView = PrimalAdaptor<TopologyView, CoordsetView>;
const ShapeView deviceShapeView {m_topologyView, m_coordsetView};
axom::for_all<ExecSpace>(
m_topologyView.numberOfZones(),
AXOM_LAMBDA(axom::IndexType zoneIndex) {
const auto shape = deviceShapeView.getShape(zoneIndex);
// Get the area or volume of the target shape (depends on the dimension).
double amount = utils::ComputeShapeAmount<CoordsetView::dimension()>::execute(shape);
valuesView[zoneIndex] = amount;
});
RecenterField¶
The axom::bump::RecenterField class uses an O2M relation to average
field data from multiple values to an averaged value. In Axom, this is used to convert a field
associated with the elements to a new field associated with the nodes.
const conduit::Node &deviceTopo = deviceMesh["topologies/mesh"];
const conduit::Node &deviceCoordset = deviceMesh["coordsets/coords"];
// Make a node to zone relation on the device.
conduit::Node deviceRelation;
bump::NodeToZoneRelationBuilder<ExecSpace> n2z;
n2z.execute(deviceTopo, deviceCoordset, deviceRelation);
// Recenter a field zonal->nodal on the device
bump::RecenterField<ExecSpace> r;
r.execute(deviceMesh["fields/easy_zonal"], deviceRelation, deviceMesh["fields/z2n"]);
// Recenter a field nodal->zonal on the device. (The elements are an o2m relation)
r.execute(deviceMesh["fields/z2n"],
deviceMesh["topologies/mesh/elements"],
deviceMesh["fields/n2z"]);
SelectedZones¶
The axom::bump::SelectedZones class creates an array view that
represents selected zone ids. The zone ids are obtained either from a Conduit options
node containing a “selectedZones” array, if the array is present. If the “selectedZones”
array is not present, the class makes an array of zone ids that selects all zones in the
associated topology.
// Get selected zones from the options.
bump::SelectedZones<ExecSpace> selectedZones(m_topologyView.numberOfZones(),
n_options_copy,
"selectedZones",
getAllocatorID());
const auto selectedZonesView = selectedZones.view();
TopologyMapper¶
The axom::bump::TopologyMapper class intersects a source mesh
with a target mesh and maps materials from the source mesh onto a new matset on the
target mesh. The source mesh must contain a “clean” matset, which is a matset where there
are no mixed-material zones. The matset identifies the unique material for each zone in
the source mesh. The source mesh could be the output of one of the BUMP algorithms.
The source and target meshes should overlap spatially. The zones in the source mesh are intersected with the zones in the target mesh and their overlaps are determined and are used to build a new matset on the target mesh. Each zone in the target mesh may recieve contributions from multiple zones and materials in the source mesh.
// Make new VFs via mapper.
using Mapper =
bump::TopologyMapper<ExecSpace, SrcTopologyView, SrcCoordsetView, SrcMatsetView, TargetTopologyView, TargetCoordsetView>;
Mapper mapper(srcTopo, srcCoordset, srcMatset, targetTopo, targetCoordset);
conduit::Node n_opts;
n_opts["source/matsetName"] = "postmir_matset";
n_opts["target/topologyName"] = "fine";
n_opts["target/matsetName"] = "fine_matset";
mapper.execute(n_dev, n_opts, n_dev);
Unique¶
The axom::bump::Unique class can take an unsorted list of values and produce a
sorted list of unique outputs, along with a list of offsets into the original values to identify
one representative value in the original list for each unique value. This class is used to help
merge points.
const int allocatorID = axom::execution_space<ExecSpace>::allocatorID();
axom::Array<int> ids {
{0, 1, 5, 4, 1, 2, 6, 5, 2, 3, 7, 6, 4, 5, 9, 8, 5, 6, 10, 9, 6, 7, 11, 10}};
EXPECT_EQ(ids.size(), 24);
EXPECT_EQ(ids.view().size(), 24);
// host->device
axom::Array<int> deviceIds(ids.size(), ids.size(), allocatorID);
axom::copy(deviceIds.data(), ids.data(), sizeof(int) * ids.size());
EXPECT_EQ(deviceIds.size(), 24);
// Make unique ids.
axom::Array<int> uIds;
axom::Array<axom::IndexType> uIndices;
bump::Unique<ExecSpace, int>::execute(deviceIds.view(), uIds, uIndices);
VariableShape¶
The axom::bump::VariableShape class behaves like a primal shape
but it can represent various 3D shapes, some not present in primal.
ZoneListBuilder¶
The axom::bump::ZoneListBuilder class takes a matset view and a list
of selected zone ids and makes two output lists of zone ids that correspond to clean zones and
mixed zones (more than 1 material in the zone). There are also methods that take into consideration
how zones are connected through their nodes so algorithms that operate on node-centered volume
fractions can operate on adjacent zones that may not be mixed but must participate in BUMP.
namespace utils = axom::bump::utilities;
axom::bump::ZoneListBuilder<ExecSpace, TopologyView, MatsetView> zlb(m_topologyView,
m_matsetView);
zlb.setAllocatorID(getAllocatorID());
[[maybe_unused]] axom::IndexType expectedSize = 0;
if(n_options.has_child(m_selectionKey))
{
auto selectedZonesView =
utils::make_array_view<axom::IndexType>(n_options.fetch_existing(m_selectionKey));
zlb.execute(m_coordsetView.numberOfNodes(), selectedZonesView, cleanZones, mixedZones);
expectedSize = selectedZonesView.size();
}
else
{
zlb.execute(m_coordsetView.numberOfNodes(), cleanZones, mixedZones);
expectedSize = m_topologyView.numberOfZones();
}