Adds a Delaunay triangulation capability to Quest#742
Conversation
c50c502 to
1d98f07
Compare
db0ad93 to
5573c19
Compare
ac1d500 to
cf7e47a
Compare
|
Note: This PR got pretty big as I was optimizing the Delaunay triangulation, which depended on large parts of |
| Array(ArrayOptions::Uninitialized, | ||
| IndexType num_elements, | ||
| IndexType capacity = 0, | ||
| int allocator_id = axom::detail::getAllocatorID<SPACE>()); |
There was a problem hiding this comment.
Note: axom::Array supports two features that can only be enabled through its constructors -- uninitialized data (via the ArrayOptions::Uninitialized tag) and a custom allocator (via the allocatorID argument) -- so I needed to add some extra overloads to support this.
I also improved the testing of this feature and ensured that when you copy and array with these features, the copy gets them as well.
There was a problem hiding this comment.
So the default behavior is to initialize the data and a user can choose to leave it uninitialized via an arg to the ctor? I think it should be the other way around.
There was a problem hiding this comment.
We did run into some issues with axom::Arrays of non-trivial type, e.g. axom::Array<BitSet> in ImplicitGrid, from the array data being uninitialized - e.g. assigning a new bitset like array[i] = BitsetType{...} would call the destructor on junk data, which led to some weird segfaults.
I think making uninitialized data opt-in also keeps axom::Array more drop-in compatible with std::vector, since vector's behavior is to default-init all elements.
There was a problem hiding this comment.
@kanye-quest -- is there a test for this?
I added this because, Delaunay::isValid() computation is using large bitsets, e.g. w/ millions of bit ( one per tetrahedron) and in ImplicitGrid::findCandidates(), the cost of initializing these bits to immediately get overwritten was substantial. I saw a substantial speedup when I disabled the initialization.
| // | ||
| // SPDX-License-Identifier: (BSD-3-Clause) | ||
|
|
||
| #include "gtest/gtest.h" |
There was a problem hiding this comment.
We didn't have any tests for the bit utility functions: popCount(), trailingZeros() and leadingZeros(), so I added new tests before making changes.
| // UNIT TESTS | ||
| //------------------------------------------------------------------------------ | ||
| TEST(utils_Utilities, log2) | ||
| TEST(utils_utilities, log2) |
There was a problem hiding this comment.
These tests weren't being run because the name didn't match the file name and we were filtering them out.
| #include "axom/config.hpp" | ||
| #include "axom/core/Macros.hpp" | ||
| #include "axom/core/Types.hpp" | ||
|
|
There was a problem hiding this comment.
I added compiler intrinsics for our CPU-based compilers, which are used extensively in slam::BitSet
In some runs of the Delaunay triangulation, axom::popCount() was taking >50% of the runtime (in a release build).
Note: the underlying optimized assembly is only enabled when the code is built in an architecture-aware manner, e.g. via the -march=native flag. I'm planning to add a ticket for us to enable this, e.g. via a new config option
| // almost all our examples are in 3D | ||
| constexpr int in3D = 3; | ||
|
|
||
| // primitives represented by doubles in 3D |
There was a problem hiding this comment.
While I was editing the primal tests, I modernized them a bit, e.g. type aliases via using instead of typedef and using the initializer-list constructors for primal's primitives instead of make_point, make_vector, ....
| @@ -6,14 +6,16 @@ | |||
| #ifndef AXOM_PRIMAL_SPHERE_HPP_ | |||
| #define AXOM_PRIMAL_SPHERE_HPP_ | |||
|
|
|||
There was a problem hiding this comment.
The changes to primal::Sphere change its API and implementation to be based on primal primitives -- e.g. it is based on primal::Point (which always has the right dimensions) instead of a pointer to an array.
| AXOM_HOST_DEVICE Point<T, NDIMS> operator+(const Vector<T, NDIMS>& V, | ||
| const Point<T, NDIMS>& P); | ||
|
|
||
| /*! |
There was a problem hiding this comment.
These changes are related to #708
I think this completes the set.
| const Point<T, 3>& p2, | ||
| const Point<T, 3>& p3) | ||
| const Point<T, 3>& p3, | ||
| double EPS = 1e-8) |
There was a problem hiding this comment.
This change significantly improves the efficiency of in_sphere() in 3D by reformulating the 5x5 determinant as a 4x4 determinant.
We have an explicit implementation of 4x4 determinants, but 5x5 determinants are based on a general LU decomposition.
| EXPECT_TRUE(sphere.getOrientation(tet[i]) == primal::ON_BOUNDARY); | ||
| } | ||
|
|
||
| // check that the orientation matches expectations |
There was a problem hiding this comment.
These tests ensure that we get the same result for in_sphere() as we do when we compute the circumsphere of a triangle/tet and then call getOrientation()
… size of the BitSet Previously, there was an assertion to check that the index was in range.
This makes it easier to loop over the cells and access the indexed data.
The previous implementation was using an ImplicitGrid. For each of vertex in a mesh with V vertices and E elements, it was getting a Bitset of O(E) bits, even though there are, on average, only O(1) elements that we actually need to check. When V and E are large, e.g. when V is O(E^6), it was taking 10-20x longer to validate the Delaunay complex than to generate it incrementally. The current approach is to use an ImplicitGrid to bootstrap the index and then copy over the precomputing intersections to a UniformGrid. This avoids excessive allocations associated with insertions into a UniformGrid.
…om a Point Other operators were added in #708
This allows us to use the function (which only works when `NDIMS==2`) without specifying the template parameter.
In anticipation of refactoring those functions.
Also adds some tests that check the the function formally satisfies the definition of orientation in terms of normals and dot products, and that `primal::orientation()` is consistent with `Plane::getOrientation()`
And adds some tests.
Also: * slight optimization for 2D area computation using this function. * optimizes some determinant-based computations in Triangle -- `ppedVolume()` and `physToBarycentric()`
Compares results to `Sphere::getOrientation()`.
Uses initializer lists for Points and Vectors as well as some more modern C++ idioms.
…y checks This is more efficient and also uses a different algorithm to validate the Delauanay complex than to construct it.
…` wants the number of zeros to its left
…y validation tests
Adds newlines at the end of files. Co-authored-by: Chris White <white238@llnl.gov>
* Revert change in default initialization for members of `axom::Array` * Use `constexpr` for some compile-time `const` variables
Thanks Arlie and Chris!
479b0cd to
d6ecb6a
Compare
| /** | ||
| * \brief Default constructor | ||
| * \note User need to call initializeBoundary(BoundingBox) before adding points. | ||
| * \note User must to call initializeBoundary(BoundingBox) before adding points. |
There was a problem hiding this comment.
I know that I'm not on the axom team and this is a small thing, but you have a typo here "User must to call". Probably you just want to say "User must call", no 'to'.
There was a problem hiding this comment.
Thanks Danny! I'll take care of it.
Summary
This PR is a feature
It adds a new Delaunay triangulation capability to quest in 2D and 3D.
The triangulation is built incrementally by inserting successive points and updating to mesh to ensure it satisfies the Delaunay condition: No vertices are in the circumsphere of any triangle/tetrahedron in the mesh. For each inserted point, we remove the triangles/tetrahedra in its "Delaunay cavity" and fill this hole with a new set of triangles/tetrahedra containing the new point (the "Delaunay ball").
The underlying triangulation is encoded using the Indexed mesh data structure with Adjacencies (IA), which is implemented using
slam's dynamic sets, relations and maps.This PR contains several other optimizations and improvements throughout axom in support of this new capability, primarily in
core,slamandprimal.quest::Delaunay.hppand slam'sIAMeshclass as well as optimizations and improvements to some geometric primitives inprimal, such asTriangle,TetrahedronandSphere.delaunay_triangulationwhich generates a triangle or tetrahedral mesh from a specified number of random points within a rectilinear bounding box.Credits: This capability was initially implemented by @raineyeh during Summer 2017 and has been sitting in a branch.
Update: Here's some rough performance data on the insertion rate (in vertices per second) while refactoring and improving the code:
I.e. the performance improved by ~200x.
There are still a few opportunities for optimization:
Update: The following shows the insertion of 100 random points within the unit square:
