Skip to content

Commit

Permalink
Merge pull request #1059 from LLNL/feature/gunney/marching-cubes-algo
Browse files Browse the repository at this point in the history
Feature/gunney/marching cubes algo
  • Loading branch information
gunney1 committed Jun 10, 2023
2 parents 31ae2e4 + 8d7eed3 commit 3276c6e
Show file tree
Hide file tree
Showing 15 changed files with 3,619 additions and 6 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The Axom project release numbers follow [Semantic Versioning](http://semver.org/
## [Unreleased] - Release date yyyy-mm-dd

### Added
- Adds MarchingCubes class implementing the marching cubes algorithm for surface detection.
- Adds the following methods to `axom::Array` to conform more closely with the `std::vector` interface:
- `Array::front()`: returns a reference to the first element
- `Array::back()`: returns a reference to the last element
Expand Down
17 changes: 16 additions & 1 deletion src/axom/core/Array.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ class Array : public ArrayBase<T, DIM, Array<T, DIM, SPACE>>
IndexType capacity = 0,
int allocator_id = axom::detail::getAllocatorID<SPACE>());

Array(const axom::StackArray<axom::IndexType, DIM>& shape,
int allocator_id = axom::detail::getAllocatorID<SPACE>());

/*!
* \brief Generic constructor for an Array of arbitrary dimension
*
Expand Down Expand Up @@ -696,7 +699,7 @@ class Array : public ArrayBase<T, DIM, Array<T, DIM, SPACE>>
*
* \note If the Array is empty the capacity can still be greater than zero.
*/
bool empty() const { return m_num_elements == 0; }
AXOM_HOST_DEVICE inline bool empty() const { return m_num_elements == 0; }

/*!
* \brief Return the number of elements stored in the data array.
Expand Down Expand Up @@ -879,6 +882,18 @@ Array<T, DIM, SPACE>::Array()
: m_allocator_id(axom::detail::getAllocatorID<SPACE>())
{ }

//------------------------------------------------------------------------------
template <typename T, int DIM, MemorySpace SPACE>
Array<T, DIM, SPACE>::Array(const axom::StackArray<axom::IndexType, DIM>& shape,
int allocator_id)
: ArrayBase<T, DIM, Array<T, DIM, SPACE>>(shape)
, m_allocator_id(allocator_id)
{
initialize(detail::packProduct(shape.m_data),
detail::packProduct(shape.m_data),
false);
}

//------------------------------------------------------------------------------
template <typename T, int DIM, MemorySpace SPACE>
template <typename... Args, typename Enable>
Expand Down
3 changes: 2 additions & 1 deletion src/axom/core/ArrayView.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ class ArrayView : public ArrayBase<T, DIM, ArrayView<T, DIM, SPACE>>
/*!
* \brief Returns true iff the ArrayView stores no elements.
*/
bool empty() const { return m_num_elements == 0; }
AXOM_HOST_DEVICE
inline bool empty() const { return m_num_elements == 0; }

