BUMP Utilities

The BUMP component contains several useful building blocks for writing algorithms for Blueprint meshes.

  • Structured as classes with an execute() method

  • Often 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();
    }