https://mooseframework.inl.gov
TriangleManifold.C
Go to the documentation of this file.
1 //* This file is part of the MOOSE framework
2 //* https://mooseframework.inl.gov
3 //*
4 //* All rights reserved, see COPYRIGHT for full restrictions
5 //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6 //*
7 //* Licensed under LGPL 2.1, please see LICENSE for details
8 //* https://www.gnu.org/licenses/lgpl-2.1.html
9 
10 #include "TriangleManifold.h"
11 
12 #include "GeometryUtils.h"
13 #include "MooseError.h"
14 
15 #include "libmesh/mesh_tools.h"
16 #include "libmesh/unstructured_mesh.h"
17 
18 #include <algorithm>
19 #include <cmath>
20 #include <cstdint>
21 #include <cstring>
22 
24 {
25 std::uint64_t
26 packCell(const std::size_t y_index, const std::size_t z_index)
27 {
28  // The acceleration structure is indexed in yz because all rays travel in the +x direction.
29  return (static_cast<std::uint64_t>(y_index) << 32) | static_cast<std::uint64_t>(z_index);
30 }
31 
32 std::size_t
33 cellIndex(const Real value, const Real min_value, const Real cell_size, const std::size_t num_cells)
34 {
35  // Clamp to the valid range so near-boundary roundoff does not produce an invalid cell id.
36  if (num_cells <= 1)
37  return 0;
38 
39  const auto index = static_cast<long long>(std::floor((value - min_value) / cell_size));
40  if (index < 0)
41  return 0;
42  if (index >= static_cast<long long>(num_cells))
43  return num_cells - 1;
44  return static_cast<std::size_t>(index);
45 }
46 
48 {
49 public:
50  explicit SurfaceChecker(UnstructuredMesh & mesh) : libMesh::MeshTetInterface(mesh) {}
51  void triangulate() override { mooseError("SurfaceChecker is not meant for triangulation."); }
52  std::string improveAndValidate();
53 };
54 }
55 
56 TriangleManifold::TriangleManifold(MeshBase & mesh, const Real surface_tolerance)
57  : _mesh(mesh),
58  _surface_tolerance(surface_tolerance),
59  _bounding_box(MeshTools::create_bounding_box(_mesh)),
60  _point_locator(_mesh.sub_point_locator())
61 {
62  mooseAssert(_surface_tolerance > 0.0, "surface_tolerance must be strictly positive.");
63  mooseAssert(mesh.is_serial(), "Input manifold mesh must be serialized.");
64  mooseAssert(mesh.mesh_dimension() == 2, "Manifold mesh must be a surface.");
65 
66  // Finish topology validation and acceleration-structure setup before queries are allowed.
67  finalize();
68 
69  _point_locator->set_close_to_point_tol(_surface_tolerance);
70 }
71 
72 bool
73 TriangleManifold::contains(const Point & point) const
74 {
75  // Most points are rejected here before we perform any per-triangle work.
77  return false;
78 
79  // By design, near-surface points count as inside for downstream subdomain tagging.
80  if (pointOnSurface(point))
81  return true;
82 
83  // Candidate filtering keeps the parity test from touching every triangle in large surfaces.
84  const auto candidates = rayCandidates(point);
85  unsigned int num_hits = 0;
86 
87  for (const auto triangle_index : candidates)
88  {
89  const auto tri = _mesh.elem_ptr(triangle_index);
90  // Triangles wholly behind the ray origin cannot contribute to the parity count.
91  if (tri->loose_bounding_box().max()(0) < point(0) - _surface_tolerance)
92  continue;
93 
94  switch (rayIntersectsTriangle(point, *tri))
95  {
97  break;
99  // Standard odd/even counting on a closed surface.
100  ++num_hits;
101  break;
103  // Edge and vertex grazing hits are exactly where parity counting becomes brittle.
104  return containsBySolidAngle(point);
105  }
106  }
107 
108  return num_hits % 2;
109 }
110 
111 void
113 {
114  // Validate that the mesh can be used as a manifold
115  // We use the same logic as determining if the mesh can the tetrahedralized.
116  auto umesh = dynamic_cast<UnstructuredMesh *>(&_mesh);
118  auto msg = checker.improveAndValidate();
119  if (!msg.empty())
120  mooseError(
121  "The inputted surface mesh cannot be treated as manifold for the following reasons:\n",
122  msg);
123 
125 }
126 
127 void
129 {
130  // Since every containment ray travels in +x, only y and z are needed to choose candidate
131  // triangles for the parity test.
132  const auto extent_y =
134  const auto extent_z =
136 
137  // Choose a roughly square yz grid scaled by aspect ratio so candidate lists stay short.
138  const auto target_cells =
139  std::max<dof_id_type>(1, static_cast<dof_id_type>(std::sqrt(numTriangles())));
140  const auto aspect = std::sqrt(extent_y / extent_z);
141 
142  _num_y_cells = std::max<dof_id_type>(
143  1, static_cast<dof_id_type>(std::lround(std::sqrt(target_cells) * aspect)));
144  _num_z_cells = std::max<std::size_t>(
145  1, static_cast<std::size_t>(std::ceil(static_cast<Real>(target_cells) / _num_y_cells)));
146 
147  _y_min = boundingBox().min()(1);
148  _z_min = boundingBox().min()(2);
149  _y_cell_size = extent_y / _num_y_cells;
150  _z_cell_size = extent_z / _num_z_cells;
151 
152  for (const auto elem : _mesh.active_element_ptr_range())
153  {
154  const auto triangle_index = elem->id();
155  const auto bbox = elem->loose_bounding_box();
156 
157  const auto y_start =
159  const auto y_stop =
161  const auto z_start =
163  const auto z_stop =
165 
166  // Insert the triangle into every grid cell touched by its yz projection.
167  for (const auto iy : make_range(y_start, y_stop + 1))
168  for (const auto iz : make_range(z_start, z_stop + 1))
169  _ray_grid[TriangleManifoldUtils::packCell(iy, iz)].push_back(triangle_index);
170  }
171 }
172 
173 bool
175 {
176  // Inflate the global box by the tolerance so near-surface points are not rejected too early.
177  return point(0) >= boundingBox().min()(0) - _surface_tolerance &&
178  point(0) <= boundingBox().max()(0) + _surface_tolerance &&
179  point(1) >= boundingBox().min()(1) - _surface_tolerance &&
180  point(1) <= boundingBox().max()(1) + _surface_tolerance &&
181  point(2) >= boundingBox().min()(2) - _surface_tolerance &&
182  point(2) <= boundingBox().max()(2) + _surface_tolerance;
183 }
184 
185 bool
187 {
188  return (*_point_locator)(point) != nullptr;
189 }
190 
193 {
194  // This is a fixed-direction Moller-Trumbore style ray/triangle intersection test.
195  static const Point direction(1.0, 0.0, 0.0);
196 
197  const Point edge1 = tri.node_ref(1) - tri.node_ref(0);
198  const Point edge2 = tri.node_ref(2) - tri.node_ref(0);
199  const Point h = direction.cross(edge2);
200  const Real determinant = edge1 * h;
201  const Real characteristic_length = std::max(edge1.norm(), edge2.norm());
202 
203  if (std::abs(determinant) <= _surface_tolerance * characteristic_length)
204  // Nearly parallel triangles are ignored because they do not provide a stable parity event.
205  return RayIntersection::Miss;
206 
207  const Real inv_determinant = 1.0 / determinant;
208  const Point s = point - tri.node_ref(0);
209  const Real u = inv_determinant * (s * h);
210  if (u < 0.0 || u > 1.0)
211  return RayIntersection::Miss;
212 
213  const Point q = s.cross(edge1);
214  const Real v = inv_determinant * (direction * q);
215  if (v < 0.0 || u + v > 1.0)
216  return RayIntersection::Miss;
217 
218  const Real t = inv_determinant * (edge2 * q);
219  if (t < 0.0)
220  // Intersections behind the ray origin do not contribute to +x parity counting.
221  return RayIntersection::Miss;
222  if (t <= _surface_tolerance)
223  // Hits too close to the ray origin are treated as ambiguous boundary situations.
225 
226  const Point intersection = point + t * direction;
227  const auto tolerance_sq = _surface_tolerance * _surface_tolerance;
228  if (geom_utils::pointSegmentDistanceSq(intersection, tri.node_ref(0), tri.node_ref(1)) <=
229  tolerance_sq ||
230  geom_utils::pointSegmentDistanceSq(intersection, tri.node_ref(1), tri.node_ref(2)) <=
231  tolerance_sq ||
232  geom_utils::pointSegmentDistanceSq(intersection, tri.node_ref(2), tri.node_ref(0)) <=
233  tolerance_sq)
234  // Edge and vertex hits are where odd/even parity counting is the least reliable.
236 
237  return RayIntersection::Hit;
238 }
239 
240 bool
242 {
243  // For a closed oriented mesh, the total solid angle is approximately +-4\pi inside and 0
244  // outside. Taking parity over components handles nested shells cleanly.
245  Real total_angle = 0.0;
246  for (const auto elem : _mesh.active_element_ptr_range())
247  total_angle +=
248  geom_utils::solidAngle(point, elem->node_ref(0), elem->node_ref(1), elem->node_ref(2));
249 
250  return std::abs(total_angle) > 2.0 * libMesh::pi;
251 }
252 
253 std::vector<dof_id_type>
255 {
256  // If the query is outside the manifold's yz extent, the fixed +x ray cannot hit anything.
257  if (point(1) < boundingBox().min()(1) - _surface_tolerance ||
258  point(1) > boundingBox().max()(1) + _surface_tolerance ||
259  point(2) < boundingBox().min()(2) - _surface_tolerance ||
260  point(2) > boundingBox().max()(2) + _surface_tolerance)
261  return {};
262 
263  const auto y_index =
265  const auto z_index =
267  const auto it = _ray_grid.find(TriangleManifoldUtils::packCell(y_index, z_index));
268  if (it == _ray_grid.end())
269  return {};
270 
271  // Return by value to keep the helper simple and avoid exposing internal storage.
272  return it->second;
273 }
274 
275 std::string
277 {
279  auto result = improve_hull_integrity();
280 
281  if (result.empty())
282  return "";
283 
284  std::ostringstream err_msg;
285  if (result.count(NON_TRI3))
286  err_msg << "- At least one non-Tri3 element was found.\n" << std::endl;
287  if (result.count(MISSING_NEIGHBOR))
288  err_msg << "- At least one triangle without three neighbors was found.\n" << std::endl;
289  if (result.count(EMPTY_MESH))
290  err_msg << "- The surface mesh was empty\n" << std::endl;
291  if (result.count(MISSING_BACKLINK))
292  err_msg << "- At least one triangle neighbor without a return neighbor link was found.\n";
293  if (result.count(BAD_NEIGHBOR_NODES))
294  err_msg << "- At least one triangle neighbor without expected node links was found.\n";
295  if (result.count(NON_ORIENTED))
296  err_msg << "- At least one triangle neighbor with an inconsistent orientation was found.\n";
297  if (result.count(BAD_NEIGHBOR_LINKS))
298  err_msg << "- At least one triangle neighbor with inconsistent node and neighbor links was "
299  "found.\n";
300  if (result.count(DEGENERATE_ELEMENT))
301  err_msg << "- At least one input triangle is degenerate, with near-zero area relative to the "
302  "manifold.\n";
303  if (result.count(DEGENERATE_MESH))
304  err_msg << "- Mesh is degenerate, with zero thickness in at least one direction.\n";
305  return err_msg.str();
306 }
bool containsBySolidAngle(const Point &point) const
Robust fallback containment query based on accumulated solid angle.
MetaPhysicL::DualNumber< V, D, asd > abs(const MetaPhysicL::DualNumber< V, D, asd > &a)
Definition: EigenADReal.h:50
const std::unique_ptr< libMesh::PointLocatorBase > _point_locator
Pre-built point locator for fast proximity-to-surface detection.
Real _z_min
Minimum global z coordinate used to map query points into yz-grid bins.
Real _z_cell_size
Width of one yz-grid cell in the z direction.
Real _y_cell_size
Width of one yz-grid cell in the y direction.
RayIntersection
Result of intersecting the positive x-direction ray with a triangle.
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application...
Definition: MooseError.h:311
libMesh::BoundingBox create_bounding_box(const MeshBase &mesh)
bool pointOnSurface(const Point &point) const
Detect whether a query point lies on or extremely near the manifold surface.
Real solidAngle(const Point &point, const Point &v0, const Point &v1, const Point &v2)
Compute the signed solid angle subtended by one oriented triangle at the query point.
MeshBase & mesh
The following methods are specializations for using the libMesh::Parallel::packed_range_* routines fo...
Real _y_min
Minimum global y coordinate used to map query points into yz-grid bins.
MeshTetInterface(UnstructuredMesh &mesh)
const Real _surface_tolerance
Absolute tolerance used throughout validation and geometric classification.
auto max(const L &left, const R &right)
std::size_t _num_z_cells
Number of yz-grid bins in the z direction.
const Node & node_ref(const unsigned int i) const
Real value(unsigned n, unsigned alpha, unsigned beta, Real x)
const Point & min() const
TypeVector< typename CompareTypes< Real, T2 >::supertype > cross(const TypeVector< T2 > &v) const
Real pointSegmentDistanceSq(const Point &point, const Point &a, const Point &b)
Compute the squared distance from a point to a 3-D line segment.
std::uint64_t packCell(const std::size_t y_index, const std::size_t z_index)
std::unordered_map< dof_id_type, std::vector< dof_id_type > > _ray_grid
Lookup from packed yz-grid cell index to triangles that could intersect the +x query ray...
void buildCandidateGrid()
Build the yz-plane lookup grid used to accelerate +x ray queries.
bool pointInsideBoundingBox(const Point &point) const
Cheap global bounding-box rejection for containment queries.
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
std::size_t cellIndex(const Real value, const Real min_value, const Real cell_size, const std::size_t num_cells)
CTSub CT_OPERATOR_BINARY CTMul CTCompareLess CTCompareGreater CTCompareEqual _arg template * sqrt(_arg)) *_arg.template D< dtag >()) CT_SIMPLE_UNARY_FUNCTION(tanh
const libMesh::BoundingBox & boundingBox() const
std::size_t numTriangles() const
TriangleManifold(MeshBase &mesh, const Real surface_tolerance)
Build a manifold classifier from a prepared surface mesh.
IntRange< T > make_range(T beg, T end)
std::set< SurfaceIntegrity > improve_hull_integrity()
std::vector< dof_id_type > rayCandidates(const Point &point) const
Get the subset of triangles whose yz extents may intersect the query ray.
SurfaceChecker(UnstructuredMesh &mesh)
const Point & max() const
auto min(const L &left, const R &right)
RayIntersection rayIntersectsTriangle(const Point &point, const libMesh::Elem &tri) const
Intersect a positive x-direction ray with a single triangle.
uint8_t dof_id_type
bool contains(const Point &point) const
void finalize()
Complete post-parse validation and acceleration-structure setup.
const Real pi
std::size_t _num_y_cells
Number of yz-grid bins in the y direction.