/*!
* \brief Returns an ArrayViewIterator to the first element of the Array
Expand Down
2 changes: 1 addition & 1 deletion src/axom/core/StackArray.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace axom
/*!
* \accelerated
* \class StackArray
*
*
* \brief Provides a wrapper for a compile time sized array, similar to
* std::array. This class is needed because NVCC doesn't capture standard
* stack arrays in device lambdas. Furthermore we can't use std::array because
Expand Down
14 changes: 14 additions & 0 deletions src/axom/quest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ if(CONDUIT_FOUND AND AXOM_ENABLE_MPI)
conduit::conduit_mpi)
endif()

blt_list_append(
TO quest_headers
ELEMENTS MarchingCubes.hpp detail/MarchingCubesImpl.hpp
IF CONDUIT_FOUND
)

blt_list_append(
TO quest_sources
ELEMENTS MarchingCubes.cpp
IF CONDUIT_FOUND
)

blt_list_append( TO quest_depends_on ELEMENTS conduit::conduit IF CONDUIT_FOUND )

if(MFEM_FOUND AND AXOM_ENABLE_KLEE AND AXOM_ENABLE_SIDRE AND AXOM_ENABLE_MFEM_SIDRE_DATACOLLECTION)
list(APPEND quest_headers Shaper.hpp
SamplingShaper.hpp
Expand Down
2 changes: 1 addition & 1 deletion src/axom/quest/DistributedClosestPoint.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -682,10 +682,10 @@ class DistributedClosestPointImpl
copy_components_to_interleaved(queryCoordsValues, xferDom["coords"]);

xferDom["cp_index"].set_external(fields.fetch_existing("cp_index/values"));
xferDom["cp_domain_index"].set_external(fields.fetch_existing("cp_domain_index/values"));
xferDom["cp_rank"].set_external(fields.fetch_existing("cp_rank/values"));
copy_components_to_interleaved(fields.fetch_existing("cp_coords/values"),
xferDom["cp_coords"]);
xferDom["cp_domain_index"].set_external(fields.fetch_existing("cp_domain_index/values"));

if(fields.has_path("cp_distance"))
{
Expand Down
245 changes: 245 additions & 0 deletions src/axom/quest/MarchingCubes.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// Copyright (c) 2017-2023, 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/core/execution/execution_space.hpp"
#include "axom/quest/MarchingCubes.hpp"
#include "axom/quest/detail/MarchingCubesImpl.hpp"
#include "conduit_blueprint.hpp"
#include "axom/fmt.hpp"

namespace axom
{
namespace quest
{
MarchingCubes::MarchingCubes(RuntimePolicy runtimePolicy,
const conduit::Node& bpMesh,
const std::string& coordsetName,
const std::string& maskField)
: m_runtimePolicy(runtimePolicy)
, m_singles()
, m_coordsetPath("coordsets/" + coordsetName)
, m_fcnPath()
, m_maskPath(maskField.empty() ? std::string() : "fields/" + maskField)
{
const bool isMultidomain = conduit::blueprint::mesh::is_multi_domain(bpMesh);
SLIC_ASSERT_MSG(
isMultidomain,
"MarchingCubes class input mesh must be in multidomain format.");

m_singles.reserve(conduit::blueprint::mesh::number_of_domains(bpMesh));
for(auto& dom : bpMesh.children())
{
m_singles.emplace_back(
new MarchingCubesSingleDomain(m_runtimePolicy, dom, coordsetName, maskField));
}
}

void MarchingCubes::setFunctionField(const std::string& fcnField)
{
m_fcnPath = "fields/" + fcnField;
for(auto& s : m_singles)
{
s->setFunctionField(fcnField);
}
}

void MarchingCubes::computeIsocontour(double contourVal)
{
SLIC_ASSERT_MSG(!m_fcnPath.empty(),
"You must call setFunctionField before computeIsocontour.");

for(int dId = 0; dId < m_singles.size(); ++dId)
{
std::unique_ptr<MarchingCubesSingleDomain>& single = m_singles[dId];
single->computeIsocontour(contourVal);
}
}

axom::IndexType MarchingCubes::getContourCellCount() const
{
axom::IndexType contourCellCount = 0;
for(int dId = 0; dId < m_singles.size(); ++dId)
{
contourCellCount += m_singles[dId]->getContourCellCount();
}
return contourCellCount;
}

axom::IndexType MarchingCubes::getContourNodeCount() const
{
axom::IndexType contourNodeCount = 0;
for(int dId = 0; dId < m_singles.size(); ++dId)
{
contourNodeCount += m_singles[dId]->getContourNodeCount();
}
return contourNodeCount;
}

void MarchingCubes::populateContourMesh(
axom::mint::UnstructuredMesh<axom::mint::SINGLE_SHAPE>& mesh,
const std::string& cellIdField,
const std::string& domainIdField)
{
if(!cellIdField.empty() &&
!mesh.hasField(cellIdField, axom::mint::CELL_CENTERED))
{
mesh.createField<axom::IndexType>(cellIdField,
axom::mint::CELL_CENTERED,
mesh.getDimension());
}

if(!domainIdField.empty() &&
!mesh.hasField(domainIdField, axom::mint::CELL_CENTERED))
{
mesh.createField<axom::IndexType>(domainIdField, axom::mint::CELL_CENTERED);
}

// Reserve space once for all local domains.
const axom::IndexType contourCellCount = getContourCellCount();
const axom::IndexType contourNodeCount = getContourNodeCount();
mesh.reserveCells(contourCellCount);
mesh.reserveNodes(contourNodeCount);

// Populate mesh from single domains and add domain id if requested.
for(int dId = 0; dId < m_singles.size(); ++dId)
{
std::unique_ptr<MarchingCubesSingleDomain>& single = m_singles[dId];

auto nPrev = mesh.getNumberOfCells();
single->populateContourMesh(mesh, cellIdField);
auto nNew = mesh.getNumberOfCells();

if(nNew > nPrev && !domainIdField.empty())
{
auto* domainIdPtr =
mesh.getFieldPtr<axom::IndexType>(domainIdField,
axom::mint::CELL_CENTERED);
// TODO: Verify that UnstructuredMesh only supports host memory.
axom::detail::ArrayOps<axom::IndexType, MemorySpace::Dynamic>::fill(
domainIdPtr,
nPrev,
nNew - nPrev,
execution_space<axom::SEQ_EXEC>::allocatorID(),
dId);
}
}
SLIC_ASSERT(mesh.getNumberOfNodes() == contourNodeCount);
SLIC_ASSERT(mesh.getNumberOfCells() == contourCellCount);
}

MarchingCubesSingleDomain::MarchingCubesSingleDomain(RuntimePolicy runtimePolicy,
const conduit::Node& dom,
const std::string& coordsetName,
const std::string& maskField)
: m_runtimePolicy(runtimePolicy)
, m_dom(nullptr)
, m_ndim(0)
, m_coordsetPath("coordsets/" + coordsetName)
, m_fcnPath()
, m_maskPath(maskField.empty() ? std::string() : "fields/" + maskField)
{
SLIC_ASSERT_MSG(
isValidRuntimePolicy(runtimePolicy),
fmt::format("Policy '{}' is not a valid runtime policy", runtimePolicy));

setDomain(dom);
return;
}

void MarchingCubesSingleDomain::setDomain(const conduit::Node& dom)
{
SLIC_ASSERT_MSG(
!conduit::blueprint::mesh::is_multi_domain(dom),
"MarchingCubesSingleDomain is single-domain only. Try MarchingCubes.");

SLIC_ASSERT(dom.has_path(m_coordsetPath));
SLIC_ASSERT(dom["topologies/mesh/type"].as_string() == "structured");

if(!m_maskPath.empty())
{
SLIC_ASSERT(dom.has_path(m_maskPath + "/values"));
}

m_dom = &dom;

const conduit::Node& dimsNode =
m_dom->fetch_existing("topologies/mesh/elements/dims");

m_ndim = dimsNode.number_of_children();

SLIC_ASSERT(m_ndim >= 2 && m_ndim <= 3);

const conduit::Node& coordsValues = dom[m_coordsetPath + "/values"];
bool isInterleaved = conduit::blueprint::mcarray::is_interleaved(coordsValues);
SLIC_ASSERT_MSG(
!isInterleaved,
"MarchingCubes currently requires contiguous coordinates layout.");
}

void MarchingCubesSingleDomain::setFunctionField(const std::string& fcnField)
{
m_fcnPath = "fields/" + fcnField;
SLIC_ASSERT(m_dom->has_path(m_fcnPath));
SLIC_ASSERT(m_dom->fetch_existing(m_fcnPath + "/association").as_string() ==
"vertex");
SLIC_ASSERT(m_dom->has_path(m_fcnPath + "/values"));
}

void MarchingCubesSingleDomain::computeIsocontour(double contourVal)
{
SLIC_ASSERT_MSG(!m_fcnPath.empty(),
"You must call setFunctionField before computeIsocontour.");

allocateImpl();
m_impl->initialize(*m_dom, m_coordsetPath, m_fcnPath, m_maskPath);
m_impl->setContourValue(contourVal);
m_impl->markCrossings();
m_impl->scanCrossings();
m_impl->computeContour();
}

void MarchingCubesSingleDomain::allocateImpl()
{
using namespace detail::marching_cubes;
if(m_runtimePolicy == RuntimePolicy::seq)
{
m_impl = m_ndim == 2
? std::unique_ptr<ImplBase>(new MarchingCubesImpl<2, axom::SEQ_EXEC>)
: std::unique_ptr<ImplBase>(new MarchingCubesImpl<3, axom::SEQ_EXEC>);
}
#ifdef _AXOM_MC_USE_OPENMP
else if(m_runtimePolicy == RuntimePolicy::omp)
{
m_impl = m_ndim == 2
? std::unique_ptr<ImplBase>(new MarchingCubesImpl<2, axom::OMP_EXEC>)
: std::unique_ptr<ImplBase>(new MarchingCubesImpl<3, axom::OMP_EXEC>);
}
#endif
#ifdef _AXOM_MC_USE_CUDA
else if(m_runtimePolicy == RuntimePolicy::cuda)
{
m_impl = m_ndim == 2
? std::unique_ptr<ImplBase>(new MarchingCubesImpl<2, axom::CUDA_EXEC<256>>)
: std::unique_ptr<ImplBase>(new MarchingCubesImpl<3, axom::CUDA_EXEC<256>>);
}
#endif
#ifdef _AXOM_MC_USE_HIP
else if(m_runtimePolicy == RuntimePolicy::hip)
{
m_impl = m_ndim == 2
? std::unique_ptr<ImplBase>(new MarchingCubesImpl<2, axom::HIP_EXEC<256>>)
: std::unique_ptr<ImplBase>(new MarchingCubesImpl<3, axom::HIP_EXEC<256>>);
}
#endif
else
{
SLIC_ERROR(axom::fmt::format(
"MarchingCubesSingleDomain has no implementation for runtime policy {}",
m_runtimePolicy));
}
}

} // end namespace quest
} // end namespace axom

0 comments on commit 3276c6e

Please sign in to comment.