diff --git a/src/axom/core/utilities/Utilities.hpp b/src/axom/core/utilities/Utilities.hpp index df17f8d362..510d505f65 100644 --- a/src/axom/core/utilities/Utilities.hpp +++ b/src/axom/core/utilities/Utilities.hpp @@ -348,6 +348,16 @@ inline AXOM_HOST_DEVICE bool isNearlyEqualRelative(RealType a, // return abs(a-b) <= max(absThresh, relThresh * maxFabs ); } +/*! + * \brief Sign of a value of any type that supports comparison and + * negation operators. + */ +template +inline int sign_of(const T& v, const T& eps = {0}) +{ + return v > eps ? 1 : v < -eps ? -1 : 0; +} + /*! * \brief Insertion sort of an array. * \accelerated diff --git a/src/axom/klee/Geometry.cpp b/src/axom/klee/Geometry.cpp index 09c8b03e6e..ff8a0f8c06 100644 --- a/src/axom/klee/Geometry.cpp +++ b/src/axom/klee/Geometry.cpp @@ -154,7 +154,7 @@ void Geometry::populateGeomInfo() m_discreteFunction = axom::Array(2, 2); m_discreteFunction(0, 0) = 0.0; m_discreteFunction(0, 1) = cone.getBaseRadius(); - m_discreteFunction(1, 1) = cone.getLength(); + m_discreteFunction(1, 0) = cone.getLength(); m_discreteFunction(1, 1) = cone.getTopRadius(); m_geomInfo["discreteFunction"].set(m_discreteFunction.data(), m_discreteFunction.size()); m_geomInfo["sorOrigin"].set(cone.getBaseCenter().data(), 3); diff --git a/src/axom/primal/geometry/Cone.hpp b/src/axom/primal/geometry/Cone.hpp index 8eb2e827d4..4584512364 100644 --- a/src/axom/primal/geometry/Cone.hpp +++ b/src/axom/primal/geometry/Cone.hpp @@ -143,7 +143,8 @@ class Cone } /*! - * \brief Returns the algebraic volume of the cone + * \brief Returns the algebraic volume of the cone, + * which is negative if the length is negative. * * Volume is only defined when NDIMS == 3. */ diff --git a/src/axom/primal/geometry/CoordinateTransformer.hpp b/src/axom/primal/geometry/CoordinateTransformer.hpp index ef983c28f7..798350fb37 100644 --- a/src/axom/primal/geometry/CoordinateTransformer.hpp +++ b/src/axom/primal/geometry/CoordinateTransformer.hpp @@ -66,6 +66,16 @@ class CoordinateTransformer */ CoordinateTransformer(const numerics::Matrix& matrix) { setMatrix(matrix); } + /*! + * @brief Contruct transformer that moves 4 starting points to 4 + * destination points. + */ + AXOM_HOST_DEVICE CoordinateTransformer(const primal::Point* startPts, + const primal::Point* destPts) + { + setByTerminusPts(startPts, destPts); + } + /*! * @brief Set the matrix, discarding the current transformation. * @param matrix [in] The transformation matrix for homogeneous diff --git a/src/axom/primal/geometry/Sphere.hpp b/src/axom/primal/geometry/Sphere.hpp index 9f89e37a32..0b52f3cbc3 100644 --- a/src/axom/primal/geometry/Sphere.hpp +++ b/src/axom/primal/geometry/Sphere.hpp @@ -178,6 +178,18 @@ class Sphere AXOM_HOST_DEVICE inline bool intersectsWith(const Sphere& sphere, double TOL = 1.e-9) const; + /*! + * \brief Tests if this sphere completely contains another sphere. + * + * \param [in] other The sphere object to check for containment + * \param [in] margin Amount that this sphere must contain the other sphere by. + * Positive means that the other sphere is "more inside". + * + * \return true if this sphere contains the other, false otherwise. + */ + AXOM_HOST_DEVICE + inline bool contains(const Sphere& other, double margin = 0.0) const; + /*! * \brief Prints the Sphere information in the given output stream. * \param [in,out] os the output stream to write to. @@ -233,6 +245,14 @@ AXOM_HOST_DEVICE inline bool Sphere::intersectsWith(const Sphere +AXOM_HOST_DEVICE inline bool Sphere::contains(const Sphere& sphere, double TOL) const +{ + const T center_sep = VectorType(sphere.getCenter(), m_center).norm(); + return (m_radius > center_sep + sphere.getRadius() + TOL); +} + //------------------------------------------------------------------------------ // implementation of free functions //------------------------------------------------------------------------------ diff --git a/src/axom/primal/operators/detail/clip_impl.hpp b/src/axom/primal/operators/detail/clip_impl.hpp index f1cdef1bf8..39280202ba 100644 --- a/src/axom/primal/operators/detail/clip_impl.hpp +++ b/src/axom/primal/operators/detail/clip_impl.hpp @@ -233,7 +233,7 @@ AXOM_HOST_DEVICE void poly_clip_vertices(Polyhedron& poly, // A probable AMD GPU 6.3 compiler bug caused the first assert to fail. // The second assert is equivalent because we expect addVertex call to // consume the next index and increment the number of vertices. - // However, when the compilers no longar cause false failures, we should + // However, when the compilers no longer cause false failures, we should // restore the first assert. // SLIC_ASSERT(newVertexIndex == expectedVertexIndex); SLIC_ASSERT(newVertexIndex == poly.numVertices() - 1); diff --git a/src/axom/primal/tests/primal_sphere.cpp b/src/axom/primal/tests/primal_sphere.cpp index 3777b75f80..4c56570477 100644 --- a/src/axom/primal/tests/primal_sphere.cpp +++ b/src/axom/primal/tests/primal_sphere.cpp @@ -150,6 +150,31 @@ void check_sphere_intersection() EXPECT_FALSE(S0.intersectsWith(S3)); } +//------------------------------------------------------------------------------ +template +void check_sphere_containment() +{ + using PointType = primal::Point; + using SphereType = primal::Sphere; + + PointType center {0.0, 0.0, 0.0}; + double tol = 1e-12; + + // STEP 0: test fully containing + SphereType S0; + EXPECT_TRUE(S0.contains(S0, -tol)); + + // STEP 1: test barely not containing. + center[0] = tol; + SphereType S1(center); + EXPECT_FALSE(S0.contains(S1)); + + // STEP 2: test partial containment. + center[0] = 0.5; + SphereType S3(center); + EXPECT_FALSE(S0.contains(S3)); +} + //------------------------------------------------------------------------------ template void check_copy_constructor() @@ -244,6 +269,13 @@ TEST(primal_sphere, sphere_sphere_intersection) check_sphere_intersection<3>(); } +//------------------------------------------------------------------------------ +TEST(primal_sphere, sphere_sphere_containment) +{ + check_sphere_containment<2>(); + check_sphere_containment<3>(); +} + //------------------------------------------------------------------------------ int main(int argc, char* argv[]) { diff --git a/src/axom/quest/CMakeLists.txt b/src/axom/quest/CMakeLists.txt index a430dfa652..ab54ae4527 100644 --- a/src/axom/quest/CMakeLists.txt +++ b/src/axom/quest/CMakeLists.txt @@ -60,6 +60,7 @@ set( quest_headers interface/signed_distance.hpp util/mesh_helpers.hpp + util/make_clipper_strategy.hpp MeshViewUtil.hpp ) @@ -90,6 +91,7 @@ set( quest_sources interface/signed_distance.cpp util/mesh_helpers.cpp + util/make_clipper_strategy.cpp ) @@ -100,6 +102,7 @@ set( quest_depends_on ) blt_list_append(TO quest_depends_on IF AXOM_ENABLE_SIDRE ELEMENTS sidre) +blt_list_append(TO quest_depends_on IF AXOM_ENABLE_BUMP ELEMENTS bump) blt_list_append(TO quest_depends_on IF SPARSEHASH_FOUND ELEMENTS sparsehash) blt_list_append(TO quest_depends_on IF MFEM_FOUND ELEMENTS mfem) @@ -150,11 +153,24 @@ if(AXOM_ENABLE_KLEE AND AXOM_ENABLE_SIDRE) if(RAJA_FOUND) list(APPEND quest_headers MeshClipper.hpp MeshClipperStrategy.hpp + detail/clipping/Plane3DClipper.hpp detail/clipping/TetClipper.hpp + detail/clipping/TetMeshClipper.hpp + detail/clipping/HexClipper.hpp + detail/clipping/SphereClipper.hpp + detail/clipping/FSorClipper.hpp + detail/clipping/SorClipper.hpp detail/clipping/MeshClipperImpl.hpp) list(APPEND quest_sources MeshClipper.cpp + MeshClipperStrategy.cpp + detail/clipping/Plane3DClipper.cpp detail/clipping/TetClipper.cpp - MeshClipperStrategy.cpp) + detail/clipping/TetMeshClipper.cpp + detail/clipping/HexClipper.cpp + detail/clipping/SphereClipper.cpp + detail/clipping/FSorClipper.cpp + detail/clipping/SorClipper.cpp + detail/clipping/MeshClipperImpl.hpp) endif() endif() diff --git a/src/axom/quest/Discretize.cpp b/src/axom/quest/Discretize.cpp index fcd317ba3e..448fdc1246 100644 --- a/src/axom/quest/Discretize.cpp +++ b/src/axom/quest/Discretize.cpp @@ -108,6 +108,8 @@ OctType new_inscribed_oct(const SphereType& sphere, OctType& o, int s, int t, in * * This routine allocates an array pointed to by \a out. The caller is responsible * to free the array. + * + * TODO: If possible, port to GPU (rewrite to be data-parallel). */ bool discretize(const SphereType& sphere, int levels, axom::Array& out, int& octcount) { @@ -125,7 +127,7 @@ bool discretize(const SphereType& sphere, int levels, axom::Array& out, octcount = count_sphere_octahedra(levels); - out = axom::Array(octcount, octcount); + out.resize(octcount); // index points to an octahedron of the last generation. We'll generate // new octahedra based on out[index]. diff --git a/src/axom/quest/Discretize.hpp b/src/axom/quest/Discretize.hpp index a815dff9e9..06396161d7 100644 --- a/src/axom/quest/Discretize.hpp +++ b/src/axom/quest/Discretize.hpp @@ -50,7 +50,8 @@ bool discretize(const SphereType& s, int levels, axom::Array& out, int& /*! * \brief Given a 2D polyline revolved around the positive X-axis, allocate * and return a list of Octahedra approximating the shape. - * \param [in] polyline The polyline to revolve around the X-axis + * \param [in] polyline The polyline to revolve around the X-axis. + * Data should be in a host-accessible memory space. * \param [in] len The number of points in \a polyline * \param [in] levels The number of refinements to perform, in addition to * a central level-zero octahedron in each segment diff --git a/src/axom/quest/InOutOctree.hpp b/src/axom/quest/InOutOctree.hpp index 4bfe21fbcb..a37fa786db 100644 --- a/src/axom/quest/InOutOctree.hpp +++ b/src/axom/quest/InOutOctree.hpp @@ -17,7 +17,6 @@ #include "axom/slic.hpp" #include "axom/slam.hpp" #include "axom/primal.hpp" -#include "axom/mint.hpp" #include "axom/spin.hpp" #include "detail/inout/BlockData.hpp" diff --git a/src/axom/quest/IntersectionShaper.hpp b/src/axom/quest/IntersectionShaper.hpp index e3a626d7d0..b7c5dd42fd 100644 --- a/src/axom/quest/IntersectionShaper.hpp +++ b/src/axom/quest/IntersectionShaper.hpp @@ -825,6 +825,7 @@ class IntersectionShaper : public Shaper // Generate the Octahedra // (octahedra m_octs will be on device) + m_octs = axom::Array(0, 0, axom::execution_space::allocatorID()); const bool disc_status = axom::quest::discretize(polyline, polyline_size, m_level, m_octs, m_octcount); @@ -1967,13 +1968,15 @@ class IntersectionShaper : public Shaper if(m_bpGrp) { auto fieldsGrp = m_bpGrp->getGroup("fields"); - SLIC_ERROR_IF(fieldsGrp == nullptr, "Input blueprint mesh lacks the 'fields' Group/Node."); - for(auto& group : fieldsGrp->groups()) + if(fieldsGrp != nullptr) { - std::string materialName = fieldNameToMaterialName(group.getName()); - if(!materialName.empty()) + for(auto& group : fieldsGrp->groups()) { - materialNames.emplace_back(materialName); + std::string materialName = fieldNameToMaterialName(group.getName()); + if(!materialName.empty()) + { + materialNames.emplace_back(materialName); + } } } } diff --git a/src/axom/quest/MeshClipper.cpp b/src/axom/quest/MeshClipper.cpp index b2e5b7910d..61b46be42a 100644 --- a/src/axom/quest/MeshClipper.cpp +++ b/src/axom/quest/MeshClipper.cpp @@ -25,7 +25,16 @@ MeshClipper::MeshClipper(quest::experimental::ShapeMesh& shapeMesh, , m_strategy(strategy) , m_impl(newImpl()) , m_verbose(false) -{ } + , m_screenLevel(3) +{ + // Initialize statistics. + m_counterStats["cellsIn"].set_int64(0); + m_counterStats["cellsOn"].set_int64(0); + m_counterStats["cellsOut"].set_int64(0); + m_counterStats["tetsIn"].set_int64(0); + m_counterStats["tetsOn"].set_int64(0); + m_counterStats["tetsOut"].set_int64(0); +} void MeshClipper::clip(axom::Array& ovlap) { @@ -35,7 +44,7 @@ void MeshClipper::clip(axom::Array& ovlap) // Resize output array and use appropriate allocator. if(ovlap.size() < cellCount || ovlap.getAllocatorID() != allocId) { - AXOM_ANNOTATE_SCOPE("MeshClipper::clip_alloc"); + AXOM_ANNOTATE_SCOPE("MeshClipper:clip_alloc"); ovlap = axom::Array(ArrayOptions::Uninitialized(), cellCount, cellCount, allocId); } clip(ovlap.view()); @@ -45,8 +54,8 @@ void MeshClipper::clip(axom::Array& ovlap) * @brief Orchestrates the geometry clipping by using the capabilities of the * MeshClipperStrategy implementation. * - * If the strategy can label cells as inside/on/outside geometry - * boundary, use that to reduce reliance on expensive clipping methods. + * If the strategy can label cells/tets as inside/on/outside geometry + * boundary, use that to reduce use of expensive primitive clipping methods. * * Regardless of labeling, try to use specialized clipping first. * If specialized methods aren't implemented, resort to discretizing @@ -58,31 +67,45 @@ void MeshClipper::clip(axom::ArrayView ovlap) const axom::IndexType cellCount = m_shapeMesh.getCellCount(); SLIC_ASSERT(ovlap.size() == cellCount); + auto& cellsInCount = *m_counterStats["cellsIn"].as_int64_ptr(); + auto& cellsOnCount = *m_counterStats["cellsOn"].as_int64_ptr(); + auto& cellsOutCount = *m_counterStats["cellsOut"].as_int64_ptr(); + auto& tetsInCount = *m_counterStats["tetsIn"].as_int64_ptr(); + auto& tetsOnCount = *m_counterStats["tetsOn"].as_int64_ptr(); + auto& tetsOutCount = *m_counterStats["tetsOut"].as_int64_ptr(); + // Try to label cells as inside, outside or on shape boundary axom::Array cellLabels; - bool withCellInOut = m_strategy->labelCellsInOut(m_shapeMesh, cellLabels); + bool withCellInOut = false; + if(m_screenLevel >= 1) + { + AXOM_ANNOTATE_BEGIN("MeshClipper:label_cells"); + withCellInOut = m_strategy->labelCellsInOut(m_shapeMesh, cellLabels); + AXOM_ANNOTATE_END("MeshClipper:label_cells"); + } bool done = false; if(withCellInOut) { - SLIC_ERROR_IF( - cellLabels.size() != m_shapeMesh.getCellCount(), - axom::fmt::format("MeshClipperStrategy '{}' did not return the correct array size of {}", - m_strategy->name(), - m_shapeMesh.getCellCount())); + SLIC_ERROR_IF(cellLabels.size() != m_shapeMesh.getCellCount(), + axom::fmt::format("MeshClipperStrategy '{}' did not return the correct" + " cell label array size of {}", + m_strategy->name(), + m_shapeMesh.getCellCount())); SLIC_ERROR_IF(cellLabels.getAllocatorID() != allocId, - axom::fmt::format("MeshClipperStrategy '{}' failed to provide cellLabels data " - "with the required allocator id {}", + axom::fmt::format("MeshClipperStrategy '{}' failed to provide" + " cellLabels data with the required allocator id {}", m_strategy->name(), allocId)); if(m_verbose) { - logLabelStats(cellLabels, "cells"); + getLabelCounts(cellLabels, cellsInCount, cellsOnCount, cellsOutCount); + logClippingStats(); } - AXOM_ANNOTATE_BEGIN("MeshClipper::processInOut"); + AXOM_ANNOTATE_BEGIN("MeshClipper:process_in_out"); m_impl->initVolumeOverlaps(cellLabels.view(), ovlap); @@ -90,18 +113,37 @@ void MeshClipper::clip(axom::ArrayView ovlap) m_impl->collectOnIndices(cellLabels.view(), cellsOnBdry); axom::Array tetLabels; - bool withTetInOut = m_strategy->labelTetsInOut(m_shapeMesh, cellsOnBdry.view(), tetLabels); + bool withTetInOut = false; + if(m_screenLevel >= 2) + { + AXOM_ANNOTATE_BEGIN("MeshClipper:label_tets"); + withTetInOut = m_strategy->labelTetsInOut(m_shapeMesh, cellsOnBdry.view(), tetLabels); + AXOM_ANNOTATE_END("MeshClipper:label_tets"); + } axom::Array tetsOnBdry; if(withTetInOut) { + SLIC_ERROR_IF(tetLabels.size() != NUM_TETS_PER_HEX * cellsOnBdry.size(), + axom::fmt::format("MeshClipperStrategy '{}' did not return the correct" + " tet label array size of {}", + m_strategy->name(), + NUM_TETS_PER_HEX * cellsOnBdry.size())); + SLIC_ERROR_IF(tetLabels.getAllocatorID() != allocId, + axom::fmt::format("MeshClipperStrategy '{}' failed to provide" + "tetLabels data with the required allocator id {}", + m_strategy->name(), + allocId)); + if(m_verbose) { - logLabelStats(tetLabels, "tets"); + getLabelCounts(tetLabels, tetsInCount, tetsOnCount, tetsOutCount); + logClippingStats(); } + m_impl->collectOnIndices(tetLabels.view(), tetsOnBdry); - m_impl->remapTetIndices(tetsOnBdry, cellsOnBdry); + m_impl->remapTetIndices(cellsOnBdry, tetsOnBdry); SLIC_ASSERT(tetsOnBdry.getAllocatorID() == m_shapeMesh.getAllocatorID()); SLIC_ASSERT(tetsOnBdry.size() <= cellsOnBdry.size() * NUM_TETS_PER_HEX); @@ -109,51 +151,47 @@ void MeshClipper::clip(axom::ArrayView ovlap) m_impl->addVolumesOfInteriorTets(cellsOnBdry.view(), tetLabels.view(), ovlap); } - AXOM_ANNOTATE_END("MeshClipper::processInOut"); + AXOM_ANNOTATE_END("MeshClipper:process_in_out"); // // If implementation has a specialized clip, use it. // + AXOM_ANNOTATE_BEGIN("MeshClipper:specialized_clip"); if(withTetInOut) { - done = m_strategy->specializedClipTets(m_shapeMesh, ovlap, tetsOnBdry); + done = m_strategy->specializedClipTets(m_shapeMesh, ovlap, tetsOnBdry, m_counterStats); } else { - done = m_strategy->specializedClipCells(m_shapeMesh, ovlap, cellsOnBdry); + done = m_strategy->specializedClipCells(m_shapeMesh, ovlap, cellsOnBdry, m_counterStats); } + AXOM_ANNOTATE_END("MeshClipper:specialized_clip"); if(!done) { - AXOM_ANNOTATE_SCOPE("MeshClipper::clip3D_limited"); + AXOM_ANNOTATE_SCOPE("MeshClipper:clip_fcn"); if(withTetInOut) { - m_impl->computeClipVolumes3DTets(tetsOnBdry.view(), ovlap); + m_impl->computeClipVolumes3DTets(tetsOnBdry.view(), ovlap, m_counterStats); } else { - m_impl->computeClipVolumes3D(cellsOnBdry.view(), ovlap); + m_impl->computeClipVolumes3D(cellsOnBdry.view(), ovlap, m_counterStats); } } - - m_localCellInCount = cellsOnBdry.size(); } else // !withCellInOut { - std::string msg = - axom::fmt::format("MeshClipper strategy '{}' did not provide in/out cell labels.\n", - m_strategy->name()); - SLIC_INFO(msg); m_impl->zeroVolumeOverlaps(ovlap); - done = m_strategy->specializedClipCells(m_shapeMesh, ovlap); + AXOM_ANNOTATE_BEGIN("MeshClipper:specialized_clip"); + done = m_strategy->specializedClipCells(m_shapeMesh, ovlap, m_counterStats); + AXOM_ANNOTATE_END("MeshClipper:specialized_clip"); if(!done) { - AXOM_ANNOTATE_SCOPE("MeshClipper::clip3D"); - m_impl->computeClipVolumes3D(ovlap); + AXOM_ANNOTATE_SCOPE("MeshClipper:clip_fcn"); + m_impl->computeClipVolumes3D(ovlap, m_counterStats); } - - m_localCellInCount = cellCount; } } @@ -197,52 +235,90 @@ std::unique_ptr MeshClipper::newImpl() return impl; } -void MeshClipper::logLabelStats(axom::ArrayView labels, const std::string& labelType) +#if defined(AXOM_USE_MPI) +template +void globalReduce(axom::Array& values, int reduceOp) { - axom::IndexType countsa[4]; - axom::IndexType countsb[4]; - getLabelCounts(labels, countsa[0], countsa[1], countsa[2]); - countsa[3] = m_shapeMesh.getCellCount(); -#ifdef AXOM_USE_MPI - MPI_Reduce(countsa, countsb, 4, axom::mpi_traits::type, MPI_SUM, 0, MPI_COMM_WORLD); + axom::Array localValues(values); + MPI_Allreduce(localValues.data(), + values.data(), + values.size(), + axom::mpi_traits::type, + reduceOp, + MPI_COMM_WORLD); #endif - std::string msg = axom::fmt::format( - "MeshClipper strategy '{}' globally labeled {} {} inside, {} on and {} outside, for mesh with " - "{} cells ({} tets)\n", - m_strategy->name(), - labelType, - countsb[0], - countsb[1], - countsb[2], - countsb[3], - countsb[3] * NUM_TETS_PER_HEX); - SLIC_INFO(msg); } -void MeshClipper::getClippingStats(axom::IndexType& localCellInCount, - axom::IndexType& globalCellInCount, - axom::IndexType& maxLocalCellInCount) const +void MeshClipper::accumulateClippingStats(conduit::Node& curStats, const conduit::Node& newStats) { - localCellInCount = m_localCellInCount; -#ifdef AXOM_USE_MPI - MPI_Reduce(&localCellInCount, - &globalCellInCount, - 1, - axom::mpi_traits::type, - MPI_SUM, - 0, - MPI_COMM_WORLD); - MPI_Reduce(&localCellInCount, - &maxLocalCellInCount, - 1, - axom::mpi_traits::type, - MPI_MAX, - 0, - MPI_COMM_WORLD); -#else - maxLocalCellInCount = localCellInCount; - globalCellInCount = localCellInCount; + for(int i = 0; i < newStats.number_of_children(); ++i) + { + const auto& newStat = newStats.child(i); + SLIC_ERROR_IF(!newStat.dtype().is_integer(), + "MeshClipper statistic must be integer" + " (at least until a need for floats arises)."); + auto& currentStat = curStats[newStat.name()]; + if(currentStat.dtype().is_empty()) + { + currentStat.set_int64(newStat.as_int64()); + } + else + { + *currentStat.as_int64_ptr() += newStat.as_int64(); + } + } +} + +conduit::Node MeshClipper::getGlobalClippingStats() const +{ + conduit::Node stats; + auto& locNode = stats["loc"]; + auto& maxNode = stats["max"]; + auto& sumNode = stats["sum"]; + + locNode.set(m_counterStats); + sumNode.set(m_counterStats); + maxNode.set(m_counterStats); + +#if defined(AXOM_USE_MPI) + // Do sum and max reductions. + axom::Array sums(0, sumNode.number_of_children()); + for(int i = 0; i < sumNode.number_of_children(); ++i) + { + sums.push_back(locNode.child(i).as_int64()); + } + axom::Array maxs(sums); + globalReduce(maxs, MPI_MAX); + globalReduce(sums, MPI_SUM); + + for(int i = 0; i < sumNode.number_of_children(); ++i) + { + *maxNode.child(i).as_int64_ptr() = maxs[i]; + *sumNode.child(i).as_int64_ptr() = sums[i]; + } #endif + + return stats; +} + +void MeshClipper::logClippingStats(bool local, bool sum, bool max) const +{ + conduit::Node stats = getGlobalClippingStats(); + if(local) + { + SLIC_INFO(std::string("MeshClipper loc-stats: ") + + stats["loc"].to_string("yaml", 2, 0, "", " ")); + } + if(sum) + { + SLIC_INFO(std::string("MeshClipper sum-stats: ") + + stats["sum"].to_string("yaml", 2, 0, "", " ")); + } + if(max) + { + SLIC_INFO(std::string("MeshClipper max-stats: ") + + stats["max"].to_string("yaml", 2, 0, "", " ")); + } } } // namespace experimental diff --git a/src/axom/quest/MeshClipper.hpp b/src/axom/quest/MeshClipper.hpp index d91df1e275..d3020c1729 100644 --- a/src/axom/quest/MeshClipper.hpp +++ b/src/axom/quest/MeshClipper.hpp @@ -11,6 +11,7 @@ #include "axom/klee/Geometry.hpp" #include "axom/quest/MeshClipperStrategy.hpp" #include "axom/quest/ShapeMesh.hpp" +#include "conduit/conduit_node.hpp" namespace axom { @@ -38,7 +39,7 @@ class MeshClipper //!@brief Whether an element is in, out or on shape boundary. using LabelType = MeshClipperStrategy::LabelType; - static constexpr axom::IndexType NUM_TETS_PER_HEX = MeshClipperStrategy::NUM_TETS_PER_HEX; + static constexpr axom::IndexType NUM_TETS_PER_HEX = ShapeMesh::NUM_TETS_PER_HEX; /*! * @brief Construct a shape clipper @@ -81,6 +82,50 @@ class MeshClipper //!@brief Dimension of the shape (2 or 3) int dimension() const { return m_shapeMesh.dimension(); } + //@{ + + /*! + * @brief Log clipping statistics. + * Intended for developer use. + * + * This is a collective method if MPI-parallel. + */ + void logClippingStats(bool local = false, bool sum = true, bool max = false) const; + + /*! + * @brief Get local assorted clipping statistics, + * intended for developer use. + */ + const conduit::Node& getClippingStats() const { return m_counterStats; } + + /*! + * @brief Get global assorted clipping statistics, + * intended for developer use. + * + * This is a collective method if MPI-parallel. + */ + conduit::Node getGlobalClippingStats() const; + + /*! + * @brief Set the level of screening, + * intended for developer use. + */ + void setScreenLevel(int screenLevel) { m_screenLevel = screenLevel; } + + /*! + * @brief Get the level of screening, + * intended for developer use. + */ + int getScreenLevel() const { return m_screenLevel; } + + /*! + * @brief Add new stats to current stats, + * intended for developer use. + */ + static void accumulateClippingStats(conduit::Node& curStats, const conduit::Node& newStats); + + //@} + /*! * @brief Single interface for methods implemented with * execution space templates. @@ -136,11 +181,12 @@ class MeshClipper axom::ArrayView ovlap) = 0; //!@brief Compute clip volumes for every cell. - virtual void computeClipVolumes3D(axom::ArrayView ovlap) = 0; + virtual void computeClipVolumes3D(axom::ArrayView ovlap, conduit::Node& statistics) = 0; //!@brief Compute clip volumes for cell in an index list. virtual void computeClipVolumes3D(const axom::ArrayView& cellIndices, - axom::ArrayView ovlap) = 0; + axom::ArrayView ovlap, + conduit::Node& statistics) = 0; /*! * @brief Compute clip volumes for cell tets in an index list. @@ -149,28 +195,24 @@ class MeshClipper * NUM_TETS_PER_HEX tets and stored consecutively. */ virtual void computeClipVolumes3DTets(const axom::ArrayView& tetIndices, - axom::ArrayView ovlap) = 0; + axom::ArrayView ovlap, + conduit::Node& statistics) = 0; //!@brief Count the number of labels of each type. virtual void getLabelCounts(axom::ArrayView labels, - axom::IndexType& inCount, - axom::IndexType& onCount, - axom::IndexType& outCount) = 0; + std::int64_t& inCount, + std::int64_t& onCount, + std::int64_t& outCount) = 0; ShapeMesh& getShapeMesh() { return m_myClipper.m_shapeMesh; } MeshClipperStrategy& getStrategy() { return *m_myClipper.m_strategy; } - private: + protected: //!@brief The MeshClipper that owns this Impl. MeshClipper& m_myClipper; }; - //! @brief For assessments, not general use. - void getClippingStats(axom::IndexType& localCellInCount, - axom::IndexType& globalCellInCount, - axom::IndexType& maxLocalCellInCount) const; - private: friend Impl; @@ -187,13 +229,13 @@ class MeshClipper * for multiple execution spaces. */ - ///@{ - //! @name Statistics - axom::IndexType m_localCellInCount {0}; - ///@} + //! @brief Statistics + conduit::Node m_counterStats; bool m_verbose; + int m_screenLevel; + #if defined(__CUDACC__) public: #endif @@ -204,14 +246,12 @@ class MeshClipper //!@name Convenience methods //!@brief Count the number of labels of each type. void getLabelCounts(const axom::Array& labels, - axom::IndexType& inCount, - axom::IndexType& onCount, - axom::IndexType& outCount) + std::int64_t& inCount, + std::int64_t& onCount, + std::int64_t& outCount) { m_impl->getLabelCounts(labels, inCount, onCount, outCount); } - - void logLabelStats(axom::ArrayView labels, const std::string& labelType); //@} }; diff --git a/src/axom/quest/MeshClipperStrategy.hpp b/src/axom/quest/MeshClipperStrategy.hpp index 48de9661ae..d14e774369 100644 --- a/src/axom/quest/MeshClipperStrategy.hpp +++ b/src/axom/quest/MeshClipperStrategy.hpp @@ -53,12 +53,13 @@ namespace experimental * false if it was a no-op. * Subclasses of MeshClipperStrategy must implement either - * - a @c specializedClipCells method or + * - a @c specializedClipCells or @c specializedClipTets method or * - one of the @c getShapesAs...() methods. * The former is prefered if the use of geometry-specific information - * can make it faster. @c labelCellsInOut is optional but if provided, - * it can improve performance by limiting the slower clipping steps - * to a subset of cells. @c getBoundingBox2D or @c getBoundingBox3D + * can make it faster. @c labelCellsInOut and @c labelTetsInOut + * are optional but if provided, + * they can improve performance by limiting the slower clipping steps + * to a smaller subset. @c getBoundingBox2D or @c getBoundingBox3D * can also improve performance by reducing computation. */ class MeshClipperStrategy @@ -95,9 +96,8 @@ class MeshClipperStrategy using Ray2DType = axom::primal::Ray; using Segment2DType = axom::primal::Segment; - //!@brief Number of tetrahedra per hexahedron decomposes into - // @internal We could use a more efficient 18-tet decomposition in the future. static constexpr axom::IndexType NUM_TETS_PER_HEX = ShapeMesh::NUM_TETS_PER_HEX; + static constexpr axom::IndexType NUM_VERTS_PER_CELL_3D = ShapeMesh::NUM_VERTS_PER_CELL_3D; /*! * @brief Construct a strategy for the given klee::Geometry object. @@ -164,6 +164,9 @@ class MeshClipperStrategy * skip if it's not. It's safe to label cells as on the boundary if * it can't be positively determined as inside or outside. * + * Degenerate cells have zero volume and should be labeled outside + * for best clipping performance. + * * @return Whether the operation was done. (A false means * not done.) * @@ -191,10 +194,14 @@ class MeshClipperStrategy * * Indices [i*NUM_TETS_PER_HEX, (i+1)*NUM_TETS_PER_HEX) in \c tetLabels * correspond to parent cell index \c c = \c cellIds[i]. - * The \c NUM_TETS_PER_HEX tets in cell \c cid have indices + * + * The \c NUM_TETS_PER_HEX tets in cell \c c have indices * [c*NUM_TETS_PER_HEX, (c+1)*NUM_TETS_PER_HEX). * in \c shapeMesh.getCellsAsTets(). * + * Degenerate tets have zero volume and MUST be labeled outside. + * Further computation can fail if degenerate tets are seen. + * * If implementation returns true, it should ensure these * post-conditions hold: * @post tetLabels.size() == NUM_TETS_PER_HEX * cellIds.size() @@ -219,6 +226,8 @@ class MeshClipperStrategy * @param [in] shapeMesh Blueprint mesh to shape into. * @param [out] ovlap Shape overlap volume of each cell * in the \c shapeMesh. It's initialized to zeros. + * @param [out] statistics Optional statistics to record + * consisting of child nodes with integer values. * * The default implementation has no specialized method, * so it's a no-op and returns false. @@ -231,16 +240,21 @@ class MeshClipperStrategy * This method need not be implemented if labelCellsInOut() * returns true. * + * Setting the statistics is not required except for getting + * accurate statistics. + * * If implementation returns true, it should ensure these * post-conditions hold: * @post ovlap.size() == shapeMesh.getCellCount() * @post ovlap.getAllocatorID() == shapeMesh.getAllocatorId() */ virtual bool specializedClipCells(quest::experimental::ShapeMesh& shapeMesh, - axom::ArrayView ovlap) + axom::ArrayView ovlap, + conduit::Node& statistics) { AXOM_UNUSED_VAR(shapeMesh); AXOM_UNUSED_VAR(ovlap); + AXOM_UNUSED_VAR(statistics); return false; } @@ -253,18 +267,24 @@ class MeshClipperStrategy * in \c shapeMesh, initialized to the cell volumes * for cell inside the shape and zero for other cells. * @param [in] cellIds Limit computation to these cell ids. + * @param [out] statistics Optional statistics to record + * consisting of child nodes with integer values. * * The default implementation has no specialized method, * so it's a no-op and returns false. * - * If this method returns false, then exactly one of the - * shape discretization methods must be provided. + * If this method returns false, then exactly one of + * getGeometryAsTets() or getGeometryAsOcts() methods must be + * provided so MeshClipper can use the general clipping methods. * * @return True if clipping was done and false if a no-op. * * This method need not be implemented if labelCellsInOut() * returns false. * + * Setting the statistics is not required except for getting + * accurate statistics. + * * @pre @c ovlap is pre-initialized for the implementation * to add or subtract partial volumes to individual cells. * @@ -275,11 +295,13 @@ class MeshClipperStrategy */ virtual bool specializedClipCells(quest::experimental::ShapeMesh& shapeMesh, axom::ArrayView ovlap, - const axom::ArrayView& cellIds) + const axom::ArrayView& cellIds, + conduit::Node& statistics) { AXOM_UNUSED_VAR(shapeMesh); AXOM_UNUSED_VAR(ovlap); AXOM_UNUSED_VAR(cellIds); + AXOM_UNUSED_VAR(statistics); return false; } @@ -293,19 +315,27 @@ class MeshClipperStrategy * done so far. Clip volumes computed by this method should * be added to the current values in this array. * + * @param [out] statistics Optional statistics to record + * consisting of child nodes with integer values. + * * @param [in] tetIds Indices of tets to clip, referring to the * shapeMesh.getCellsAsTets() array. tetIds[i] is the * \c (tetIds[i]%NUM_TETS_PER_HEX)-th tetrahedron of cell * \c = \c tetIds[i]/NUM_TETS_PER_HEX. Its overlap volume should * be added to \c ovlap[c]. + * + * Setting the statistics is not required except for getting + * accurate statistics. */ virtual bool specializedClipTets(quest::experimental::ShapeMesh& shapeMesh, axom::ArrayView ovlap, - const axom::ArrayView& tetIds) + const axom::ArrayView& tetIds, + conduit::Node& statistics) { AXOM_UNUSED_VAR(shapeMesh); AXOM_UNUSED_VAR(ovlap); AXOM_UNUSED_VAR(tetIds); + AXOM_UNUSED_VAR(statistics); return false; } @@ -313,8 +343,8 @@ class MeshClipperStrategy * @brief Get the geometry as discrete tetrahedra, or return false. * * @param [in] shapeMesh Blueprint mesh to shape into. - * @param [out] tets Array of tetrahedra filling the space of the shape, - * fully transformed. + * @param [out] tets Array of non-degenerate tetrahedra filling the + * space of the shape, fully transformed. * * Subclasses implementing this routine should snap to zero any * output vertex coordinate that is close to zero. @@ -338,8 +368,8 @@ class MeshClipperStrategy * @brief Get the geometry as discrete octahedra, or return false. * * @param [in] shapeMesh Blueprint mesh to shape into. - * @param [out] octs Array of octahedra filling the space of the shape, - * fully transformed. + * @param [out] tets Array of non-degenerate octahedra filling the + * space of the shape, fully transformed. * * Subclasses implementing this routine should snap to zero any * output vertex coordinate that is close to zero. diff --git a/src/axom/quest/SamplingShaper.hpp b/src/axom/quest/SamplingShaper.hpp index fe5f0c1cf3..f9a9e290af 100644 --- a/src/axom/quest/SamplingShaper.hpp +++ b/src/axom/quest/SamplingShaper.hpp @@ -15,10 +15,8 @@ #include "axom/config.hpp" #include "axom/core.hpp" #include "axom/slic.hpp" -#include "axom/slam.hpp" #include "axom/primal.hpp" #include "axom/mint.hpp" -#include "axom/spin.hpp" #include "axom/klee.hpp" #ifndef AXOM_USE_MFEM diff --git a/src/axom/quest/ShapeMesh.cpp b/src/axom/quest/ShapeMesh.cpp index 7dffd70eae..4a9cb10e97 100644 --- a/src/axom/quest/ShapeMesh.cpp +++ b/src/axom/quest/ShapeMesh.cpp @@ -184,6 +184,7 @@ void ShapeMesh::precomputeMeshData() getCellsAsHexes(); getCellsAsTets(); getCellVolumes(); + getTetVolumes(); getCellBoundingBoxes(); getCellLengths(); getCellNodeConnectivity(); @@ -217,6 +218,15 @@ axom::ArrayView ShapeMesh::getCellVolumes() return m_hexVolumes.view(); } +axom::ArrayView ShapeMesh::getTetVolumes() +{ + if(m_tetVolumes.size() != m_cellCount * NUM_TETS_PER_HEX) + { + computeTetVolumes(); + } + return m_tetVolumes.view(); +} + axom::ArrayView ShapeMesh::getCellBoundingBoxes() { if(m_hexBbs.size() != m_cellCount) @@ -624,6 +634,34 @@ void ShapeMesh::computeHexVolumes() } } +void ShapeMesh::computeTetVolumes() +{ + AXOM_ANNOTATE_SCOPE("ShapeMesh::computeTetVolumes"); + switch(m_runtimePolicy) + { + case RuntimePolicy::seq: + computeTetVolumesImpl(); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case RuntimePolicy::omp: + computeTetVolumesImpl(); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case RuntimePolicy::cuda: + computeTetVolumesImpl>(); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case RuntimePolicy::hip: + computeTetVolumesImpl>(); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } +} + void ShapeMesh::computeHexBbs() { AXOM_ANNOTATE_SCOPE("ShapeMesh::computeHexBoundingBoxes"); @@ -734,10 +772,10 @@ void ShapeMesh::computeCellsAsHexesImpl() SLIC_ASSERT(m_dim == NDIM); // or we shouldn't be here. - auto vertexCoords = getVertexCoords3D(); - const axom::ArrayView& vX = vertexCoords[0]; - const axom::ArrayView& vY = vertexCoords[1]; - const axom::ArrayView& vZ = vertexCoords[2]; + const auto& vertexCoords = getVertexCoords3D(); + const auto& vX = vertexCoords[0]; + const auto& vY = vertexCoords[1]; + const auto& vZ = vertexCoords[2]; axom::ArrayView connView = getCellNodeConnectivity(); @@ -750,7 +788,6 @@ void ShapeMesh::computeCellsAsHexesImpl() axom::for_all( m_cellCount, AXOM_LAMBDA(axom::IndexType cellId) { - // Set each hexahedral element vertices auto& hex = cellsAsHexesView[cellId]; for(int vi = 0; vi < NUM_VERTS_PER_HEX; ++vi) @@ -791,7 +828,7 @@ void ShapeMesh::computeCellsAsTetsImpl() AXOM_LAMBDA(axom::IndexType cellId) { const auto& hex = cellsAsHexesView[cellId]; auto* firstTetPtr = &cellsAsTetsView[cellId * NUM_TETS_PER_HEX]; - hex.triangulate(firstTetPtr); + hexToTets(hex, firstTetPtr); }); } @@ -809,6 +846,20 @@ void ShapeMesh::computeHexVolumesImpl() AXOM_LAMBDA(axom::IndexType i) { hexVolumesView[i] = cellsAsHexes[i].volume(); }); } +template +void ShapeMesh::computeTetVolumesImpl() +{ + axom::IndexType tetCount = m_cellCount * NUM_TETS_PER_HEX; + m_tetVolumes = axom::Array(ArrayOptions::Uninitialized(), tetCount, tetCount, m_allocId); + + auto cellsAsTets = getCellsAsTets(); + + auto tetVolumesView = m_tetVolumes.view(); + axom::for_all( + tetCount, + AXOM_LAMBDA(axom::IndexType i) { tetVolumesView[i] = cellsAsTets[i].volume(); }); +} + template void ShapeMesh::computeHexBbsImpl() { diff --git a/src/axom/quest/ShapeMesh.hpp b/src/axom/quest/ShapeMesh.hpp index b57ced3d15..63faa25dd9 100644 --- a/src/axom/quest/ShapeMesh.hpp +++ b/src/axom/quest/ShapeMesh.hpp @@ -58,11 +58,23 @@ class ShapeMesh using Point3DType = primal::Point; using TetrahedronType = primal::Tetrahedron; using HexahedronType = primal::Hexahedron; + using Plane3DType = axom::primal::Plane; using BoundingBox3DType = primal::BoundingBox; - //!@brief Number of tetrahedra per hexahedron decomposes into - // @internal We could use a more efficient 18-tet decomposition in the future. - static constexpr axom::IndexType NUM_TETS_PER_HEX = HexahedronType::NUM_TRIANGULATE; + /*! + * @brief Number of tetrahedra per hexahedron decomposes into + * @see hexToTets() + * + * @internal Values of 24 and 18 are valid. 18 is likely more + * performant because it generates fewer tets. + * + * @internal The code branches on the value at a low level, + * but this should be optimized out by the compiler. + */ + static constexpr axom::IndexType NUM_TETS_PER_HEX = 18; + + //!@brief Number of vertices per cell. + static constexpr axom::IndexType NUM_VERTS_PER_CELL_3D = 8; /*! * @brief Constructor with computational mesh in a conduit::Node. @@ -153,6 +165,21 @@ class ShapeMesh // zero to zero. Default threshold is 1e-10. void setZeroThreshold(double threshold) { m_zeroThreshold = threshold; } + /*! + * @brief Decompose a hexahedron into NUM_TETS_PER_HEX tetrahedra. + * @param hex [in] The hexahedron + * @param tets [out] Pointer to space for NUM_TETS_PER_HEX tetrahedra. + * + * To avoid ambiguity due to the choice of 2 diagonals for dividing + * each hex face into 2 triangles, we introduce a face-centered + * point at the average of the face vertices and decompose the face + * into 4 triangles. + * + * It is expected that this method will be used in long inner + * loops, so it is bare-bones for best performance. + */ + AXOM_HOST_DEVICE inline static void hexToTets(const HexahedronType& hex, TetrahedronType* tets); + //@{ //!@name Accessors to mesh data. //@} @@ -172,6 +199,8 @@ class ShapeMesh axom::ArrayView getCellsAsHexes(); //!@brief Get volume of mesh cells. axom::ArrayView getCellVolumes(); + //!@brief Get volumes of tets in getCellsAsTets(). + axom::ArrayView getTetVolumes(); //!@brief Get characteristic lengths of mesh cells. axom::ArrayView getCellLengths(); axom::ArrayView getCellBoundingBoxes(); @@ -276,6 +305,9 @@ class ShapeMesh //!@brief Volumes of hex cells. axom::Array m_hexVolumes; + //!@brief Volumes of cell tets. + axom::Array m_tetVolumes; + //!@brief Characteristic lengths of cells. axom::Array m_cellLengths; @@ -288,6 +320,7 @@ class ShapeMesh void computeCellsAsHexes(); void computeCellsAsTets(); void computeHexVolumes(); + void computeTetVolumes(); void computeHexBbs(); void computeCellLengths(); void computeVertPoints(); @@ -306,6 +339,9 @@ class ShapeMesh template void computeHexVolumesImpl(); + template + void computeTetVolumesImpl(); + template void computeHexBbsImpl(); @@ -345,6 +381,118 @@ class ShapeMesh const conduit::DataType& dtype); }; +AXOM_HOST_DEVICE inline void ShapeMesh::hexToTets(const HexahedronType& hex, TetrahedronType* tets) +{ + AXOM_STATIC_ASSERT(NUM_TETS_PER_HEX == 24 || NUM_TETS_PER_HEX == 18); + + if(NUM_TETS_PER_HEX == 24) + { + hex.triangulate(tets); + } + else + { + // Tets sharing the axis between hex vertices 4 and 2. + tets[0][0] = hex[4]; + tets[0][1] = hex[2]; + tets[0][2] = hex[1]; + tets[0][3] = hex[0]; + + tets[1][0] = hex[4]; + tets[1][1] = hex[2]; + tets[1][2] = hex[0]; + tets[1][3] = hex[3]; + + tets[2][0] = hex[4]; + tets[2][1] = hex[2]; + tets[2][2] = hex[3]; + tets[2][3] = hex[7]; + + tets[3][0] = hex[4]; + tets[3][1] = hex[2]; + tets[3][2] = hex[7]; + tets[3][3] = hex[6]; + + tets[4][0] = hex[4]; + tets[4][1] = hex[2]; + tets[4][2] = hex[6]; + tets[4][3] = hex[5]; + + tets[5][0] = hex[4]; + tets[5][1] = hex[2]; + tets[5][2] = hex[5]; + tets[5][3] = hex[1]; + + // Centroids of the 6 faces. + Point3DType mp0473 = Point3DType::midpoint(Point3DType::midpoint(hex[0], hex[4]), + Point3DType::midpoint(hex[7], hex[3])); + Point3DType mp1562 = Point3DType::midpoint(Point3DType::midpoint(hex[1], hex[5]), + Point3DType::midpoint(hex[6], hex[2])); + Point3DType mp0451 = Point3DType::midpoint(Point3DType::midpoint(hex[0], hex[4]), + Point3DType::midpoint(hex[5], hex[1])); + Point3DType mp3762 = Point3DType::midpoint(Point3DType::midpoint(hex[3], hex[7]), + Point3DType::midpoint(hex[6], hex[2])); + Point3DType mp0123 = Point3DType::midpoint(Point3DType::midpoint(hex[0], hex[1]), + Point3DType::midpoint(hex[2], hex[3])); + Point3DType mp4567 = Point3DType::midpoint(Point3DType::midpoint(hex[4], hex[5]), + Point3DType::midpoint(hex[6], hex[7])); + + // Tets from the 6 faces (two per face). + tets[6][0] = hex[4]; + tets[6][1] = hex[6]; + tets[6][2] = hex[7]; + tets[6][3] = mp4567; + tets[7][0] = hex[4]; + tets[7][1] = hex[5]; + tets[7][2] = hex[6]; + tets[7][3] = mp4567; + + tets[8][0] = hex[0]; + tets[8][1] = hex[2]; + tets[8][2] = hex[3]; + tets[8][3] = mp0123; + tets[9][0] = hex[0]; + tets[9][1] = hex[1]; + tets[9][2] = hex[2]; + tets[9][3] = mp0123; + + tets[10][0] = hex[4]; + tets[10][1] = hex[0]; + tets[10][2] = hex[3]; + tets[10][3] = mp0473; + tets[11][0] = hex[4]; + tets[11][1] = hex[3]; + tets[11][2] = hex[7]; + tets[11][3] = mp0473; + + tets[12][0] = hex[5]; + tets[12][1] = hex[1]; + tets[12][2] = hex[2]; + tets[12][3] = mp1562; + tets[13][0] = hex[5]; + tets[13][1] = hex[1]; + tets[13][2] = hex[2]; + tets[13][3] = mp1562; + + tets[14][0] = hex[4]; + tets[14][1] = hex[5]; + tets[14][2] = hex[1]; + tets[14][3] = mp0451; + tets[15][0] = hex[4]; + tets[15][1] = hex[1]; + tets[15][2] = hex[0]; + tets[15][3] = mp0451; + + tets[16][0] = hex[7]; + tets[16][1] = hex[6]; + tets[16][2] = hex[2]; + tets[16][3] = mp3762; + tets[17][0] = hex[7]; + tets[17][1] = hex[6]; + tets[17][2] = hex[3]; + tets[17][3] = mp3762; + } +} + } // namespace experimental } // namespace quest } // namespace axom diff --git a/src/axom/quest/detail/Discretize_detail.hpp b/src/axom/quest/detail/Discretize_detail.hpp index a1ea0c6815..54d64daf8e 100644 --- a/src/axom/quest/detail/Discretize_detail.hpp +++ b/src/axom/quest/detail/Discretize_detail.hpp @@ -151,6 +151,7 @@ int discrSeg(const Point2D &a, const Point2D &b, int levels, axom::ArrayView::allocatorID(); +#if 0 // Assert input assumptions SLIC_ASSERT(b[0] - a[0] >= 0); SLIC_ASSERT(a[1] >= 0); @@ -165,6 +166,7 @@ int discrSeg(const Point2D &a, const Point2D &b, int levels, axom::ArrayView &polyline, axom::Array &out, int &octcount) { - int allocId = axom::execution_space::allocatorID(); + SLIC_ERROR_IF(!axom::execution_space::usesAllocId(out.getAllocatorID()), + axom::fmt::format("Execution space {} cannot access allocator id {}", + axom::execution_space::name(), + out.getAllocatorID())); + // Check for invalid input. If any segment is invalid, exit returning false. bool stillValid = true; int segmentcount = pointcount - 1; @@ -276,7 +282,7 @@ bool discretize(const axom::ArrayView &polyline, // invalid if a.x > b.x if(a[0] > b[0]) { - stillValid = false; + // stillValid = false; } if(a[1] < 0 || b[1] < 0) { @@ -293,7 +299,8 @@ bool discretize(const axom::ArrayView &polyline, // That was the octahedron count for one segment. Multiply by the number // of segments we will compute. int totaloctcount = segoctcount * segmentcount; - out = axom::Array(totaloctcount, totaloctcount, allocId); + out.empty(); + out.resize(axom::ArrayOptions::Uninitialized(), totaloctcount); axom::ArrayView out_view = out.view(); octcount = 0; diff --git a/src/axom/quest/detail/clipping/FSorClipper.cpp b/src/axom/quest/detail/clipping/FSorClipper.cpp new file mode 100644 index 0000000000..e7a9ead150 --- /dev/null +++ b/src/axom/quest/detail/clipping/FSorClipper.cpp @@ -0,0 +1,756 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "axom/config.hpp" + +#include "axom/core/numerics/matvecops.hpp" +#include "axom/core/utilities/Utilities.hpp" +#include "axom/primal/operators/squared_distance.hpp" +#include "axom/quest/Discretize.hpp" +#include "axom/quest/detail/clipping/FSorClipper.hpp" +#include "axom/fmt.hpp" + +#include + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +FSorClipper::FSorClipper(const klee::Geometry& kGeom, const std::string& name) + : MeshClipperStrategy(kGeom) + , m_name(name.empty() ? std::string("FSor") : name) + , m_maxRadius(0.0) + , m_minRadius(numerics::floating_point_limits::max()) + , m_transformer() +{ + extractClipperInfo(); + + combineRadialSegments(m_sorCurve); + axom::Array turnIndices = findZSwitchbacks(m_sorCurve.view()); + if(turnIndices.size() > 2) + { + // The 2 "turns" allowed are the first and last points. Anything else is a switchback. + SLIC_ERROR( + "FSorClipper does not work when a curve doubles back" + " in the axial direction. Use SorClipper instead."); + } + + for(auto& pt : m_sorCurve) + { + m_maxRadius = fmax(m_maxRadius, pt[1]); + m_minRadius = fmin(m_minRadius, pt[1]); + } + SLIC_ERROR_IF(m_minRadius < 0.0, + axom::fmt::format("FSorClipper '{}' has a negative radius", m_name)); + + // Combine internal and external rotations into m_transformer. + m_transformer.applyRotation(Vector3DType({1, 0, 0}), m_sorDirection); + m_transformer.applyTranslation(m_sorOrigin.array()); + m_transformer.applyMatrix(m_extTrans); + m_invTransformer = m_transformer.getInverse(); + + for(const auto& pt : m_sorCurve) + { + m_curveBb.addPoint(pt); + } +} + +FSorClipper::FSorClipper(const klee::Geometry& kGeom, + const std::string& name, + axom::ArrayView sorCurve, + const Point3DType& sorOrigin, + const Vector3DType& sorDirection, + axom::IndexType levelOfRefinement) + : MeshClipperStrategy(kGeom) + , m_name(name.empty() ? std::string("FSor") : name) + , m_sorCurve(sorCurve, axom::execution_space::allocatorID()) + , m_maxRadius(0.0) + , m_minRadius(numerics::floating_point_limits::max()) + , m_sorOrigin(sorOrigin) + , m_sorDirection(sorDirection) + , m_levelOfRefinement(levelOfRefinement) + , m_transformer() +{ + combineRadialSegments(m_sorCurve); + axom::Array turnIndices = findZSwitchbacks(m_sorCurve.view()); + if(turnIndices.size() > 2) + { + // The 2 "turns" allowed are the first and last points. Anything else is a switchback. + SLIC_ERROR( + "FSorClipper does not work when a curve doubles back" + " in the axial direction. Use SorClipper instead."); + } + + for(auto& pt : m_sorCurve) + { + m_maxRadius = fmax(m_maxRadius, pt[1]); + m_minRadius = fmin(m_minRadius, pt[1]); + } + SLIC_ERROR_IF(m_minRadius < 0.0, + axom::fmt::format("FSorClipper '{}' has a negative radius", m_name)); + + // Combine internal and external rotations into m_transformer. + m_transformer.applyRotation(Vector3DType({1, 0, 0}), m_sorDirection); + m_transformer.applyTranslation(m_sorOrigin.array()); + m_transformer.applyMatrix(m_extTrans); + m_invTransformer = m_transformer.getInverse(); + + for(const auto& pt : m_sorCurve) + { + m_curveBb.addPoint(pt); + } +} + +bool FSorClipper::labelCellsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& labels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "FSorClipper requires a 3D mesh."); + + const int allocId = shapeMesh.getAllocatorID(); + const auto cellCount = shapeMesh.getCellCount(); + if(labels.size() < cellCount || labels.getAllocatorID() != allocId) + { + labels = axom::Array(ArrayOptions::Uninitialized(), cellCount, cellCount, allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelCellsInOutImpl(shapeMesh, labels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelCellsInOutImpl(shapeMesh, labels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelCellsInOutImpl>(shapeMesh, labels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelCellsInOutImpl>(shapeMesh, labels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +bool FSorClipper::labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "FSorClipper requires a 3D mesh."); + + const int allocId = shapeMesh.getAllocatorID(); + const auto cellCount = cellIds.size(); + const auto tetCount = cellCount * NUM_TETS_PER_HEX; + if(tetLabels.size() < tetCount || tetLabels.getAllocatorID() != allocId) + { + tetLabels = axom::Array(ArrayOptions::Uninitialized(), tetCount, tetCount, allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +/* + * Implementation: (reverse) transform the mesh vertices to the r-z + * frame where the curve is defined as a r(z) function. It's easier to + * determine whether the point is in the sor that way. +*/ +template +void FSorClipper::labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView labels) +{ + axom::Array bbOn; + axom::Array bbUnder; + computeCurveBoxes(shapeMesh, bbOn, bbUnder); + const axom::ArrayView bbOnView = bbOn.view(); + const axom::ArrayView bbUnderView = bbUnder.view(); + + const auto cellCount = shapeMesh.getCellCount(); + auto meshHexes = shapeMesh.getCellsAsHexes(); + auto meshCellVolumes = shapeMesh.getCellVolumes(); + auto invTransformer = m_invTransformer; + constexpr double EPS = 1e-10; + + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType cellId) { + if(axom::utilities::isNearlyEqual(meshCellVolumes[cellId], 0.0, EPS)) + { + labels[cellId] = LabelType::LABEL_OUT; + return; + } + auto cellHex = meshHexes[cellId]; + for(int vi = 0; vi < HexahedronType::NUM_HEX_VERTS; ++vi) + { + invTransformer.transform(cellHex[vi].array()); + } + BoundingBox2DType cellBbInRz = estimateBoundingBoxInRz(cellHex); + labels[cellId] = rzBbToLabel(cellBbInRz, bbOnView, bbUnderView); + }); +} + +template +void FSorClipper::labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView labels) +{ + axom::Array bbOn; + axom::Array bbUnder; + computeCurveBoxes(shapeMesh, bbOn, bbUnder); + const axom::ArrayView bbOnView = bbOn.view(); + const axom::ArrayView bbUnderView = bbUnder.view(); + + const auto cellCount = cellIds.size(); + auto meshHexes = shapeMesh.getCellsAsHexes(); + auto tetVolumes = shapeMesh.getTetVolumes(); + auto invTransformer = m_invTransformer; + constexpr double EPS = 1e-10; + + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType ci) { + axom::IndexType cellId = cellIds[ci]; + + HexahedronType hex = meshHexes[cellId]; + for(int vi = 0; vi < HexahedronType::NUM_HEX_VERTS; ++vi) + { + invTransformer.transform(hex[vi].array()); + } + + TetrahedronType cellTets[NUM_TETS_PER_HEX]; + ShapeMesh::hexToTets(hex, cellTets); + + for(IndexType ti = 0; ti < NUM_TETS_PER_HEX; ++ti) + { + axom::IndexType tetId = cellId * NUM_TETS_PER_HEX + ti; + LabelType& tetLabel = labels[ci * NUM_TETS_PER_HEX + ti]; + if(axom::utilities::isNearlyEqual(tetVolumes[tetId], 0.0, EPS)) + { + tetLabel = LabelType::LABEL_OUT; + continue; + } + const TetrahedronType& tet = cellTets[ti]; + BoundingBox2DType bbInRz = estimateBoundingBoxInRz(tet); + tetLabel = rzBbToLabel(bbInRz, bbOnView, bbUnderView); + } + }); +} + +/* + Compute bounding box in rz space for a tet or hex geometry in + body frame (the 3D frame with the rotation along +x). + + 1. Rotate vertices into the rz plane. + 2. Compute bounding box for vertices. + 3. Expand 2D bounding box to contain edge that may + intersect SOR between vertices. +*/ +template +AXOM_HOST_DEVICE FSorClipper::BoundingBox2DType FSorClipper::estimateBoundingBoxInRz( + const PolyhedronType& vertices) +{ + FSorClipper::BoundingBox2DType bbInRz; + + // Range of vertex angles in cylindrical coordinates. + double minAngle = numerics::floating_point_limits::max(); + double maxAngle = -numerics::floating_point_limits::max(); + + for(IndexType vi = 0; vi < vertices.numVertices(); ++vi) + { + auto& vert = vertices[vi]; + Point2DType vertOnXPlane {vert[1], vert[2]}; + Point2DType vertOnRz { + vert[0], + std::sqrt(numerics::dot_product(vertOnXPlane.data(), vertOnXPlane.data(), 2))}; + bbInRz.addPoint(vertOnRz); + + double angle = atan2(vertOnXPlane[1], vertOnXPlane[0]); + minAngle = std::min(minAngle, angle); + maxAngle = std::max(maxAngle, angle); + } +#if 1 + /* + The geometry can be closer to the rotation axis than its + individual vertices are, depending on the angle (about the axis) + between the vertices. Given the angle, scale the bottom of bbInRz + for the worst case. + */ + double angleRange = maxAngle - minAngle; + double factor = angleRange > M_PI ? 0.0 : cos(angleRange / 2); + auto newMin = bbInRz.getMin(); + newMin[1] *= factor; + bbInRz.addPoint(newMin); +#endif +#if 0 + /* + Jeff's method to account for the angle. Faster, but less + discriminating, I think. + */ + auto newMin = bbInRz.getMin(); + newMin[1] -= 0.5 * cellLength; + if (newMin[1] < 0.0) newMin[1] = 0.0; + bbInRz.addPoint(newMin); +#endif + return bbInRz; +} + +/* + Compute label based on a bounding box in rz space. + + - If bbInRz is close to any bbOn, label it ON. + - Else if bbInRz touches any bbUnder, label it IN. + It cannot possibly be partially outside, because it + doesn't cross the boundary or even touch any bbOn. + - Else, label bbInRz OUT. + + We expect bbOn and bbUnder to be small arrays, so we use + linear searches. If that's too slow, we can use a BVH. +*/ +AXOM_HOST_DEVICE inline MeshClipperStrategy::LabelType FSorClipper::rzBbToLabel( + const BoundingBox2DType& bbInRz, + const axom::ArrayView& bbOn, + const axom::ArrayView& bbUnder) +{ + LabelType label = LabelType::LABEL_OUT; + + for(const auto& bbOn : bbOn) + { + double sqDist = axom::primal::squared_distance(bbInRz, bbOn); + if(sqDist <= 0.0) + { + label = LabelType::LABEL_ON; + } + } + + if(label == LabelType::LABEL_OUT) + { + for(const auto& bbUnder : bbUnder) + { + if(bbInRz.intersectsWith(bbUnder)) + { + label = LabelType::LABEL_IN; + } + } + } + + return label; +} + +/* +*/ +template +void FSorClipper::computeCurveBoxes(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& bbOn, + axom::Array& bbUnder) +{ + /* + * Compute bounding boxes bbOn, which cover the curve segments, and + * bbUnder, which cover the space between bbOn and the z axis. bbOn + * includes end caps, the segments that join the curve to the + * z-axis. + */ + const int allocId = shapeMesh.getAllocatorID(); + const IndexType cellCount = shapeMesh.getCellCount(); + + axom::ArrayView cellLengths = shapeMesh.getCellLengths(); + + using ReducePolicy = typename axom::execution_space::reduce_policy; + using LoopPolicy = typename execution_space::loop_policy; + RAJA::ReduceSum sumCharLength(0.0); + RAJA::forall( + RAJA::RangeSegment(0, cellCount), + AXOM_LAMBDA(axom::IndexType cellId) { sumCharLength += cellLengths[cellId]; }); + double avgCharLength = sumCharLength.get() / cellCount; + + /* + Subdivide the SOR curve and place it with the correct allocator. + Create temporary sorCurve that is equivalent to m_sorCurve but + - with long segments subdivided into subsegments based on + characteristic length of mesh cells. + - with memory from allocId. + */ + axom::Array sorCurve = subdivideCurve(m_sorCurve, + 3 * avgCharLength /* maxMean */, + -1 /* maxDz, negative disables */, + -1 /* minDz, negative disables */); + sorCurve = axom::Array(sorCurve, allocId); + auto sorCurveView = sorCurve.view(); + + /* + Compute 2 sets of boxes. + - bbOn have boxes over each segment. + - bbUnder have boxes from the z axis to the bottom of bbOn. + Add add to bbOn boxes representing the vertical endcaps of the curve. + */ + auto segCount = sorCurve.size() - 1; + bbOn = axom::Array(segCount + 2, segCount + 2, allocId); + bbUnder = axom::Array(segCount, segCount, allocId); + auto bbOnView = bbOn.view(); + auto bbUnderView = bbUnder.view(); + + axom::for_all( + segCount, + AXOM_LAMBDA(axom::IndexType i) { + BoundingBox2DType& on = bbOnView[i]; + BoundingBox2DType& under = bbUnderView[i]; + on.addPoint(sorCurveView[i]); + on.addPoint(sorCurveView[i + 1]); + Point2DType underMin {on.getMin()[0], 0.0}; + Point2DType underMax {on.getMax()[0], on.getMin()[1]}; + under = BoundingBox2DType(underMin, underMax); + }); + + axom::Array endCaps(2, 2); + endCaps[0].addPoint(m_sorCurve.front()); + endCaps[0].addPoint(Point2DType {m_sorCurve.front()[0], 0.0}); + endCaps[1].addPoint(m_sorCurve.back()); + endCaps[1].addPoint(Point2DType {m_sorCurve.back()[0], 0.0}); + axom::copy(&bbOn[segCount], endCaps.data(), endCaps.size() * sizeof(BoundingBox2DType)); +} + +/* + * Replace SOR curve segments that have bounding boxes that overlap + * too much beyond what the segments actually overlap. + * + * Goal: Split up segments with excessively large bounding boxes, + * which reach too far beyond the SOR curve. These are long diagonal + * segments. But don't split up segments aligned close to z or r + * directions, because they don't have excessively large bounding + * boxes for their size. We do this by limiting the harmonic mean of + * the r and z sides of the bounding boxes. + */ +Array FSorClipper::subdivideCurve(const Array& sorCurveIn, + double maxMean, + double maxDz, + double minDz) +{ + Array sorCurveOut; + + if(sorCurveIn.empty()) + { + return sorCurveOut; + } + + // Reserve guessed total number of points needed + sorCurveOut.reserve(sorCurveIn.size() * 1.2 + 10); + sorCurveOut.push_back(sorCurveIn[0]); + + for(IndexType i = 1; i < sorCurveIn.size(); ++i) + { + const Point2DType& segStart = sorCurveIn[i - 1]; + const Point2DType& segEnd = sorCurveIn[i]; + + const auto delta = segEnd.array() - segStart.array(); + const auto absDelta = axom::abs(delta); + const double segDz = absDelta[0]; + const double segDr = absDelta[1]; + const double segMean = 2 * segDz * segDr / (segDz + segDr); + + int numSplitsByMean = + maxMean <= 0 && segMean > maxMean ? 0 : static_cast(std::ceil(segMean / maxMean)) - 1; + int numSplitsByDz = + maxDz <= 0 && segDz > maxDz ? 0 : static_cast(std::ceil(segDz / maxDz)) - 1; + + // Prevent dz from falling below minDz + int numSplitsByMinDz = minDz <= 0 && segDz > minDz ? 0 : static_cast(segDz / minDz) - 1; + + int numSplits = std::min(std::max(numSplitsByMean, numSplitsByDz), numSplitsByMinDz); + + for(int j = 1; j < numSplits; ++j) + { + double t = static_cast(j) / numSplits; + Point2DType newPt(segStart.array() + t * delta); + sorCurveOut.push_back(newPt); + } + sorCurveOut.push_back(segEnd); + } + + return sorCurveOut; +} + +bool FSorClipper::getGeometryAsOcts(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& octs) +{ + AXOM_ANNOTATE_SCOPE("FSorClipper::getGeometryAsOcts"); + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + getGeometryAsOctsImpl(shapeMesh, octs); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + getGeometryAsOctsImpl(shapeMesh, octs); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + getGeometryAsOctsImpl>(shapeMesh, octs); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + getGeometryAsOctsImpl>(shapeMesh, octs); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +/* + Compute octahedral geometry representation, with an execution policy. + + Side effect: m_sorCurve data is reallocated to the shapeMesh allocator, + if it's not there yet. +*/ +template +bool FSorClipper::getGeometryAsOctsImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& octs) +{ + const int allocId = shapeMesh.getAllocatorID(); + octs = axom::Array(0, 0, allocId); + + const auto cellCount = shapeMesh.getCellCount(); + + // Compute an average characteristic length for the mesh cells. + using ReducePolicy = typename axom::execution_space::reduce_policy; + using LoopPolicy = typename execution_space::loop_policy; +#if 1 + axom::ArrayView cellVolumes = shapeMesh.getCellVolumes(); + RAJA::ReduceSum sumVolume(0.0); + RAJA::forall( + RAJA::RangeSegment(0, cellCount), + AXOM_LAMBDA(axom::IndexType cellId) { sumVolume += cellVolumes[cellId]; }); + double avgVolume = sumVolume.get() / cellCount; + double avgCharLength = pow(avgVolume, 1. / 3); +#else + axom::ArrayView cellLengths = shapeMesh.getCellLengths(); + RAJA::ReduceSum sumCharLength(0.0); + RAJA::forall( + RAJA::RangeSegment(0, cellCount), + AXOM_LAMBDA(axom::IndexType cellId) { sumCharLength += cellLengths[cellId]; }); + double avgCharLength = sumCharLength.get() / cellCount; +#endif + + axom::Array sorCurve = subdivideCurve(m_sorCurve, + 3 * avgCharLength /* maxMean */, + 3 * avgCharLength /* maxDz */, + 2 * avgCharLength /* minDz */); + + // Generate the Octahedra + int octCount = 0; + const bool good = axom::quest::discretize(sorCurve.view(), + int(sorCurve.size()), + m_levelOfRefinement, + octs, + octCount); + + AXOM_UNUSED_VAR(good); + SLIC_ASSERT(good); + SLIC_ASSERT(octCount == octs.size()); + + auto transformer = m_transformer; + auto octsView = octs.view(); + axom::for_all( + octCount, + AXOM_LAMBDA(axom::IndexType iOct) { + OctahedronType& oct = octsView[iOct]; + for(int iVert = 0; iVert < OctahedronType::NUM_VERTS; ++iVert) + { + transformer.transform(oct[iVert].array()); + } + }); + + SLIC_INFO(axom::fmt::format("FSorClipper '{}' {}-level refinement got {} geometry octs from {} curve points.", + name(), + m_levelOfRefinement, + octs.size(), + sorCurve.size())); + + return true; +} + +/* + Combine consecutive radial segments in SOR curve. Change in place. +*/ +void FSorClipper::combineRadialSegments(axom::Array& sorCurve) +{ + int ptCount = sorCurve.size(); + if(ptCount < 3) + { + return; + } + + constexpr double eps = 1e-14; + + // Set sorCurve[j] to sorCurve[i] where j <= i, skipping points + // joining consecutive radial segments. + + int j = 1; + bool prevIsRadial = axom::utilities::isNearlyEqual(sorCurve[j][0] - sorCurve[j - 1][0], eps); + bool curIsRadial = false; + for(int i = 2; i < ptCount; ++i) + { + curIsRadial = axom::utilities::isNearlyEqual(sorCurve[i][0] - sorCurve[i - 1][0], eps); + /* + Current and previous segments share point j. If both are + consecutive radial segments, discard point j by overwriting it + with point i. Else, copy point i to a new point j. + */ + if(!(curIsRadial && prevIsRadial)) + { + ++j; + } + sorCurve[j] = sorCurve[i]; + prevIsRadial = curIsRadial; + } + sorCurve.resize(j + 1); +} + +/* + Find points along the r-z curve where the z-coordinate changes direction. + + Cases 1 and 2 below show direction changes at point o. Case 3 + shows a potential change at the radial segment, but not a real + change. (Radial segments have constant z and align with the radial + direction.) To decide between cases 2 and 3, defer until the + segment after the radial segment. (The next segment is not radial + because adjacent radials have been combined by combineRadialSegments.) + For case 2, prefer to split at the point closer to the axis of + rotation. + + r ^ + (or y) | (1) (2) (3) + | Single Radial Radial + | point segment segment w/o + | change change change + | + | \ \ \ + | \ \ \ + | o | | + | / o \ + | / / \ + +-------------------------------------> z (or x) +*/ +axom::Array FSorClipper::findZSwitchbacks(axom::ArrayView pts) +{ + const axom::IndexType segCount = pts.size() - 1; + SLIC_ASSERT(segCount > 0); + + // boundaryIdx is where curve's axial direction changes, plus end points. + axom::Array boundaryIdx(0, 2); + boundaryIdx.push_back(0); + + constexpr double eps = 1e-14; + + if(segCount > 1) + { + // Direction is whether z increases or decreases along the curve. + // curDir is the current direction, ignoring radial segments, + // which don't change z. + int curDir = axom::utilities::sign_of(pts[1][0] - pts[0][0], eps); + if(curDir == 0) + { + curDir = axom::utilities::sign_of(pts[2][0] - pts[1][0], eps); + } + + // Detect where z changes direction, and note those indices. + for(axom::IndexType i = 1; i < segCount; ++i) + { + int segDir = axom::utilities::sign_of(pts[i + 1][0] - pts[i][0], eps); + if(segDir == 0) + { + // Radial segment may or may not indicate change. Decide with next segment. + continue; + } + if(segDir != curDir) + { + // Direction change + int prevSegDir = axom::utilities::sign_of(pts[i][0] - pts[i - 1][0], eps); + if(prevSegDir != 0) + { + // Case 1, a clear turn not involving a radial segment. + boundaryIdx.push_back(i); + } + else + { + // Case 2, involving a radial segment. + // Use the radially-closer point of the segment. + int splitI = pts[i][1] < pts[i - 1][1] ? i : i - 1; + boundaryIdx.push_back(splitI); + } + curDir = segDir; + SLIC_ASSERT(curDir != 0); // curDir ignores radial segments. + } + } + } + boundaryIdx.push_back(pts.size() - 1); + return boundaryIdx; +} + +void FSorClipper::extractClipperInfo() +{ + auto sorOriginArray = m_info.fetch_existing("sorOrigin").as_double_array(); + auto sorDirectionArray = m_info.fetch_existing("sorDirection").as_double_array(); + for(int d = 0; d < 3; ++d) + { + m_sorOrigin[d] = sorOriginArray[d]; + m_sorDirection[d] = sorDirectionArray[d]; + } + + auto discreteFunctionArray = m_info.fetch_existing("discreteFunction").as_double_array(); + auto n = discreteFunctionArray.number_of_elements(); + + SLIC_ERROR_IF( + n % 2 != 0, + axom::fmt::format( + "***FSorClipper: Discrete function must have an even number of values. It has {}.", + n)); + + m_sorCurve.resize(axom::ArrayOptions::Uninitialized(), n / 2); + for(int i = 0; i < n / 2; ++i) + { + m_sorCurve[i] = Point2DType {discreteFunctionArray[i * 2], discreteFunctionArray[i * 2 + 1]}; + } + + m_levelOfRefinement = m_info.fetch_existing("levelOfRefinement").to_double(); +} + +} // namespace experimental +} // end namespace quest +} // end namespace axom diff --git a/src/axom/quest/detail/clipping/FSorClipper.hpp b/src/axom/quest/detail/clipping/FSorClipper.hpp new file mode 100644 index 0000000000..e46dd0031a --- /dev/null +++ b/src/axom/quest/detail/clipping/FSorClipper.hpp @@ -0,0 +1,210 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#ifndef AXOM_QUEST_FSORCLIPPER_HPP +#define AXOM_QUEST_FSORCLIPPER_HPP + +#include "axom/klee/Geometry.hpp" +#include "axom/quest/MeshClipperStrategy.hpp" +#include "axom/primal/geometry/CoordinateTransformer.hpp" + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +/*! + * @brief Geometry clipping operations for 3D + * surface-of-revolution geometries. + + * This implementation requires the SOR curve to be a function. + * It requires axial coordinates to be monotonic but doesn't require + * them to be strictly monotonic. For SOR curves where the axial + * coordinates change directions, use SorClipper. + + * The SOR specification may include axis orientation and location + * in addition to any external transformation. +*/ +class FSorClipper : public MeshClipperStrategy +{ +public: + /*! + * @brief Constructor. + + * @param [in] kGeom Describes the shape to place + * into the mesh. + * @param [in] name To override the default strategy name + */ + FSorClipper(const klee::Geometry& kGeom, const std::string& name = ""); + + /*! + * @brief Construct from geometric specifications. + */ + FSorClipper(const klee::Geometry& kGeom, + const std::string& name, + axom::ArrayView sorCurve, + const Point3DType& sorOrigin, + const Vector3DType& sorDirection, + axom::IndexType levelOfRefinement); + + virtual ~FSorClipper() = default; + + const std::string& name() const override { return m_name; } + + bool labelCellsInOut(quest::experimental::ShapeMesh& shappeMesh, + axom::Array& label) override; + + bool labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) override; + + bool getGeometryAsOcts(quest::experimental::ShapeMesh& shappeMesh, + axom::Array& octs) override; + + axom::ArrayView getSorCurve() const { return m_sorCurve.view(); } + + //@{ + //! @name Utilities shared with SorClipper for handling SOR. + /*! + * @brief Find division points between curve sections where z (x) + * changes directions. + + * @param sorCurve Set of at least 2 2D points describing a curve + * in r-z space (in host array). + + * @return Indices of switchbacks, plus the first and last indices. + */ + static axom::Array findZSwitchbacks(axom::ArrayView pts); + + /* + * @brief Combine consecutive radial segments of the curve into a + * single segment. + + * This step is necessary because some other steps assume there are + * no consecutive radial segments. + */ + static void combineRadialSegments(axom::Array& sorCurve); + //@} + +#if !defined(__CUDACC__) +private: +#endif + std::string m_name; + + /*! + * @brief The discrete r(z) curve as an array of y(x) points. + + * This data is before internal or external transformations. + * It may include points on each end to connect the curve to + * the axis of rotation. + */ + axom::Array m_sorCurve; + + //! @brief Bounding box of points in m_sorCurve; + BoundingBox2DType m_curveBb; + + //! @brief Maximum radius of the SOR. + double m_maxRadius; + + //! @brief Minimum radius of the SOR. + double m_minRadius; + + //!@brief The point corresponding to z=0 on the SOR axis. + Point3DType m_sorOrigin; + + //!@brief SOR axis in 3D space, in the direction of increasing z. + Vector3DType m_sorDirection; + + //!@brief Level of refinement for discretizing curved + // analytical shapes and surfaces of revolutions. + axom::IndexType m_levelOfRefinement = 0; + + /*! + * @brief Boxes (in rz space) on the curve. + + * The curve lies completely in these boxes and includes the planes + * of the base and top. Points in these boxes are require more + * computation to determine their signed distance. + */ + axom::Array m_bbOn; + + /*! + * @brief Boxes (in rz space) completely under the curve. + + * These boxes lie completely under the curve. + */ + axom::Array m_bbUnder; + + //!@brief Internal and external transforms (includes m_sorDirection and m_sorOrigin). + axom::primal::experimental::CoordinateTransformer m_transformer; + + /*! + * @brief Inverse of m_transformer. + * + * Axom supports vector scaling. @see axom::klee::Scale. This means + * a SOR may be transformed into a shape that we cannot represent. + * Therefore, we don't transform the shape until after it's discretized. + * When needed, we will inverse-transform the mesh. + */ + axom::primal::experimental::CoordinateTransformer m_invTransformer; + + template + void labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView label); + + template + void labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView tetLabels); + + template + void computeCurveBoxes(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& bbOn, + axom::Array& bbUnder); + + /*! + * @brief Compute 2D bounding box of a polyhedron in the r-z plane. + * @tparam PolyhedronType Either TetrahedronType or HexahedronType. + */ + template + AXOM_HOST_DEVICE BoundingBox2DType estimateBoundingBoxInRz(const PolyhedronType& vertices); + + AXOM_HOST_DEVICE inline MeshClipperStrategy::LabelType rzBbToLabel( + const BoundingBox2DType& bbInRz, + const axom::ArrayView& bbOn, + const axom::ArrayView& bbUnder); + + // Extract clipper info from MeshClipperStrategy::m_info. + void extractClipperInfo(); + + /*! + * @brief Subdivide large segments of the SOR curve to make + * screening more precise. + * + * @param sorCurveIn [in] Un-divided SOR curve + * @param maxMean [in] Subdivide segment if the harmonic mean + * of its bounding box sides exceeds this value. + * @param maxDz [in] Subdivide segment if its length along the + * axis of symmetry exceeds this value. + * @param minDz [in] Don't subdivide segments below this dz. + */ + axom::Array subdivideCurve(const Array& sorCurveIn, + double maxMean, + double maxDz, + double minDz); + + //!@brief Compute geometry as octs, by policy. + template + bool getGeometryAsOctsImpl(quest::experimental::ShapeMesh& shappeMesh, + axom::Array& octs); +}; + +} // namespace experimental +} // namespace quest +} // namespace axom + +#endif // AXOM_QUEST_FSORCLIPPER_HPP diff --git a/src/axom/quest/detail/clipping/HexClipper.cpp b/src/axom/quest/detail/clipping/HexClipper.cpp new file mode 100644 index 0000000000..4eb12574e1 --- /dev/null +++ b/src/axom/quest/detail/clipping/HexClipper.cpp @@ -0,0 +1,340 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "axom/config.hpp" + +#include "axom/quest/detail/clipping/HexClipper.hpp" + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +HexClipper::HexClipper(const klee::Geometry& kGeom, const std::string& name) + : MeshClipperStrategy(kGeom) + , m_name(name.empty() ? std::string("Hex") : name) + , m_transformer(m_extTrans) +{ + extractClipperInfo(); + + for(int i = 0; i < HexahedronType::NUM_HEX_VERTS; ++i) + { + m_hex[i] = m_transformer.getTransformed(m_hexBeforeTrans[i]); + } + + axom::StackArray geomTets; + ShapeMesh::hexToTets(m_hex, geomTets.data()); + m_tets.reserve(geomTets.size()); + constexpr double EPS = 1e-10; + for(const auto& tet : geomTets) + { + if(!axom::utilities::isNearlyEqual(tet.volume(), 0.0, EPS)) + { + m_tets.push_back(tet); + } + } + + for(int i = 0; i < HexahedronType::NUM_HEX_VERTS; ++i) + { + m_hexBb.addPoint(m_hex[i]); + } + + computeSurface(); +} + +bool HexClipper::labelCellsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& labels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "HexClipper requires a 3D mesh."); + + int allocId = shapeMesh.getAllocatorID(); + auto cellCount = shapeMesh.getCellCount(); + if(labels.size() < cellCount || labels.getAllocatorID() != shapeMesh.getAllocatorID()) + { + labels = axom::Array(ArrayOptions::Uninitialized(), cellCount, cellCount, allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelCellsInOutImpl(shapeMesh, labels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelCellsInOutImpl(shapeMesh, labels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelCellsInOutImpl>(shapeMesh, labels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelCellsInOutImpl>(shapeMesh, labels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +bool HexClipper::labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) +{ + const axom::IndexType cellCount = cellIds.size(); + const int allocId = shapeMesh.getAllocatorID(); + + if(tetLabels.size() < cellCount * NUM_TETS_PER_HEX || + tetLabels.getAllocatorID() != shapeMesh.getAllocatorID()) + { + tetLabels = axom::Array(ArrayOptions::Uninitialized(), + cellCount * NUM_TETS_PER_HEX, + cellCount * NUM_TETS_PER_HEX, + allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +template +void HexClipper::labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView labels) +{ + const auto cellCount = shapeMesh.getCellCount(); + const int allocId = shapeMesh.getAllocatorID(); + const auto cellBbs = shapeMesh.getCellBoundingBoxes(); + const auto cellsAsHexes = shapeMesh.getCellsAsHexes(); + const auto cellVolumes = shapeMesh.getCellVolumes(); + const auto hexBb = m_hexBb; + const auto surfaceTriangles = m_surfaceTriangles; + axom::Array tets(m_tets, allocId); + axom::ArrayView tetsView = tets.view(); + constexpr double EPS = 1e-10; + + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType cellId) { + auto& cellLabel = labels[cellId]; + if(axom::utilities::isNearlyEqual(cellVolumes[cellId], 0.0, EPS)) + { + cellLabel = LabelType::LABEL_OUT; + return; + } + auto& cellBb = cellBbs[cellId]; + const auto& cellHex = cellsAsHexes[cellId]; + cellLabel = polyhedronToLabel(cellHex, cellBb, hexBb, tetsView, surfaceTriangles); + }); + + return; +} + +template +void HexClipper::labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView tetLabels) +{ + const axom::IndexType cellCount = cellIds.size(); + const int allocId = shapeMesh.getAllocatorID(); + auto meshHexes = shapeMesh.getCellsAsHexes(); + auto tetVolumes = shapeMesh.getTetVolumes(); + const auto hexBb = m_hexBb; + const auto surfaceTriangles = m_surfaceTriangles; + axom::Array tets(m_tets, allocId); + axom::ArrayView tetsView = tets.view(); + constexpr double EPS = 1e-10; + + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType ci) { + axom::IndexType cellId = cellIds[ci]; + const HexahedronType& hex = meshHexes[cellId]; + + TetrahedronType cellTets[NUM_TETS_PER_HEX]; + ShapeMesh::hexToTets(hex, cellTets); + + for(IndexType ti = 0; ti < NUM_TETS_PER_HEX; ++ti) + { + const TetrahedronType& cellTet = cellTets[ti]; + LabelType& tetLabel = tetLabels[ci * NUM_TETS_PER_HEX + ti]; + axom::IndexType tetId = cellId * NUM_TETS_PER_HEX + ti; + if(axom::utilities::isNearlyEqual(tetVolumes[tetId], 0.0, EPS)) + { + tetLabel = LabelType::LABEL_OUT; + continue; + } + BoundingBox3DType cellTetBb {cellTet[0], cellTet[1], cellTet[2], cellTet[3]}; + tetLabel = polyhedronToLabel(cellTet, cellTetBb, hexBb, tetsView, surfaceTriangles); + } + }); + return; +} + +template +AXOM_HOST_DEVICE inline MeshClipperStrategy::LabelType HexClipper::polyhedronToLabel( + const Polyhedron& verts, + const BoundingBox3DType& vertsBb, + const BoundingBox3DType& hexBb, + const axom::ArrayView& hexTets, + const axom::StackArray& surfaceTriangles) const +{ + /* + If vertsBb and hexBb don't intersect, nothing intersects. + This check is not technically needed, because the thecks + below can catch it, but it is fast and can avoid the more + expensive surface triangle intersection checks below. + */ + if(!hexBb.intersectsWith(vertsBb)) + { + return LabelType::LABEL_OUT; + } + + // If vertsBb intersects hex surface, there's a high chance cell does too. + for(int ti = 0; ti < 24; ++ti) + { + const auto& surfTri = surfaceTriangles[ti]; + if(axom::primal::intersect(surfTri, vertsBb)) + { + return LabelType::LABEL_ON; + } + } + /* + After eliminating possibility that polyhedron is on the surface, + it's either completely inside or completely out. + It's IN if any part of it is IN, so check an arbitrary vertex. + Note: Should the arbitrary vertex be some weird corner case, we could + use an alternative, like averaging two opposite corners, 0 and 6. + */ + constexpr double eps = 1e-12; + const Point3DType& ptInCell(verts[0]); + for(const auto& tet : hexTets) + { + if(tet.contains(ptInCell, eps)) + { + return LabelType::LABEL_IN; + } + } + return LabelType::LABEL_OUT; +} + +bool HexClipper::getGeometryAsTets(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& tets) +{ + int allocId = shapeMesh.getAllocatorID(); + if(tets.getAllocatorID() != allocId || tets.size() != m_tets.size()) + { + tets = axom::Array(m_tets.size(), m_tets.size(), allocId); + } + axom::copy(tets.data(), m_tets.data(), m_tets.size() * sizeof(TetrahedronType)); + return true; +} + +void HexClipper::extractClipperInfo() +{ + const auto v0 = m_info.fetch_existing("v0").as_double_array(); + const auto v1 = m_info.fetch_existing("v1").as_double_array(); + const auto v2 = m_info.fetch_existing("v2").as_double_array(); + const auto v3 = m_info.fetch_existing("v3").as_double_array(); + const auto v4 = m_info.fetch_existing("v4").as_double_array(); + const auto v5 = m_info.fetch_existing("v5").as_double_array(); + const auto v6 = m_info.fetch_existing("v6").as_double_array(); + const auto v7 = m_info.fetch_existing("v7").as_double_array(); + for(int d = 0; d < 3; ++d) + { + m_hexBeforeTrans[0][d] = v0[d]; + m_hexBeforeTrans[1][d] = v1[d]; + m_hexBeforeTrans[2][d] = v2[d]; + m_hexBeforeTrans[3][d] = v3[d]; + m_hexBeforeTrans[4][d] = v4[d]; + m_hexBeforeTrans[5][d] = v5[d]; + m_hexBeforeTrans[6][d] = v6[d]; + m_hexBeforeTrans[7][d] = v7[d]; + } +} + +void HexClipper::computeSurface() +{ + // Hex vertex shorthands + // See the Hexahedron class documentation, especially the ASCII art. + const auto& p = m_hex[0]; + const auto& q = m_hex[1]; + const auto& r = m_hex[2]; + const auto& s = m_hex[3]; + const auto& t = m_hex[4]; + const auto& u = m_hex[5]; + const auto& v = m_hex[6]; + const auto& w = m_hex[7]; + + // 6 face centers, right handed, oriented inward. + Point3DType pswt(axom::NumericArray {p.array() + s.array() + w.array() + t.array()} / 4); + Point3DType quvr(axom::NumericArray {q.array() + u.array() + v.array() + r.array()} / 4); + + Point3DType ptuq(axom::NumericArray {p.array() + t.array() + u.array() + q.array()} / 4); + Point3DType srvw(axom::NumericArray {s.array() + r.array() + v.array() + w.array()} / 4); + + Point3DType pqrs(axom::NumericArray {p.array() + q.array() + r.array() + s.array()} / 4); + Point3DType twvu(axom::NumericArray {t.array() + w.array() + v.array() + u.array()} / 4); + + m_surfaceTriangles[0] = Triangle3DType(pswt, p, s); + m_surfaceTriangles[1] = Triangle3DType(pswt, s, w); + m_surfaceTriangles[2] = Triangle3DType(pswt, w, t); + m_surfaceTriangles[3] = Triangle3DType(pswt, t, p); + + m_surfaceTriangles[4] = Triangle3DType(quvr, q, u); + m_surfaceTriangles[5] = Triangle3DType(quvr, u, v); + m_surfaceTriangles[6] = Triangle3DType(quvr, v, r); + m_surfaceTriangles[7] = Triangle3DType(quvr, r, q); + + m_surfaceTriangles[8] = Triangle3DType(ptuq, p, t); + m_surfaceTriangles[9] = Triangle3DType(ptuq, t, u); + m_surfaceTriangles[10] = Triangle3DType(ptuq, u, q); + m_surfaceTriangles[11] = Triangle3DType(ptuq, q, p); + + m_surfaceTriangles[12] = Triangle3DType(srvw, s, r); + m_surfaceTriangles[13] = Triangle3DType(srvw, r, v); + m_surfaceTriangles[14] = Triangle3DType(srvw, v, w); + m_surfaceTriangles[15] = Triangle3DType(srvw, w, s); + + m_surfaceTriangles[16] = Triangle3DType(pqrs, p, q); + m_surfaceTriangles[17] = Triangle3DType(pqrs, q, r); + m_surfaceTriangles[18] = Triangle3DType(pqrs, r, s); + m_surfaceTriangles[19] = Triangle3DType(pqrs, s, p); + + m_surfaceTriangles[20] = Triangle3DType(twvu, t, w); + m_surfaceTriangles[21] = Triangle3DType(twvu, w, v); + m_surfaceTriangles[22] = Triangle3DType(twvu, v, u); + m_surfaceTriangles[23] = Triangle3DType(twvu, u, t); +} + +} // namespace experimental +} // end namespace quest +} // end namespace axom diff --git a/src/axom/quest/detail/clipping/HexClipper.hpp b/src/axom/quest/detail/clipping/HexClipper.hpp new file mode 100644 index 0000000000..4afb5f3a30 --- /dev/null +++ b/src/axom/quest/detail/clipping/HexClipper.hpp @@ -0,0 +1,108 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#ifndef AXOM_QUEST_HEXCLIPPER_HPP +#define AXOM_QUEST_HEXCLIPPER_HPP + +#include "axom/klee/Geometry.hpp" +#include "axom/quest/MeshClipperStrategy.hpp" +#include "axom/primal/geometry/CoordinateTransformer.hpp" + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +/*! + * @brief Geometry clipping operations for sphere geometries. +*/ +class HexClipper : public MeshClipperStrategy +{ +public: + /*! + * @brief Constructor. + * + * @param [in] kGeom Describes the shape to place + * into the mesh. + * @param [in] name To override the default strategy name + * + * \c kGeom.asHierarchy() must contain the following data: + * - v0, v1, v2, ..., v8: each contains a 3D coordinates of the + * hexahedron vertices, in the order used by primal::Hexahedron. + * The hex may be degenerate, but when subdivided into tetrahedra, + * none of them may be inverted (have negative volume). + */ + HexClipper(const klee::Geometry& kGeom, const std::string& name = ""); + + virtual ~HexClipper() = default; + + const std::string& name() const override { return m_name; } + + /*! + * If a mesh cell has all vertices outside the geometry, it labeled outside. + * This will miss cases where an edge of the cell passes through the geometry. + */ + bool labelCellsInOut(quest::experimental::ShapeMesh& shappeMesh, + axom::Array& label) override; + + bool labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) override; + + bool getGeometryAsTets(quest::experimental::ShapeMesh& shappeMesh, + axom::Array& tets) override; + +#if !defined(__CUDACC__) +private: +#endif + std::string m_name; + + //!@brief Hexahedron before transformation. + HexahedronType m_hexBeforeTrans; + + //!@brief Hexahedron after transformation. + HexahedronType m_hex; + + //!@brief Bounding box of m_hex. + BoundingBox3DType m_hexBb; + + //!@brief Tetrahedralized version of of m_hex. + axom::Array m_tets; + + //!@brief Triangles on the discretized hex surface, oriented inward. + axom::StackArray m_surfaceTriangles; + + axom::primal::experimental::CoordinateTransformer m_transformer; + + template + void labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView label); + + template + void labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView tetLabels); + + //!@brief Compute LabelType for a polyhedron (hex or tet in our case). + template + AXOM_HOST_DEVICE inline LabelType polyhedronToLabel( + const Polyhedron& verts, + const BoundingBox3DType& vertsBb, + const BoundingBox3DType& hexBb, + const axom::ArrayView& hexTets, + const axom::StackArray& surfaceTriangles) const; + + void extractClipperInfo(); + + void computeSurface(); +}; + +} // namespace experimental +} // namespace quest +} // namespace axom + +#endif // AXOM_QUEST_HEXCLIPPER_HPP diff --git a/src/axom/quest/detail/clipping/MeshClipperImpl.hpp b/src/axom/quest/detail/clipping/MeshClipperImpl.hpp index fd352aa602..94f48baa35 100644 --- a/src/axom/quest/detail/clipping/MeshClipperImpl.hpp +++ b/src/axom/quest/detail/clipping/MeshClipperImpl.hpp @@ -6,13 +6,17 @@ #ifndef AXOM_MESHCLIPPERIMPL_HPP_ #define AXOM_MESHCLIPPERIMPL_HPP_ +#include "axom/config.hpp" + #ifndef AXOM_USE_RAJA #error "quest::MeshClipper requires RAJA." #endif +#include "axom/core/numerics/matvecops.hpp" #include "axom/quest/MeshClipperStrategy.hpp" #include "axom/quest/MeshClipper.hpp" #include "axom/spin/BVH.hpp" +#include "axom/primal/geometry/CoordinateTransformer.hpp" #include "RAJA/RAJA.hpp" namespace axom @@ -39,6 +43,12 @@ class MeshClipperImpl : public MeshClipper::Impl { public: using LabelType = MeshClipper::LabelType; + using Point3DType = primal::Point; + using Plane3DType = primal::Plane; + using BoundingBoxType = primal::BoundingBox; + using TetrahedronType = primal::Tetrahedron; + using OctahedronType = primal::Octahedron; + using CoordTransformer = primal::experimental::CoordinateTransformer; MeshClipperImpl(MeshClipper& clipper) : MeshClipper::Impl(clipper) { } @@ -109,7 +119,7 @@ class MeshClipperImpl : public MeshClipper::Impl return; }; - AXOM_ANNOTATE_SCOPE("MeshClipper::collect_unlabeleds"); + AXOM_ANNOTATE_SCOPE("MeshClipper:collect_indices"); /*! * 1. Generate tmpLabels, having a value of 1 where labels is LABEL_ON and zero elsewhere. * 2. Inclusive scan on tmpLabels to generate values that step up at LABEL_ON cells. @@ -124,42 +134,40 @@ class MeshClipperImpl : public MeshClipper::Impl const axom::IndexType labelCount = labels.size(); - axom::Array tmpLabels(labels.shape(), labels.getAllocatorID()); + axom::ReduceSum onCountReduce{0}; + axom::Array tmpLabels(ArrayOptions::Uninitialized(), + 1 + labels.size(), + 0, + labels.getAllocatorID()); + tmpLabels.fill(0, 1, 0); auto tmpLabelsView = tmpLabels.view(); axom::for_all( labelCount, - AXOM_LAMBDA(axom::IndexType ci) { tmpLabelsView[ci] = labels[ci] == LabelType::LABEL_ON; }); + AXOM_LAMBDA(axom::IndexType ci) { + bool isOn = labels[ci] == LabelType::LABEL_ON; + tmpLabelsView[1 + ci] = isOn; + onCountReduce += isOn; + }); RAJA::inclusive_scan_inplace(RAJA::make_span(tmpLabels.data(), tmpLabels.size()), RAJA::operators::plus {}); - axom::IndexType onCount; // Count of tets labeled ON. - axom::copy(&onCount, &tmpLabels.back(), sizeof(onCount)); - + axom::IndexType onCount = onCountReduce.get(); if(onIndices.size() < onCount || onIndices.getAllocatorID() != labels.getAllocatorID()) { onIndices = axom::Array {axom::ArrayOptions::Uninitialized(), onCount, - onCount, + 0, labels.getAllocatorID()}; } auto onIndicesView = onIndices.view(); - - LabelType firstLabel = LabelType::LABEL_IN; - axom::copy(&firstLabel, &labels[0], sizeof(firstLabel)); - if(firstLabel == LabelType::LABEL_ON) - { - axom::IndexType zero = 0; - axom::copy(&onIndices[0], &zero, sizeof(zero)); - } - axom::for_all( 1, - labelCount, + 1 + labelCount, AXOM_LAMBDA(axom::IndexType i) { if(tmpLabelsView[i] != tmpLabelsView[i - 1]) { - onIndicesView[tmpLabelsView[i - 1]] = i; + onIndicesView[tmpLabelsView[i] - 1] = i - 1; } }); } @@ -184,99 +192,69 @@ class MeshClipperImpl : public MeshClipper::Impl }); } + // Work space for clip counters. + struct ClippingStats + { + axom::ReduceSum inSum{0}; + axom::ReduceSum onSum{0}; + axom::ReduceSum outSum{0}; + axom::ReduceSum missSum{0}; + ClippingStats() + : inSum(0) + , onSum(0) + , outSum(0) + , missSum(0) + {} + void copyTo(conduit::Node& stats) + { + // Place clip counts in statistics container. + std::int64_t clipsInCount = inSum.get(); + std::int64_t clipsOnCount = onSum.get(); + std::int64_t clipsOutCount = outSum.get(); + std::int64_t clipsMissCount = missSum.get(); + stats["clipsIn"].set_int64(clipsInCount); + stats["clipsOn"].set_int64(clipsOnCount); + stats["clipsOut"].set_int64(clipsOutCount); + stats["clipsMiss"].set_int64(clipsMissCount); + stats["clipsSum"] = clipsInCount + clipsOnCount + clipsOutCount; + } + }; + /* * Clip tets from the mesh with tets or octs from the clipping * geometry. This implementation was lifted from IntersectionShaper * and modified to work both tet and oct representations of the * geometry. */ - void computeClipVolumes3D(axom::ArrayView ovlap) override + void computeClipVolumes3D(axom::ArrayView ovlap, conduit::Node& statistics) override { - AXOM_ANNOTATE_SCOPE("MeshClipper::computeClipVolumes3D"); - - using BoundingBoxType = primal::BoundingBox; + using ATOMIC_POL = typename axom::execution_space::atomic_policy; ShapeMesh& shapeMesh = getShapeMesh(); - const int allocId = shapeMesh.getAllocatorID(); - const IndexType cellCount = shapeMesh.getCellCount(); - SLIC_INFO(axom::fmt::format( - "MeshClipper::computeClipVolumes3D: Getting discrete geometry for shape '{}'", - getStrategy().name())); - - // - // Get the geometry in discrete pieces, which can be tets or octs. - // - auto& strategy = getStrategy(); + /* + * Geometry as discrete tets or octs, and their bounding boxes. + */ axom::Array> geomAsTets; axom::Array> geomAsOcts; - const bool useOcts = strategy.getGeometryAsOcts(shapeMesh, geomAsOcts); - const bool useTets = strategy.getGeometryAsTets(shapeMesh, geomAsTets); - SLIC_ASSERT(useOcts || geomAsOcts.empty()); - SLIC_ASSERT(useTets || geomAsTets.empty()); - if(useTets == useOcts) - { - SLIC_ERROR( - axom::fmt::format("Problem with MeshClipperStrategy implementation '{}'." - " Implementations that don't provide a specializedClip function" - " must provide exactly one getGeometryAsOcts() or getGeometryAsTets()." - " This implementation provides {}.", - strategy.name(), - int(useOcts) + int(useTets))); - } - + axom::Array pieceBbs; + spin::BVH<3, ExecSpace, double> bvh; + bool useTets = getDiscreteGeometry(geomAsTets, geomAsOcts, pieceBbs, bvh); auto geomTetsView = geomAsTets.view(); auto geomOctsView = geomAsOcts.view(); - SLIC_INFO(axom::fmt::format("{:-^80}", " Inserting shapes' bounding boxes into BVH ")); - - // - // Generate the BVH over the (bounding boxes of the) discretized geometry - // - const axom::IndexType bbCount = useTets ? geomAsTets.size() : geomAsOcts.size(); - axom::Array pieceBbs(bbCount, bbCount, allocId); - axom::ArrayView pieceBbsView = pieceBbs.view(); - - // Get the bounding boxes for the shapes - if(useTets) - { - axom::for_all( - pieceBbsView.size(), - AXOM_LAMBDA(axom::IndexType i) { - pieceBbsView[i] = primal::compute_bounding_box(geomTetsView[i]); - }); - } - else - { - axom::for_all( - pieceBbsView.size(), - AXOM_LAMBDA(axom::IndexType i) { - pieceBbsView[i] = primal::compute_bounding_box(geomOctsView[i]); - }); - } - - spin::BVH<3, ExecSpace, double> bvh; - bvh.initialize(pieceBbsView, pieceBbsView.size()); - - SLIC_INFO(axom::fmt::format("{:-^80}", " Querying the BVH tree ")); - - axom::ArrayView cellBbsView = shapeMesh.getCellBoundingBoxes(); - - // Find which shape bounding boxes intersect hexahedron bounding boxes - SLIC_INFO( - axom::fmt::format("{:-^80}", " Finding shape candidates for each hexahedral element ")); + /* + * Find which shape bounding boxes intersect hexahedron bounding boxes + */ - axom::Array offsets(cellCount, cellCount, allocId); + AXOM_ANNOTATE_BEGIN("MeshClipper:find_candidates"); axom::Array counts(cellCount, cellCount, allocId); + axom::Array offsets(cellCount, cellCount, allocId); axom::Array candidates; - AXOM_ANNOTATE_BEGIN("bvh.findBoundingBoxes"); - bvh.findBoundingBoxes(offsets, counts, candidates, cellCount, cellBbsView); - AXOM_ANNOTATE_END("bvh.findBoundingBoxes"); - - // Get the total number of candidates - using ATOMIC_POL = typename axom::execution_space::atomic_policy; + bvh.findBoundingBoxes(offsets, counts, candidates, cellCount, shapeMesh.getCellBoundingBoxes()); + AXOM_ANNOTATE_END("MeshClipper:find_candidates"); const auto countsView = counts.view(); const int candidateCount = candidates.size(); @@ -293,7 +271,7 @@ class MeshClipperImpl : public MeshClipper::Impl allocId); auto shapeCandidatesView = shapeCandidates.view(); - // Tetrahedra from hexes (24 for each hex) + // Tetrahedra from hexes auto cellsAsTets = shapeMesh.getCellsAsTets(); // Index into 'tets' @@ -337,77 +315,71 @@ class MeshClipperImpl : public MeshClipper::Impl }); } - constexpr double EPS = 1e-10; - constexpr bool tryFixOrientation = false; - - { - tetCandidatesCount = NUM_TETS_PER_HEX * candidates.size(); - AXOM_ANNOTATE_SCOPE("MeshClipper::clipLoop"); + tetCandidatesCount = NUM_TETS_PER_HEX * candidates.size(); #if defined(AXOM_DEBUG) - // Verifying: this should always pass. - if(tetCandidatesCountPtr != &tetCandidatesCount) - { - axom::copy(&tetCandidatesCount, tetCandidatesCountPtr, sizeof(IndexType)); - } - SLIC_ASSERT(tetCandidatesCount == candidateCount * NUM_TETS_PER_HEX); + // Verifying: this should always pass. + if(tetCandidatesCountPtr != &tetCandidatesCount) + { + axom::copy(&tetCandidatesCount, tetCandidatesCountPtr, sizeof(IndexType)); + } + SLIC_ASSERT(tetCandidatesCount == candidateCount * NUM_TETS_PER_HEX); #endif - SLIC_INFO( - axom::fmt::format("Running clip loop on {} candidate tets for of all {} hexes in the mesh", - tetCandidatesCount, - cellCount)); + SLIC_DEBUG( + axom::fmt::format("Running clip loop on {} candidate pieces for of all {} hexes in the mesh", + tetCandidatesCount, + cellCount)); - if(useTets) - { - axom::for_all( - tetCandidatesCount, - AXOM_LAMBDA(axom::IndexType i) { - const int index = hexIndicesView[i]; - const int shapeIndex = shapeCandidatesView[i]; - const int tetIndex = tetIndicesView[i]; - - const auto poly = primal::clip(geomTetsView[shapeIndex], - cellsAsTets[tetIndex], - EPS, - tryFixOrientation); - - // Poly is valid - if(poly.numVertices() >= 4) - { - // Workaround - intermediate volume variable needed for - // CUDA Pro/E test case correctness - double volume = poly.volume(); - SLIC_ASSERT(volume >= 0); - RAJA::atomicAdd(ovlap.data() + index, volume); - } - }); - } - else // useOcts - { - axom::for_all( - tetCandidatesCount, - AXOM_LAMBDA(axom::IndexType i) { - const int index = hexIndicesView[i]; - const int shapeIndex = shapeCandidatesView[i]; - const int tetIndex = tetIndicesView[i]; - - const auto poly = primal::clip(geomOctsView[shapeIndex], - cellsAsTets[tetIndex], - EPS, - tryFixOrientation); - - // Poly is valid - if(poly.numVertices() >= 4) - { - // Workaround - intermediate volume variable needed for - // CUDA Pro/E test case correctness - double volume = poly.volume(); - SLIC_ASSERT(volume >= 0); - RAJA::atomicAdd(ovlap.data() + index, volume); - } - }); - } + /* + * Statistics from the clip loop. + * Count number of times the piece was found inside/outside/on a mesh tet boundary. + * Be sure to use kernel-compatible memory. + */ + ClippingStats clipStats; + + const auto screenLevel = m_myClipper.getScreenLevel(); + + AXOM_ANNOTATE_BEGIN("MeshClipper:clipLoop_notScreened"); + if(useTets) + { + axom::for_all( + tetCandidatesCount, + AXOM_LAMBDA(axom::IndexType i) { + const int tetIndex = tetIndicesView[i]; + const auto& cellTet = cellsAsTets[tetIndex]; + if(cellTet.degenerate()) + { + clipStats.outSum += 1; + return; + } + const int cellId = hexIndicesView[i]; + const int pieceId = shapeCandidatesView[i]; + const TetrahedronType& geomPiece = geomTetsView[pieceId]; + computeMeshTetGeomPieceOverlap(cellTet, geomPiece, ovlap.data() + cellId, clipStats, screenLevel); + }); + } + else // useOcts + { + axom::for_all( + tetCandidatesCount, + AXOM_LAMBDA(axom::IndexType i) { + const int tetIndex = tetIndicesView[i]; + const auto& cellTet = cellsAsTets[tetIndex]; + if(cellTet.degenerate()) + { + clipStats.outSum += 1; + return; + } + const int cellId = hexIndicesView[i]; + const int pieceId = shapeCandidatesView[i]; + const OctahedronType& geomPiece = geomOctsView[pieceId]; + computeMeshTetGeomPieceOverlap(cellTet, geomPiece, ovlap.data() + cellId, clipStats, screenLevel); + }); } + AXOM_ANNOTATE_END("MeshClipper:clipLoop_notScreened"); + + clipStats.copyTo(statistics); + statistics["clipsCandidates"].set_int64(tetCandidatesCount); if(tetCandidatesCountPtr != &tetCandidatesCount) { @@ -422,99 +394,48 @@ class MeshClipperImpl : public MeshClipper::Impl * on the boundary. */ void computeClipVolumes3D(const axom::ArrayView& cellIndices, - axom::ArrayView ovlap) override + axom::ArrayView ovlap, + conduit::Node& statistics) override { - AXOM_ANNOTATE_SCOPE("MeshClipper::computeClipVolumes3D:limited"); - - using BoundingBoxType = primal::BoundingBox; + using ATOMIC_POL = typename axom::execution_space::atomic_policy; ShapeMesh& shapeMesh = getShapeMesh(); - const int allocId = shapeMesh.getAllocatorID(); - const IndexType cellCount = shapeMesh.getCellCount(); - SLIC_INFO(axom::fmt::format( - "MeshClipper::computeClipVolumes3D: Getting discrete geometry for shape '{}'", - getStrategy().name())); - - auto& strategy = getStrategy(); + /* + * Geometry as discrete tets or octs, and their bounding boxes. + */ axom::Array> geomAsTets; axom::Array> geomAsOcts; - const bool useOcts = strategy.getGeometryAsOcts(shapeMesh, geomAsOcts); - const bool useTets = strategy.getGeometryAsTets(shapeMesh, geomAsTets); - SLIC_ASSERT(useOcts || geomAsOcts.empty()); - SLIC_ASSERT(useTets || geomAsTets.empty()); - if(useTets == useOcts) - { - SLIC_ERROR( - axom::fmt::format("Problem with MeshClipperStrategy implementation '{}'." - " Implementations that don't provide a specializedClip function" - " must provide exactly one getGeometryAsOcts() or getGeometryAsTets()." - " This implementation provides {}.", - strategy.name(), - int(useOcts) + int(useTets))); - } - + axom::Array pieceBbs; + spin::BVH<3, ExecSpace, double> bvh; + bool useTets = getDiscreteGeometry(geomAsTets, geomAsOcts, pieceBbs, bvh); auto geomTetsView = geomAsTets.view(); auto geomOctsView = geomAsOcts.view(); - SLIC_INFO(axom::fmt::format("{:-^80}", " Inserting shapes' bounding boxes into BVH ")); - - // Generate the BVH tree over the shape's discretized geometry - // axis-aligned bounding boxes. "pieces" refers to tets or octs. - const axom::IndexType bbCount = useTets ? geomAsTets.size() : geomAsOcts.size(); - axom::Array pieceBbs(bbCount, bbCount, allocId); - axom::ArrayView pieceBbsView = pieceBbs.view(); - - // Get the bounding boxes for the shapes - if(useTets) - { - axom::for_all( - pieceBbsView.size(), - AXOM_LAMBDA(axom::IndexType i) { - pieceBbsView[i] = primal::compute_bounding_box(geomTetsView[i]); - }); - } - else - { - axom::for_all( - pieceBbsView.size(), - AXOM_LAMBDA(axom::IndexType i) { - pieceBbsView[i] = primal::compute_bounding_box(geomOctsView[i]); - }); - } - - // Insert shapes' Bounding Boxes into BVH. - spin::BVH<3, ExecSpace, double> bvh; - bvh.initialize(pieceBbsView, pieceBbsView.size()); - - SLIC_INFO(axom::fmt::format("{:-^80}", " Querying the BVH tree ")); + /* + * Find which shape bounding boxes intersect hexahedron bounding boxes + */ + AXOM_ANNOTATE_BEGIN("MeshClipper:find_candidates"); + // Find which shape bounding boxes intersect hexahedron bounding boxes // Create a temporary subset of cell bounding boxes, // containing only those listed in cellIndices. - const axom::IndexType limitedCellCount = cellIndices.size(); axom::ArrayView cellBbsView = shapeMesh.getCellBoundingBoxes(); + const axom::IndexType limitedCellCount = cellIndices.size(); axom::Array limitedCellBbs(limitedCellCount, limitedCellCount, allocId); axom::ArrayView limitedCellBbsView = limitedCellBbs.view(); axom::for_all( limitedCellBbsView.size(), AXOM_LAMBDA(axom::IndexType i) { limitedCellBbsView[i] = cellBbsView[cellIndices[i]]; }); - // Find which shape bounding boxes intersect hexahedron bounding boxes - SLIC_INFO( - axom::fmt::format("{:-^80}", " Finding shape candidates for each hexahedral element ")); - - axom::Array offsets(limitedCellCount, limitedCellCount, allocId); axom::Array counts(limitedCellCount, limitedCellCount, allocId); + axom::Array offsets(limitedCellCount, limitedCellCount, allocId); axom::Array candidates; - AXOM_ANNOTATE_BEGIN("bvh.findBoundingBoxes"); bvh.findBoundingBoxes(offsets, counts, candidates, limitedCellCount, limitedCellBbsView); - AXOM_ANNOTATE_END("bvh.findBoundingBoxes"); - - // Get the total number of candidates - using ATOMIC_POL = typename axom::execution_space::atomic_policy; + AXOM_ANNOTATE_END("MeshClipper:find_candidates"); const auto countsView = counts.view(); const int candidateCount = candidates.size(); @@ -531,7 +452,7 @@ class MeshClipperImpl : public MeshClipper::Impl allocId); auto shapeCandidatesView = shapeCandidates.view(); - // Tetrahedrons from hexes (24 for each hex) + // Tetrahedrons from hexes auto cellsAsTets = shapeMesh.getCellsAsTets(); // Index into 'tets' @@ -575,92 +496,85 @@ class MeshClipperImpl : public MeshClipper::Impl }); } - SLIC_INFO(axom::fmt::format( - "Running clip loop on {} candidate tets for the select {} hexes of the full {} cells", + SLIC_DEBUG(axom::fmt::format( + "Running clip loop on {} candidate pieces for the select {} hexes of the full {} mesh cells", tetCandidatesCount, cellIndices.size(), cellCount)); - constexpr double EPS = 1e-10; - constexpr bool tryFixOrientation = false; - - { - tetCandidatesCount = NUM_TETS_PER_HEX * candidates.size(); - AXOM_ANNOTATE_SCOPE("MeshClipper::clipLoop_limited"); + tetCandidatesCount = NUM_TETS_PER_HEX * candidates.size(); #if defined(AXOM_DEBUG) - // Verifying: this should always pass. - if(tetCandidatesCountPtr != &tetCandidatesCount) - { - axom::copy(&tetCandidatesCount, tetCandidatesCountPtr, sizeof(IndexType)); - } - SLIC_ASSERT(tetCandidatesCount == candidateCount * NUM_TETS_PER_HEX); + // Verifying: this should always pass. + if(tetCandidatesCountPtr != &tetCandidatesCount) + { + axom::copy(&tetCandidatesCount, tetCandidatesCountPtr, sizeof(IndexType)); + } + SLIC_ASSERT(tetCandidatesCount == candidateCount * NUM_TETS_PER_HEX); #endif - if(useTets) - { - axom::for_all( - tetCandidatesCount, - AXOM_LAMBDA(axom::IndexType i) { - int index = hexIndicesView[i]; // index into limited mesh hex array - index = cellIndices[index]; // Now, it indexes into the full hex array. - - const int shapeIndex = shapeCandidatesView[i]; // index into pieces array - int tetIndex = - tetIndicesView[i]; // index into BVH results, implicit because BVH results specify hexes, not tets. - int tetIndex1 = tetIndex / NUM_TETS_PER_HEX; - int tetIndex2 = tetIndex % NUM_TETS_PER_HEX; - tetIndex = cellIndices[tetIndex1] * NUM_TETS_PER_HEX + - tetIndex2; // Now it indexes into the full tets-from-hexes array. - - const auto poly = primal::clip(geomTetsView[shapeIndex], - cellsAsTets[tetIndex], - EPS, - tryFixOrientation); - - // Poly is valid - if(poly.numVertices() >= 4) - { - // Workaround - intermediate volume variable needed for - // CUDA Pro/E test case correctness - double volume = poly.volume(); - SLIC_ASSERT(volume >= 0); - RAJA::atomicAdd(ovlap.data() + index, volume); - } - }); - } - else // useOcts - { - axom::for_all( - tetCandidatesCount, - AXOM_LAMBDA(axom::IndexType i) { - int index = hexIndicesView[i]; // index into limited mesh hex array - index = cellIndices[index]; // Now, it indexes into the full hex array. - - const int shapeIndex = shapeCandidatesView[i]; // index into pieces array - int tetIndex = - tetIndicesView[i]; // index into BVH results, implicit because BVH results specify hexes, not tets. - int tetIndex1 = tetIndex / NUM_TETS_PER_HEX; - int tetIndex2 = tetIndex % NUM_TETS_PER_HEX; - tetIndex = cellIndices[tetIndex1] * NUM_TETS_PER_HEX + - tetIndex2; // Now it indexes into the full tets-from-hexes array. - - const auto poly = primal::clip(geomOctsView[shapeIndex], - cellsAsTets[tetIndex], - EPS, - tryFixOrientation); - - // Poly is valid - if(poly.numVertices() >= 4) - { - // Workaround - intermediate volume variable needed for - // CUDA Pro/E test case correctness - double volume = poly.volume(); - SLIC_ASSERT(volume >= 0); - RAJA::atomicAdd(ovlap.data() + index, volume); - } - }); - } + ClippingStats clipStats; + + const auto screenLevel = m_myClipper.getScreenLevel(); + + AXOM_ANNOTATE_BEGIN("MeshClipper:clipLoop_hexScreened"); + if(useTets) + { + axom::for_all( + tetCandidatesCount, + AXOM_LAMBDA(axom::IndexType i) { + int tetIndex = + tetIndicesView[i]; // index into BVH results, implicit because BVH results specify hexes, not tets. + int tetIndex1 = tetIndex / NUM_TETS_PER_HEX; + int tetIndex2 = tetIndex % NUM_TETS_PER_HEX; + tetIndex = cellIndices[tetIndex1] * NUM_TETS_PER_HEX + + tetIndex2; // Now it indexes into the full tets-from-hexes array. + + const auto& cellTet = cellsAsTets[tetIndex]; + if(cellTet.degenerate()) + { + clipStats.outSum += 1; + return; + } + + int cellId = hexIndicesView[i]; // index into limited mesh hex array + cellId = cellIndices[cellId]; // Now, it indexes into the full hex array. + + const int pieceId = shapeCandidatesView[i]; // index into pieces array + const TetrahedronType& geomPiece = geomTetsView[pieceId]; + computeMeshTetGeomPieceOverlap(cellTet, geomPiece, ovlap.data() + cellId, clipStats, screenLevel); + }); + } + else // useOcts + { + axom::for_all( + tetCandidatesCount, + AXOM_LAMBDA(axom::IndexType i) { + int tetIndex = + tetIndicesView[i]; // index into BVH results, implicit because BVH results specify hexes, not tets. + int tetIndex1 = tetIndex / NUM_TETS_PER_HEX; + int tetIndex2 = tetIndex % NUM_TETS_PER_HEX; + tetIndex = cellIndices[tetIndex1] * NUM_TETS_PER_HEX + + tetIndex2; // Now it indexes into the full tets-from-hexes array. + + const auto& cellTet = cellsAsTets[tetIndex]; + if(cellTet.degenerate()) + { + clipStats.outSum += 1; + return; + } + + int cellId = hexIndicesView[i]; // index into limited mesh hex array + cellId = cellIndices[cellId]; // Now, it indexes into the full hex array. + + const int pieceId = shapeCandidatesView[i]; // index into pieces array + const OctahedronType& geomPiece = geomOctsView[pieceId]; + computeMeshTetGeomPieceOverlap(cellTet, geomPiece, ovlap.data() + cellId, clipStats, screenLevel); + }); } + AXOM_ANNOTATE_END("MeshClipper:clipLoop_hexScreened"); + + clipStats.copyTo(statistics); + statistics["clipsCandidates"].set_int64(tetCandidatesCount); if(tetCandidatesCountPtr != &tetCandidatesCount) { @@ -675,79 +589,34 @@ class MeshClipperImpl : public MeshClipper::Impl * potentially on the boundary. */ void computeClipVolumes3DTets(const axom::ArrayView& tetIndices, - axom::ArrayView ovlap) override + axom::ArrayView ovlap, + conduit::Node& statistics) override { - AXOM_ANNOTATE_SCOPE("MeshClipper::computeClipVolumes3D:limited"); - - using BoundingBoxType = primal::BoundingBox; - using TetrahedronType = primal::Tetrahedron; - using OctahedronType = primal::Octahedron; - ShapeMesh& shapeMesh = getShapeMesh(); auto meshTets = getShapeMesh().getCellsAsTets(); const int allocId = shapeMesh.getAllocatorID(); - SLIC_INFO(axom::fmt::format( - "MeshClipper::computeClipVolumes3D: Getting discrete geometry for shape '{}'", - getStrategy().name())); - - auto& strategy = getStrategy(); - axom::Array geomAsTets; - axom::Array geomAsOcts; - const bool useOcts = strategy.getGeometryAsOcts(shapeMesh, geomAsOcts); - const bool useTets = strategy.getGeometryAsTets(shapeMesh, geomAsTets); - SLIC_ASSERT(useOcts || geomAsOcts.empty()); - SLIC_ASSERT(useTets || geomAsTets.empty()); - if(useTets == useOcts) - { - SLIC_ERROR( - axom::fmt::format("Problem with MeshClipperStrategy implementation '{}'." - " Implementations that don't provide a specializedClip function" - " must provide exactly one getGeometryAsOcts() or getGeometryAsTets()." - " This implementation provides {}.", - strategy.name(), - int(useOcts) + int(useTets))); - } - + /* + * Geometry as discrete tets or octs, and their bounding boxes. + */ + axom::Array> geomAsTets; + axom::Array> geomAsOcts; + axom::Array pieceBbs; + spin::BVH<3, ExecSpace, double> bvh; + bool useTets = getDiscreteGeometry(geomAsTets, geomAsOcts, pieceBbs, bvh); auto geomTetsView = geomAsTets.view(); auto geomOctsView = geomAsOcts.view(); - SLIC_INFO(axom::fmt::format("{:-^80}", " Inserting shapes' bounding boxes into BVH ")); - - // Generate the BVH tree over the shape's discretized geometry - // axis-aligned bounding boxes. "pieces" refers to tets or octs. - const axom::IndexType bbCount = useTets ? geomAsTets.size() : geomAsOcts.size(); - axom::Array pieceBbs(bbCount, bbCount, allocId); - axom::ArrayView pieceBbsView = pieceBbs.view(); - - // Get the bounding boxes for the shapes - if(useTets) - { - axom::for_all( - pieceBbsView.size(), - AXOM_LAMBDA(axom::IndexType i) { - pieceBbsView[i] = primal::compute_bounding_box(geomTetsView[i]); - }); - } - else - { - axom::for_all( - pieceBbsView.size(), - AXOM_LAMBDA(axom::IndexType i) { - pieceBbsView[i] = primal::compute_bounding_box(geomOctsView[i]); - }); - } - - // Insert shapes' Bounding Boxes into BVH. - spin::BVH<3, ExecSpace, double> bvh; - bvh.initialize(pieceBbsView, pieceBbsView.size()); - - SLIC_INFO(axom::fmt::format("{:-^80}", " Querying the BVH tree ")); + /* + * Find which shape bounding boxes intersect hexahedron bounding boxes + */ + AXOM_ANNOTATE_BEGIN("MeshClipper:find_candidates"); // Create a temporary subset of tet bounding boxes, // containing only those listed in tetIndices. + // The BVH searches on this array. const axom::IndexType tetCount = tetIndices.size(); axom::Array tetBbs(tetCount, tetCount, allocId); axom::ArrayView tetBbsView = tetBbs.view(); @@ -762,15 +631,129 @@ class MeshClipperImpl : public MeshClipper::Impl axom::Array counts(tetCount, tetCount, allocId); axom::Array offsets(tetCount, tetCount, allocId); - + axom::Array candidates; auto countsView = counts.view(); auto offsetsView = offsets.view(); +#if 1 + // Get the BVH traverser for doing the 2-pass search manually. + const auto bvhTraverser = bvh.getTraverser(); + /* + * Predicate for traversing the BVH. We enter BVH nodes + * whose bounding boxes intersect the query bounding box. + */ + auto traversePredBox = [] AXOM_HOST_DEVICE(const BoundingBoxType& queryBbox, + const BoundingBoxType& bvhBbox) -> bool { + return queryBbox.intersectsWith(bvhBbox); + }; + AXOM_UNUSED_VAR(traversePredBox); + auto traversePredTetId = [=] AXOM_HOST_DEVICE(const IndexType& queryTetId, + const BoundingBoxType& bvhBbox) -> bool { + const auto& queryTet = meshTets[tetIndices[queryTetId]]; + return tetBoxIntersects(queryTet, bvhBbox); + }; - AXOM_ANNOTATE_BEGIN("bvh.findBoundingBoxes"); - axom::Array candidates; + /* + * First pass: count number of collisions each of the tetBbs makes + * with the BVH leaves. Populate the counts array. + */ + axom::ReduceSum totalCountReduce(0); + axom::for_all( + tetCount, + AXOM_LAMBDA(axom::IndexType iTet) { + axom::IndexType count = 0; + auto countCollisions = [&](std::int32_t currentNode, const std::int32_t* leafNodes) { + // countCollisions is only called at the leaves. + auto& tetId = tetIndices[iTet]; + const auto& meshTet = meshTets[tetId]; + + auto pieceId = leafNodes[currentNode]; + // ++count; return; + if(useTets) + { + const auto& piece = geomTetsView[pieceId]; + if(tetTetIntersects(meshTet, piece)) + { + ++count; + } + } + else + { + const auto& piece = geomOctsView[pieceId]; + if(tetOctIntersects(meshTet, piece)) + { + ++count; + } + } + }; + // bvhTraverser.traverse_tree(tetBbsView[iTet], countCollisions, traversePredBox); + bvhTraverser.traverse_tree(iTet, countCollisions, traversePredTetId); + countsView[iTet] = count; + totalCountReduce += count; + }); + + // Compute the offsets array using a prefix scan of counts. + axom::exclusive_scan(counts, offsets); + const IndexType nCollisions = totalCountReduce.get(); + + /* + * Allocate 2 arrays to hold info about the meshTet/geometry piece collisions. + * - candidates: geometry pieces in a potential collision, actually their indices. + * - candToTetIdId: indicates the meshTets in the collision, + * where candToTetIdId[i] corresponds to meshTets[tetIndices[i]]. + */ + candidates = axom::Array(nCollisions, nCollisions, allocId); + axom::Array candToTetIdId(candidates.size(), candidates.size(), allocId); + auto candidatesView = candidates.view(); + auto candToTetIdIdView = candToTetIdId.view(); + + /* + * Second pass: Populate tet-candidate piece collision arrays. + */ + axom::for_all( + tetCount, + AXOM_LAMBDA(axom::IndexType iTet) { + auto offset = offsetsView[iTet]; + + // PrimitiveType cellTet = tetBbsView[iTet]; + // Eventually, use the tet, not its BB. + // Record indices of the tet and the candidate that collided. + // Eventually, bypass if tet and candidate can be shown not to collide. + auto recordCollision = [&](std::int32_t currentNode, const std::int32_t* leafs) { + auto& tetId = tetIndices[iTet]; + const auto& meshTet = meshTets[tetId]; + auto pieceId = leafs[currentNode]; + bool record = false; + // record = true; + if(useTets) + { + const auto& piece = geomTetsView[pieceId]; + if(tetTetIntersects(meshTet, piece)) + { + record = true; + } + } + else + { + const auto& piece = geomOctsView[pieceId]; + if(tetOctIntersects(meshTet, piece)) + { + record = true; + } + } + if(record) + { + candToTetIdIdView[offset] = iTet; + candidatesView[offset] = pieceId; + ++offset; + } + }; + + // bvhTraverser.traverse_tree(tetBbsView[iTet], recordCollision, traversePredBox); + bvhTraverser.traverse_tree(iTet, recordCollision, traversePredTetId); + }); +#else bvh.findBoundingBoxes(offsets, counts, candidates, tetBbsView.size(), tetBbsView); auto candidatesView = candidates.view(); - AXOM_ANNOTATE_END("bvh.findBoundingBoxes"); axom::Array candToTetIdId(candidates.size(), candidates.size(), allocId); auto candToTetIdIdView = candToTetIdId.view(); @@ -781,36 +764,32 @@ class MeshClipperImpl : public MeshClipper::Impl auto offset = offsetsView[i]; for(int j = 0; j < count; ++j) candToTetIdIdView[offset + j] = i; }); +#endif + AXOM_ANNOTATE_END("MeshClipper:find_candidates"); - // Find which shape bounding boxes intersect hexahedron bounding boxes - SLIC_INFO(axom::fmt::format("Finding shape candidates for {} tet elements ", tetIndices.size())); + SLIC_DEBUG(axom::fmt::format( + "Running clip loop on {} candidate pieces for the select {} tets of the full {} mesh cells", + candidates.size(), + tetCount, + shapeMesh.getCellCount())); - using ATOMIC_POL = typename axom::execution_space::atomic_policy; - constexpr double EPS = 1e-10; - constexpr bool tryFixOrientation = false; + ClippingStats clipStats; + const auto screenLevel = m_myClipper.getScreenLevel(); + + AXOM_ANNOTATE_BEGIN("MeshClipper:clipLoop_tetScreened"); if(useTets) { axom::for_all( candidates.size(), AXOM_LAMBDA(axom::IndexType iCand) { - auto pieceId = candidatesView[iCand]; - const axom::primal::Tetrahedron& geomPiece = geomTetsView[pieceId]; - auto tetIdId = candToTetIdIdView[iCand]; auto tetId = tetIndices[tetIdId]; - const auto& tet = meshTets[tetId]; - - const auto poly = primal::clip(tet, geomPiece, EPS, tryFixOrientation); - - if(poly.numVertices() >= 4) - { - // Poly is valid - double volume = poly.volume(); - SLIC_ASSERT(volume >= 0); - auto cellId = tetId / NUM_TETS_PER_HEX; - RAJA::atomicAdd(ovlap.data() + cellId, volume); - } + auto cellId = tetId / NUM_TETS_PER_HEX; + auto pieceId = candidatesView[iCand]; + const auto& meshTet = meshTets[tetId]; + const TetrahedronType& geomPiece = geomTetsView[pieceId]; + computeMeshTetGeomPieceOverlap(meshTet, geomPiece, ovlap.data() + cellId, clipStats, screenLevel); }); } else // useOcts @@ -818,39 +797,439 @@ class MeshClipperImpl : public MeshClipper::Impl axom::for_all( candidates.size(), AXOM_LAMBDA(axom::IndexType iCand) { - auto pieceId = candidatesView[iCand]; - const axom::primal::Octahedron& geomPiece = geomOctsView[pieceId]; - auto tetIdId = candToTetIdIdView[iCand]; auto tetId = tetIndices[tetIdId]; - const auto& tet = meshTets[tetId]; + auto cellId = tetId / NUM_TETS_PER_HEX; + auto pieceId = candidatesView[iCand]; + const auto& meshTet = meshTets[tetId]; + const OctahedronType& geomPiece = geomOctsView[pieceId]; + computeMeshTetGeomPieceOverlap(meshTet, geomPiece, ovlap.data() + cellId, clipStats, screenLevel); + }); + } + AXOM_ANNOTATE_END("MeshClipper:clipLoop_tetScreened"); - const auto poly = primal::clip(tet, geomPiece, EPS, tryFixOrientation); + clipStats.copyTo(statistics); + statistics["clipsCandidates"].set_int64(candidates.size()); - if(poly.numVertices() >= 4) - { - // Poly is valid - double volume = poly.volume(); - SLIC_ASSERT(volume >= 0); - auto cellId = tetId / NUM_TETS_PER_HEX; - RAJA::atomicAdd(ovlap.data() + cellId, volume); - } + SLIC_DEBUG(axom::fmt::format("")); + } // end of computeClipVolumes3DTets() function + + /*! + * @brief Get the geometry in discrete pieces, + * which can be tets or octs, and place them in a search tree. + * @return true if geometry is composed of tetrahedra, fals if octahedra. + */ + bool getDiscreteGeometry(axom::Array>& geomAsTets, + axom::Array>& geomAsOcts, + axom::Array& pieceBbs, + spin::BVH<3, ExecSpace, double>& bvh) + { + auto& strategy = getStrategy(); + ShapeMesh& shapeMesh = getShapeMesh(); + int allocId = shapeMesh.getAllocatorID(); + + AXOM_ANNOTATE_BEGIN("MeshClipper:get_geometry"); + const bool useOcts = strategy.getGeometryAsOcts(shapeMesh, geomAsOcts); + const bool useTets = strategy.getGeometryAsTets(shapeMesh, geomAsTets); + AXOM_ANNOTATE_END("MeshClipper:get_geometry"); + + if(useTets) + { + SLIC_ASSERT(geomAsTets.getAllocatorID() == allocId); + } + else + { + SLIC_ASSERT(geomAsOcts.getAllocatorID() == allocId); + } + if(useTets == useOcts) + { + SLIC_ERROR( + axom::fmt::format("Problem with MeshClipperStrategy implementation '{}'." + " Implementations that don't provide a specializedClip function" + " must provide exactly one of either getGeometryAsOcts() or" + " getGeometryAsTets(). This implementation provides {}.", + strategy.name(), + int(useOcts) + int(useTets))); + } + + SLIC_DEBUG(axom::fmt::format("Geometry {} has {} discrete {}s", + strategy.name(), + useTets ? geomAsTets.size() : geomAsOcts.size(), + useTets ? "tet" : "oct")); + + /* + * Get the bounding boxes for the discrete geometry pieces. + * If debug build, check for degenerate pieces. + */ + const axom::IndexType bbCount = useTets ? geomAsTets.size() : geomAsOcts.size(); + pieceBbs = axom::Array(bbCount, bbCount, allocId); + axom::ArrayView pieceBbsView = pieceBbs.view(); + + if(useTets) + { + auto geomTetsView = geomAsTets.view(); + axom::for_all( + pieceBbsView.size(), + AXOM_LAMBDA(axom::IndexType i) { + pieceBbsView[i] = primal::compute_bounding_box(geomTetsView[i]); +#if defined(AXOM_DEBUG) + SLIC_ASSERT(!geomTetsView[i].degenerate()); +#endif + }); + } + else + { + auto geomOctsView = geomAsOcts.view(); + axom::for_all( + pieceBbsView.size(), + AXOM_LAMBDA(axom::IndexType i) { + pieceBbsView[i] = primal::compute_bounding_box(geomOctsView[i]); }); } - } // end of computeClipVolumes3DTets() function + bvh.setAllocatorID(allocId); + bvh.setTolerance(EPS); + bvh.setScaleFactor(BVH_SCALE_FACTOR); + bvh.initialize(pieceBbsView, pieceBbsView.size()); + + return useTets; + } + + /*! + * @brief Volume of a tetrahedron from discretized geometry. + */ + AXOM_HOST_DEVICE static inline double geomPieceVolume(const TetrahedronType& tet) + { + return tet.volume(); + } + + /*! + * @brief Volume of a octahedron from discretized geometry. + * + * Assumes octahedron is convex. + */ + AXOM_HOST_DEVICE static inline double geomPieceVolume(const OctahedronType& oct) + { + TetrahedronType tets[] = {TetrahedronType(oct[0], oct[3], oct[1], oct[2]), + TetrahedronType(oct[0], oct[3], oct[2], oct[4]), + TetrahedronType(oct[0], oct[3], oct[4], oct[5]), + TetrahedronType(oct[0], oct[3], oct[5], oct[1])}; + double octVol = 0.0; + for(int i = 0; i < 4; ++i) + { + double tetVol = tets[i].signedVolume(); + SLIC_ASSERT(tetVol >= -EPS); // Tet may be degenerate but not inverted. + octVol += axom::utilities::abs(tetVol); + } + return octVol; + } + + /*! + * @brief Compute overlap volume between a reference tet (from the shape mesh) + * and a piece (tet or oct) of the discretized geometry. + * + * Because primal::clip is so expensive, we do a conservative + * overlap check on @c meshTet and @c geomPiece to avoid clipping. + * + * @return results of check whether the piece is IN/ON/OUT of the tet. + * + * @tparam TetOrOctType either a TetrahedronType or OctahedronType, + * the two types a geometry can be discretized into. + */ + template + AXOM_HOST_DEVICE static inline LabelType computeMeshTetGeomPieceOverlap( + const TetrahedronType& meshTet, + const TetOrOctType& geomPiece, + double* overlapVolume, + const ClippingStats& clipStats, + int screenLevel) + { + using ATOMIC_POL = typename axom::execution_space::atomic_policy; + constexpr bool tryFixOrientation = false; + if(screenLevel >= 3) + { + LabelType geomLabel = labelPieceInOutOfTet(meshTet, geomPiece); + if(geomLabel == LabelType::LABEL_OUT) + { + clipStats.outSum += 1; + return geomLabel; + } + if(geomLabel == LabelType::LABEL_IN) + { + auto contribVol = geomPieceVolume(geomPiece); + RAJA::atomicAdd(overlapVolume, contribVol); + clipStats.inSum += 1; + return geomLabel; + } + } + + clipStats.onSum += 1; + const auto poly = primal::clip(meshTet, geomPiece, EPS, tryFixOrientation); + if(poly.numVertices() >= 4) + { + // Poly is valid + auto contribVol = poly.volume(); + SLIC_ASSERT(contribVol >= 0); + RAJA::atomicAdd(overlapVolume, contribVol); + } + else + { + clipStats.missSum += 1; + } + + return LabelType::LABEL_ON; + } + + /*! + * @brief Compute whether a tetrahedron or octhedron is inside, + * outside or on the boundary of a reference tetrahedron, + * and conservatively label it as on, if not known. + * + * @internal To reduce repeatedly computing toUnitTet for + * the same tet, precompute that in the calling function + * and use it instead of tet. + */ + template + AXOM_HOST_DEVICE static inline LabelType labelPieceInOutOfTet(const TetrahedronType& tet, + const TetOrOctType& piece) + { + Point3DType unitTet[] = {{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; + CoordTransformer toUnitTet(&tet[0], unitTet); + + /* + * Count (transformed) piece vertices above/below unitTet as unitTet + * rests on its 4 sides. Sides 0-2 are perpendicular to the axes. + * Side 3 is the diagonal side. + */ + int vsAbove[4] = {0, 0, 0, 0}; + int vsBelow[4] = {0, 0, 0, 0}; + int vsTetSide[4] = {0, 0, 0, 0}; + for(int i = 0; i < TetOrOctType::NUM_VERTS; ++i) + { + auto pVert = toUnitTet.getTransformed(piece[i]); + // h4 is height of pVert above the diagonal face, scaled by sqrt(3). + // h4 of 1 is right at the unitTet's height of sqrt(3). + double h4 = 1 - (pVert[0] + pVert[1] + pVert[2]); + vsAbove[0] += pVert[0] >= 1; + vsAbove[1] += pVert[1] >= 1; + vsAbove[2] += pVert[2] >= 1; + vsAbove[3] += h4 >= 1; + vsBelow[0] += pVert[0] <= 0; + vsBelow[1] += pVert[1] <= 0; + vsBelow[2] += pVert[2] <= 0; + vsBelow[3] += h4 <= 0; + vsTetSide[0] += pVert[0] >= 0; + vsTetSide[1] += pVert[1] >= 0; + vsTetSide[2] += pVert[2] >= 0; + vsTetSide[3] += h4 >= 0; + } + if(vsAbove[0] == TetOrOctType::NUM_VERTS || vsAbove[1] == TetOrOctType::NUM_VERTS || + vsAbove[2] == TetOrOctType::NUM_VERTS || vsAbove[3] == TetOrOctType::NUM_VERTS || + vsBelow[0] == TetOrOctType::NUM_VERTS || vsBelow[1] == TetOrOctType::NUM_VERTS || + vsBelow[2] == TetOrOctType::NUM_VERTS || vsBelow[3] == TetOrOctType::NUM_VERTS) + { + return LabelType::LABEL_OUT; + } + if(vsTetSide[0] == TetOrOctType::NUM_VERTS && vsTetSide[1] == TetOrOctType::NUM_VERTS && + vsTetSide[2] == TetOrOctType::NUM_VERTS && vsTetSide[3] == TetOrOctType::NUM_VERTS) + { + return LabelType::LABEL_IN; + } + return LabelType::LABEL_ON; + } + + /*! + * @brief Whether a tet and a bounding box intersect. + * Answer may be a false positive but never a false negative. + */ + AXOM_HOST_DEVICE static inline bool tetBoxIntersects(const TetrahedronType& tet, + const BoundingBoxType& box) + { + if(box.contains(tet[0]) || box.contains(tet[1]) || box.contains(tet[2]) || box.contains(tet[3])) + { + return true; + } + + Point3DType unitTet[] = {{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; + CoordTransformer toUnitTet(&tet[0], unitTet); + + int vsAbove[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + int vsBelow[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + for(int i = 0; i < 8; ++i) + { + Point3DType boxVert {(i & 1) == 0 ? box.getMin()[0] : box.getMax()[0], + (i & 2) == 0 ? box.getMin()[1] : box.getMax()[1], + (i & 4) == 0 ? box.getMin()[2] : box.getMax()[2]}; + toUnitTet.transform(boxVert.array()); + // h4 is height of boxVert above the diagonal face, scaled by sqrt(3). + // h4 of 1 is right at the unitTet's height of sqrt(3). + double h4 = 1 - (boxVert[0] + boxVert[1] + boxVert[2]); + vsAbove[0] += boxVert[0] >= 1; + vsAbove[1] += boxVert[1] >= 1; + vsAbove[2] += boxVert[2] >= 1; + vsAbove[3] += h4 >= 1; + vsBelow[0] += boxVert[0] <= 0; + vsBelow[1] += boxVert[1] <= 0; + vsBelow[2] += boxVert[2] <= 0; + vsBelow[3] += h4 <= 0; + } + if(vsAbove[0] == 8 || vsAbove[1] == 8 || vsAbove[2] == 8 || vsAbove[3] == 8 || + vsBelow[0] == 8 || vsBelow[1] == 8 || vsBelow[2] == 8 || vsBelow[3] == 8) + { + return false; + } + return true; + } + + /*! + * @brief Whether a tet and the convex hull of an octahedron intersects. + * Answer may be a false positive but never a false negative. + */ + AXOM_HOST_DEVICE static inline bool tetOctIntersects(const TetrahedronType& tet, + const OctahedronType& oct) + { + Point3DType unitTet[] = {{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; + CoordTransformer toUnitTet(&tet[0], unitTet); + int octVertsAbove[OctahedronType::NUM_VERTS] = {0, 0, 0, 0, 0, 0}; + int octVertsBelow[OctahedronType::NUM_VERTS] = {0, 0, 0, 0, 0, 0}; + for(int i = 0; i < OctahedronType::NUM_VERTS; ++i) + { + auto octVert = toUnitTet.getTransformed(oct[i]); + // h4 is height of octVert above the diagonal face, scaled by sqrt(3). + // h4 of 1 is right at the unitTet's height of sqrt(3). + double h4 = 1 - (octVert[0] + octVert[1] + octVert[2]); + octVertsAbove[0] += octVert[0] >= 1; + octVertsAbove[1] += octVert[1] >= 1; + octVertsAbove[2] += octVert[2] >= 1; + octVertsAbove[3] += h4 >= 1; + octVertsBelow[0] += octVert[0] <= 0; + octVertsBelow[1] += octVert[1] <= 0; + octVertsBelow[2] += octVert[2] <= 0; + octVertsBelow[3] += h4 <= 0; + } + if(octVertsAbove[0] == OctahedronType::NUM_VERTS || + octVertsAbove[1] == OctahedronType::NUM_VERTS || + octVertsAbove[2] == OctahedronType::NUM_VERTS || + octVertsAbove[3] == OctahedronType::NUM_VERTS || + octVertsBelow[0] == OctahedronType::NUM_VERTS || + octVertsBelow[1] == OctahedronType::NUM_VERTS || + octVertsBelow[2] == OctahedronType::NUM_VERTS || octVertsBelow[3] == OctahedronType::NUM_VERTS) + { + return false; + } + + // Indices of the vertices of each of the 8 faces of the octagon, oriented inside. + using ThreeIds = int[3]; + ThreeIds octFIds[8] = + {{0, 2, 4}, {0, 5, 1}, {2, 1, 3}, {4, 3, 5}, {1, 5, 3}, {3, 4, 2}, {5, 0, 4}, {1, 2, 0}}; + for(int fi = 0; fi < 8; ++fi) + { + // Construct plane of face fi of the oct. + const ThreeIds& fIds = octFIds[fi]; + auto& v0 = oct[fIds[0]]; + auto& v1 = oct[fIds[1]]; + auto& v2 = oct[fIds[2]]; + axom::primal::Vector r1(v0, v1); + axom::primal::Vector r2(v0, v2); + axom::primal::Vector normal = axom::primal::Vector::cross_product(r1, r2); + if(normal.squared_norm() < EPS) + { + continue; + } // Skip degenerate face + axom::primal::Plane octBase(normal, v0); + + // Compute height range of vertices not in face fi. + double maxHeight = 0.0; + double minHeight = 0.0; + const ThreeIds& nonFids = octFIds[(fi + 4) % 8]; // 3 oct vertices not part of face fi. + for(int vi = 0; vi < 3; ++vi) + { + const auto& vert = oct[nonFids[vi]]; + double vertHeight = octBase.signedDistance(vert); + if(maxHeight < vertHeight) + { + maxHeight = vertHeight; + } + if(minHeight > vertHeight) + { + minHeight = vertHeight; + } + } + + // Number of tet vertices outside [minHeight,maxHeight].. + int tetVertsAbove = 0; + int tetVertsBelow = 0; + for(int ti = 0; ti < 4; ++ti) + { + const auto& tetVert = tet[ti]; + double tetVertHeight = octBase.signedDistance(tetVert); + tetVertsAbove += tetVertHeight >= maxHeight; + tetVertsBelow += tetVertHeight <= minHeight; + } + + if(tetVertsAbove == TetrahedronType::NUM_VERTS || tetVertsBelow == TetrahedronType::NUM_VERTS) + { + return false; + } + } + return true; + } + + /*! + * @brief Whether a tet and another tet intersects. + * Answer may be a false positive but never a false negative. + */ + AXOM_HOST_DEVICE static inline bool tetTetIntersects(const TetrahedronType& tetA, + const TetrahedronType& tetB, + bool flip = true) + { + Point3DType unitTet[] = {{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; + CoordTransformer toUnitTet(&tetA[0], unitTet); + + int vsAbove[TetrahedronType::NUM_VERTS] = {0, 0, 0, 0}; + int vsBelow[TetrahedronType::NUM_VERTS] = {0, 0, 0, 0}; + for(int i = 0; i < TetrahedronType::NUM_VERTS; ++i) + { + const auto bVert = toUnitTet.getTransformed(tetB[i].array()); + // h4 is height of bVert above the diagonal face, scaled by sqrt(3). + // h4 of 1 is right at the unitTet's height of sqrt(3). + double h4 = 1 - (bVert[0] + bVert[1] + bVert[2]); + vsAbove[0] += bVert[0] >= 1; + vsAbove[1] += bVert[1] >= 1; + vsAbove[2] += bVert[2] >= 1; + vsAbove[3] += h4 >= 1; + vsBelow[0] += bVert[0] <= 0; + vsBelow[1] += bVert[1] <= 0; + vsBelow[2] += bVert[2] <= 0; + vsBelow[3] += h4 <= 0; + } + if(vsAbove[0] == TetrahedronType::NUM_VERTS || vsAbove[1] == TetrahedronType::NUM_VERTS || + vsAbove[2] == TetrahedronType::NUM_VERTS || vsAbove[3] == TetrahedronType::NUM_VERTS) + { + return false; + } + + if(flip) + { + // Cannot claim no-intersection checking whether tetB above or below tetA. + // So try checking whether tetA is above or below tetB. + return tetTetIntersects(tetB, tetA, false); + } + + return true; + } void getLabelCounts(axom::ArrayView labels, - axom::IndexType& inCount, - axom::IndexType& onCount, - axom::IndexType& outCount) override + std::int64_t& inCount, + std::int64_t& onCount, + std::int64_t& outCount) override { - AXOM_ANNOTATE_SCOPE("MeshClipper::getLabelCounts"); + AXOM_ANNOTATE_SCOPE("MeshClipper:count_labels"); using ReducePolicy = typename axom::execution_space::reduce_policy; using LoopPolicy = typename execution_space::loop_policy; - RAJA::ReduceSum inSum(0); - RAJA::ReduceSum onSum(0); - RAJA::ReduceSum outSum(0); + RAJA::ReduceSum inSum(0); + RAJA::ReduceSum onSum(0); + RAJA::ReduceSum outSum(0); RAJA::forall( RAJA::RangeSegment(0, labels.size()), AXOM_LAMBDA(axom::IndexType cellId) { @@ -868,12 +1247,14 @@ class MeshClipperImpl : public MeshClipper::Impl onSum += 1; } }); - inCount = static_cast(inSum.get()); - onCount = static_cast(onSum.get()); - outCount = static_cast(outSum.get()); + inCount = static_cast(inSum.get()); + onCount = static_cast(onSum.get()); + outCount = static_cast(outSum.get()); } private: + static constexpr double EPS = 1e-10; + static constexpr double BVH_SCALE_FACTOR = 1.0; static constexpr int MAX_VERTS_FOR_TET_CLIPPING = 32; static constexpr int MAX_NBRS_PER_VERT_FOR_TET_CLIPPING = 8; static constexpr int MAX_VERTS_FOR_OCT_CLIPPING = 32; diff --git a/src/axom/quest/detail/clipping/Plane3DClipper.cpp b/src/axom/quest/detail/clipping/Plane3DClipper.cpp new file mode 100644 index 0000000000..b39db89c1d --- /dev/null +++ b/src/axom/quest/detail/clipping/Plane3DClipper.cpp @@ -0,0 +1,415 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "axom/config.hpp" + +#include "axom/quest/detail/clipping/Plane3DClipper.hpp" + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +Plane3DClipper::Plane3DClipper(const klee::Geometry& kGeom, const std::string& name) + : MeshClipperStrategy(kGeom) + , m_name(name.empty() ? std::string("Plane3D") : name) +{ + extractClipperInfo(); +} + +bool Plane3DClipper::labelCellsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& labels) +{ + int allocId = shapeMesh.getAllocatorID(); + auto cellCount = shapeMesh.getCellCount(); + if(labels.size() < cellCount || labels.getAllocatorID() != shapeMesh.getAllocatorID()) + { + labels = axom::Array(ArrayOptions::Uninitialized(), cellCount, cellCount, allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelCellsInOutImpl(shapeMesh, labels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelCellsInOutImpl(shapeMesh, labels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelCellsInOutImpl>(shapeMesh, labels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelCellsInOutImpl>(shapeMesh, labels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +bool Plane3DClipper::labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) +{ + int allocId = shapeMesh.getAllocatorID(); + const auto cellCount = cellIds.size(); + const auto tetCount = cellCount * NUM_TETS_PER_HEX; + if(tetLabels.size() < tetCount || tetLabels.getAllocatorID() != allocId) + { + tetLabels = axom::Array(ArrayOptions::Uninitialized(), tetCount, tetCount, allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +bool Plane3DClipper::specializedClipCells(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + conduit::Node& statistics) +{ + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + specializedClipCellsImpl(shapeMesh, ovlap, statistics); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + specializedClipCellsImpl(shapeMesh, ovlap, statistics); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + specializedClipCellsImpl>(shapeMesh, ovlap, statistics); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + specializedClipCellsImpl>(shapeMesh, ovlap, statistics); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +bool Plane3DClipper::specializedClipCells(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + const axom::ArrayView& cellIds, + conduit::Node& statistics) +{ + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + specializedClipCellsImpl(shapeMesh, ovlap, cellIds, statistics); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + specializedClipCellsImpl(shapeMesh, ovlap, cellIds, statistics); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + specializedClipCellsImpl>(shapeMesh, ovlap, cellIds, statistics); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + specializedClipCellsImpl>(shapeMesh, ovlap, cellIds, statistics); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +bool Plane3DClipper::specializedClipTets(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + const axom::ArrayView& tetIds, + conduit::Node& statistics) +{ + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + specializedClipTetsImpl(shapeMesh, ovlap, tetIds, statistics); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + specializedClipTetsImpl(shapeMesh, ovlap, tetIds, statistics); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + specializedClipTetsImpl>(shapeMesh, ovlap, tetIds, statistics); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + specializedClipTetsImpl>(shapeMesh, ovlap, tetIds, statistics); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +template +void Plane3DClipper::labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView labels) +{ + int allocId = shapeMesh.getAllocatorID(); + auto cellCount = shapeMesh.getCellCount(); + auto vertCount = shapeMesh.getVertexCount(); + auto cellVolumes = shapeMesh.getCellVolumes(); + constexpr double EPS = 1e-10; + + const auto& vertCoords = shapeMesh.getVertexCoords3D(); + const auto& vX = vertCoords[0]; + const auto& vY = vertCoords[1]; + const auto& vZ = vertCoords[2]; + + /* + Compute whether vertices are inside shape. + */ + axom::Array vertIsInside {ArrayOptions::Uninitialized(), vertCount, vertCount, allocId}; + auto vertIsInsideView = vertIsInside.view(); + SLIC_ASSERT(axom::execution_space::usesAllocId(vX.getAllocatorID())); + SLIC_ASSERT(axom::execution_space::usesAllocId(vY.getAllocatorID())); + SLIC_ASSERT(axom::execution_space::usesAllocId(vZ.getAllocatorID())); + SLIC_ASSERT(axom::execution_space::usesAllocId(vertIsInsideView.getAllocatorID())); + + auto plane = m_plane; + axom::for_all( + vertCount, + AXOM_LAMBDA(axom::IndexType vertId) { + primal::Point3D vert {vX[vertId], vY[vertId], vZ[vertId]}; + double signedDist = plane.signedDistance(vert); + vertIsInsideView[vertId] = signedDist > 0; + }); + + /* + * Label cell by whether it has vertices inside, outside or both. + */ + axom::ArrayView connView = shapeMesh.getCellNodeConnectivity(); + SLIC_ASSERT(connView.shape()[1] == NUM_VERTS_PER_CELL_3D); + + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType cellId) { + if(axom::utilities::isNearlyEqual(cellVolumes[cellId], 0.0, EPS)) + { + labels[cellId] = LabelType::LABEL_OUT; + return; + } + auto cellVertIds = connView[cellId]; + bool hasIn = vertIsInsideView[cellVertIds[0]]; + bool hasOut = !hasIn; + for(int vi = 0; vi < NUM_VERTS_PER_CELL_3D; ++vi) + { + int vertId = cellVertIds[vi]; + bool isIn = vertIsInsideView[vertId]; + hasIn |= isIn; + hasOut |= !isIn; + } + labels[cellId] = !hasOut ? LabelType::LABEL_IN + : !hasIn ? LabelType::LABEL_OUT + : LabelType::LABEL_ON; + }); + + return; +} + +template +void Plane3DClipper::labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView tetLabels) +{ + auto cellCount = cellIds.size(); + auto meshTets = shapeMesh.getCellsAsTets(); + auto meshTetVolumes = shapeMesh.getTetVolumes(); + + auto plane = m_plane; + + constexpr double EPS = 1e-10; + + /* + * Label tet by whether it has vertices inside, outside or both. + * Degenerate tets as outside, because they contribute no volume. + */ + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType ci) { + axom::IndexType cellId = cellIds[ci]; + + const TetrahedronType* tetsForCell = &meshTets[cellId * NUM_TETS_PER_HEX]; + const double* tetVolumesForCell = &meshTetVolumes[cellId * NUM_TETS_PER_HEX]; + + for(IndexType ti = 0; ti < NUM_TETS_PER_HEX; ++ti) + { + const auto& tet = tetsForCell[ti]; + LabelType& tetLabel = tetLabels[ci * NUM_TETS_PER_HEX + ti]; + + if(axom::utilities::isNearlyEqual(tetVolumesForCell[ti], 0.0, EPS)) + { + tetLabel = LabelType::LABEL_OUT; + continue; + } + + bool hasIn = false; + bool hasOut = false; + for(int vi = 0; vi < TetrahedronType::NUM_VERTS; ++vi) + { + const auto& vert = tet[vi]; + double signedDist = plane.signedDistance(vert); + hasIn |= signedDist > 0; + hasOut |= signedDist < 0; + } + tetLabel = !hasOut ? LabelType::LABEL_IN + : !hasIn ? LabelType::LABEL_OUT + : LabelType::LABEL_ON; + } + }); + + return; +} + +template +void Plane3DClipper::specializedClipCellsImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + conduit::Node& statistics) +{ + axom::IndexType cellCount = shapeMesh.getCellCount(); + axom::Array cellIds(cellCount, cellCount, shapeMesh.getAllocatorID()); + auto cellIdsView = cellIds.view(); + axom::for_all(cellCount, AXOM_LAMBDA(axom::IndexType i) { cellIdsView[i] = i; }); + specializedClipCellsImpl(shapeMesh, ovlap, cellIds, statistics); +} + +template +void Plane3DClipper::specializedClipCellsImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + const axom::ArrayView& cellIds, + conduit::Node& statistics) +{ + using ATOMIC_POL = typename axom::execution_space::atomic_policy; + constexpr double EPS = 1e-10; + + int allocId = shapeMesh.getAllocatorID(); + + auto cellsAsTets = shapeMesh.getCellsAsTets(); + + auto plane = m_plane; + + const std::int64_t zero = 0; + std::int64_t& contribCount = *(statistics["contribs"] = zero).as_int64_ptr(); + std::int64_t* contribCountPtr = axom::allocate(1, allocId); + axom::copy(contribCountPtr, &contribCount, sizeof(zero)); + + axom::for_all( + cellIds.size(), + AXOM_LAMBDA(axom::IndexType i) { + axom::IndexType cellId = cellIds[i]; + const TetrahedronType* tetsInHex = cellsAsTets.data() + cellId * NUM_TETS_PER_HEX; + double vol = 0.0; + for(int ti = 0; ti < NUM_TETS_PER_HEX; ++ti) + { + const auto& tet = tetsInHex[ti]; + primal::Polyhedron overlap = primal::clip(tet, plane, EPS); + auto volume = overlap.volume(); + vol += volume; + RAJA::atomicAdd(contribCountPtr, std::int64_t(volume >= EPS)); + } + ovlap[cellId] = vol; + }); + + statistics["clips"].set_int64(cellIds.size() * NUM_TETS_PER_HEX); + axom::copy(&contribCount, contribCountPtr, sizeof(contribCount)); + axom::deallocate(contribCountPtr); +} + +template +void Plane3DClipper::specializedClipTetsImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + const axom::ArrayView& tetIds, + conduit::Node& statistics) +{ + constexpr double EPS = 1e-10; + using ATOMIC_POL = typename axom::execution_space::atomic_policy; + + int allocId = shapeMesh.getAllocatorID(); + + auto meshTets = shapeMesh.getCellsAsTets(); + IndexType tetCount = tetIds.size(); + auto plane = m_plane; + + const std::int64_t zero = 0; + std::int64_t& contribCount = *(statistics["contribs"] = zero).as_int64_ptr(); + std::int64_t* contribCountPtr = axom::allocate(1, allocId); + axom::copy(contribCountPtr, &contribCount, sizeof(zero)); + + axom::for_all( + tetCount, + AXOM_LAMBDA(axom::IndexType ti) { + axom::IndexType tetId = tetIds[ti]; + axom::IndexType cellId = tetId / NUM_TETS_PER_HEX; + const auto& tet = meshTets[tetId]; + primal::Polyhedron overlap = primal::clip(tet, plane, EPS); + double vol = overlap.volume(); + RAJA::atomicAdd(ovlap.data() + cellId, vol); + RAJA::atomicAdd(contribCountPtr, std::int64_t(vol >= EPS)); + }); + + statistics["clips"].set_int64(tetIds.size()); + axom::copy(&contribCount, contribCountPtr, sizeof(contribCount)); + axom::deallocate(contribCountPtr); +} + +void Plane3DClipper::extractClipperInfo() +{ + const auto normal = m_info.fetch_existing("normal").as_double_array(); + const double offset = m_info.fetch_existing("offset").as_double(); + Vector3DType nVec; + for(int d = 0; d < 3; ++d) + { + nVec[d] = normal[d]; + } + m_plane = Plane3DType(nVec, offset); +} + +} // namespace experimental +} // end namespace quest +} // end namespace axom diff --git a/src/axom/quest/detail/clipping/Plane3DClipper.hpp b/src/axom/quest/detail/clipping/Plane3DClipper.hpp new file mode 100644 index 0000000000..37acda4502 --- /dev/null +++ b/src/axom/quest/detail/clipping/Plane3DClipper.hpp @@ -0,0 +1,102 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#ifndef AXOM_QUEST_PLANE3DCLIPPER_HPP +#define AXOM_QUEST_PLANE3DCLIPPER_HPP + +#include "axom/klee/Geometry.hpp" +#include "axom/quest/MeshClipperStrategy.hpp" + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +/*! + * @brief Geometry clipping operations for plane geometries. +*/ +class Plane3DClipper : public MeshClipperStrategy +{ +public: + /*! + * @brief Constructor. + + * @param [in] kGeom Describes the shape to place + * into the mesh. + * @param [in] name To override the default strategy name + + * Clipping operations for a semi-infinite half-space + * on the positive normal direction of a plane. + */ + Plane3DClipper(const klee::Geometry& kGeom, const std::string& name = ""); + + virtual ~Plane3DClipper() = default; + + const std::string& name() const override { return m_name; } + + bool labelCellsInOut(quest::experimental::ShapeMesh& shappeMesh, + axom::Array& label) override; + + bool labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) override; + + bool specializedClipCells(quest::experimental::ShapeMesh& shappeMesh, + axom::ArrayView ovlap, + conduit::Node& statistics) override; + + bool specializedClipCells(quest::experimental::ShapeMesh& shappeMesh, + axom::ArrayView ovlap, + const axom::ArrayView& cellIds, + conduit::Node& statistics) override; + + bool specializedClipTets(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + const axom::ArrayView& tetIds, + conduit::Node& statistics) override; + +#if !defined(__CUDACC__) +private: +#endif + std::string m_name; + + axom::primal::Plane m_plane; + + template + void labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView label); + + template + void labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView label); + + template + void specializedClipCellsImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + conduit::Node& statistics); + + template + void specializedClipCellsImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + const axom::ArrayView& cellIds, + conduit::Node& statistics); + + template + void specializedClipTetsImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + const axom::ArrayView& tetIds, + conduit::Node& statistics); + + void extractClipperInfo(); +}; + +} // namespace experimental +} // namespace quest +} // namespace axom + +#endif // AXOM_QUEST_PLANE3DCLIPPER_HPP diff --git a/src/axom/quest/detail/clipping/SorClipper.cpp b/src/axom/quest/detail/clipping/SorClipper.cpp new file mode 100644 index 0000000000..375f6af3a3 --- /dev/null +++ b/src/axom/quest/detail/clipping/SorClipper.cpp @@ -0,0 +1,199 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "axom/config.hpp" + +#include "axom/quest/Discretize.hpp" +#include "axom/quest/detail/clipping/SorClipper.hpp" +#include "axom/quest/MeshClipper.hpp" +#include "axom/spin/BVH.hpp" +#include "axom/fmt.hpp" + +#include + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +SorClipper::SorClipper(const klee::Geometry& kGeom, const std::string& name) + : MeshClipperStrategy(kGeom) + , m_name(name.empty() ? std::string("Sor") : name) + , m_maxRadius(0.0) + , m_minRadius(std::numeric_limits::max()) +{ + extractClipperInfo(); + + for(auto& pt : m_sorCurve) + { + m_maxRadius = fmax(m_maxRadius, pt[1]); + m_minRadius = fmin(m_minRadius, pt[1]); + } + SLIC_ERROR_IF(m_minRadius < 0.0, + axom::fmt::format("SorClipper '{}' has a negative radius", m_name)); + + for(const auto& pt : m_sorCurve) + { + m_curveBb.addPoint(pt); + } + + FSorClipper::combineRadialSegments(m_sorCurve); + + axom::Array> sections; + splitIntoMonotonicSections(m_sorCurve.view(), sections); + for(int i = 0; i < sections.size(); ++i) + { + axom::ArrayView section = sections[i].view(); + std::string sectionName = axom::fmt::format("{}.section{:02d}", m_name, i); + m_fsorStrategies.push_back(std::make_shared(kGeom, + sectionName, + section, + m_sorOrigin, + m_sorDirection, + m_levelOfRefinement)); + } +} + +bool SorClipper::specializedClipCells(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + conduit::Node& statistics) +{ + /* + * The SOR curve has been split into SOR functions that do not double + * back on itself. We compute the overlaps for each section and + * accumulate them with the correct sign. (Functions going backward + * remove stuff from the functions above them, so they contribute a + * negative volume.) + * + * By convention, backward curves should generate negative volume, + * but for some reason, the cone discretization functionality always + * generates positive volumes. We correct this by manually applying + * the correct sign. + */ + const axom::IndexType cellCount = ovlap.size(); + axom::Array tmpOvlap(cellCount, cellCount, ovlap.getAllocatorID()); + for(auto& fsorStrategy : m_fsorStrategies) + { + tmpOvlap.fill(0.0); + MeshClipper clipper(shapeMesh, fsorStrategy); + clipper.setVerbose(false); + clipper.clip(tmpOvlap); + auto sorCurve = fsorStrategy->getSorCurve(); + const auto firstZ = sorCurve[0][0]; + const auto lastZ = sorCurve[sorCurve.size() - 1][0]; + int sign = axom::utilities::sign_of(lastZ - firstZ, 0.0); + accumulateData(ovlap, tmpOvlap.view(), double(sign), shapeMesh.getRuntimePolicy()); + MeshClipper::accumulateClippingStats(statistics, clipper.getClippingStats()); + } + return true; +} + +/* + * Split curve into sections that goes monotonically up or down the + * axis of symmetry. If x changes directions at a radial segment, + * split at the end with greater y value. A radial segment is one with + * constant x (or z), pointing in the y (or radial) direction. + * + * This method assumes there are no consecutive radial segments + * (combineRadialSegments has been called on pts). +*/ +void SorClipper::splitIntoMonotonicSections(axom::ArrayView pts, + axom::Array>& sections) +{ + AXOM_ANNOTATE_SCOPE("SorClipper::splitIntoMonotonicSections"); + axom::Array splitIdx = FSorClipper::findZSwitchbacks(pts); + + const axom::IndexType sectionCount = splitIdx.size() - 1; + sections.clear(); + sections.resize(sectionCount); + for(axom::IndexType i = 0; i < sectionCount; ++i) + { + axom::IndexType firstInSection = splitIdx[i]; + axom::IndexType lastInSection = splitIdx[i + 1]; + auto& curSection = sections[i]; + curSection.reserve(lastInSection - firstInSection + 1); + for(axom::IndexType j = firstInSection; j <= lastInSection; ++j) + { + curSection.push_back(pts[j]); + } + } +} + +// Compute a += b. +template +void accumulateDataImpl(axom::ArrayView a, axom::ArrayView b, double scale) +{ + SLIC_ASSERT(a.size() == b.size()); + axom::for_all(a.size(), AXOM_LAMBDA(axom::IndexType i) { a[i] += scale * b[i]; }); +} + +void SorClipper::accumulateData(axom::ArrayView a, + axom::ArrayView b, + double scale, + axom::runtime_policy::Policy runtimePolicy) +{ + using RuntimePolicy = axom::runtime_policy::Policy; + if(runtimePolicy == RuntimePolicy::seq) + { + accumulateDataImpl(a, b, scale); + } +#ifdef AXOM_RUNTIME_POLICY_USE_OPENMP + else if(runtimePolicy == RuntimePolicy::omp) + { + accumulateDataImpl(a, b, scale); + } +#endif +#ifdef AXOM_RUNTIME_POLICY_USE_CUDA + else if(runtimePolicy == RuntimePolicy::cuda) + { + accumulateDataImpl>(a, b, scale); + } +#endif +#ifdef AXOM_RUNTIME_POLICY_USE_HIP + else if(runtimePolicy == RuntimePolicy::hip) + { + accumulateDataImpl>(a, b, scale); + } +#endif + else + { + SLIC_ERROR(axom::fmt::format("Unrecognized runtime policy {}", runtimePolicy)); + } + return; +} + +void SorClipper::extractClipperInfo() +{ + auto sorOriginArray = m_info.fetch_existing("sorOrigin").as_double_array(); + auto sorDirectionArray = m_info.fetch_existing("sorDirection").as_double_array(); + for(int d = 0; d < 3; ++d) + { + m_sorOrigin[d] = sorOriginArray[d]; + m_sorDirection[d] = sorDirectionArray[d]; + } + + auto discreteFunctionArray = m_info.fetch_existing("discreteFunction").as_double_array(); + auto n = discreteFunctionArray.number_of_elements(); + + SLIC_ERROR_IF( + n % 2 != 0, + axom::fmt::format( + "***SorClipper: Discrete function must have an even number of values. It has {}.", + n)); + + m_sorCurve.resize(axom::ArrayOptions::Uninitialized(), n / 2); + for(int i = 0; i < n / 2; ++i) + { + m_sorCurve[i] = Point2DType {discreteFunctionArray[i * 2], discreteFunctionArray[i * 2 + 1]}; + } + + m_levelOfRefinement = m_info.fetch_existing("levelOfRefinement").to_double(); +} + +} // namespace experimental +} // end namespace quest +} // end namespace axom diff --git a/src/axom/quest/detail/clipping/SorClipper.hpp b/src/axom/quest/detail/clipping/SorClipper.hpp new file mode 100644 index 0000000000..1a00fbb68f --- /dev/null +++ b/src/axom/quest/detail/clipping/SorClipper.hpp @@ -0,0 +1,106 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#ifndef AXOM_QUEST_SORCLIPPER_HPP +#define AXOM_QUEST_SORCLIPPER_HPP + +#include "axom/klee/Geometry.hpp" +#include "axom/quest/MeshClipperStrategy.hpp" +#include "axom/quest/detail/clipping/FSorClipper.hpp" +#include "axom/primal/geometry/CoordinateTransformer.hpp" + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +/*! + * @brief Geometry clipping operations for 3D + * surface-of-revolution geometries. + * + * This implementation allows the SOR function to have non-monotonic + * axial coordinates. For SOR curves that don't, the less complex + * FSorClipper is sufficient. + * + * The SOR specification may include rotation and translation + * internally, in addition to any external transformation. +*/ +class SorClipper : public MeshClipperStrategy +{ +public: + /*! + * @brief Constructor. + * + * @param [in] kGeom Describes the shape to place + * into the mesh. + * @param [in] name To override the default strategy name + */ + SorClipper(const klee::Geometry& kGeom, const std::string& name = ""); + + virtual ~SorClipper() = default; + + const std::string& name() const override { return m_name; } + + bool specializedClipCells(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView ovlap, + conduit::Node& statistics) override; + +#if !defined(__CUDACC__) +private: +#endif + std::string m_name; + + axom::Array> m_fsorStrategies; + + /*! + * @brief The discrete r(z) curve as an array of y(x) points. + * + * This data is before internal or external transformations. + * It may include points on each end to connect the curve to + * the axis of rotation. + */ + axom::Array m_sorCurve; + + //! @brief Bounding box of points in m_sorCurve; + BoundingBox2DType m_curveBb; + + //! @brief Maximum radius of the SOR. + double m_maxRadius; + + //! @brief Minimum radius of the SOR. + double m_minRadius; + + //!@brief The point corresponding to z=0 on the SOR axis. + Point3DType m_sorOrigin; + + //!@brief SOR axis in 3D space, in the direction of increasing z. + Vector3DType m_sorDirection; + + //!@brief Level of refinement for discretizing curved + // analytical shapes and surfaces of revolutions. + axom::IndexType m_levelOfRefinement = 0; + + //!@brief Array implementation of a += b. + void accumulateData(axom::ArrayView a, + axom::ArrayView b, + double scale, + axom::runtime_policy::Policy runtimePolicy); + + // Extract clipper info from MeshClipperStrategy::m_info. + void extractClipperInfo(); + + void splitIntoMonotonicSections(axom::ArrayView pts, + axom::Array>& sections); + + void initializeFSorClippers(); +}; + +} // namespace experimental +} // namespace quest +} // namespace axom + +#endif // AXOM_QUEST_SORCLIPPER_HPP diff --git a/src/axom/quest/detail/clipping/SphereClipper.cpp b/src/axom/quest/detail/clipping/SphereClipper.cpp new file mode 100644 index 0000000000..ffec80c4f0 --- /dev/null +++ b/src/axom/quest/detail/clipping/SphereClipper.cpp @@ -0,0 +1,276 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "axom/config.hpp" + +#include "axom/quest/Discretize.hpp" +#include "axom/quest/detail/clipping/SphereClipper.hpp" + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +SphereClipper::SphereClipper(const klee::Geometry& kGeom, const std::string& name) + : MeshClipperStrategy(kGeom) + , m_name(name.empty() ? std::string("Sphere") : name) + , m_transformer(m_extTrans) +{ + extractClipperInfo(); + + transformSphere(); +} + +bool SphereClipper::labelCellsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& labels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "SphereClipper requires a 3D mesh."); + + int allocId = shapeMesh.getAllocatorID(); + auto cellCount = shapeMesh.getCellCount(); + if(labels.size() < cellCount || labels.getAllocatorID() != shapeMesh.getAllocatorID()) + { + labels = axom::Array(ArrayOptions::Uninitialized(), cellCount, cellCount, allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelCellsInOutImpl(shapeMesh, labels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelCellsInOutImpl(shapeMesh, labels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelCellsInOutImpl>(shapeMesh, labels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelCellsInOutImpl>(shapeMesh, labels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +template +void SphereClipper::labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView labels) +{ + auto cellCount = shapeMesh.getCellCount(); + auto cellsAsHexes = shapeMesh.getCellsAsHexes(); + auto cellVolumes = shapeMesh.getCellVolumes(); + constexpr double EPS = 1e-10; + auto sphere = m_sphere; + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType cellId) { + LabelType& cellLabel = labels[cellId]; + if(axom::utilities::isNearlyEqual(cellVolumes[cellId], 0.0, EPS)) + { + cellLabel = LabelType::LABEL_OUT; + return; + } + const auto& hex = cellsAsHexes[cellId]; + cellLabel = polyhedronToLabel(hex, sphere); + }); + return; +} + +bool SphereClipper::labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "SphereClipper requires a 3D mesh."); + + const axom::IndexType cellCount = cellIds.size(); + const int allocId = shapeMesh.getAllocatorID(); + + if(tetLabels.size() < cellCount * NUM_TETS_PER_HEX || + tetLabels.getAllocatorID() != shapeMesh.getAllocatorID()) + { + tetLabels = axom::Array(ArrayOptions::Uninitialized(), + cellCount * NUM_TETS_PER_HEX, + cellCount * NUM_TETS_PER_HEX, + allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +template +void SphereClipper::labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView tetLabels) +{ + const axom::IndexType cellCount = cellIds.size(); + auto meshHexes = shapeMesh.getCellsAsHexes(); + auto tetVolumes = shapeMesh.getTetVolumes(); + constexpr double EPS = 1e-10; + auto sphere = m_sphere; + + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType ci) { + axom::IndexType cellId = cellIds[ci]; + const HexahedronType& hex = meshHexes[cellId]; + + TetrahedronType cellTets[NUM_TETS_PER_HEX]; + ShapeMesh::hexToTets(hex, cellTets); + + for(IndexType ti = 0; ti < NUM_TETS_PER_HEX; ++ti) + { + LabelType& tetLabel = tetLabels[ci * NUM_TETS_PER_HEX + ti]; + const axom::IndexType tetId = cellId * NUM_TETS_PER_HEX + ti; + if(axom::utilities::isNearlyEqual(tetVolumes[tetId], 0.0, EPS)) + { + tetLabel = LabelType::LABEL_OUT; + continue; + } + const TetrahedronType& tet = cellTets[ti]; + tetLabel = polyhedronToLabel(tet, sphere); + } + }); + return; +} + +template +AXOM_HOST_DEVICE inline MeshClipperStrategy::LabelType SphereClipper::polyhedronToLabel( + const Polyhedron& verts, + const SphereType& sphere) const +{ + /* + If bounding box of polyhedron is more than the radius distance + from center, it is LABEL_OUT. (Comparing vertices for this check + can miss intersections by edges and facets, so we compare bounding + box.) + + Otherwise, polyhedron either LABEL_ON or LABEL_IN. Sphere is + convex, so polyhedron is IN only if all vertices are inside. + */ + BoundingBox3DType bb(verts[0]); + auto vertCount = Polyhedron::numVertices(); + for(int i = 1; i < vertCount; ++i) + { + bb.addPoint(verts[i]); + } + + const double sqRad = sphere.getRadius() * sphere.getRadius(); + + double sqDistToBb = primal::squared_distance(sphere.getCenter(), bb); + + if(sqDistToBb >= sqRad) + { + return LabelType::LABEL_OUT; + } + + for(int i = 0; i < vertCount; ++i) + { + const auto& vert = verts[i]; + double sqDistToVert = axom::primal::squared_distance(sphere.getCenter(), vert); + if(sqDistToVert > sqRad) + { + return LabelType::LABEL_ON; + } + } + return LabelType::LABEL_IN; +} + +/* + TODO: If possible: Port to GPU. Will need to rewrite quest/Discretize.[ch]pp. +*/ +bool SphereClipper::getGeometryAsOcts(quest::experimental::ShapeMesh& shapeMesh, + axom::Array>& octs) +{ + AXOM_ANNOTATE_SCOPE("SphereClipper::getGeometryAsOcts"); + int octCount = 0; + axom::quest::discretize(m_sphereBeforeTrans, m_levelOfRefinement, octs, octCount); + + auto octsView = octs.view(); + auto transformer = m_transformer; + int allocId = shapeMesh.getAllocatorID(); + axom::for_all( + octCount, + AXOM_LAMBDA(axom::IndexType iOct) { + OctahedronType& oct = octsView[iOct]; + for(int iVert = 0; iVert < OctType::NUM_VERTS; ++iVert) + { + Point3DType& ptCoords = oct[iVert]; + transformer.transform(ptCoords.array()); + } + }); + + // The disretize method uses host data. Place into proper space if needed. + if(octs.getAllocatorID() != allocId) + { + octs = axom::Array>(octs, allocId); + } + + SLIC_INFO(axom::fmt::format("SphereClipper '{}' {}-level refined got {} geometry octs.", + name(), m_levelOfRefinement, octs.size())); + return true; +} + +void SphereClipper::extractClipperInfo() +{ + const auto c = m_info.fetch_existing("center").as_double_array(); + const double radius = m_info.fetch_existing("radius").as_double(); + Point3DType center; + for(int d = 0; d < 3; ++d) + { + center[d] = c[d]; + } + m_sphereBeforeTrans = SphereType(center, radius); + m_levelOfRefinement = m_info.fetch_existing("levelOfRefinement").to_int32(); +} + +// Include external transformations in m_sphere. +void SphereClipper::transformSphere() +{ + const auto& centerBeforeTrans = m_sphereBeforeTrans.getCenter(); + const double radiusBeforeTrans = m_sphereBeforeTrans.getRadius(); + Point3DType surfacePtBeforeTrans {centerBeforeTrans.array() + + Point3DType::NumericArray {radiusBeforeTrans, 0, 0}}; + + auto center = m_transformer.getTransformed(centerBeforeTrans); + Point3DType surfacePoint = m_transformer.getTransformed(surfacePtBeforeTrans); + const double radius = Vector3DType(center, surfacePoint).norm(); + m_sphere = SphereType(center, radius); +} + +} // namespace experimental +} // end namespace quest +} // end namespace axom diff --git a/src/axom/quest/detail/clipping/SphereClipper.hpp b/src/axom/quest/detail/clipping/SphereClipper.hpp new file mode 100644 index 0000000000..9902779db5 --- /dev/null +++ b/src/axom/quest/detail/clipping/SphereClipper.hpp @@ -0,0 +1,88 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#ifndef AXOM_QUEST_SPHERECLIPPER_HPP +#define AXOM_QUEST_SPHERECLIPPER_HPP + +#include "axom/klee/Geometry.hpp" +#include "axom/quest/MeshClipperStrategy.hpp" +#include "axom/primal/geometry/CoordinateTransformer.hpp" + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +/*! + * @brief Geometry clipping operations for sphere geometries. +*/ +class SphereClipper : public MeshClipperStrategy +{ +public: + /*! + * @brief Constructor. + * + * @param [in] kGeom Describes the shape to place + * into the mesh. + * @param [in] name To override the default strategy name + */ + SphereClipper(const klee::Geometry& kGeom, const std::string& name = ""); + + virtual ~SphereClipper() = default; + + const std::string& name() const override { return m_name; } + + bool labelCellsInOut(quest::experimental::ShapeMesh& shappeMesh, + axom::Array& label) override; + + bool labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) override; + + bool getGeometryAsOcts(quest::experimental::ShapeMesh& shappeMesh, + axom::Array>& octs) override; + +#if !defined(__CUDACC__) +private: +#endif + std::string m_name; + + //!@brief Sphere before external transformations. + SphereType m_sphereBeforeTrans; + + //!@brief Sphere after external transformations. + SphereType m_sphere; + + //!@brief External transformations. + axom::primal::experimental::CoordinateTransformer m_transformer; + + int m_levelOfRefinement; + + template + void labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView label); + + template + void labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView tetLabels); + + //!@brief Compute LabelType for a polyhedron (hex or tet in our case). + template + AXOM_HOST_DEVICE inline MeshClipperStrategy::LabelType polyhedronToLabel(const Polyhedron& verts, + const SphereType& sphere) const; + + void extractClipperInfo(); + + void transformSphere(); +}; + +} // namespace experimental +} // namespace quest +} // namespace axom + +#endif // AXOM_QUEST_SPHERECLIPPER_HPP diff --git a/src/axom/quest/detail/clipping/TetClipper.cpp b/src/axom/quest/detail/clipping/TetClipper.cpp index 564f60cbb3..3eee2cb5a6 100644 --- a/src/axom/quest/detail/clipping/TetClipper.cpp +++ b/src/axom/quest/detail/clipping/TetClipper.cpp @@ -17,19 +17,306 @@ namespace experimental TetClipper::TetClipper(const klee::Geometry& kGeom, const std::string& name) : MeshClipperStrategy(kGeom) , m_name(name.empty() ? std::string("Tet") : name) - , m_transformer(m_extTrans) + , m_extTransformer(m_extTrans) { extractClipperInfo(); for(int i = 0; i < TetrahedronType::NUM_VERTS; ++i) { - m_tet[i] = m_transformer.getTransformed(m_tetBeforeTrans[i]); + m_tet[i] = m_extTransformer.getTransformed(m_tetBeforeTrans[i]); } - for(int i = 0; i < TetrahedronType::NUM_VERTS; ++i) + /* + * Compute the transformation from m_tet to a unit tet. Location of + * points w.r.t. the tet are done in the unit tet space. The unit + * tet has heights 1, 1, 1 and 1/sqrt(3) as it rests on each of its 4 + * faces. Height w.r.t. the 4th face are scaled by sqrt(3) to make + * it come out to 1. So height < 0 means means below the tet and + * height > 1 means above the tet. + * + * Points with any height outside [0,1] cannot possibly be in the tet. + * Points with all 4 heights in [0,1] must be in the tet. + */ + Point3DType unitTetPts[] = {{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; + m_toUnitTet.setByTerminusPts(&m_tet[0], unitTetPts); +} + +bool TetClipper::labelCellsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& cellLabels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "FSorClipper requires a 3D mesh."); + + const int allocId = shapeMesh.getAllocatorID(); + const auto cellCount = shapeMesh.getCellCount(); + if(cellLabels.size() < cellCount || cellLabels.getAllocatorID() != allocId) + { + cellLabels = axom::Array(ArrayOptions::Uninitialized(), cellCount, cellCount, allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelCellsInOutImpl(shapeMesh, cellLabels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelCellsInOutImpl(shapeMesh, cellLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelCellsInOutImpl>(shapeMesh, cellLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelCellsInOutImpl>(shapeMesh, cellLabels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +template +void TetClipper::labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellLabels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "TetClipper requires a 3D mesh."); + /* + * Compute whether the mesh vertices are above the tet or below as + * the tet rests on its 4 facets. + * + * - For any facet the tet rests on, if all cell vertices are + * above the tet or all are below, cell is OUT. + * + * - If all cell vertices are on the tet side of all facets, + * the cell is IN. + * + * - Otherwise, cell is ON. + */ + + int allocId = shapeMesh.getAllocatorID(); + auto vertCount = shapeMesh.getVertexCount(); + auto cellCount = shapeMesh.getCellCount(); + auto meshCellVolumes = shapeMesh.getCellVolumes(); + + const auto& vertCoords = shapeMesh.getVertexCoords3D(); + const auto& vX = vertCoords[0]; + const auto& vY = vertCoords[1]; + const auto& vZ = vertCoords[2]; + SLIC_ASSERT(axom::execution_space::usesAllocId(vX.getAllocatorID())); + SLIC_ASSERT(axom::execution_space::usesAllocId(vY.getAllocatorID())); + SLIC_ASSERT(axom::execution_space::usesAllocId(vZ.getAllocatorID())); + + axom::ArrayView connView = shapeMesh.getCellNodeConnectivity(); + SLIC_ASSERT(connView.shape() == + (axom::StackArray {cellCount, HexahedronType::NUM_HEX_VERTS})); + + axom::Array below[4]; + axom::Array above[4]; + axom::ArrayView belowView[4]; + axom::ArrayView aboveView[4]; + for(IndexType p = 0; p < 4; ++p) + { + below[p] = axom::Array(ArrayOptions::Uninitialized(), vertCount, vertCount, allocId); + above[p] = axom::Array(ArrayOptions::Uninitialized(), vertCount, vertCount, allocId); + belowView[p] = below[p].view(); + aboveView[p] = above[p].view(); + } + + auto toUnitTet = m_toUnitTet; + + axom::for_all( + vertCount, + AXOM_LAMBDA(axom::IndexType vertId) { + // vh is the heights of the vertex in the space of the unit tet. + // See comment on m_toUnitTet. + axom::NumericArray vh({vX[vertId], vY[vertId], vZ[vertId], 0}); + toUnitTet.transform(vh[0], vh[1], vh[2]); + vh[3] = 1 - vh[0] - vh[1] - vh[2]; + + for(int p = 0; p < 4; ++p) + { + belowView[p][vertId] = vh[p] < 0; + aboveView[p][vertId] = vh[p] > 1; + } + }); + + constexpr double EPS = 1e-10; + + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType cellId) { + if(axom::utilities::isNearlyEqual(meshCellVolumes[cellId], 0.0, EPS)) + { + cellLabels[cellId] = LabelType::LABEL_OUT; + return; + } + + LabelType& cellLabel = cellLabels[cellId]; + auto cellVertIds = connView[cellId]; + + cellLabel = LabelType::LABEL_ON; + bool vertsAreOnTetSideOfAllPlanes = true; + for(IndexType p = 0; p < 4; ++p) + { + bool allVertsBelow = true; + bool allVertsAbove = true; + for(int vi = 0; vi < HexahedronType::NUM_HEX_VERTS; ++vi) + { + int vertId = cellVertIds[vi]; + auto vertIsBelow = belowView[p][vertId]; + auto vertIsAbove = aboveView[p][vertId]; + allVertsBelow &= vertIsBelow; + allVertsAbove &= vertIsAbove; + vertsAreOnTetSideOfAllPlanes &= !vertIsBelow; + } + if(allVertsBelow || allVertsAbove) + { + cellLabel = LabelType::LABEL_OUT; + break; + } + } + if(cellLabel != LabelType::LABEL_OUT && vertsAreOnTetSideOfAllPlanes) + { + cellLabel = LabelType::LABEL_IN; + } + }); + + return; +} + +bool TetClipper::labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "TetClipper requires a 3D mesh."); + + const int allocId = shapeMesh.getAllocatorID(); + const auto cellCount = cellIds.size(); + const auto tetCount = cellCount * NUM_TETS_PER_HEX; + if(tetLabels.size() < tetCount || tetLabels.getAllocatorID() != allocId) + { + tetLabels = axom::Array(ArrayOptions::Uninitialized(), tetCount, tetCount, allocId); + } + + switch(shapeMesh.getRuntimePolicy()) { - m_bb.addPoint(m_tet[i]); + case axom::runtime_policy::Policy::seq: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); } + return true; +} + +template +void TetClipper::labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView tetLabels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "TetClipper requires a 3D mesh."); + /* + * Compute whether the tets in hexes listed in cellIds + * are in, out or on the boundary. + * + * Picture the tet resting on each of of its 4 faces. + * - For any facet the tet rests on, if all cell vertices are + * above the tet or all are below, cell is OUT. + * + * - If all cell vertices are on the tet side of all facets, + * the cell is IN. + * + * - Otherwise, cell is ON. + */ + + auto meshTets = shapeMesh.getCellsAsTets(); + auto tetVolumes = shapeMesh.getTetVolumes(); + + const axom::IndexType cellCount = cellIds.size(); + + auto toUnitTet = m_toUnitTet; + constexpr double EPS = 1e-10; + + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType ci) { + axom::IndexType cellId = cellIds[ci]; + + const TetrahedronType* tetsForCell = &meshTets[cellId * NUM_TETS_PER_HEX]; + + for(IndexType ti = 0; ti < NUM_TETS_PER_HEX; ++ti) + { + const TetrahedronType& cellTet = tetsForCell[ti]; + LabelType& tetLabel = tetLabels[ci * NUM_TETS_PER_HEX + ti]; + const axom::IndexType tetId = cellId * NUM_TETS_PER_HEX + ti; + + if(axom::utilities::isNearlyEqual(tetVolumes[tetId], 0.0, EPS)) + { + tetLabel = LabelType::LABEL_OUT; + continue; + } + + tetLabel = LabelType::LABEL_ON; + + bool allVertsBelow = true; + bool allVertsAbove = true; + bool vertsAreOnTetSideOfAllPlanes = true; + + for(IndexType vi = 0; vi < 4; ++vi) + { + const auto& vert = cellTet[vi]; + + // vh is the heights of vert in the space of the unit tet. + // See comment on m_toUnitTet. + axom::NumericArray vh({vert[0], vert[1], vert[2], 0}); + toUnitTet.transform(vh[0], vh[1], vh[2]); + vh[3] = 1 - vh[0] - vh[1] - vh[2]; + + // Where vertex vi is w.r.t. the tet resting on side pj. + for(int pj = 0; pj < 4; ++pj) + { + bool vertIsBelow = vh[pj] < 0; + bool vertIsAbove = vh[pj] > 1; + + allVertsBelow &= vertIsBelow; + allVertsAbove &= vertIsAbove; + vertsAreOnTetSideOfAllPlanes &= !vertIsBelow; + } + + if(allVertsBelow || allVertsAbove) + { + tetLabel = LabelType::LABEL_OUT; + break; + } + } + + if(tetLabel != LabelType::LABEL_OUT && vertsAreOnTetSideOfAllPlanes) + { + tetLabel = LabelType::LABEL_IN; + } + } + }); + + return; } bool TetClipper::getGeometryAsTets(quest::experimental::ShapeMesh& shapeMesh, @@ -63,6 +350,30 @@ void TetClipper::extractClipperInfo() m_tetBeforeTrans[2][d] = v2[d]; m_tetBeforeTrans[3][d] = v3[d]; } + + bool fixOrientation = false; + if(m_info.has_child("fixOrientation")) + { + fixOrientation = bool(m_info.fetch_existing("fixOrientation").as_int()); + } + + if(fixOrientation) + { + m_tetBeforeTrans.checkAndFixOrientation(); + } + else + { + constexpr double EPS = 1e-10; + double signedVol = m_tetBeforeTrans.signedVolume(); + if(signedVol < -EPS) + { + SLIC_ERROR( + axom::fmt::format("TetClipper tet {} has negative volume {}.:" + " (See TetClipper's 'fixOrientation' flag.)", + m_tetBeforeTrans, + signedVol)); + } + } } } // namespace experimental diff --git a/src/axom/quest/detail/clipping/TetClipper.hpp b/src/axom/quest/detail/clipping/TetClipper.hpp index c0062ea498..530ce312ff 100644 --- a/src/axom/quest/detail/clipping/TetClipper.hpp +++ b/src/axom/quest/detail/clipping/TetClipper.hpp @@ -33,6 +33,9 @@ class TetClipper : public MeshClipperStrategy * \c kGeom.asHierarchy() must contain the following data: * - v0, v1, v2, v3: each contains a 3D coordinates of the * tetrahedron vertices, in the order used by primal::Tetrahedron. + * The tet may be degenerate, but not inverted (negative volume). + * - "fixOrientation": Whether to fix inverted tetrahedra + * instead of aborting. */ TetClipper(const klee::Geometry& kGeom, const std::string& name = ""); @@ -40,13 +43,20 @@ class TetClipper : public MeshClipperStrategy const std::string& name() const override { return m_name; } + bool labelCellsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& cellLabels) override; + + bool labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) override; + /*! * @copydoc MeshClipperStrategy::getGeometryAsTets() * * \c tets will have length one, because the geometry for this * class is a single tetrahedron. */ - bool getGeometryAsTets(quest::experimental::ShapeMesh& shappeMesh, + bool getGeometryAsTets(quest::experimental::ShapeMesh& shapeMesh, axom::Array& tets) override; #if !defined(__CUDACC__) @@ -54,15 +64,26 @@ class TetClipper : public MeshClipperStrategy #endif std::string m_name; - //!@brief Tetrahedron before transformation. + //!@brief Tetrahedron before external transformation. TetrahedronType m_tetBeforeTrans; - //!@brief Tetrahedron after transformation. + //!@brief Tetrahedron after external transformation. TetrahedronType m_tet; - axom::primal::BoundingBox m_bb; + //!@brief External transformation. + axom::primal::experimental::CoordinateTransformer m_extTransformer; + + //!@brief Transformation of m_tet to unit tet. + axom::primal::experimental::CoordinateTransformer m_toUnitTet; + + template + void labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellLabel); - axom::primal::experimental::CoordinateTransformer m_transformer; + template + void labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellsOnBdry, + axom::ArrayView tetLabels); // Extract clipper info from MeshClipperStrategy::m_info. void extractClipperInfo(); diff --git a/src/axom/quest/detail/clipping/TetMeshClipper.cpp b/src/axom/quest/detail/clipping/TetMeshClipper.cpp new file mode 100644 index 0000000000..124b483934 --- /dev/null +++ b/src/axom/quest/detail/clipping/TetMeshClipper.cpp @@ -0,0 +1,1119 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "axom/config.hpp" + +#include "axom/mint/mesh/Mesh.hpp" +#include "axom/mint/mesh/UnstructuredMesh.hpp" +#include "axom/spin/BVH.hpp" +#include "axom/quest/detail/clipping/TetMeshClipper.hpp" +#include "axom/bump.hpp" +#include "axom/core/execution/for_all.hpp" +#include "axom/core/execution/scans.hpp" + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +TetMeshClipper::TetMeshClipper(const klee::Geometry& kGeom, const std::string& name) + : MeshClipperStrategy(kGeom) + , m_name(name.empty() ? std::string("TetMesh") : name) + , m_topoName(kGeom.getBlueprintTopology()) + , m_tetCount(0) + , m_transformer(m_extTrans) +{ + SLIC_ASSERT(!m_topoName.empty()); + + extractClipperInfo(); + + transformCoordset(); + + computeTets(); +} + +bool TetMeshClipper::labelCellsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& labels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "TetMeshClipper requires a 3D mesh."); + + int allocId = shapeMesh.getAllocatorID(); + auto cellCount = shapeMesh.getCellCount(); + if(labels.size() < cellCount || labels.getAllocatorID() != allocId) + { + labels = axom::Array(ArrayOptions::Uninitialized(), cellCount, 0, allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelCellsInOutImpl(shapeMesh, labels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelCellsInOutImpl(shapeMesh, labels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelCellsInOutImpl>(shapeMesh, labels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelCellsInOutImpl>(shapeMesh, labels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +bool TetMeshClipper::labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) +{ + SLIC_ERROR_IF(shapeMesh.dimension() != 3, "TetMeshClipper requires a 3D mesh."); + + int allocId = shapeMesh.getAllocatorID(); + const axom::IndexType tetCount = cellIds.size() * NUM_TETS_PER_HEX; + if(tetLabels.size() < tetCount || tetLabels.getAllocatorID() != allocId) + { + tetLabels = axom::Array(ArrayOptions::Uninitialized(), tetCount, 0, allocId); + } + + switch(shapeMesh.getRuntimePolicy()) + { + case axom::runtime_policy::Policy::seq: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + labelTetsInOutImpl(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + labelTetsInOutImpl>(shapeMesh, cellIds, tetLabels.view()); + break; +#endif + default: + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + } + return true; +} + +#if 1 +/* + * Alternative way: + * - Put surface triangles in BVH. + * - Create a bounding box and a ray for every hex. The ray + * originates from the bounding box center and points away from + * the center of m_tetMeshBb. + * - Use BVH::findBoundingBoxes and BVH::findRay to get surface + * triangles near the bounding boxes and rays. We won't need + * both for most of the hexes, but it may be faster than building + * index lists of where we need them. + * - Loop through the hexes. + * - If hex bb intersects any surface triangle bb, hex is ON. + * - Else, the hex is either IN or OUT. It can't possibly by ON. + * Count number of surface triangles the hex's ray intersects. + * Use bool intersect(const Triangle& tri, const Ray& ray) + * If count is odd, hex is IN, if even, OUT. + */ +template +void TetMeshClipper::labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView labels) +{ + int allocId = shapeMesh.getAllocatorID(); + auto cellCount = shapeMesh.getCellCount(); + + // Copy m_tetMesh array data to allocId if it's not done yet. + copy_tetmesh_arrays_to(allocId); + + /* + Compute surface triangles of the tet mesh. + */ + AXOM_ANNOTATE_BEGIN("TetMeshClipper::compute_surface"); + axom::Array surfTris = + computeGeometrySurface(shapeMesh.getRuntimePolicy(), allocId); + AXOM_ANNOTATE_END("TetMeshClipper::compute_surface"); + auto surfTrisView = surfTris.view(); + + /* + Surface triangles (as bounding boxes) in BVH. + */ + AXOM_ANNOTATE_BEGIN("TetMeshClipper::make_surf_bvh"); + axom::Array surfTrisAsBbs(surfTris.size(), 0, allocId); + auto surfTrisAsBbsView = surfTrisAsBbs.view(); + + axom::for_all( + surfTris.size(), + AXOM_LAMBDA(axom::IndexType fi) { + const auto& surfTri = surfTrisView[fi]; + surfTrisAsBbsView[fi] = axom::primal::compute_bounding_box(surfTri); + }); + + spin::BVH<3, ExecSpace, double> bvh; + bvh.initialize(surfTrisAsBbsView, surfTrisAsBbsView.size()); + AXOM_ANNOTATE_END("TetMeshClipper::make_surf_bvh"); + + /* + Compute rays. Each ray originates from its hex center and point + away from the center of the tet mesh. + */ + AXOM_ANNOTATE_BEGIN("TetMeshClipper::make_rays"); + Point3DType geomCenter = m_tetMeshBb.getCentroid(); // Estimate of tet mesh center. + axom::ArrayView hexBbs = shapeMesh.getCellBoundingBoxes(); + axom::ArrayView hexes = shapeMesh.getCellsAsHexes(); + axom::Array hexRays(hexes.size(), 0, allocId); + auto hexRaysView = hexRays.view(); + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType cellIdx) { + Point3DType hexCenter = hexBbs[cellIdx].getCentroid(); + Vector3DType direction(geomCenter, hexCenter); + hexRaysView[cellIdx] = Ray3DType(hexCenter, direction); + }); + AXOM_ANNOTATE_END("TetMeshClipper::make_rays"); + + /* + * Find candidate surface triangles near the cells' bounding boxes and rays. + */ + AXOM_ANNOTATE_BEGIN("TetMeshClipper::get_surf_near_bbs"); + axom::Array bbOffsets(cellCount, 0, allocId); + axom::Array bbCounts(cellCount, 0, allocId); + axom::Array bbCandidates; + bvh.findBoundingBoxes(bbOffsets, bbCounts, bbCandidates, hexBbs.size(), hexBbs); + AXOM_ANNOTATE_END("TetMeshClipper::get_surf_near_bbs"); + + AXOM_ANNOTATE_BEGIN("TetMeshClipper::get_surf_near_rays"); + axom::Array rayOffsets(cellCount, 0, allocId); + axom::Array rayCounts(cellCount, 0, allocId); + axom::Array rayCandidates; + bvh.findRays(rayOffsets, rayCounts, rayCandidates, hexRaysView.size(), hexRaysView); + AXOM_ANNOTATE_END("TetMeshClipper::get_surf_near_rays"); + + auto bbCountsView = bbCounts.view(); + auto bbOffsetsView = bbOffsets.view(); + auto bbCandidatesView = bbCandidates.view(); + + auto rayCountsView = rayCounts.view(); + auto rayOffsetsView = rayOffsets.view(); + auto rayCandidatesView = rayCandidates.view(); + + const double EPS = 1e-12; + + AXOM_ANNOTATE_BEGIN("TetMeshClipper::compute_labels"); + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType cellId) { + LabelType& label = labels[cellId]; + + { + // Label cell as ON boundary if it's near the boundary. + const auto& hexBb = hexBbs[cellId]; + auto candidateCount = bbCountsView[cellId]; + auto candidateOffset = bbOffsetsView[cellId]; + auto* candidateIds = bbCandidatesView.data() + candidateOffset; + for(int ci = 0; ci < candidateCount; ++ci) + { + axom::IndexType candidateId = candidateIds[ci]; + const Triangle3DType& candidate = surfTrisView[candidateId]; + bool intersects = axom::primal::intersect(candidate, hexBb); + if(intersects) + { + label = LabelType::LABEL_ON; + return; + } + } + } + + /* + * At this point, the cell must be IN or OUT. No need to account + * for the possibility that it's ON. + */ + + { + const Ray3DType& hexRay = hexRaysView[cellId]; + auto candidateCount = rayCountsView[cellId]; + auto candidateOffset = rayOffsetsView[cellId]; + auto* candidateIds = rayCandidatesView.data() + candidateOffset; + axom::IndexType surfaceCrossingCount = 0; + for(int ci = 0; ci < candidateCount; ++ci) + { + axom::IndexType candidateId = candidateIds[ci]; + Triangle3DType candidate = surfTrisView[candidateId]; + double contactT; + Point3DType contactPt; // contact point in unnormalized barycentric coordinates. + bool touches = axom::primal::intersect(candidate, hexRay, contactT, contactPt); + if(touches) + { + /* + * Grazing contact requires more logic and computation to + * determine if the ray crosses the boundary. Label the + * hex as ON the boundary so the clipping computation + * will handle this edge case. + */ + contactPt.array() /= contactPt[0] + contactPt[1] + contactPt[2]; // Normalize + bool grazing = axom::utilities::isNearlyEqual(contactPt[0], EPS) || + axom::utilities::isNearlyEqual(contactPt[1], EPS) || + axom::utilities::isNearlyEqual(contactPt[2], EPS); + if(grazing) + { + label = LabelType::LABEL_ON; + return; + } + ++surfaceCrossingCount; + } + } + label = surfaceCrossingCount % 2 == 0 ? LabelType::LABEL_OUT : LabelType::LABEL_IN; + } + }); + AXOM_ANNOTATE_END("TetMeshClipper::compute_labels"); +} +#else +/* + * This is an old version, kept around until performance evaluation + * determines which is better. + + * 1. Compute whether vertices are in or out of the tet mesh. + * 2. Determine whether cells are in, out or on the tet mesh + * boundary. + * Unlike the TetClipper, this doesn't check edge-tet intersections, + * so it has errors. These errors should shrink with mesh resolution. + * If needed, we can implement edge-tet detection for TetMeshClipper. +*/ +template +void TetMeshClipper::labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& labels) +{ + int allocId = shapeMesh.getAllocatorID(); + auto vertCount = shapeMesh.getVertexCount(); + + /* + * Tets (as bounding boxes) in BVH. + */ + axom::Array tetsAsBbs(m_tets.size(), m_tets.size(), allocId); + auto tetsAsBbsView = tetsAsBbs.view(); + auto tetsView = m_tets.view(); + + axom::Array tmpTets; + if(allocId != m_tets.getAllocatorID()) + { + tmpTets = axom::Array(m_tets, allocId); + tetsView = tmpTets.view(); + } + axom::for_all( + tetsView.size(), + AXOM_LAMBDA(axom::IndexType vi) { + const auto& tet = tetsView[vi]; + tetsAsBbsView[vi] = axom::primal::compute_bounding_box(tet); + }); + + spin::BVH<3, ExecSpace, double> bvh; + bvh.initialize(tetsAsBbsView, tetsAsBbsView.size()); + + /* + * Compute whether vertices are inside tet mesh. + * (Use BVH to narrow the search to nearby candidate tets.) + */ + axom::Array vertIsInside {ArrayOptions::Uninitialized(), vertCount, vertCount, allocId}; + vertIsInside.fill(false); + auto vertIsInsideView = vertIsInside.view(); + + axom::ArrayView vertPointsView = shapeMesh.getVertexPoints(); + + axom::Array offsets(vertCount, vertCount, allocId); + axom::Array counts(vertCount, vertCount, allocId); + axom::Array candidates; + bvh.findPoints(offsets, counts, candidates, vertCount, vertPointsView); + + auto countsView = counts.view(); + auto offsetsView = offsets.view(); + auto candidatesView = candidates.view(); + axom::for_all( + vertCount, + AXOM_LAMBDA(axom::IndexType vertId) { + auto candidateCount = countsView[vertId]; + bool& isInside = vertIsInsideView[vertId]; + if(!isInside && candidateCount > 0) + { + auto candidateIds = &candidatesView[offsetsView[vertId]]; + auto& vertex = vertPointsView[vertId]; + for(int ci = 0; ci < candidateCount && !isInside; ++ci) + { + axom::IndexType tetId = candidateIds[ci]; + const auto& tet = tetsView[tetId]; + isInside |= tet.contains(vertex); + } + } + }); + + vertexInsideToCellLabel(shapeMesh, vertIsInsideView, labels); +} +#endif + +/* + * Alternative way: + * - Put surface triangles in BVH. + * - Create a bounding box and a ray for every tet. The ray + * originates from the bounding box center and points away from + * the center of m_tetMeshBb. + * - Use BVH::findBoundingBoxes and BVH::findRay to get surface + * triangles near the bounding boxes and rays. We won't need + * both for most of the tets, but it may be faster than building + * index lists of where we need them. + * - Loop through the tets. + * - If tet bb intersects any surface triangle bb, hex is ON. + * - Else, the tet is either IN or OUT. It can't possibly by ON. + * Count number of surface triangles the tet's ray intersects. + * Use bool intersect(const Triangle& tri, const Ray& ray) + * If count is odd, tet is IN, if even, OUT. + */ +template +void TetMeshClipper::labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView tetLabels) +{ + int allocId = shapeMesh.getAllocatorID(); + auto cellCount = cellIds.size(); + auto tetCount = cellCount * NUM_TETS_PER_HEX; + + const auto meshTets = shapeMesh.getCellsAsTets(); + auto tetVolumes = shapeMesh.getTetVolumes(); + + // Copy m_tetMesh array data to allocId if it's not done yet. + copy_tetmesh_arrays_to(allocId); + + /* + Compute surface triangles of the tet mesh. + */ + AXOM_ANNOTATE_BEGIN("TetMeshClipper::compute_surface"); + axom::Array surfTris = + computeGeometrySurface(shapeMesh.getRuntimePolicy(), allocId); + AXOM_ANNOTATE_END("TetMeshClipper::compute_surface"); + auto surfTrisView = surfTris.view(); + + /* + Surface triangles (as bounding boxes) in BVH. + */ + AXOM_ANNOTATE_BEGIN("TetMeshClipper::make_surf_bvh"); + axom::Array surfTrisAsBbs(surfTris.size(), 0, allocId); + auto surfTrisAsBbsView = surfTrisAsBbs.view(); + + axom::for_all( + surfTris.size(), + AXOM_LAMBDA(axom::IndexType fi) { + const auto& surfTri = surfTrisView[fi]; + surfTrisAsBbsView[fi] = axom::primal::compute_bounding_box(surfTri); + }); + + spin::BVH<3, ExecSpace, double> bvh; + bvh.initialize(surfTrisAsBbsView, surfTrisAsBbsView.size()); + AXOM_ANNOTATE_END("TetMeshClipper::make_surf_bvh"); + + /* + Compute rays. Each ray originates from its hex center and point + away from the center of the tet mesh. + */ + AXOM_ANNOTATE_BEGIN("TetMeshClipper::make_rays"); + Point3DType geomCenter = m_tetMeshBb.getCentroid(); // Estimate of tet mesh center. + axom::Array tetBbs(axom::ArrayOptions::Uninitialized(), + tetCount, + tetCount, + allocId); + auto tetBbsView = tetBbs.view(); + axom::Array tetRays(axom::ArrayOptions::Uninitialized(), tetCount, tetCount, allocId); + auto tetRaysView = tetRays.view(); + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType ci) { + auto cellId = cellIds[ci]; + auto* tetsForCell = &meshTets[cellId * NUM_TETS_PER_HEX]; + for(int ti = 0; ti < NUM_TETS_PER_HEX; ++ti) + { + const auto& tet = tetsForCell[ti]; + Point3DType tetCenter((tet[0].array() + tet[1].array() + tet[2].array()) / 3); + Vector3DType direction(geomCenter, tetCenter); + tetRaysView[ci * NUM_TETS_PER_HEX + ti] = Ray3DType(tetCenter, direction); + tetBbsView[ci * NUM_TETS_PER_HEX + ti] = BoundingBox3DType {tet[0], tet[1], tet[2]}; + } + }); + AXOM_ANNOTATE_END("TetMeshClipper::make_rays"); + + /* + * Find candidate surface triangles near the tets' bounding boxes and rays. + */ + AXOM_ANNOTATE_BEGIN("TetMeshClipper::get_surf_near_bbs"); + axom::Array bbOffsets(tetCount, 0, allocId); + axom::Array bbCounts(tetCount, 0, allocId); + axom::Array bbCandidates; + bvh.findBoundingBoxes(bbOffsets, bbCounts, bbCandidates, tetBbs.size(), tetBbsView); + AXOM_ANNOTATE_END("TetMeshClipper::get_surf_near_bbs"); + + AXOM_ANNOTATE_BEGIN("TetMeshClipper::get_surf_near_rays"); + axom::Array rayOffsets(tetCount, 0, allocId); + axom::Array rayCounts(tetCount, 0, allocId); + axom::Array rayCandidates; + bvh.findRays(rayOffsets, rayCounts, rayCandidates, tetRaysView.size(), tetRaysView); + AXOM_ANNOTATE_END("TetMeshClipper::get_surf_near_rays"); + + auto bbCountsView = bbCounts.view(); + auto bbOffsetsView = bbOffsets.view(); + auto bbCandidatesView = bbCandidates.view(); + + auto rayCountsView = rayCounts.view(); + auto rayOffsetsView = rayOffsets.view(); + auto rayCandidatesView = rayCandidates.view(); + + const double EPS = 1e-12; + + AXOM_ANNOTATE_BEGIN("TetMeshClipper::compute_labels"); + axom::for_all( + tetCount, + AXOM_LAMBDA(axom::IndexType ti) { + LabelType& label = tetLabels[ti]; + + axom::IndexType ci = ti / NUM_TETS_PER_HEX; + axom::IndexType tii = ti % NUM_TETS_PER_HEX; + axom::IndexType tetId = ci * NUM_TETS_PER_HEX + tii; + if(axom::utilities::isNearlyEqual(tetVolumes[tetId], 0.0, EPS)) + { + label = LabelType::LABEL_OUT; + return; + } + + { + // Label tet as ON boundary if it's near the boundary. + const auto& tetBb = tetBbsView[ti]; + auto candidateCount = bbCountsView[ti]; + auto candidateOffset = bbOffsetsView[ti]; + auto* candidateIds = bbCandidatesView.data() + candidateOffset; + for(int ci = 0; ci < candidateCount; ++ci) + { + axom::IndexType candidateId = candidateIds[ci]; + const Triangle3DType& candidate = surfTrisView[candidateId]; + bool intersects = axom::primal::intersect(candidate, tetBb); + if(intersects) + { + label = LabelType::LABEL_ON; + return; + } + } + } + + /* + * At this point, the cell must be IN or OUT. No need to account + * for the possibility that it's ON. + */ + + { + const Ray3DType& tetRay = tetRaysView[ti]; + auto candidateCount = rayCountsView[ti]; + auto candidateOffset = rayOffsetsView[ti]; + auto* candidateIds = rayCandidatesView.data() + candidateOffset; + axom::IndexType surfaceCrossingCount = 0; + for(int ci = 0; ci < candidateCount; ++ci) + { + axom::IndexType candidateId = candidateIds[ci]; + Triangle3DType candidate = surfTrisView[candidateId]; + double contactT; + Point3DType contactPt; // contact point in unnormalized barycentric coordinates. + bool touches = axom::primal::intersect(candidate, tetRay, contactT, contactPt); + if(touches) + { + /* + * Grazing contact requires more logic and computation to + * determine if the ray crosses the boundary. Label the + * tet as ON the boundary so the clipping computation + * will handle this edge case. + */ + contactPt.array() /= contactPt[0] + contactPt[1] + contactPt[2]; // Normalize + bool grazing = axom::utilities::isNearlyEqual(contactPt[0], EPS) || + axom::utilities::isNearlyEqual(contactPt[1], EPS) || + axom::utilities::isNearlyEqual(contactPt[2], EPS); + if(grazing) + { + label = LabelType::LABEL_ON; + return; + } + ++surfaceCrossingCount; + } + } + label = surfaceCrossingCount % 2 == 0 ? LabelType::LABEL_OUT : LabelType::LABEL_IN; + } + }); + AXOM_ANNOTATE_END("TetMeshClipper::compute_labels"); +} + +/* + * Label cell outside if no vertex is in the bounding box. + * Otherwise, label it on boundary, because we don't know. +*/ +template +void TetMeshClipper::vertexInsideToCellLabel(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView& vertIsInside, + axom::Array& labels) +{ + axom::ArrayView hexConnView = shapeMesh.getCellNodeConnectivity(); + SLIC_ASSERT(hexConnView.shape() == + (axom::StackArray {shapeMesh.getCellCount(), + HexahedronType::NUM_HEX_VERTS})); + + if(labels.size() < shapeMesh.getCellCount() || + labels.getAllocatorID() != shapeMesh.getAllocatorID()) + { + labels = axom::Array(ArrayOptions::Uninitialized(), + shapeMesh.getCellCount(), + shapeMesh.getCellCount(), + shapeMesh.getAllocatorID()); + } + auto labelsView = labels.view(); + + axom::for_all( + shapeMesh.getCellCount(), + AXOM_LAMBDA(axom::IndexType cellId) { + auto cellVertIds = hexConnView[cellId]; + bool hasIn = vertIsInside[cellVertIds[0]]; + bool hasOut = !hasIn; + for(int vi = 0; vi < HexahedronType::NUM_HEX_VERTS; ++vi) + { + int vertId = cellVertIds[vi]; + bool isIn = vertIsInside[vertId]; + hasIn |= isIn; + hasOut |= !isIn; + } + labelsView[cellId] = !hasOut ? LabelType::LABEL_IN + : !hasIn ? LabelType::LABEL_OUT + : LabelType::LABEL_ON; + }); + + return; +} + +bool TetMeshClipper::getGeometryAsTets(quest::experimental::ShapeMesh& shapeMesh, + axom::Array& tets) +{ + tets = axom::Array(m_tets, shapeMesh.getAllocatorID()); + return true; +} + +// Compute m_tets. Keep data on host. We don't know what allocator the ShapeMesh uses. +void TetMeshClipper::computeTets() +{ + AXOM_ANNOTATE_SCOPE("TetMeshClipper::computeTets"); + const int hostAllocId = axom::execution_space::allocatorID(); + + m_tets = axom::Array(m_tetCount, m_tetCount, hostAllocId); + + /* + * 1. Initialize a mint Mesh intermediary from the blueprint mesh. + * mint::getMesh() utility for this saves some coding. + * 2. Populate a tet array on the host (because mint data works only for host). + */ + + axom::sidre::DataStore ds; + auto* tetMeshGrp = ds.getRoot()->createGroup("blueprintMesh"); + tetMeshGrp->importConduitTree(m_tetMesh); + + const bool addExtraDataForMint = true; + if(addExtraDataForMint) + { + /* + * Constructing a mint mesh from meshGrp fails unless we add some + * extra data. Blueprint doesn't require this extra data. (The mesh + * passes conduit's Blueprint verification.) This should be fixed, + * or we should write better blueprint support utilities. + */ + auto* topoGrp = tetMeshGrp->getGroup("topologies")->getGroup(m_topoName); + auto* coordValuesGrp = + tetMeshGrp->getGroup("coordsets")->getGroup(m_coordsetName)->getGroup("values"); + /* + * Make the coordinate arrays 2D to use mint::Mesh. + * For some reason, mint::Mesh requires the arrays to be + * 2D, even though the second dimension is always 1. + */ + axom::IndexType curShape[2]; + int curDim; + curDim = coordValuesGrp->getView("x")->getShape(2, curShape); + assert(curDim == 1); + const axom::IndexType vertsShape[2] = {curShape[0], 1}; + coordValuesGrp->getView("x")->reshapeArray(2, vertsShape); + coordValuesGrp->getView("y")->reshapeArray(2, vertsShape); + coordValuesGrp->getView("z")->reshapeArray(2, vertsShape); + + // Make connectivity array 2D for the same reason. + auto* elementsGrp = topoGrp->getGroup("elements"); + auto* tetVertConnView = elementsGrp->getView("connectivity"); + curDim = tetVertConnView->getShape(2, curShape); + constexpr axom::IndexType NUM_VERTS_PER_TET = 4; + SLIC_ASSERT(curDim == 1 || curDim == 2); + if(curDim == 1) + { + SLIC_ASSERT(curShape[0] % NUM_VERTS_PER_TET == 0); + axom::IndexType connShape[2] = {curShape[0] / NUM_VERTS_PER_TET, NUM_VERTS_PER_TET}; + tetVertConnView->reshapeArray(2, connShape); + } + + // mint::Mesh requires connectivity strides, even though Blueprint doesn't. + if(!elementsGrp->hasView("stride")) + { + elementsGrp->createViewScalar("stride", NUM_VERTS_PER_TET, hostAllocId); + } + + // mint::Mesh requires field group, even though Blueprint doesn't. + if(!tetMeshGrp->hasGroup("fields")) + { + tetMeshGrp->createGroup("fields"); + } + } + std::shared_ptr> mintMesh { + (mint::UnstructuredMesh*)axom::mint::getMesh(tetMeshGrp, + m_topoName)}; + + bool fixOrientation = false; + if(m_info.has_child("fixOrientation")) + { + fixOrientation = bool(m_info.fetch_existing("fixOrientation").as_int()); + } + + // Initialize tetrahedra and check for bad orientations. + constexpr double EPS = 1e-10; + IndexType nodeIds[4]; + Point3DType pts[4]; + for(int i = 0; i < m_tetCount; i++) + { + mintMesh->getCellNodeIDs(i, nodeIds); + + mintMesh->getNode(nodeIds[0], pts[0].data()); + mintMesh->getNode(nodeIds[1], pts[1].data()); + mintMesh->getNode(nodeIds[2], pts[2].data()); + mintMesh->getNode(nodeIds[3], pts[3].data()); + + m_tets[i] = TetrahedronType({pts[0], pts[1], pts[2], pts[3]}); + + if(fixOrientation) + { + m_tets[i].checkAndFixOrientation(); + } + else + { + double signedVol = m_tets[i].signedVolume(); + if(signedVol < -EPS) + { + SLIC_ERROR( + axom::fmt::format("TetMeshClipper's tet {}, {}, has a negative volume {}.:" + " (See TetMeshClipper's 'fixOrientation' flag.)", + i, + m_tets[i], + signedVol)); + } + } + } +} + +void TetMeshClipper::extractClipperInfo() +{ + m_topoName = m_info.fetch_existing("topologyName").as_string(); + + m_tetMesh = m_info.fetch_existing("klee::Geometry:tetMesh"); + + SLIC_ASSERT( + m_tetMesh.fetch_existing("topologies").fetch_existing(m_topoName).fetch_existing("type").as_string() == + "unstructured"); + { + std::string whyBad; + bool good = isValidTetMesh(m_tetMesh, whyBad); + if(!good) + { + SLIC_ERROR(axom::fmt::format("TetMeshClipper given bad tet mesh: {}", whyBad)); + } + } + + conduit::Node& topoNode = m_tetMesh.fetch_existing("topologies").fetch_existing(m_topoName); + + bool isMultiDomain = conduit::blueprint::mesh::is_multi_domain(m_tetMesh); + SLIC_ERROR_IF(isMultiDomain, "TetMeshClipper does not support multi-domain tet meshes yet."); + + const auto topoDim = conduit::blueprint::mesh::topology::dims(topoNode); + SLIC_ASSERT(topoDim == 3); + SLIC_ASSERT(conduit::blueprint::mesh::topology::dims(topoNode) == 3); + + m_tetCount = conduit::blueprint::mesh::topology::length(topoNode); + + m_coordsetName = topoNode.fetch_existing("coordset").as_string(); +} + +bool TetMeshClipper::isValidTetMesh(const conduit::Node& tetMesh, std::string& whyBad) const +{ + bool rval = true; + + conduit::Node info; + rval = conduit::blueprint::mesh::verify(tetMesh, info); + + if(rval) + { + std::string topoType = tetMesh.fetch("topologies")[m_topoName]["type"].as_string(); + rval = topoType == "unstructured"; + info[0].set_string("Topology is not unstructured."); + } + + if(rval) + { + std::string elemShape = tetMesh.fetch("topologies")[m_topoName]["elements/shape"].as_string(); + rval = elemShape == "tet"; + info[0].set_string("Topology elements are not tet."); + } + + whyBad = info.to_summary_string(); + + return rval; +} + +void TetMeshClipper::transformCoordset() +{ + // Apply transformations + auto& oldCoordset = m_tetMesh.fetch_existing("coordsets").fetch_existing(m_coordsetName); + const std::string newCoordsetName = m_coordsetName + ".trans"; + conduit::Node& coordset = m_tetMesh.fetch("coordsets")[newCoordsetName]; + coordset.set_node(oldCoordset); + auto transformer = m_transformer; + conduit::index_t count = conduit::blueprint::mesh::coordset::length(coordset); + axom::ArrayView xV(coordset.fetch_existing("values/x").as_double_ptr(), count); + axom::ArrayView yV(coordset.fetch_existing("values/y").as_double_ptr(), count); + axom::ArrayView zV(coordset.fetch_existing("values/z").as_double_ptr(), count); + axom::for_all( + count, + AXOM_LAMBDA(axom::IndexType i) { + transformer.transform(xV[i], yV[i], zV[i]); + m_tetMeshBb.addPoint(Point3DType {xV[i], yV[i], zV[i]}); + }); + m_tetMesh.fetch_existing("topologies") + .fetch_existing(m_topoName) + .fetch_existing("coordset") + .set_string(newCoordsetName); + m_coordsetName = newCoordsetName; +} + +axom::Array TetMeshClipper::computeGeometrySurface( + axom::runtime_policy::Policy policy, + int allocId) +{ + AXOM_ANNOTATE_SCOPE("TetMeshClipper::computeGeometrySurface"); + switch(policy) + { + case axom::runtime_policy::Policy::seq: + return computeGeometrySurface(allocId); + break; +#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) + case axom::runtime_policy::Policy::omp: + return computeGeometrySurface(allocId); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_CUDA) + case axom::runtime_policy::Policy::cuda: + return computeGeometrySurface>(allocId); + break; +#endif +#if defined(AXOM_RUNTIME_POLICY_USE_HIP) + case axom::runtime_policy::Policy::hip: + return computeGeometrySurface>(allocId); + break; +#endif + } + SLIC_ERROR("Axom Internal error: Unhandled execution policy."); + return {}; +} + +template +axom::Array TetMeshClipper::computeGeometrySurface(int allocId) +{ + /* + * Make a view of the tet mesh topology. + * + * Note: Using axom::IndexType for integers. Users should configure + * Axom with the type that they plan to use. Mixing different + * IndexTypes in the same Axom build is not supported. + */ + namespace bumpViews = axom::bump::views; + namespace bumpUtils = axom::bump::utilities; + using bumpShape = bumpViews::TetShape; + + constexpr axom::IndexType FACES_PER_TET = 4; + constexpr axom::IndexType VERTS_PER_TET = 4; + constexpr axom::IndexType VERTS_PER_FACE = 3; + + const std::string topoName = axom::fmt::format("{}.{}", m_topoName, allocId); + conduit::Node& tetTopo = m_tetMesh["topologies"][topoName]; + conduit::Node& tetVertConn = tetTopo.fetch_existing("elements").fetch_existing("connectivity"); + const auto tetVertConnView = bumpUtils::make_array_view(tetVertConn); + bumpViews::UnstructuredTopologySingleShapeView topoView(tetVertConnView); + + conduit::Node& polyTopo = m_tetMesh["topologies"]["polyhedral"]; + make_polyhedral_topology(tetTopo, polyTopo); + const auto& faceVertConn = polyTopo.fetch_existing("subelements/connectivity"); + const axom::IndexType faceCount = faceVertConn.dtype().number_of_elements() / VERTS_PER_FACE; + const auto faceVertsView = bumpUtils::make_array_view(faceVertConn); + SLIC_ASSERT(faceVertsView.size() == faceCount * VERTS_PER_FACE); + + conduit::Node& polyElemFaceConn = polyTopo["elements/connectivity"]; + const auto polyElemFaceView = bumpUtils::make_array_view(polyElemFaceConn); + SLIC_ASSERT(polyElemFaceView.size() == m_tetCount * FACES_PER_TET); + + const std::string coordsName = polyTopo["coordset"].as_string(); + const conduit::Node& coordsNode = m_tetMesh.fetch_existing("coordsets").fetch_existing(coordsName); + auto xs = bumpUtils::make_array_view(coordsNode["values/x"]); + auto ys = bumpUtils::make_array_view(coordsNode["values/y"]); + auto zs = bumpUtils::make_array_view(coordsNode["values/z"]); + + /* + * Compute tet faces as triangles. + * Compute rays from triangle centroid, in normal direction. + */ + axom::Array faceTris(faceCount, faceCount, allocId); + axom::Array faceRays(faceCount, faceCount, allocId); + auto faceTrisView = faceTris.view(); + auto faceRaysView = faceRays.view(); + axom::for_all( + faceCount, + AXOM_LAMBDA(axom::IndexType faceIdx) { + axom::IndexType* vertIds = faceVertsView.data() + faceIdx * VERTS_PER_FACE; + auto vId0 = vertIds[0]; + auto vId1 = vertIds[1]; + auto vId2 = vertIds[2]; + Point3DType v0 {xs[vId0], ys[vId0], zs[vId0]}; + Point3DType v1 {xs[vId1], ys[vId1], zs[vId1]}; + Point3DType v2 {xs[vId2], ys[vId2], zs[vId2]}; + auto& faceTri = faceTrisView[faceIdx] = Triangle3DType(v0, v1, v2); + faceRaysView[faceIdx] = Ray3DType(faceTri.centroid(), faceTri.normal()); + }); + + const auto tetFaceConn = polyTopo["elements/connectivity"]; + const auto tetFacesView = bumpUtils::make_array_view(tetFaceConn); + SLIC_ASSERT(tetFacesView.size() == 4 * m_tetCount); + + /* + * Compute whether faces have tets on each side. + */ + axom::Array hasCellOnFrontSide(faceCount, 0, allocId); + axom::Array hasCellOnBackSide(faceCount, 0, allocId); + hasCellOnFrontSide.fill(false); + hasCellOnBackSide.fill(false); + auto hasCellOnFrontSideView = hasCellOnFrontSide.view(); + auto hasCellOnBackSideView = hasCellOnBackSide.view(); + axom::for_all( + m_tetCount, + AXOM_LAMBDA(IndexType tetId) { + Point3DType cellCentroid({0.0, 0.0, 0.0}); + axom::IndexType* vertIdxs = tetVertConnView.data() + tetId * VERTS_PER_TET; + TetrahedronType tet; + for(int vi = 0; vi < VERTS_PER_TET; ++vi) + { + axom::IndexType vIdx = vertIdxs[vi]; + Point3DType vertCoords {xs[vIdx], ys[vIdx], zs[vIdx]}; + cellCentroid.array() += vertCoords.array(); + tet[vi] = vertCoords; + } + cellCentroid.array() /= VERTS_PER_TET; + + for(int fi = 0; fi < FACES_PER_TET; ++fi) + { + axom::IndexType faceIdx = polyElemFaceView[tetId * FACES_PER_TET + fi]; + const auto& faceTri = faceTrisView[faceIdx]; + Ray3DType faceRay(faceTri.centroid(), faceTri.normal()); + const Vector3DType& faceDir = faceRay.direction(); + auto cellToFace = cellCentroid.array() - faceRay.origin().array(); + double distParam = + faceDir[0] * cellToFace[0] + faceDir[1] * cellToFace[1] + faceDir[2] * cellToFace[2]; + bool& hasCell = + distParam >= 0 ? hasCellOnFrontSideView[faceIdx] : hasCellOnBackSideView[faceIdx]; + hasCell = true; + } + }); + + /* + * Mark faces touching only 1 cell. + */ + axom::Array hasCellOnOneSide(ArrayOptions::Uninitialized(), faceCount, 0, allocId); + auto hasCellOnOneSideView = hasCellOnOneSide.view(); + axom::for_all( + faceCount, + AXOM_LAMBDA(IndexType faceId) { + hasCellOnOneSideView[faceId] = + (hasCellOnFrontSideView[faceId] + hasCellOnBackSideView[faceId]) == 1; + }); + + /* + * Get running total of surface triangle count using prefix-sum scan. + * Then use the results to populate array of those faces. + */ + axom::Array prefixSum(faceCount + 1, 0, allocId); + prefixSum.fill(0); + auto prefixSumView = prefixSum.view(); + axom::inclusive_scan(hasCellOnOneSide, + axom::ArrayView(prefixSum.data() + 1, faceCount)); + + axom::IndexType surfFaceCount = -1; + axom::copy(&surfFaceCount, prefixSumView.data() + prefixSumView.size() - 1, sizeof(surfFaceCount)); + + axom::Array surfFaceIds(surfFaceCount, 0, allocId); + axom::Array surfTris(surfFaceCount, 0, allocId); + auto surfFaceIdsView = surfFaceIds.view(); + auto surfTrisView = surfTris.view(); + axom::for_all( + faceCount, + AXOM_LAMBDA(axom::IndexType faceIdx) { + if(prefixSumView[faceIdx] != prefixSumView[faceIdx + 1]) + { + auto runningTotal = prefixSumView[faceIdx]; + surfFaceIdsView[runningTotal] = faceIdx; + surfTrisView[runningTotal] = faceTrisView[faceIdx]; + } + }); + + return surfTris; +} + +void TetMeshClipper::writeTrianglesToVTK(const axom::Array& triangles, + const std::string& filename) +{ + std::ofstream ofs(filename); + if(!ofs) + { + std::cerr << "Cannot open file for writing: " << filename << std::endl; + return; + } + + axom::Array hostTriangles(triangles, axom::MALLOC_ALLOCATOR_ID); + + // Header + ofs << "# vtk DataFile Version 3.0\n"; + ofs << "Triangle mesh\n"; + ofs << "ASCII\n"; + ofs << "DATASET POLYDATA\n"; + + // Write points + ofs << "POINTS " << hostTriangles.size() * 3 << " double\n"; + for(const auto& tri : hostTriangles) + { + for(int i = 0; i < 3; ++i) + { + const auto& pt = tri[i]; + ofs << pt[0] << " " << pt[1] << " " << pt[2] << "\n"; + } + } + + // Write polygons (triangles) + ofs << "POLYGONS " << hostTriangles.size() << " " << hostTriangles.size() * 4 << "\n"; + for(int i = 0; i < hostTriangles.size(); ++i) + { + ofs << "3 " << i * 3 << " " << i * 3 + 1 << " " << i * 3 + 2 << "\n"; + } + + ofs.close(); +} + +template +void TetMeshClipper::make_polyhedral_topology(conduit::Node& tetTopo, conduit::Node& polyTopo) +{ + namespace bumpViews = axom::bump::views; + namespace bumpUtils = axom::bump::utilities; + using bumpShape = bumpViews::TetShape; + + // const conduit::Node& tetTopo = tetMesh["topologies"][m_topoName]; + SLIC_ASSERT(tetTopo["type"].as_string() == std::string("unstructured")); + auto tetTopoView = bumpViews::make_unstructured_single_shape_topology::view(tetTopo); + using TopologyView = decltype(tetTopoView); + using ConnectivityType = typename TopologyView::ConnectivityType; + + bump::MakePolyhedralTopology polyTopoMaker(tetTopoView); + polyTopoMaker.execute(tetTopo, polyTopo); + bump::MergePolyhedralFaces::execute(polyTopo); +} + +/* + * Copy a conduit Node, with special allocator ids for new arrays. +*/ +void TetMeshClipper::copy_node_with_array_allocator(conduit::Node& src, + conduit::Node& dst, + conduit::index_t conduitArrayAllocId) +{ + if(src.number_of_children() == 0) // Leaf node + { + auto& srcDtype = src.dtype(); + if(srcDtype.number_of_elements() > 1 && !srcDtype.is_string()) + { + dst.set_allocator(conduitArrayAllocId); + } + else + { + dst.set_allocator(src.allocator()); + } + dst.set(src); + } + else // Branch: recurse into children + { + for(const auto& name : src.child_names()) + { + conduit::Node& childSrc = src[name]; + conduit::Node& childDst = dst[name]; + copy_node_with_array_allocator(childSrc, childDst, conduitArrayAllocId); + } + } + return; +} + +void TetMeshClipper::copy_hierarchy_with_array_allocator(conduit::Node& hierarchy, + const std::string& srcPath, + const std::string& dstPath, + int allocId) +{ + if(hierarchy.has_path(dstPath)) + { + return; + } // Already done. + + conduit::Node& src = hierarchy.fetch_existing(srcPath); + conduit::Node& dst = hierarchy[dstPath]; + + const conduit::index_t conduitArrayAllocId = sidre::ConduitMemory::axomAllocIdToConduit(allocId); + copy_node_with_array_allocator(src, dst, conduitArrayAllocId); + return; +} + +/* + * Copy m_tetMesh array data into allocId if it's not there yet. + * (Copy only the arrays this object uses: coordset and connectivity.) + * We give new array data keys ".". + * If the new array exists, no need to redo it. +*/ +void TetMeshClipper::copy_tetmesh_arrays_to(int allocId) +{ + const std::string oldTopoPath = "topologies/" + m_topoName; + const std::string oldCoordsetPath = "coordsets/" + m_coordsetName; + const std::string newTopoPath = axom::fmt::format("{}.{}", oldTopoPath, allocId); + const std::string newCoordsetPath = axom::fmt::format("{}.{}", oldCoordsetPath, allocId); + + copy_hierarchy_with_array_allocator(m_tetMesh, oldTopoPath, newTopoPath, allocId); + copy_hierarchy_with_array_allocator(m_tetMesh, oldCoordsetPath, newCoordsetPath, allocId); + + m_tetMesh[newTopoPath + "/coordset"].set(axom::fmt::format("{}.{}", m_coordsetName, allocId)); +} + +// Run cellKernel through a cell loop. +// Run faceKernel through a face loop. + +} // namespace experimental +} // end namespace quest +} // end namespace axom diff --git a/src/axom/quest/detail/clipping/TetMeshClipper.hpp b/src/axom/quest/detail/clipping/TetMeshClipper.hpp new file mode 100644 index 0000000000..5ffa64b07a --- /dev/null +++ b/src/axom/quest/detail/clipping/TetMeshClipper.hpp @@ -0,0 +1,161 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#ifndef AXOM_QUEST_TETMESHCLIPPER_HPP +#define AXOM_QUEST_TETMESHCLIPPER_HPP + +#include "axom/klee/Geometry.hpp" +#include "axom/quest/MeshClipperStrategy.hpp" +#include "axom/primal/geometry/CoordinateTransformer.hpp" + +// Implementation requires Conduit. +#include "conduit_blueprint.hpp" + +namespace axom +{ +namespace quest +{ +namespace experimental +{ + +/*! + * @brief Geometry clipping operations for tetrahedral mesh geometries. + */ +class TetMeshClipper : public MeshClipperStrategy +{ +public: + /*! + * @brief Constructor. + * + * @param [in] kGeom Describes the shape to place + * into the mesh. + * @param [in] name To override the default strategy name + * + * \c kGeom.asHierarchy() must contain the following data: + * - "klee::Geometry:tetMesh": A blueprint tetrahedral mesh. + * The tetrahedra may be degenerate, but not inverted (negative volume). + * - "topologyName": The mesh's blueprint topology name + * - "fixOrientation": Whether to fix inverted tetrahedra + * instead of aborting. + */ + TetMeshClipper(const klee::Geometry& kGeom, const std::string& name = ""); + + virtual ~TetMeshClipper() = default; + + const std::string& name() const override { return m_name; } + + bool labelCellsInOut(quest::experimental::ShapeMesh& shappeMesh, + axom::Array& label) override; + + bool labelTetsInOut(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::Array& tetLabels) override; + + bool getGeometryAsTets(quest::experimental::ShapeMesh& shappeMesh, + axom::Array& tets) override; + +#if !defined(__CUDACC__) +private: +#endif + std::string m_name; + + //! @brief Topology to use in the Blueprint tet mesh. + std::string m_topoName; + + //! @brief Coordset to use in the Blueprint tet mesh. + std::string m_coordsetName; + + //! @brief Tet mesh in Blueprint format. + conduit::Node m_tetMesh; + + //! @brief Bounding box of the tet mesh. + axom::primal::BoundingBox m_tetMeshBb; + + //! @brief Number of tets in the tet mesh. + axom::IndexType m_tetCount; + + //! @brief Geometry as tetrahedra. + axom::Array m_tets; + + /*! + * @brief Combined external transformation. + * + * (TetMesh has no internal transformation.) + */ + axom::primal::experimental::CoordinateTransformer m_transformer; + + template + void labelCellsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView label); + + template + void labelTetsInOutImpl(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView cellIds, + axom::ArrayView tetLabels); + + template + void vertexInsideToCellLabel(quest::experimental::ShapeMesh& shapeMesh, + axom::ArrayView& vertIsInside, + axom::Array& labels); + + // Extract clipper info from MeshClipperStrategy::m_info. + void extractClipperInfo(); + + // Check validity of tetMesh for our purposes. + bool isValidTetMesh(const conduit::Node& tetMesh, std::string& whyBad) const; + + /*! + * @brief Add a transformed coordset to m_tetMesh. + * + * The transformed version is the original coordset, transformed + * through m_transformer. It has the name m_coordsetName + + * ".trans". + */ + void transformCoordset(); + + void computeTets(); + + //@{ + //!@name For computing surface of m_tetMesh. + /*! + * @brief Entry point for computing geometry surface. + * + * This computation is independent of the shapee mesh, except that we + * need the policy and allocator id. + */ + axom::Array computeGeometrySurface(axom::runtime_policy::Policy policy, + int allocId); + template + + axom::Array computeGeometrySurface(int allocId); + + /*! + * @brief Add a polyhedral topology to an unstructured tet mesh. + * @param tetMesh Input unstructured tet mesh, single domain. + * @param polyTopo Output unstructured polyhedral topology. + */ + + template + void make_polyhedral_topology(conduit::Node& tetTopo, conduit::Node& polyTopo); + + //!@brief Write out for debugging + void writeTrianglesToVTK(const axom::Array& triangles, const std::string& filename); + //@} + + void copy_node_with_array_allocator(conduit::Node& src, + conduit::Node& dst, + conduit::index_t conduitAllocId); + void copy_hierarchy_with_array_allocator(conduit::Node& hierarchy, + const std::string& srcPath, + const std::string& dstPath, + int allocId); + void copy_tetmesh_arrays_to(int allocId); +}; + +} // namespace experimental +} // namespace quest +} // namespace axom + +#endif // AXOM_QUEST_TETMESHCLIPPER_HPP diff --git a/src/axom/quest/docs/sphinx/point_mesh_query_cpp.rst b/src/axom/quest/docs/sphinx/point_mesh_query_cpp.rst index 8663f160ab..e1479e01bf 100644 --- a/src/axom/quest/docs/sphinx/point_mesh_query_cpp.rst +++ b/src/axom/quest/docs/sphinx/point_mesh_query_cpp.rst @@ -56,7 +56,7 @@ Signed Distance --------------- The C++ signed distance query is provided by the ``quest::SignedDistance`` class, -which wraps an instance of ``primal::BVHTree``. +which wraps an instance of ``spin::BVH``. Examples from ``/src/axom/quest/tests/quest_signed_distance.cpp``. Class header: diff --git a/src/axom/quest/examples/CMakeLists.txt b/src/axom/quest/examples/CMakeLists.txt index cb4880ae32..6155b29a86 100644 --- a/src/axom/quest/examples/CMakeLists.txt +++ b/src/axom/quest/examples/CMakeLists.txt @@ -337,6 +337,7 @@ endif() if((CONDUIT_FOUND OR (AXOM_ENABLE_MPI AND MFEM_FOUND AND MFEM_USE_MPI AND AXOM_ENABLE_SIDRE AND AXOM_ENABLE_MFEM_SIDRE_DATACOLLECTION)) + AND RAJA_FOUND AND AXOM_ENABLE_KLEE) if(MFEM_FOUND) set(optional_dependency, "mfem") @@ -368,7 +369,7 @@ if((CONDUIT_FOUND OR blt_list_append(TO _policies ELEMENTS "hip" IF AXOM_ENABLE_HIP) endif() - set(_testshapes "tetmesh" "tet" "hex" "sphere" "cyl" "cone" "sor" "all" "plane") + set(_testshapes "tetmesh" "tet" "hex" "sphere" "cyl" "cone" "sor" "plane" "tetmesh,tet,hex") foreach(_policy ${_policies}) foreach(_testshape ${_testshapes}) set(_testname "quest_shape_in_memory_${_policy}_${_testshape}") @@ -377,11 +378,11 @@ if((CONDUIT_FOUND OR COMMAND quest_shape_in_memory_ex --policy ${_policy} --testShape ${_testshape} - --refinements 2 - --scale 0.75 0.75 0.75 - --dir 0.2 0.4 0.8 + --refinements 5 + --scale .99 .99 .99 + --dir 8 4 2 --meshType bpSidre - inline_mesh --min -2 -2 -2 --max 2 2 2 --res 30 30 30 + inline_mesh --min -2 -2 -2 --max 2 2 2 --res 16 16 16 NUM_MPI_TASKS ${_nranks}) endforeach() endforeach() @@ -399,9 +400,9 @@ if((CONDUIT_FOUND OR COMMAND quest_shape_in_memory_ex --policy ${_policy} --testShape ${_testshape} - --refinements 2 - --scale 0.75 0.75 0.75 - --dir 0.2 0.4 0.8 + --refinements 5 + --scale .99 .99 .99 + --dir 8 4 2 --meshType ${_testMeshType} inline_mesh --min -2 -2 -2 --max 2 2 2 --res 8 8 8 NUM_MPI_TASKS ${_nranks}) diff --git a/src/axom/quest/examples/quest_shape_in_memory.cpp b/src/axom/quest/examples/quest_shape_in_memory.cpp index f3feb070de..bd67b0ac55 100644 --- a/src/axom/quest/examples/quest_shape_in_memory.cpp +++ b/src/axom/quest/examples/quest_shape_in_memory.cpp @@ -27,6 +27,10 @@ #error Shaping functionality requires Axom to be configured with Conduit #endif +#if !defined(AXOM_USE_RAJA) + #error Shaping test requires Axom to be configured with RAJA +#endif + #include "conduit_relay_io_blueprint.hpp" #if defined(AXOM_USE_MFEM) @@ -92,10 +96,10 @@ struct Input } // The shape to run. - std::string testShape {"tetmesh"}; + std::vector testShape; // The shapes this example is set up to run. const std::set - availableShapes {"tetmesh", "tri", "sphere", "cyl", "cone", "sor", "tet", "hex", "plane", "all"}; + availableShapes {"tetmesh", "tri", "sphere", "cyl", "cone", "sor", "tet", "hex", "plane"}; RuntimePolicy policy {RuntimePolicy::seq}; int outputOrder {2}; @@ -230,8 +234,12 @@ struct Input ->capture_default_str(); app.add_option("-s,--testShape", testShape) - ->description("The shape to run") - ->check(axom::CLI::IsMember(availableShapes)); + ->description( + "The shape(s) to run. Specifying multiple shapes will override scaling and translations " + "to shrink shapes and shift them to individual octants of the mesh.") + ->check(axom::CLI::IsMember(availableShapes)) + ->delimiter(',') + ->expected(1, 60); #ifdef AXOM_USE_CALIPER app.add_option("--caliper", annotationMode) @@ -333,6 +341,40 @@ struct Input }; // struct Input Input params; +/************************************************************ + * Shared variables. + ************************************************************/ + +const std::string topoName = "mesh"; +const std::string coordsetName = "coords"; +int cellCount = -1; +// Translation to individual octants (override) when running multiple shapes. +// Except that the plane is never moved. +std::vector> translations {{1, 1, -1}, + {-1, 1, -1}, + {-1, -1, -1}, + {1, -1, -1}, + {1, 1, 1}, + {-1, 1, 1}, + {-1, -1, 1}, + {1, -1, 1}}; +int translationIdx = 0; // To track what translations have been used. +std::map shapeReps; // Repetitions of the geometry. +std::map exactOverlapVols; +std::map errorToleranceRel; // Relative error tolerance. +std::map errorToleranceAbs; // Absolute error tolerance. +double vScale = 1.0; // Volume scale due to geometry scale. + +const auto hostAllocId = axom::execution_space::allocatorID(); +int arrayAllocId = axom::INVALID_ALLOCATOR_ID; + +// Computational mesh in different forms, initialized in main +#if defined(AXOM_USE_MFEM) +std::shared_ptr shapingDC; +#endif +axom::sidre::Group* compMeshGrp = nullptr; +std::shared_ptr compMeshNode; + // Start property for all 3D shapes. axom::klee::TransformableGeometryProperties startProp {axom::klee::Dimensions::Three, axom::klee::LengthUnit::unspecified}; @@ -353,14 +395,23 @@ void addScaleOperator(axom::klee::CompositeOperator& compositeOp) } // Add translate operator. -void addTranslateOperator(axom::klee::CompositeOperator& compositeOp, - double shiftx, - double shifty, - double shiftz) +void addTranslateOperator(axom::klee::CompositeOperator& compositeOp) { - primal::Vector3D shift({shiftx, shifty, shiftz}); - auto translateOp = std::make_shared(shift, startProp); - compositeOp.addOperator(translateOp); + if(params.testShape.size() > 1) + { + const axom::NumericArray& shifts = + translations[(translationIdx++) % translations.size()]; + primal::Vector3D shift({shifts[0], shifts[1], shifts[2]}); + auto translateOp = std::make_shared(shift, startProp); + compositeOp.addOperator(translateOp); + } + else + { + // Use zero shift as a smoke test. + primal::Vector3D shift({0, 0, 0}); + auto translateOp = std::make_shared(shift, startProp); + compositeOp.addOperator(translateOp); + } } // Add operator to rotate x-axis to params.direction, if it is given. @@ -448,20 +499,6 @@ void printMfemMeshInfo(mfem::Mesh* mesh, const std::string& prefixMessage = "") * Shared variables. ************************************************************/ -const std::string topoName = "mesh"; -const std::string coordsetName = "coords"; -int cellCount = -1; - -const auto hostAllocId = axom::execution_space::allocatorID(); -int arrayAllocId = axom::INVALID_ALLOCATOR_ID; - -// Computational mesh in different forms, initialized in main -#if defined(AXOM_USE_MFEM) -std::shared_ptr shapingDC; -#endif -axom::sidre::Group* compMeshGrp = nullptr; -std::shared_ptr compMeshNode; - axom::sidre::Group* createBoxMesh(axom::sidre::Group* meshGrp) { switch(params.getBoxDim()) @@ -555,55 +592,6 @@ void finalizeLogger() } } -// Single triangle ShapeSet. -axom::klee::ShapeSet create2DShapeSet(sidre::DataStore& ds) -{ - sidre::Group* meshGroup = ds.getRoot()->createGroup("triangleMesh"); - const std::string topo = "mesh"; - const std::string coordset = "coords"; - axom::mint::UnstructuredMesh triangleMesh(2, - axom::mint::CellType::TRIANGLE, - meshGroup, - topo, - coordset); - - const double lll = 2.0; - - // Insert tet at origin. - triangleMesh.appendNode(0.0, 0.0); - triangleMesh.appendNode(lll, 0.0); - triangleMesh.appendNode(0.0, lll); - axom::IndexType conn[3] = {0, 1, 2}; - triangleMesh.appendCell(conn); - - SLIC_ERROR_ROOT_IF(!axom::mint::blueprint::isValidRootGroup(meshGroup), - "Triangle mesh blueprint is not valid"); - - axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Two, - axom::klee::LengthUnit::unspecified}; - axom::klee::Geometry triangleGeom(prop, triangleMesh.getSidreGroup(), topo, nullptr); - - std::vector shapes; - axom::klee::Shape triangleShape( - "triangle", - "AL", - {}, - {}, - axom::klee::Geometry {prop, triangleMesh.getSidreGroup(), topo, nullptr}); - shapes.push_back( - axom::klee::Shape {"triangle", - "AL", - {}, - {}, - axom::klee::Geometry {prop, triangleMesh.getSidreGroup(), topo, nullptr}}); - - axom::klee::ShapeSet shapeSet; - shapeSet.setShapes(shapes); - shapeSet.setDimensions(axom::klee::Dimensions::Two); - - return shapeSet; -} - axom::klee::Shape createShape_Sphere() { Point3D center = params.center.empty() ? Point3D {0, 0, 0} : Point3D {params.center.data()}; @@ -616,7 +604,7 @@ axom::klee::Shape createShape_Sphere() auto compositeOp = std::make_shared(startProp); addScaleOperator(*compositeOp); addRotateOperator(*compositeOp); - addTranslateOperator(*compositeOp, 1, 1, 1); + addTranslateOperator(*compositeOp); const axom::IndexType levelOfRefinement = params.refinementLevel; axom::klee::Geometry sphereGeometry(prop, sphere, levelOfRefinement, compositeOp); @@ -662,7 +650,7 @@ axom::klee::Shape createShape_TetMesh(sidre::DataStore& ds) auto compositeOp = std::make_shared(startProp); addScaleOperator(*compositeOp); addRotateOperator(*compositeOp); - addTranslateOperator(*compositeOp, -1, 1, 1); + addTranslateOperator(*compositeOp); axom::klee::Geometry tetMeshGeometry(prop, tetMesh.getSidreGroup(), topo, compositeOp); axom::klee::Shape tetShape("tetmesh", "TETMESH", {}, {}, tetMeshGeometry); @@ -672,7 +660,7 @@ axom::klee::Shape createShape_TetMesh(sidre::DataStore& ds) axom::klee::Geometry createGeometry_Sor(axom::primal::Point& sorBase, axom::primal::Vector& sorDirection, - axom::Array& discreteFunction, + axom::ArrayView discreteFunction, std::shared_ptr& compositeOp) { axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, @@ -690,7 +678,22 @@ axom::klee::Geometry createGeometry_Sor(axom::primal::Point& sorBase, return sorGeometry; } -axom::klee::Shape createShape_Sor() +double computeVolume_Sor(axom::Array& discreteFunction) +{ + using ConeType = axom::primal::Cone; + axom::IndexType segmentCount = discreteFunction.shape()[0]; + double vol = 0.0; + for(axom::IndexType s = 0; s < segmentCount - 1; ++s) + { + ConeType cone(discreteFunction(s, 1), + discreteFunction(s + 1, 1), + discreteFunction(s + 1, 0) - discreteFunction(s, 0)); + vol += cone.volume(); + } + return vol; +} + +axom::klee::Shape createShape_Sor(const std::string& shapeName) { Point3D sorBase = params.center.empty() ? Point3D {0.0, 0.0, 0.0} : Point3D {params.center.data()}; axom::primal::Vector sorDirection = params.direction.empty() @@ -700,36 +703,40 @@ axom::klee::Shape createShape_Sor() // discreteFunction are discrete z-r pairs describing the function // to be rotated around the z axis. axom::Array discreteFunction({numIntervals + 1, 2}, axom::ArrayStrideOrder::ROW); - double zLen = params.length < 0 ? 1.6 : params.length; + double zLen = params.length < 0 ? 2.40 : params.length; double zShift = -zLen / 2; - double maxR = params.radius < 0 ? 0.75 : params.radius; + double maxR = params.radius < 0 ? 1.10 : params.radius; double dz = zLen / numIntervals; - discreteFunction[0][0] = 0 * dz + zShift; - discreteFunction[0][1] = 0.0 * maxR; - discreteFunction[1][0] = 1 * dz + zShift; - discreteFunction[1][1] = 0.8 * maxR; - discreteFunction[2][0] = 2 * dz + zShift; - discreteFunction[2][1] = 0.4 * maxR; - discreteFunction[3][0] = 3 * dz + zShift; - discreteFunction[3][1] = 0.5 * maxR; - discreteFunction[4][0] = 4 * dz + zShift; - discreteFunction[4][1] = 1.0 * maxR; - discreteFunction[5][0] = 5 * dz + zShift; - discreteFunction[5][1] = 0.0; + discreteFunction(0, 0) = 0 * dz + zShift; + discreteFunction(0, 1) = 0.0 * maxR; + discreteFunction(1, 0) = 1 * dz + zShift; + discreteFunction(1, 1) = 0.8 * maxR; + discreteFunction(2, 0) = 2 * dz + zShift; + discreteFunction(2, 1) = 0.4 * maxR; + discreteFunction(3, 0) = 3 * dz + zShift; + discreteFunction(3, 1) = 0.5 * maxR; + discreteFunction(4, 0) = 4 * dz + zShift; + discreteFunction(4, 1) = 1.0 * maxR; + discreteFunction(5, 0) = 5 * dz + zShift; + discreteFunction(5, 1) = 1.0 * maxR; auto compositeOp = std::make_shared(startProp); addScaleOperator(*compositeOp); - addTranslateOperator(*compositeOp, -1, -1, 1); + addTranslateOperator(*compositeOp); axom::klee::Geometry sorGeometry = createGeometry_Sor(sorBase, sorDirection, discreteFunction, compositeOp); - axom::klee::Shape sorShape("sor", "SOR", {}, {}, sorGeometry); + axom::klee::Shape sorShape(shapeName, shapeName + ".mat", {}, {}, sorGeometry); + + exactOverlapVols[shapeName] = vScale * computeVolume_Sor(discreteFunction); + errorToleranceRel[shapeName] = 0.04; + errorToleranceAbs[shapeName] = 0.15; return sorShape; } -axom::klee::Shape createShape_Cylinder() +axom::klee::Shape createShape_Cylinder(const std::string& shapeName) { Point3D sorBase = params.center.empty() ? Point3D {0.0, 0.0, 0.0} : Point3D {params.center.data()}; axom::primal::Vector sorDirection = params.direction.empty() @@ -738,8 +745,8 @@ axom::klee::Shape createShape_Cylinder() // discreteFunction are discrete z-r pairs describing the function // to be rotated around the z axis. axom::Array discreteFunction({2, 2}, axom::ArrayStrideOrder::ROW); - double radius = params.radius < 0 ? 0.5 : params.radius; - double height = params.length < 0 ? 1.2 : params.length; + double radius = params.radius < 0 ? 0.695 : params.radius; + double height = params.length < 0 ? 2.78 : params.length; discreteFunction[0][0] = -height / 2; discreteFunction[0][1] = radius; discreteFunction[1][0] = height / 2; @@ -747,17 +754,22 @@ axom::klee::Shape createShape_Cylinder() auto compositeOp = std::make_shared(startProp); addScaleOperator(*compositeOp); - addTranslateOperator(*compositeOp, 1, -1, 1); + addTranslateOperator(*compositeOp); axom::klee::Geometry sorGeometry = createGeometry_Sor(sorBase, sorDirection, discreteFunction, compositeOp); - axom::klee::Shape sorShape("cyl", "CYL", {}, {}, sorGeometry); + axom::klee::Shape sorShape(shapeName, shapeName + ".mat", {}, {}, sorGeometry); + + exactOverlapVols[shapeName] = vScale * computeVolume_Sor(discreteFunction); + // error tolerance for 2 levels of refinement + errorToleranceRel[shapeName] = 0.05; + errorToleranceAbs[shapeName] = 0.2; return sorShape; } -axom::klee::Shape createShape_Cone() +axom::klee::Shape createShape_Cone(const std::string& shapeName) { Point3D sorBase = params.center.empty() ? Point3D {0.0, 0.0, 0.0} : Point3D {params.center.data()}; axom::primal::Vector sorDirection = params.direction.empty() @@ -766,9 +778,9 @@ axom::klee::Shape createShape_Cone() // discreteFunction are discrete z-r pairs describing the function // to be rotated around the z axis. axom::Array discreteFunction({2, 2}, axom::ArrayStrideOrder::ROW); - double baseRadius = params.radius < 0 ? 0.7 : params.radius; - double topRadius = params.radius2 < 0 ? 0.1 : params.radius2; - double height = params.length < 0 ? 1.3 : params.length; + double baseRadius = params.radius < 0 ? 1.23 : params.radius; + double topRadius = params.radius2 < 0 ? 0.176 : params.radius2; + double height = params.length < 0 ? 2.3 : params.length; discreteFunction[0][0] = -height / 2; discreteFunction[0][1] = baseRadius; discreteFunction[1][0] = height / 2; @@ -776,17 +788,21 @@ axom::klee::Shape createShape_Cone() auto compositeOp = std::make_shared(startProp); addScaleOperator(*compositeOp); - addTranslateOperator(*compositeOp, 1, 1, -1); + addTranslateOperator(*compositeOp); axom::klee::Geometry sorGeometry = createGeometry_Sor(sorBase, sorDirection, discreteFunction, compositeOp); - axom::klee::Shape sorShape("cone", "CONE", {}, {}, sorGeometry); + axom::klee::Shape sorShape(shapeName, shapeName + ".mat", {}, {}, sorGeometry); + + exactOverlapVols[shapeName] = vScale * computeVolume_Sor(discreteFunction); + errorToleranceRel[shapeName] = 0.05; + errorToleranceAbs[shapeName] = 0.2; return sorShape; } -axom::klee::Shape createShape_Tet() +axom::klee::Shape createShape_Tet(const std::string& shapeName) { axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, axom::klee::LengthUnit::unspecified}; @@ -794,32 +810,35 @@ axom::klee::Shape createShape_Tet() SLIC_ASSERT(params.scaleFactors.empty() || params.scaleFactors.size() == 3); // Tetrahedron at origin. - const double len = params.length < 0 ? 0.8 : params.length; - const Point3D a {-len, -len, -len}; - const Point3D b {+len, -len, -len}; - const Point3D c {+len, +len, -len}; - const Point3D d {-len, +len, +len}; + const double len = params.length < 0 ? 1.55 : params.length; + const Point3D a {Point3D::NumericArray {1., 0., -1.} * len}; + const Point3D b {Point3D::NumericArray {-.8, 1, -1.} * len}; + const Point3D c {Point3D::NumericArray {-.8, -1, -1.} * len}; + const Point3D d {Point3D::NumericArray {0., 0., +1.} * len}; const primal::Tetrahedron tet {a, b, c, d}; auto compositeOp = std::make_shared(startProp); addScaleOperator(*compositeOp); addRotateOperator(*compositeOp); - addTranslateOperator(*compositeOp, -1, 1, -1); + addTranslateOperator(*compositeOp); + exactOverlapVols[shapeName] = vScale * tet.volume(); + errorToleranceRel[shapeName] = 1e-6; + errorToleranceAbs[shapeName] = 1e-8; axom::klee::Geometry tetGeometry(prop, tet, compositeOp); - axom::klee::Shape tetShape("tet", "TET", {}, {}, tetGeometry); + axom::klee::Shape tetShape(shapeName, shapeName + ".mat", {}, {}, tetGeometry); return tetShape; } -axom::klee::Shape createShape_Hex() +axom::klee::Shape createShape_Hex(const std::string& shapeName) { axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, axom::klee::LengthUnit::unspecified}; SLIC_ASSERT(params.scaleFactors.empty() || params.scaleFactors.size() == 3); - const double md = params.length < 0 ? 0.6 : params.length / 2; + const double md = params.length < 0 ? 0.82 : params.length / 2; const double lg = 1.2 * md; const double sm = 0.8 * md; const Point3D p {-lg, -md, -sm}; @@ -835,15 +854,18 @@ axom::klee::Shape createShape_Hex() auto compositeOp = std::make_shared(startProp); addScaleOperator(*compositeOp); addRotateOperator(*compositeOp); - addTranslateOperator(*compositeOp, -1, -1, -1); + addTranslateOperator(*compositeOp); + exactOverlapVols[shapeName] = vScale * hex.volume(); + errorToleranceRel[shapeName] = 1e-6; + errorToleranceAbs[shapeName] = 1e-8; axom::klee::Geometry hexGeometry(prop, hex, compositeOp); - axom::klee::Shape hexShape("hex", "HEX", {}, {}, hexGeometry); + axom::klee::Shape hexShape(shapeName, shapeName + ".mat", {}, {}, hexGeometry); return hexShape; } -axom::klee::Shape createShape_Plane() +axom::klee::Shape createShape_Plane(const std::string& shapeName) { axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, axom::klee::LengthUnit::unspecified}; @@ -868,21 +890,21 @@ axom::klee::Shape createShape_Plane() const primal::Plane plane {normal, center, true}; axom::klee::Geometry planeGeometry(prop, plane, scaleOp); - axom::klee::Shape planeShape("plane", "PLANE", {}, {}, planeGeometry); + axom::klee::Shape planeShape(shapeName, shapeName + ".mat", {}, {}, planeGeometry); + + // Exact mesh overlap volume, assuming plane passes through center of box mesh. + using Pt3D = primal::Point; + Pt3D lower(params.boxMins.data()); + Pt3D upper(params.boxMaxs.data()); + auto diag = upper.array() - lower.array(); + double meshVolume = diag[0] * diag[1] * diag[2]; + exactOverlapVols[shapeName] = 0.5 * meshVolume; + errorToleranceRel[shapeName] = 1e-6; + errorToleranceAbs[shapeName] = 1e-8; return planeShape; } -//!@brief Create a ShapeSet with a single shape. -axom::klee::ShapeSet createShapeSet(const axom::klee::Shape& shape) -{ - axom::klee::ShapeSet shapeSet; - shapeSet.setShapes(std::vector {shape}); - shapeSet.setDimensions(axom::klee::Dimensions::Three); - - return shapeSet; -} - double volumeOfTetMesh(const axom::mint::UnstructuredMesh& tetMesh) { using TetType = axom::primal::Tetrahedron; @@ -933,6 +955,138 @@ double areaOfTriMesh(const axom::mint::UnstructuredMesh create2DShapeSet(sidre::DataStore& ds) +{ + sidre::Group* meshGroup = ds.getRoot()->createGroup("triangleMesh"); + AXOM_UNUSED_VAR(meshGroup); // variable is only referenced in debug configs + const std::string topo = "mesh"; + const std::string coordset = "coords"; + axom::mint::UnstructuredMesh triangleMesh(2, + axom::mint::CellType::TRIANGLE, + meshGroup, + topo, + coordset); + + double lll = 2.0; + + // Insert tet at origin. + triangleMesh.appendNode(0.0, 0.0); + triangleMesh.appendNode(lll, 0.0); + triangleMesh.appendNode(0.0, lll); + axom::IndexType conn[3] = {0, 1, 2}; + triangleMesh.appendCell(conn); + + SLIC_ASSERT(axom::mint::blueprint::isValidRootGroup(meshGroup)); + + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Two, + axom::klee::LengthUnit::unspecified}; + axom::klee::Geometry triangleGeom(prop, triangleMesh.getSidreGroup(), topo, nullptr); + + std::vector shapes; + axom::klee::Shape triangleShape( + "triangle", + "AL", + {}, + {}, + axom::klee::Geometry {prop, triangleMesh.getSidreGroup(), topo, nullptr}); + shapes.push_back( + axom::klee::Shape {"triangle", + "AL", + {}, + {}, + axom::klee::Geometry {prop, triangleMesh.getSidreGroup(), topo, nullptr}}); + + axom::klee::ShapeSet shapeSet; + shapeSet.setShapes(shapes); + shapeSet.setDimensions(axom::klee::Dimensions::Two); + + double shapeMeshVol = areaOfTriMesh(triangleMesh); + exactOverlapVols[triangleShape.getName()] = shapeMeshVol; + errorToleranceRel[triangleShape.getName()] = 1e-6; + errorToleranceAbs[triangleShape.getName()] = 1e-8; + + return shapes; +} + +axom::klee::Shape createShape_Sphere(const std::string& shapeName) +{ + Point3D center = params.center.empty() ? Point3D {0, 0, 0} : Point3D {params.center.data()}; + double radius = params.radius < 0 ? 1.0 : params.radius; + axom::primal::Sphere sphere {center, radius}; + + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, + axom::klee::LengthUnit::unspecified}; + + auto compositeOp = std::make_shared(startProp); + addScaleOperator(*compositeOp); + addRotateOperator(*compositeOp); + addTranslateOperator(*compositeOp); + + const axom::IndexType levelOfRefinement = params.refinementLevel; + axom::klee::Geometry sphereGeometry(prop, sphere, levelOfRefinement, compositeOp); + axom::klee::Shape sphereShape(shapeName, shapeName + ".mat", {}, {}, sphereGeometry); + exactOverlapVols[shapeName] = vScale * 4. / 3 * M_PI * radius * radius * radius; + errorToleranceRel[shapeName] = 0.1; + errorToleranceAbs[shapeName] = 0.38; + + return sphereShape; +} + +axom::klee::Shape createShape_TetMesh(sidre::DataStore& ds, const std::string& shapeName) +{ + // Shape a tetrahedal mesh. + sidre::Group* meshGroup = ds.getRoot()->createGroup(shapeName); + AXOM_UNUSED_VAR(meshGroup); // variable is only referenced in debug configs + const std::string topo = "mesh"; + const std::string coordset = "coords"; + axom::mint::UnstructuredMesh tetMesh(3, + axom::mint::CellType::TET, + meshGroup, + topo, + coordset); + + double lll = params.length < 0 ? 1.17 : params.length; + + // Insert tets around origin. + tetMesh.appendNode(-lll, -lll, -lll); + tetMesh.appendNode(+lll, -lll, -lll); + tetMesh.appendNode(-lll, +lll, -lll); + tetMesh.appendNode(-lll, -lll, +lll); + tetMesh.appendNode(+lll, +lll, +lll); + tetMesh.appendNode(-lll, +lll, +lll); + tetMesh.appendNode(+lll, +lll, -lll); + tetMesh.appendNode(+lll, -lll, +lll); + axom::IndexType conn0[4] = {0, 1, 2, 3}; + tetMesh.appendCell(conn0); + axom::IndexType conn1[4] = {4, 5, 6, 7}; + tetMesh.appendCell(conn1); + + SLIC_ASSERT(axom::mint::blueprint::isValidRootGroup(meshGroup)); + + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, + axom::klee::LengthUnit::unspecified}; + + auto compositeOp = std::make_shared(startProp); + addScaleOperator(*compositeOp); + addRotateOperator(*compositeOp); + addTranslateOperator(*compositeOp); + + axom::klee::Geometry tetMeshGeometry(prop, tetMesh.getSidreGroup(), topo, compositeOp); + axom::klee::Shape tetShape(shapeName, shapeName + ".mat", {}, {}, tetMeshGeometry); + + exactOverlapVols[shapeName] = vScale * volumeOfTetMesh(tetMesh); + errorToleranceRel[shapeName] = 1e-6; + errorToleranceAbs[shapeName] = 1e-8; + + return tetShape; +} + #if defined(AXOM_USE_MFEM) /*! * @brief Return the element volumes as a sidre::View. @@ -1086,7 +1240,7 @@ axom::sidre::View* getElementVolumes( * Most of this is lifted from IntersectionShaper::runShapeQueryImpl. */ template -axom::sidre::View* getElementVolumes( +axom::sidre::View* getElementVolumesImpl( sidre::Group* meshGrp, const std::string& volFieldName = std::string("elementVolumes")) { @@ -1296,6 +1450,34 @@ axom::sidre::View* getElementVolumes( return volSidreView; } +axom::sidre::View* getElementVolumes( + sidre::Group* meshGrp, + const std::string& volFieldName = std::string("elementVolumes")) +{ + switch(params.policy) + { +#if defined(AXOM_USE_CUDA) + case RuntimePolicy::cuda: + return getElementVolumesImpl>(meshGrp, volFieldName); + break; +#endif +#if defined(AXOM_USE_HIP) + case RuntimePolicy::hip: + return getElementVolumesImpl>(meshGrp, volFieldName); + break; +#endif +#if defined(AXOM_USE_OMP) + case RuntimePolicy::omp: + return getElementVolumesImpl(meshGrp, volFieldName); + break; +#endif + case RuntimePolicy::seq: + default: + return getElementVolumesImpl(meshGrp, volFieldName); + break; + } + return nullptr; +} #if defined(AXOM_USE_MFEM) /*! @@ -1349,7 +1531,7 @@ double sumMaterialVolumesImpl(sidre::Group* meshGrp, const std::string& material // Get cell volumes from meshGrp. const std::string volsName = "vol_" + material; - axom::sidre::View* elementVols = getElementVolumes(meshGrp, volsName); + axom::sidre::View* elementVols = getElementVolumesImpl(meshGrp, volsName); axom::ArrayView elementVolsView(elementVols->getData(), elementVols->getNumElements()); // Get material volume fractions @@ -1540,11 +1722,23 @@ int main(int argc, char** argv) exit(retval); } + if(params.testShape.size() > 1) + { + SLIC_WARNING( + "Multiple test configurations specified.\n" + "Scaling by half to shrink the geometries\n" + "and move them to individual octants so they don't overlap\n" + "with each other."); + params.scaleFactors.resize(3, 1.0); + for(auto& f : params.scaleFactors) f *= 0.5; + } + vScale = params.scaleFactors[0] * params.scaleFactors[1] * params.scaleFactors[2]; + axom::utilities::raii::AnnotationsWrapper annotations_raii_wrapper(params.annotationMode); const int arrayAllocId = axom::policyToDefaultAllocatorID(params.policy); - AXOM_ANNOTATE_SCOPE("quest shaping example"); + AXOM_ANNOTATE_BEGIN("quest shaping example"); AXOM_ANNOTATE_BEGIN("init"); // Storage for the shape geometry meshes. @@ -1553,58 +1747,58 @@ int main(int argc, char** argv) //--------------------------------------------------------------------------- // Create simple ShapeSet for the example. //--------------------------------------------------------------------------- - axom::klee::ShapeSet shapeSet; - - if(params.testShape == "tetmesh") - { - shapeSet = createShapeSet(createShape_TetMesh(ds)); - } - else if(params.testShape == "tet") - { - shapeSet = createShapeSet(createShape_Tet()); - } - else if(params.testShape == "tri") - { - SLIC_ERROR_IF(params.getBoxDim() != 2, "This example is only in 2D."); - shapeSet = create2DShapeSet(ds); - } - else if(params.testShape == "hex") + std::vector shapesVec; + for(const auto& tg : params.testShape) { - shapeSet = createShapeSet(createShape_Hex()); - } - else if(params.testShape == "sphere") - { - shapeSet = createShapeSet(createShape_Sphere()); - } - else if(params.testShape == "cyl") - { - shapeSet = createShapeSet(createShape_Cylinder()); - } - else if(params.testShape == "cone") - { - shapeSet = createShapeSet(createShape_Cone()); - } - else if(params.testShape == "sor") - { - shapeSet = createShapeSet(createShape_Sor()); - } - else if(params.testShape == "plane") - { - shapeSet = createShapeSet(createShape_Plane()); - } - else if(params.testShape == "all") - { - std::vector shapesVec; - shapesVec.push_back(createShape_TetMesh(ds)); - shapesVec.push_back(createShape_Tet()); - shapesVec.push_back(createShape_Hex()); - shapesVec.push_back(createShape_Sphere()); - shapesVec.push_back(createShape_Sor()); - shapesVec.push_back(createShape_Cylinder()); - shapesVec.push_back(createShape_Cone()); - shapeSet.setShapes(shapesVec); - shapeSet.setDimensions(axom::klee::Dimensions::Three); + if(shapeReps.count(tg) == 0) + { + shapeReps[tg] = 0; + } + std::string name = axom::fmt::format("{}.{}", tg, shapeReps[tg]++); + + if(tg == "plane") + { + shapesVec.push_back(createShape_Plane(name)); + } + else if(tg == "hex") + { + shapesVec.push_back(createShape_Hex(name)); + } + else if(tg == "sphere") + { + shapesVec.push_back(createShape_Sphere(name)); + } + else if(tg == "tetmesh") + { + shapesVec.push_back(createShape_TetMesh(ds, name)); + } + else if(tg == "tet") + { + shapesVec.push_back(createShape_Tet(name)); + } + else if(tg == "sor") + { + shapesVec.push_back(createShape_Sor(name)); + } + else if(tg == "cyl") + { + shapesVec.push_back(createShape_Cylinder(name)); + } + else if(tg == "cone") + { + shapesVec.push_back(createShape_Cone(name)); + } + else if(tg == "tri") + { + SLIC_ERROR_IF(params.getBoxDim() != 2, "This example is only in 2D."); + shapesVec = create2DShapeSet(ds); + } } + axom::klee::ShapeSet shapeSet; + + shapeSet.setShapes(shapesVec); + shapeSet.setDimensions(params.getBoxDim() == 2 ? axom::klee::Dimensions::Two + : axom::klee::Dimensions::Three); // Save the discrete shapes for viz and testing. auto* shapeMeshGroup = ds.getRoot()->createGroup("shapeMeshGroup"); @@ -1794,16 +1988,19 @@ int main(int argc, char** argv) shape.getMaterial(), shapeFormat))); + const auto annotationName = "shaping:" + shape.getName(); + AXOM_ANNOTATE_BEGIN(annotationName); // Load the shape from file. This also applies any transformations. shaper->loadShape(shape); - slic::flushStreams(); + // slic::flushStreams(); // Generate a spatial index over the shape shaper->prepareShapeQuery(shapeSet.getDimensions(), shape); - slic::flushStreams(); + // slic::flushStreams(); // Query the mesh against this shape shaper->runShapeQuery(shape); + AXOM_ANNOTATE_END(annotationName); slic::flushStreams(); // Apply the replacement rules for this shape against the existing materials @@ -1945,43 +2142,49 @@ int main(int argc, char** argv) // shape mesh for closes shape. As long as the shapes don't overlap, this // should be a good correctness check. //--------------------------------------------------------------------------- - auto* meshVerificationGroup = ds.getRoot()->createGroup("meshVerification"); for(const auto& shape : shapeSet.getShapes()) { - axom::quest::DiscreteShape dShape(shape, meshVerificationGroup); - auto shapeMesh = - std::dynamic_pointer_cast>( - dShape.createMeshRepresentation()); - double shapeMeshVol = - params.getBoxDim() == 3 ? volumeOfTetMesh(*shapeMesh) : areaOfTriMesh(*shapeMesh); - SLIC_INFO(axom::fmt::format("{:-^80}", - axom::fmt::format("Shape '{}' discrete geometry has {} cells", - shape.getName(), - shapeMesh->getNumberOfCells()))); - - const std::string& materialName = shape.getMaterial(); - double shapeVol = -1; - if(params.useBlueprintSidre() || params.useBlueprintConduit()) - { - shapeVol = sumMaterialVolumes(compMeshGrp, materialName); - } + std::string fieldName = "shape_vol_frac_" + shape.getName(); + axom::ArrayView vfView = getFieldAsArrayView(fieldName); + axom::Array vfHostArray(vfView, axom::execution_space::allocatorID()); + vfView = vfHostArray.view(); + axom::sidre::View* elementVolsVu = nullptr; #if defined(AXOM_USE_MFEM) if(params.useMfem()) { - shapeVol = sumMaterialVolumes(shapingDC.get(), materialName); + elementVolsVu = getElementVolumes(shapingDC.get(), "elementVolumes"); } + else + { + elementVolsVu = getElementVolumes(compMeshGrp, "elementVolumes"); + } +#else + elementVolsVu = getElementVolumes(compMeshGrp, "elementVolumes"); #endif - double correctShapeVol = params.testShape == "plane" ? params.boxMeshVolume() / 2 : shapeMeshVol; + axom::ArrayView elementVolsView(elementVolsVu->getData(), + elementVolsVu->getNumElements()); + axom::Array elementVols(elementVolsView, hostAllocId); + elementVolsView = elementVols.view(); + using ReducePolicy = typename axom::execution_space::reduce_policy; + RAJA::ReduceSum shapedVolume(0); + axom::for_all( + cellCount, + AXOM_LAMBDA(axom::IndexType i) { shapedVolume += vfView[i] * elementVolsView[i]; }); + double shapeVol = shapedVolume.get(); + double correctShapeVol = exactOverlapVols.at(shape.getName()); SLIC_ASSERT(correctShapeVol > 0.0); // Indicates error in the test setup. double diff = shapeVol - correctShapeVol; - bool err = !axom::utilities::isNearlyEqualRelative(shapeVol, correctShapeVol, 1e-6, 1e-8); + bool err = !axom::utilities::isNearlyEqualRelative(shapeVol, + correctShapeVol, + errorToleranceRel.at(shape.getName()), + errorToleranceAbs.at(shape.getName())); failCounts += err; SLIC_INFO(axom::fmt::format( "{:-^80}", axom::fmt::format("Material '{}' in shape '{}' has volume {} vs {}, diff of {}, {}.", - materialName, + shape.getMaterial(), shape.getName(), shapeVol, correctShapeVol, @@ -2022,6 +2225,10 @@ int main(int argc, char** argv) SLIC_INFO(axom::fmt::format("{:-^80}", "")); slic::flushStreams(); + AXOM_ANNOTATE_END("quest shaping example"); + + SLIC_INFO(axom::fmt::format("exiting with failure count {}", failCounts)); + finalizeLogger(); return failCounts; diff --git a/src/axom/quest/tests/CMakeLists.txt b/src/axom/quest/tests/CMakeLists.txt index 14ad1e8188..08524de9aa 100644 --- a/src/axom/quest/tests/CMakeLists.txt +++ b/src/axom/quest/tests/CMakeLists.txt @@ -254,9 +254,12 @@ if(CONDUIT_FOUND AND RAJA_FOUND AND AXOM_ENABLE_SIDRE) blt_list_append(TO _policies ELEMENTS "hip" IF AXOM_ENABLE_HIP) endif() - foreach(_meshType "bpSidre" "bpConduit") + set(_testgeoms "plane" "tet" "hex" "sphere" "tetmesh" "sor" "cyl" "cone" "plane,hex,tetmesh") + # set(_testgeoms "tetmesh" "tet" "hex" "sphere" "cyl" "cone" "sor" "plane" "plane+hex") + set(_meshTypes "bpSidre" "bpConduit") + foreach(_meshType ${_meshTypes}) foreach(_policy ${_policies}) - foreach(_testgeom "tet") + foreach(_testgeom ${_testgeoms}) set(_testname "quest_mesh_clipper_${_meshType}_${_policy}_${_testgeom}") axom_add_test( NAME ${_testname} @@ -268,6 +271,7 @@ if(CONDUIT_FOUND AND RAJA_FOUND AND AXOM_ENABLE_SIDRE) --refinements 5 --scale .99 .99 .99 --dir 8 4 2 + --screenLevel 2 --meshType ${_meshType} inline_mesh --min -2 -2 -2 --max 2 2 2 --res 16 16 16 NUM_MPI_TASKS ${_nranks}) diff --git a/src/axom/quest/tests/quest_discretize.cpp b/src/axom/quest/tests/quest_discretize.cpp index f2d9ff1bfb..b2fb7f9d51 100644 --- a/src/axom/quest/tests/quest_discretize.cpp +++ b/src/axom/quest/tests/quest_discretize.cpp @@ -167,7 +167,7 @@ void degenerate_segment_test(const char* label, axom::Array& polyline, SCOPED_TRACE(label); - axom::Array generated; + axom::Array generated(0, 0, axom::execution_space::allocatorID()); if(expsuccess) { EXPECT_TRUE(axom::quest::discretize(polyline, len, gens, generated, octcount)); @@ -200,13 +200,19 @@ void run_degen_segment_tests() axom::Array polyline(2, 2, allocID); +#if 0 +// We now want to support cases where z can increase, decrease or remains unchanged. polyline[0] = {0., 0.}; polyline[1] = {0., 0.}; degenerate_segment_test("a.x == b.x, a.y == b.y", polyline, 2, true); +#endif +#if 0 +// We now want to support cases where z can increase, decrease or remains unchanged. polyline[0] = {1., 0.}; polyline[1] = {1., 1.}; degenerate_segment_test("a.x == b.x, a.y != b.y", polyline, 2, true); +#endif polyline[0] = {1., -0.1}; polyline[1] = {1.5, 1.}; @@ -220,9 +226,12 @@ void run_degen_segment_tests() polyline[1] = {1.5, -.1}; degenerate_segment_test("b.y < 0", polyline, 2, false); +#if 0 +// We now want to support cases where z can increase, decrease or remains unchanged. polyline[0] = {.5, 1.}; polyline[1] = {0., 1.}; degenerate_segment_test("a.x > b.x", polyline, 2, false); +#endif } template @@ -250,7 +259,7 @@ void segment_test(const char* label, axom::Array& polyline, int len) axom::Array handcut; discretized_segment(polyline[0], polyline[1], handcut); - axom::Array generatedDevice; + axom::Array generatedDevice(0, 0, axom::execution_space::allocatorID()); int octcount = 0; axom::quest::discretize(polyline, len, generations, generatedDevice, octcount); @@ -325,7 +334,7 @@ void multi_segment_test(const char* label, axom::Array& polyline, int l int generation = 0; - axom::Array generatedDevice; + axom::Array generatedDevice(0, 0, axom::execution_space::allocatorID()); int octcount = 0; axom::quest::discretize(polyline, len, generations, generatedDevice, octcount); diff --git a/src/axom/quest/tests/quest_mesh_clipper.cpp b/src/axom/quest/tests/quest_mesh_clipper.cpp index 1daaba635c..b7824c0727 100644 --- a/src/axom/quest/tests/quest_mesh_clipper.cpp +++ b/src/axom/quest/tests/quest_mesh_clipper.cpp @@ -27,7 +27,13 @@ #include "axom/sidre.hpp" #include "axom/klee.hpp" #include "axom/quest.hpp" +#include "axom/quest/detail/clipping/HexClipper.hpp" +#include "axom/quest/detail/clipping/Plane3DClipper.hpp" +#include "axom/quest/detail/clipping/FSorClipper.hpp" +#include "axom/quest/detail/clipping/SorClipper.hpp" +#include "axom/quest/detail/clipping/SphereClipper.hpp" #include "axom/quest/detail/clipping/TetClipper.hpp" +#include "axom/quest/detail/clipping/TetMeshClipper.hpp" #include "axom/fmt.hpp" #include "axom/CLI11.hpp" @@ -101,7 +107,17 @@ struct Input // The shape to run. std::vector testGeom; // The shapes this example is set up to run. - const std::set availableShapes {"tet"}; // More geometries to come. + const std::set availableShapes {"tetmesh", + "sphere", + "cyl", + "cone", + "sor", + "tet", + "hex", + "plane", + "tet2", + "tetmesh2", + "hex2"}; RuntimePolicy policy {RuntimePolicy::seq}; int refinementLevel {7}; @@ -110,6 +126,8 @@ struct Input std::string backgroundMaterial; + int screenLevel = -1; + // clang-format off enum class MeshType { bpSidre = 0, bpConduit = 1 }; const std::map meshTypeChoices @@ -137,10 +155,14 @@ struct Input void parse(int argc, char** argv, axom::CLI::App& app) { + app.add_option("--screenLevel", screenLevel) + ->description("Developer feature for MeshClipper.") + ->capture_default_str(); + app.add_option("-o,--outputFile", outputFile)->description("Path to output file(s)"); app.add_flag("-v,--verbose,!--no-verbose", m_verboseOutput) - ->description("Enable/disable verbose output") + ->description("Enable/disable verbose output, including SLIC_DEBUG") ->capture_default_str(); app.add_option("--meshType", meshType) @@ -270,7 +292,8 @@ const std::string matsetName = "matset"; const std::string coordsetName = "coords"; int cellCount = -1; // Translation to individual octants (override) when running multiple shapes. -// Except that the plane is never moved. +// Exception: the plane always placed at origin to facilitate finding its +// exact overlap volume. const double tDist = 0.9; // Bias toward origin to help keep shape inside domain. std::vector> translations {{tDist, tDist, -tDist}, {-tDist, tDist, -tDist}, @@ -387,6 +410,7 @@ void initializeLogger() #ifdef AXOM_USE_MPI int num_ranks = 1; MPI_Comm_size(MPI_COMM_WORLD, &num_ranks); + SLIC_ERROR_IF(num_ranks > 1, "Sorry, this test is serial."); if(num_ranks > 1) { std::string fmt = "[][]: \n"; @@ -417,6 +441,264 @@ void finalizeLogger() } } +double volumeOfTetMesh(const axom::mint::UnstructuredMesh& tetMesh) +{ + using TetType = axom::primal::Tetrahedron; + axom::StackArray nodesShape {tetMesh.getNumberOfNodes()}; + axom::ArrayView x(tetMesh.getCoordinateArray(0), nodesShape); + axom::ArrayView y(tetMesh.getCoordinateArray(1), nodesShape); + axom::ArrayView z(tetMesh.getCoordinateArray(2), nodesShape); + const axom::IndexType tetCount = tetMesh.getNumberOfCells(); + axom::Array tetVolumes(tetCount, tetCount); + double meshVolume = 0.0; + for(axom::IndexType ic = 0; ic < tetCount; ++ic) + { + const axom::IndexType* nodeIds = tetMesh.getCellNodeIDs(ic); + TetType tet; + for(int j = 0; j < 4; ++j) + { + auto cornerNodeId = nodeIds[j]; + tet[j][0] = x[cornerNodeId]; + tet[j][1] = y[cornerNodeId]; + tet[j][2] = z[cornerNodeId]; + } + meshVolume += tet.volume(); + } + return meshVolume; +} + +/* + * For the test shapes, try to get good volume with compact shape + * that stays in domain when rotated (else volume check is invalid). +*/ + +axom::klee::Geometry createGeom_Sphere(const std::string& geomName) +{ + Point3D center = params.center.empty() ? Point3D {0, 0, 0} : Point3D {params.center.data()}; + double radius = params.radius < 0 ? 1.0 : params.radius; + axom::primal::Sphere sphere {center, radius}; + + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, + axom::klee::LengthUnit::unspecified}; + + auto compositeOp = std::make_shared(startProp); + addScaleOperator(*compositeOp); + addRotateOperator(*compositeOp); + addTranslateOperator(*compositeOp); + + const axom::IndexType levelOfRefinement = params.refinementLevel; + axom::klee::Geometry sphereGeometry(prop, sphere, levelOfRefinement, compositeOp); + exactGeomVols[geomName] = vScale * 4. / 3 * M_PI * radius * radius * radius; + errorToleranceRel[geomName] = 1e-3; + // Tolerance should account for discretization errors. + errorToleranceRel[geomName] = params.refinementLevel <= 5 ? 0.0015 : 0.0001; + errorToleranceAbs[geomName] = errorToleranceRel[geomName] * exactGeomVols[geomName]; + + return sphereGeometry; +} + +axom::klee::Geometry createGeom_TetMesh(sidre::DataStore& ds, const std::string& geomName) +{ + // Shape a tetrahedal mesh. + sidre::Group* meshGroup = ds.getRoot()->createGroup(geomName); + + AXOM_UNUSED_VAR(meshGroup); // variable is only referenced in debug configs + const std::string topo = "mesh"; + const std::string coordset = "coords"; + + axom::mint::UnstructuredMesh tetMesh(3, + axom::mint::CellType::TET, + meshGroup, + topo, + coordset); + + double lll = params.length < 0 ? 1.17 : params.length; + + // Insert tets around origin. + tetMesh.appendNode(-lll, -lll, -lll); + tetMesh.appendNode(+lll, -lll, -lll); + tetMesh.appendNode(-lll, +lll, -lll); + tetMesh.appendNode(-lll, -lll, +lll); + tetMesh.appendNode(+lll, +lll, +lll); + tetMesh.appendNode(-lll, +lll, +lll); + tetMesh.appendNode(+lll, +lll, -lll); + tetMesh.appendNode(+lll, -lll, +lll); + axom::IndexType conn0[4] = {0, 1, 2, 3}; + tetMesh.appendCell(conn0); + axom::IndexType conn1[4] = {4, 5, 6, 7}; + tetMesh.appendCell(conn1); + axom::IndexType conn2[4] = {1, 2, 3, 5}; + tetMesh.appendCell(conn2); + + SLIC_ASSERT(axom::mint::blueprint::isValidRootGroup(meshGroup)); + meshGroup->destroyGroup("fields"); + + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, + axom::klee::LengthUnit::unspecified}; + + auto compositeOp = std::make_shared(startProp); + addScaleOperator(*compositeOp); + addRotateOperator(*compositeOp); + addTranslateOperator(*compositeOp); + + axom::klee::Geometry tetMeshGeometry(prop, tetMesh.getSidreGroup(), topo, compositeOp); + + exactGeomVols[geomName] = vScale * volumeOfTetMesh(tetMesh); + errorToleranceRel[geomName] = 0.005; + errorToleranceAbs[geomName] = errorToleranceRel[geomName] * exactGeomVols[geomName]; + + return tetMeshGeometry; +} + +axom::klee::Geometry createGeometry_Sor(axom::primal::Point& sorBase, + axom::primal::Vector& sorDirection, + axom::ArrayView discreteFunction, + std::shared_ptr& compositeOp) +{ + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, + axom::klee::LengthUnit::unspecified}; + + const axom::IndexType levelOfRefinement = params.refinementLevel; + axom::klee::Geometry sorGeometry(prop, + discreteFunction, + sorBase, + sorDirection, + levelOfRefinement, + compositeOp); + return sorGeometry; +} + +double computeVolume_Sor(axom::ArrayView discreteFunction) +{ + using ConeType = axom::primal::Cone; + axom::IndexType segmentCount = discreteFunction.shape()[0]; + double vol = 0.0; + for(axom::IndexType s = 0; s < segmentCount - 1; ++s) + { + ConeType cone(discreteFunction(s, 1), + discreteFunction(s + 1, 1), + discreteFunction(s + 1, 0) - discreteFunction(s, 0)); + vol += cone.volume(); + } + return vol; +} + +axom::klee::Geometry createGeom_Sor(const std::string& geomName) +{ + Point3D sorBase = params.center.empty() ? Point3D {0.0, 0.0, 0.0} : Point3D {params.center.data()}; + axom::primal::Vector sorDirection = params.direction.empty() + ? primal::Vector3D {1.0, 0.0, 0.0} + : primal::Vector3D {params.direction.data()}; + // discreteFunction is discrete z-r pairs describing the function + // to be rotated around the z axis. + using Point2DType = axom::primal::Point; + double zLen = 0.5 * (params.length < 0 ? 2.40 : params.length); + double maxR = params.radius < 0 ? 1.10 : params.radius; + axom::Array discretePts(0, 10); +#if 1 + discretePts.push_back(Point2DType({-1.0 * zLen, 1.0 * maxR})); + discretePts.push_back(Point2DType({0.4 * zLen, 1.0 * maxR})); + discretePts.push_back(Point2DType({0.4 * zLen, 0.7 * maxR})); + discretePts.push_back(Point2DType({1.0 * zLen, 0.7 * maxR})); + discretePts.push_back(Point2DType({1.0 * zLen, 0.4 * maxR})); + discretePts.push_back(Point2DType({0.5 * zLen, 0.4 * maxR})); + discretePts.push_back(Point2DType({0.5 * zLen, 0.3 * maxR})); + discretePts.push_back(Point2DType({0.0 * zLen, 0.3 * maxR})); + discretePts.push_back(Point2DType({0.0 * zLen, 0.5 * maxR})); + discretePts.push_back(Point2DType({0.2 * zLen, 0.5 * maxR})); + discretePts.push_back(Point2DType({0.2 * zLen, 0.7 * maxR})); + discretePts.push_back(Point2DType({-1.0 * zLen, 0.7 * maxR})); +#else + discretePts.push_back(Point2DType({-1.0 * zLen, 0.4 * maxR})); + discretePts.push_back(Point2DType({0.0 * zLen, 1.0 * maxR})); + discretePts.push_back(Point2DType({0.6 * zLen, 1.0 * maxR})); + discretePts.push_back(Point2DType({1.0 * zLen, 0.8 * maxR})); + discretePts.push_back(Point2DType({1.0 * zLen, 0.6 * maxR})); + discretePts.push_back(Point2DType({0.2 * zLen, 0.4 * maxR})); + discretePts.push_back(Point2DType({0.0 * zLen, 0.0 * maxR})); +#endif + axom::ArrayView discreteFunction((const double*)discretePts.data(), + discretePts.size(), + 2); + + auto compositeOp = std::make_shared(startProp); + addScaleOperator(*compositeOp); + addTranslateOperator(*compositeOp); + + axom::klee::Geometry sorGeometry = + createGeometry_Sor(sorBase, sorDirection, discreteFunction, compositeOp); + + exactGeomVols[geomName] = vScale * computeVolume_Sor(discreteFunction); + // Tolerance should account for discretization errors. + errorToleranceRel[geomName] = params.refinementLevel <= 5 ? 0.007 : 0.0063; + errorToleranceAbs[geomName] = errorToleranceRel[geomName] * exactGeomVols[geomName]; + + return sorGeometry; +} + +axom::klee::Geometry createGeom_Cylinder(const std::string& geomName) +{ + Point3D sorBase = params.center.empty() ? Point3D {0.0, 0.0, 0.0} : Point3D {params.center.data()}; + axom::primal::Vector sorDirection = params.direction.empty() + ? primal::Vector3D {1.0, 0.0, 0.0} + : primal::Vector3D {params.direction.data()}; + // discreteFunction are discrete z-r pairs describing the function + // to be rotated around the z axis. + axom::Array discreteFunction({2, 2}, axom::ArrayStrideOrder::ROW); + double radius = params.radius < 0 ? 0.695 : params.radius; + double height = params.length < 0 ? 2.78 : params.length; + discreteFunction(0, 0) = -height / 2; + discreteFunction(0, 1) = radius; + discreteFunction(1, 0) = height / 2; + discreteFunction(1, 1) = radius; + + auto compositeOp = std::make_shared(startProp); + addScaleOperator(*compositeOp); + addTranslateOperator(*compositeOp); + + axom::klee::Geometry sorGeometry = + createGeometry_Sor(sorBase, sorDirection, discreteFunction, compositeOp); + + exactGeomVols[geomName] = vScale * computeVolume_Sor(discreteFunction); + // Tolerance should account for discretization errors. + errorToleranceRel[geomName] = params.refinementLevel <= 5 ? 0.00075 : 0.00005; + errorToleranceAbs[geomName] = errorToleranceRel[geomName] * exactGeomVols[geomName]; + + return sorGeometry; +} + +axom::klee::Geometry createGeom_Cone(const std::string& geomName) +{ + Point3D sorBase = params.center.empty() ? Point3D {0.0, 0.0, 0.0} : Point3D {params.center.data()}; + axom::primal::Vector sorDirection = params.direction.empty() + ? primal::Vector3D {1.0, 0.0, 0.0} + : primal::Vector3D {params.direction.data()}; + // discreteFunction are discrete z-r pairs describing the function + // to be rotated around the z axis. + axom::Array discreteFunction({2, 2}, axom::ArrayStrideOrder::ROW); + double baseRadius = params.radius < 0 ? 1.23 : params.radius; + double topRadius = params.radius2 < 0 ? 0.176 : params.radius2; + double height = params.length < 0 ? 2.3 : params.length; + discreteFunction(0, 0) = -height / 2; + discreteFunction(0, 1) = baseRadius; + discreteFunction(1, 0) = height / 2; + discreteFunction(1, 1) = topRadius; + + auto compositeOp = std::make_shared(startProp); + addScaleOperator(*compositeOp); + addTranslateOperator(*compositeOp); + + axom::klee::Geometry sorGeometry = + createGeometry_Sor(sorBase, sorDirection, discreteFunction, compositeOp); + + exactGeomVols[geomName] = vScale * computeVolume_Sor(discreteFunction); + // Tolerance should account for discretization errors. + errorToleranceRel[geomName] = params.refinementLevel <= 5 ? 0.00075 : 0.00005; + errorToleranceAbs[geomName] = errorToleranceRel[geomName] * exactGeomVols[geomName]; + + return sorGeometry; +} + axom::klee::Geometry createGeom_Tet(const std::string& geomName) { axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, @@ -443,6 +725,160 @@ axom::klee::Geometry createGeom_Tet(const std::string& geomName) return tetGeometry; } +axom::klee::Geometry createGeom_Tet2(const std::string& geomName) +{ + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, + axom::klee::LengthUnit::unspecified}; + + // Tetrahedron at origin. + const Point3D a {Point3D::NumericArray {-10, 10, 0}}; + const Point3D b {Point3D::NumericArray {-30, 30, 0}}; + const Point3D c {Point3D::NumericArray {-30, 0, 0}}; + const Point3D d {Point3D::NumericArray {-30, 30, 20}}; + const primal::Tetrahedron tet {a, b, c, d}; + + auto compositeOp = std::make_shared(startProp); + addScaleOperator(*compositeOp); + addRotateOperator(*compositeOp); + addTranslateOperator(*compositeOp); + exactGeomVols[geomName] = vScale * tet.volume(); + errorToleranceRel[geomName] = 1e-12; + errorToleranceAbs[geomName] = errorToleranceRel[geomName] * exactGeomVols[geomName]; + + axom::klee::Geometry tetGeometry(prop, tet, compositeOp); + + return tetGeometry; +} + +axom::klee::Geometry createGeom_TetMesh2(sidre::DataStore& ds, const std::string& geomName) +{ + // Shape a tetrahedal mesh. + sidre::Group* meshGroup = ds.getRoot()->createGroup(geomName); + + AXOM_UNUSED_VAR(meshGroup); // variable is only referenced in debug configs + const std::string topo = "mesh"; + const std::string coordset = "coords"; + + axom::mint::UnstructuredMesh tetMesh(3, + axom::mint::CellType::TET, + meshGroup, + topo, + coordset); + + tetMesh.appendNode(-10, 10, 0); + tetMesh.appendNode(-30, 30, 0); + tetMesh.appendNode(-30, 0, 0); + tetMesh.appendNode(-30, 30, 20); + axom::IndexType conn0[4] = {0, 1, 2, 3}; + tetMesh.appendCell(conn0); + + SLIC_ASSERT(axom::mint::blueprint::isValidRootGroup(meshGroup)); + meshGroup->destroyGroup("fields"); + + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, + axom::klee::LengthUnit::unspecified}; + + auto compositeOp = std::make_shared(startProp); + addScaleOperator(*compositeOp); + addRotateOperator(*compositeOp); + addTranslateOperator(*compositeOp); + + axom::klee::Geometry tetMeshGeometry(prop, tetMesh.getSidreGroup(), topo, compositeOp); + + exactGeomVols[geomName] = vScale * volumeOfTetMesh(tetMesh); + errorToleranceRel[geomName] = 0.005; + errorToleranceAbs[geomName] = errorToleranceRel[geomName] * exactGeomVols[geomName]; + + return tetMeshGeometry; +} + +axom::klee::Geometry createGeom_Hex(const std::string& geomName) +{ + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, + axom::klee::LengthUnit::unspecified}; + + const double md = params.length < 0 ? 0.82 : params.length / 2; + const double lg = 1.2 * md; + const double sm = 0.8 * md; + const Point3D p {-lg, -md, -sm}; + const Point3D q {+lg, -md, -sm}; + const Point3D r {+lg, +md, -sm}; + const Point3D s {-lg, +md, -sm}; + const Point3D t {-lg, -md, +sm}; + const Point3D u {+lg, -md, +sm}; + const Point3D v {+lg, +md, +sm}; + const Point3D w {-lg, +md, +sm}; + const primal::Hexahedron hex {p, q, r, s, t, u, v, w}; + + auto compositeOp = std::make_shared(startProp); + addScaleOperator(*compositeOp); + addRotateOperator(*compositeOp); + addTranslateOperator(*compositeOp); + exactGeomVols[geomName] = vScale * hex.volume(); + errorToleranceRel[geomName] = 0.000075; + errorToleranceAbs[geomName] = 0.0003; + + axom::klee::Geometry hexGeometry(prop, hex, compositeOp); + + return hexGeometry; +} + +axom::klee::Geometry createGeom_Hex2(const std::string& geomName) +{ + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, + axom::klee::LengthUnit::unspecified}; + + // rlin -31 -31 -31 -9 -31 -31 -9 -9 -31 -31 -9 -31 -31 -31 -9 -9 -31 -9 -9 -9 -9 -31 -9 -9 rlinReg + const Point3D p {-31, -31, -31}; + const Point3D q {-9, -31, -31}; + const Point3D r {-9, -9, -31}; + const Point3D s {-31, -9, -31}; + const Point3D t {-31, -31, -9}; + const Point3D u {-9, -31, -9}; + const Point3D v {-9, -9, -9}; + const Point3D w {-31, -9, -9}; + const primal::Hexahedron hex {p, q, r, s, t, u, v, w}; + + auto compositeOp = std::make_shared(startProp); + exactGeomVols[geomName] = hex.volume(); + errorToleranceRel[geomName] = 0.000075; + errorToleranceAbs[geomName] = 0.0003; + + axom::klee::Geometry hexGeometry(prop, hex, compositeOp); + + return hexGeometry; +} + +axom::klee::Geometry createGeom_Plane(const std::string& geomName) +{ + axom::klee::TransformableGeometryProperties prop {axom::klee::Dimensions::Three, + axom::klee::LengthUnit::unspecified}; + + // Create a plane crossing center of mesh. No matter the normal, + // it cuts the mesh in half. + Point3D center {0.5 * + (axom::NumericArray(params.boxMins.data()) + + axom::NumericArray(params.boxMaxs.data()))}; + primal::Vector normal = params.direction.empty() + ? primal::Vector3D {1.0, 0.0, 0.0} + : primal::Vector3D {params.direction.data()}.unitVector(); + const primal::Plane plane {normal, center, true}; + + axom::klee::Geometry planeGeometry(prop, plane, {nullptr}); + + // Exact mesh overlap volume, assuming plane passes through center of box mesh. + using Pt3D = primal::Point; + Pt3D lower(params.boxMins.data()); + Pt3D upper(params.boxMaxs.data()); + auto diag = upper.array() - lower.array(); + double meshVolume = diag[0] * diag[1] * diag[2]; + exactGeomVols[geomName] = 0.5 * meshVolume; + errorToleranceRel[geomName] = 1e-6; + errorToleranceAbs[geomName] = 1e-8; + + return planeGeometry; +} + /*! @brief Return the element volumes as a sidre::View containing the volumes in an array. @@ -823,12 +1259,64 @@ int main(int argc, char** argv) } std::string name = axom::fmt::format("{}.{}", tg, geomReps[tg]++); - if(tg == "tet") + if(tg == "plane") + { + geomStrategies.push_back( + std::make_shared(createGeom_Plane(name), name)); + } + else if(tg == "hex") + { + geomStrategies.push_back( + std::make_shared(createGeom_Hex(name), name)); + } + else if(tg == "sphere") + { + geomStrategies.push_back( + std::make_shared(createGeom_Sphere(name), name)); + } + else if(tg == "tetmesh") + { + geomStrategies.push_back( + std::make_shared(createGeom_TetMesh(ds, name), + name)); + } + else if(tg == "tet") { geomStrategies.push_back( std::make_shared(createGeom_Tet(name), name)); } - // More geometries to come. + else if(tg == "cyl") + { + geomStrategies.push_back( + std::make_shared(createGeom_Cylinder(name), name)); + } + else if(tg == "cone") + { + geomStrategies.push_back( + std::make_shared(createGeom_Cone(name), name)); + } + else if(tg == "sor") + { + geomStrategies.push_back( + std::make_shared(createGeom_Sor(name), name)); + } + + else if(tg == "tet2") + { + geomStrategies.push_back( + std::make_shared(createGeom_Tet2(name), name)); + } + else if(tg == "tetmesh2") + { + geomStrategies.push_back( + std::make_shared(createGeom_TetMesh2(ds, name), + name)); + } + else if(tg == "hex2") + { + geomStrategies.push_back( + std::make_shared(createGeom_Hex2(name), name)); + } } { @@ -900,11 +1388,19 @@ int main(int argc, char** argv) quest::experimental::MeshClipper clipper(sMesh, geomStrategies[i]); clipper.setVerbose(params.isVerbose()); + if(params.screenLevel >= 0) + { + clipper.setScreenLevel(params.screenLevel); + } + SLIC_INFO(axom::fmt::format("MeshClipper screen level: {}", clipper.getScreenLevel())); + axom::Array ovlap; AXOM_ANNOTATE_BEGIN(annotationName); clipper.clip(ovlap); AXOM_ANNOTATE_END(annotationName); + clipper.logClippingStats(); + // Save volume fractions in mesh, for plotting and checking. sMesh.setMatsetFromVolume(geomStrategies[i]->name(), ovlap.view(), false); diff --git a/src/axom/quest/util/make_clipper_strategy.cpp b/src/axom/quest/util/make_clipper_strategy.cpp new file mode 100644 index 0000000000..d39b93f692 --- /dev/null +++ b/src/axom/quest/util/make_clipper_strategy.cpp @@ -0,0 +1,78 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "axom/config.hpp" + +#ifdef AXOM_USE_SIDRE + #include "axom/quest/util/make_clipper_strategy.hpp" + #include "axom/quest/detail/clipping/Plane3DClipper.hpp" + #include "axom/quest/detail/clipping/TetClipper.hpp" + #include "axom/quest/detail/clipping/TetMeshClipper.hpp" + #include "axom/quest/detail/clipping/HexClipper.hpp" + #include "axom/quest/detail/clipping/SphereClipper.hpp" + #include "axom/quest/detail/clipping/FSorClipper.hpp" + #include "axom/quest/detail/clipping/SorClipper.hpp" + #include "axom/slic/interface/slic_macros.hpp" + +namespace axom +{ +namespace quest +{ +namespace util +{ +using namespace axom::quest::experimental; + +std::shared_ptr make_clipper_strategy(const axom::klee::Geometry& kleeGeometry, + const std::string& name) +{ + std::shared_ptr strategy; + + const std::string& format = kleeGeometry.getFormat(); + const std::string& instanceName = !name.empty() ? name : kleeGeometry.getFormat(); + + if(format == "plane3D") + { + strategy.reset(new Plane3DClipper(kleeGeometry, instanceName)); + } + else if(format == "tet3D") + { + strategy.reset(new TetClipper(kleeGeometry, instanceName)); + } + else if(format == "blueprint-tets") + { + strategy.reset(new TetMeshClipper(kleeGeometry, instanceName)); + } + else if(format == "hex3D") + { + strategy.reset(new HexClipper(kleeGeometry, instanceName)); + } + else if(format == "sphere3D") + { + strategy.reset(new SphereClipper(kleeGeometry, instanceName)); + } + else if(format == "sor3D") + { + strategy.reset(new SorClipper(kleeGeometry, instanceName)); + } + else if(format == "cyl3D") + { + strategy.reset(new FSorClipper(kleeGeometry, instanceName)); + } + else if(format == "cone3D") + { + strategy.reset(new FSorClipper(kleeGeometry, instanceName)); + } + else + { + SLIC_ERROR(axom::fmt::format("Unrecognized Klee Geometry format {}.", format)); + } + + return strategy; +} + +} // namespace util +} // namespace quest +} // namespace axom +#endif // AXOM_USE_SIDRE diff --git a/src/axom/quest/util/make_clipper_strategy.hpp b/src/axom/quest/util/make_clipper_strategy.hpp new file mode 100644 index 0000000000..eb0f7e54a8 --- /dev/null +++ b/src/axom/quest/util/make_clipper_strategy.hpp @@ -0,0 +1,43 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#ifndef AXOM_MAKE_CLIPPER_STRATEGY_HPP +#define AXOM_MAKE_CLIPPER_STRATEGY_HPP + +#include "axom/config.hpp" + +// MeshClipper depends on sidre +#ifdef AXOM_USE_SIDRE + + #include "axom/klee/Geometry.hpp" + #include "axom/quest/MeshClipperStrategy.hpp" + +namespace axom +{ +namespace quest +{ +namespace util +{ +using experimental::MeshClipperStrategy; + +/*! + * @brief Return a new MeshClipperStrategy implementation + * to clip the specified klee Geometry. + * @param [in] kleeGeometry + * @param [in] name Name of strategy instance + * + * This method chooses the correct implementations for known + * klee geometry formats. It throws an error for unrecognized + * formats. +*/ +std::shared_ptr make_clipper_strategy(const axom::klee::Geometry& kleeGeometry, + const std::string& name = ""); + +} // namespace util +} // namespace quest +} // namespace axom + +#endif // AXOM_USE_SIDRE +#endif // AXOM_MAKE_CLIPPER_STRATEGY_HPP diff --git a/src/axom/quest/util/mesh_helpers.cpp b/src/axom/quest/util/mesh_helpers.cpp index 8e4209341f..4b763f1d83 100644 --- a/src/axom/quest/util/mesh_helpers.cpp +++ b/src/axom/quest/util/mesh_helpers.cpp @@ -239,30 +239,41 @@ axom::sidre::Group* make_unstructured_blueprint_box_mesh_2d(axom::sidre::Group* void convert_blueprint_structured_explicit_to_unstructured_3d(axom::sidre::Group* meshGrp, const std::string& topoName, - axom::runtime_policy::Policy runtimePolicy) + axom::runtime_policy::Policy runtimePolicy, + const std::string& ugTopoName) { if(runtimePolicy == axom::runtime_policy::Policy::seq) { - convert_blueprint_structured_explicit_to_unstructured_impl_3d(meshGrp, topoName); + convert_blueprint_structured_explicit_to_unstructured_3d_impl( + meshGrp, + topoName, + ugTopoName.empty() ? topoName : ugTopoName); } #if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) if(runtimePolicy == axom::runtime_policy::Policy::omp) { - convert_blueprint_structured_explicit_to_unstructured_impl_3d(meshGrp, topoName); + convert_blueprint_structured_explicit_to_unstructured_3d_impl( + meshGrp, + topoName, + ugTopoName.empty() ? topoName : ugTopoName); } #endif #if defined(AXOM_RUNTIME_POLICY_USE_CUDA) if(runtimePolicy == axom::runtime_policy::Policy::cuda) { - convert_blueprint_structured_explicit_to_unstructured_impl_3d>(meshGrp, - topoName); + convert_blueprint_structured_explicit_to_unstructured_3d_impl>( + meshGrp, + topoName, + ugTopoName.empty() ? topoName : ugTopoName); } #endif #if defined(AXOM_RUNTIME_POLICY_USE_HIP) if(runtimePolicy == axom::runtime_policy::Policy::hip) { - convert_blueprint_structured_explicit_to_unstructured_impl_3d>(meshGrp, - topoName); + convert_blueprint_structured_explicit_to_unstructured_3d_impl>( + meshGrp, + topoName, + ugTopoName.empty() ? topoName : ugTopoName); } #endif } @@ -273,57 +284,73 @@ void convert_blueprint_structured_explicit_to_unstructured_2d(axom::sidre::Group { if(runtimePolicy == axom::runtime_policy::Policy::seq) { - convert_blueprint_structured_explicit_to_unstructured_impl_2d(meshGrp, topoName); + convert_blueprint_structured_explicit_to_unstructured_2d_impl(meshGrp, topoName); } #if defined(AXOM_RUNTIME_POLICY_USE_OPENMP) if(runtimePolicy == axom::runtime_policy::Policy::omp) { - convert_blueprint_structured_explicit_to_unstructured_impl_2d(meshGrp, topoName); + convert_blueprint_structured_explicit_to_unstructured_2d_impl(meshGrp, topoName); } #endif #if defined(AXOM_RUNTIME_POLICY_USE_CUDA) if(runtimePolicy == axom::runtime_policy::Policy::cuda) { - convert_blueprint_structured_explicit_to_unstructured_impl_2d>(meshGrp, + convert_blueprint_structured_explicit_to_unstructured_2d_impl>(meshGrp, topoName); } #endif #if defined(AXOM_RUNTIME_POLICY_USE_HIP) if(runtimePolicy == axom::runtime_policy::Policy::hip) { - convert_blueprint_structured_explicit_to_unstructured_impl_2d>(meshGrp, + convert_blueprint_structured_explicit_to_unstructured_2d_impl>(meshGrp, topoName); } #endif } template -void convert_blueprint_structured_explicit_to_unstructured_impl_3d(axom::sidre::Group* meshGrp, - const std::string& topoName) +void convert_blueprint_structured_explicit_to_unstructured_3d_impl(axom::sidre::Group* meshGrp, + const std::string& topoName, + const std::string& ugTopoName) { AXOM_ANNOTATE_SCOPE("convert_to_unstructured"); // Note: MSVC required `static` to pass DIM to the axom::for_all w/ C++14 // this restriction might be lifted w/ C++17 static constexpr int DIM = 3; - const std::string& coordsetName = - meshGrp->getView("topologies/" + topoName + "/coordset")->getString(); + sidre::Group* topologiesGrp = meshGrp->getGroup("topologies"); + axom::sidre::Group* topoGrp = topologiesGrp->getGroup(topoName); + axom::sidre::Group* ugTopoGrp = + ugTopoName == topoName ? topoGrp : topologiesGrp->createGroup(ugTopoName); + + const std::string& coordsetName = topoGrp->getView("coordset")->getString(); sidre::Group* coordsetGrp = meshGrp->getGroup("coordsets")->getGroup(coordsetName); SLIC_ASSERT(std::string(coordsetGrp->getView("type")->getString()) == "explicit"); - axom::sidre::Group* topoGrp = meshGrp->getGroup("topologies")->getGroup(topoName); - axom::sidre::View* topoTypeView = topoGrp->getView("type"); - SLIC_ASSERT(std::string(topoTypeView->getString()) == "structured"); - topoTypeView->setString("unstructured"); - topoGrp->createView("elements/shape")->setString("hex"); + if(!ugTopoGrp->hasView("coordset")) + { + ugTopoGrp->createViewString("coordset", coordsetName); + } + else + { + SLIC_ASSERT(ugTopoGrp->getView("coordset")->isString()); + SLIC_ASSERT(std::string(ugTopoGrp->getView("coordset")->getString()) == coordsetName); + } + + SLIC_ASSERT(std::string(topoGrp->getView("type")->getString()) == "structured"); + axom::sidre::View* ugTopoTypeView = + ugTopoGrp == topoGrp ? ugTopoGrp->getView("type") : ugTopoGrp->createView("type"); + ugTopoTypeView->setString("unstructured"); + axom::sidre::View* shapeView = ugTopoGrp->createView("elements/shape"); + shapeView->setString("hex"); axom::sidre::Group* topoElemGrp = topoGrp->getGroup("elements"); axom::sidre::Group* topoDimsGrp = topoElemGrp->getGroup("dims"); // Assuming no ghost, but we should eventually support ghosts. - SLIC_ASSERT(!topoGrp->hasGroup("elements/offsets")); - SLIC_ASSERT(!topoGrp->hasGroup("elements/strides")); + SLIC_ASSERT(!topoElemGrp->hasGroup("offsets")); + SLIC_ASSERT(!topoElemGrp->hasGroup("strides")); const axom::StackArray cShape { axom::IndexType(topoDimsGrp->getView("i")->getNode().to_value()), @@ -335,10 +362,10 @@ void convert_blueprint_structured_explicit_to_unstructured_impl_3d(axom::sidre:: const axom::StackArray connShape {cCount, 8}; axom::sidre::View* connView = - topoGrp->createViewWithShapeAndAllocate("elements/connectivity", - axom::sidre::detail::SidreTT::id, - 2, - connShape.begin()); + ugTopoGrp->createViewWithShapeAndAllocate("elements/connectivity", + axom::sidre::detail::SidreTT::id, + 2, + connShape.begin()); axom::ArrayView connArrayView( static_cast(connView->getVoidPtr()), connShape); @@ -370,15 +397,15 @@ void convert_blueprint_structured_explicit_to_unstructured_impl_3d(axom::sidre:: { AXOM_ANNOTATE_BEGIN("add_extra"); /* - Constructing a mint mesh from meshGrp fails unless we add some - extra data. Blueprint doesn't require this extra data. (The mesh - passes conduit's Blueprint verification.) This should be fixed, - or we should write better blueprint support utilities. + * Constructing a mint mesh from meshGrp fails unless we add some + * extra data. Blueprint doesn't require this extra data. (The mesh + * passes conduit's Blueprint verification.) This should be fixed, + * or we should write better blueprint support utilities. */ /* - Make the coordinate arrays 2D to use mint::Mesh. - For some reason, mint::Mesh requires the arrays to be - 2D, even though the second dimension is always 1. + * Make the coordinate arrays 2D to use mint::Mesh. + * For some reason, mint::Mesh requires the arrays to be + * 2D, even though the second dimension is always 1. */ axom::IndexType curShape[2]; int curDim; @@ -391,7 +418,7 @@ void convert_blueprint_structured_explicit_to_unstructured_impl_3d(axom::sidre:: valuesGrp->getView("z")->reshapeArray(2, vertsShape); // Make connectivity array 2D for the same reason. - auto* elementsGrp = topoGrp->getGroup("elements"); + auto* elementsGrp = ugTopoGrp->getGroup("elements"); auto* connView = elementsGrp->getView("connectivity"); curDim = connView->getShape(2, curShape); constexpr axom::IndexType NUM_VERTS_PER_HEX = 8; @@ -406,8 +433,6 @@ void convert_blueprint_structured_explicit_to_unstructured_impl_3d(axom::sidre:: // mint::Mesh requires connectivity strides, even though Blueprint doesn't. elementsGrp->createViewScalar("stride", NUM_VERTS_PER_HEX); - // mint::Mesh requires field group, even though Blueprint doesn't. - meshGrp->createGroup("fields"); AXOM_ANNOTATE_END("add_extra"); } @@ -423,7 +448,7 @@ void convert_blueprint_structured_explicit_to_unstructured_impl_3d(axom::sidre:: } template -void convert_blueprint_structured_explicit_to_unstructured_impl_2d(axom::sidre::Group* meshGrp, +void convert_blueprint_structured_explicit_to_unstructured_2d_impl(axom::sidre::Group* meshGrp, const std::string& topoName) { AXOM_ANNOTATE_SCOPE("convert_to_unstructured"); @@ -494,15 +519,15 @@ void convert_blueprint_structured_explicit_to_unstructured_impl_2d(axom::sidre:: { AXOM_ANNOTATE_SCOPE("add_extra"); /* - Constructing a mint mesh from meshGrp fails unless we add some - extra data. Blueprint doesn't require this extra data. (The mesh - passes conduit's Blueprint verification.) This should be fixed, - or we should write better blueprint support utilities. + * Constructing a mint mesh from meshGrp fails unless we add some + * extra data. Blueprint doesn't require this extra data. (The mesh + * passes conduit's Blueprint verification.) This should be fixed, + * or we should write better blueprint support utilities. */ /* - Make the coordinate arrays 2D to use mint::Mesh. - For some reason, mint::Mesh requires the arrays to be - 2D, even though the second dimension is always 1. + * Make the coordinate arrays 2D to use mint::Mesh. + * For some reason, mint::Mesh requires the arrays to be + * 2D, even though the second dimension is always 1. */ axom::IndexType curShape[2] = {}; auto* valuesGrp = coordsetGrp->getGroup("values"); @@ -525,9 +550,6 @@ void convert_blueprint_structured_explicit_to_unstructured_impl_2d(axom::sidre:: // mint::Mesh requires connectivity strides, even though Blueprint doesn't. constexpr axom::IndexType BIT_SPECIFIC_NUM_VERTS_PER_QUAD = 4; elementsGrp->createViewScalar("stride", BIT_SPECIFIC_NUM_VERTS_PER_QUAD); - - // mint::Mesh requires field group, even though Blueprint doesn't. - meshGrp->createGroup("fields"); } return; diff --git a/src/axom/quest/util/mesh_helpers.hpp b/src/axom/quest/util/mesh_helpers.hpp index 6e367402d3..b9ec2ccd24 100644 --- a/src/axom/quest/util/mesh_helpers.hpp +++ b/src/axom/quest/util/mesh_helpers.hpp @@ -152,6 +152,8 @@ axom::sidre::Group* make_unstructured_blueprint_box_mesh_2d( * \param runtimePolicy Runtime policy, see axom::runtime_policy. * Memory in \c meshGrp must be compatible with the * specified policy. + * \param ugTopoName Name of the unstructured topology to create. + * Defaults to topoName. * * All input mesh data are expected to have the allocator id of * meshGrp->getDefaultAllocatorID(). On output, they will also have @@ -160,18 +162,20 @@ axom::sidre::Group* make_unstructured_blueprint_box_mesh_2d( */ void convert_blueprint_structured_explicit_to_unstructured_3d(axom::sidre::Group* meshGrp, const std::string& topoName, - axom::runtime_policy::Policy runtimePolicy); + axom::runtime_policy::Policy runtimePolicy, + const std::string& ugTopoName = ""); template -void convert_blueprint_structured_explicit_to_unstructured_impl_3d(axom::sidre::Group* meshGrp, - const std::string& topoName); +void convert_blueprint_structured_explicit_to_unstructured_3d_impl(axom::sidre::Group* meshGrp, + const std::string& topoName, + const std::string& ugTopoName); void convert_blueprint_structured_explicit_to_unstructured_2d(axom::sidre::Group* meshGrp, const std::string& topoName, axom::runtime_policy::Policy runtimePolicy); template -void convert_blueprint_structured_explicit_to_unstructured_impl_2d(axom::sidre::Group* meshGrp, +void convert_blueprint_structured_explicit_to_unstructured_2d_impl(axom::sidre::Group* meshGrp, const std::string& topoName); #if defined(AXOM_USE_CONDUIT)