https://mooseframework.inl.gov
Loading...
Searching...
No Matches
XYZDelaunayGenerator.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
11
12#include "CastUniquePointer.h"
13#include "MooseMeshUtils.h"
14#include "MooseUtils.h"
16
17#include "libmesh/elem.h"
18#include "libmesh/int_range.h"
19#include "libmesh/mesh_modification.h"
20#include "libmesh/mesh_netgen_interface.h"
21#include "libmesh/mesh_serializer.h"
22#include "libmesh/parsed_function.h"
23#include "libmesh/replicated_mesh.h"
24
26
27namespace std
28{
29template <>
30struct hash<std::tuple<libMesh::Point, libMesh::Point, libMesh::Point>>
31{
32 std::size_t operator()(const std::tuple<libMesh::Point, libMesh::Point, libMesh::Point> & p) const
33 {
34 std::size_t seed = 0;
35 libMesh::boostcopy::hash_combine(seed, std::hash<libMesh::Point>()(std::get<0>(p)));
36 libMesh::boostcopy::hash_combine(seed, std::hash<libMesh::Point>()(std::get<1>(p)));
37 libMesh::boostcopy::hash_combine(seed, std::hash<libMesh::Point>()(std::get<2>(p)));
38 return seed;
39 }
40};
41
42} // namespace std
43
46{
48
49 MooseEnum algorithm("BINARY EXHAUSTIVE", "BINARY");
50
51 params.addRequiredParam<MeshGeneratorName>(
52 "boundary",
53 "The input MeshGenerator defining the output outer boundary. The input mesh (the output mesh "
54 "of the input mesh generator) can either "
55 "include 3D volume elements or 2D surface elements.");
56
57 params.addParam<SubdomainName>("output_subdomain_name",
58 "Subdomain name to set on new triangles.");
59
60 params.addParam<BoundaryName>(
61 "output_boundary",
62 "Boundary name to set on new outer boundary. Default ID: 0 if no hole meshes are stitched; "
63 "or maximum boundary ID of all the stitched hole meshes + 1.");
64 params.addParam<std::vector<BoundaryName>>(
65 "hole_boundaries",
66 "Boundary names to set on holes. Default IDs are numbered up from 1 if no hole meshes are "
67 "stitched; or from maximum boundary ID of all the stitched hole meshes + 2.");
68
69 params.addParam<bool>("smooth_triangulation",
70 false,
71 "Whether to do Laplacian mesh smoothing on the generated triangles.");
72 params.addParam<std::vector<MeshGeneratorName>>(
73 "holes",
74 {},
75 "The MeshGenerators that create meshes defining the holes. A hole mesh must contain either "
76 "3D volume "
77 "elements where the external surface of the mesh works as the closed manifold that defines "
78 "the hole, or 2D surface elements that form the closed manifold that defines the hole.");
79 params.addParam<std::vector<bool>>(
80 "stitch_holes", std::vector<bool>(), "Whether to stitch to the mesh defining each hole.");
81 params.addParam<bool>("convert_holes_for_stitching",
82 false,
83 "Whether to convert the 3D hole meshes with non-TRI3 surface sides into "
84 "a compatible form.");
85
86 MooseEnum conversion_method("ALL SURFACE", "ALL");
87 params.addParam<MooseEnum>(
88 "conversion_method",
89 conversion_method,
90 "The method to convert 3D hole meshes into compatible meshes. Options are "
91 "ALL: convert all elements into TET4; SURFACE: convert all exterior surface elements using a "
92 "mix of pyramids and tetrahedrals.");
93 params.addParam<SubdomainName>("converted_tet_element_subdomain_name_suffix",
94 "to_tet",
95 "The suffix to be added to the original subdomain name for the "
96 "subdomains containing the elements converted to TET4.");
97 params.addParam<SubdomainName>(
98 "converted_pyramid_element_subdomain_name_suffix",
99 "to_pyramid",
100 "The suffix to be added to the original subdomain name for the subdomains containing the "
101 "elements converted to PYRAMID5.");
102
103 params.addRangeCheckedParam<Real>(
104 "desired_volume",
105 0,
106 "desired_volume>=0",
107 "Desired (maximum) tetrahedral volume, or 0 to skip uniform refinement");
108
109 params.addParam<bool>(
110 "combined_stitching",
111 false,
112 "Whether to stitch all holes in one combined stitching step. This is efficient if a great "
113 "number of holes are to be stitched. But it is hard to debug if problems arise.");
114 params.addParam<MooseEnum>(
115 "algorithm",
116 algorithm,
117 "Control the use of binary search for the nodes of the stitched surfaces.");
118 params.renameParam("algorithm", "stitching_algorithm", "12/10/2026");
119 params.addParam<bool>(
120 "verbose_stitching", false, "Whether mesh hole stitching should have verbose output.");
121
122 MooseEnum tet_elem_type("TET4 TET10 TET14 DEFAULT", "DEFAULT");
123 params.addParam<MooseEnum>(
124 "tet_element_type", tet_elem_type, "Type of the tetrahedral elements to be generated.");
125
126 params.addClassDescription(
127 "Creates tetrahedral 3D meshes within boundaries defined by input meshes.");
128
129 return params;
130}
131
133 : MeshGenerator(parameters),
134 _bdy_ptr(getMesh("boundary")),
135 _desired_volume(getParam<Real>("desired_volume")),
136 _output_subdomain_id(0),
137 _smooth_tri(getParam<bool>("smooth_triangulation")),
138 _hole_ptrs(getMeshes("holes")),
139 _stitch_holes(getParam<std::vector<bool>>("stitch_holes")),
140 _convert_holes_for_stitching(getParam<bool>("convert_holes_for_stitching")),
141 _conversion_method(parameters.get<MooseEnum>("conversion_method")),
142 _combined_stitching(parameters.get<bool>("combined_stitching")),
143 _algorithm(parameters.get<MooseEnum>("algorithm")),
144 _verbose_stitching(parameters.get<bool>("verbose_stitching")),
145 _tet_elem_type(parameters.get<MooseEnum>("tet_element_type"))
146{
147 if (!_stitch_holes.empty() && _stitch_holes.size() != _hole_ptrs.size())
148 paramError("stitch_holes", "Need one stitch_holes entry per hole, if specified.");
149
150 if (isParamValid("hole_boundaries"))
151 {
152 auto & hole_boundaries = getParam<std::vector<BoundaryName>>("hole_boundaries");
153 if (hole_boundaries.size() != _hole_ptrs.size())
154 paramError("hole_boundaries", "Need one hole_boundaries entry per hole, if specified.");
155 }
156
157 if (isParamSetByUser("conversion_method") && !_convert_holes_for_stitching)
159 "conversion_method",
160 "This parameter is only applicable when convert_holes_for_stitching is set to true.");
161}
162
163std::unique_ptr<MeshBase>
165{
166#ifdef LIBMESH_HAVE_NETGEN
167 // Put the boundary mesh in a local pointer
168 std::unique_ptr<UnstructuredMesh> mesh =
169 dynamic_pointer_cast<UnstructuredMesh>(std::move(_bdy_ptr));
170
171 // The libMesh Netgen removes all the sideset info
172 // But it keeps some nodeset info for the retained nodes
173 // We need to clear these nodeset info as they could overlap with upcoming boundary info
174 mesh->get_boundary_info().clear_boundary_node_ids();
175
176 // Map the requested element type to a libMesh tet type. "DEFAULT" (and "TET4")
177 // keep the libMesh default of first-order TET4. "TET10"/"TET14" request
178 // second-/complete-order output; libMesh's NetGen interface then inherits any
179 // curved mid-edge nodes from a second-order (TRI6/TRI7) input surface.
180 const libMesh::ElemType tet_type = (_tet_elem_type == "TET10") ? libMesh::TET10
181 : (_tet_elem_type == "TET14") ? libMesh::TET14
183 const bool second_order_output = (tet_type != libMesh::TET4);
184
185 // Get ready to triangulate its boundary
187
189
191
192 ngint.elem_type() = tet_type;
193
194 // The hole meshes will be used for hole boundary identification and optionally for stitching.
195 // if a hole mesh contains 3D volume elements but has non-TRI3 surface side elements, it cannot be
196 // used directly for stitching. But it can be converted into an all-TET4 mesh to support hole
197 // boundary identification
198 for (const auto hole_i : index_range(_hole_ptrs))
199 {
200 UnstructuredMesh & hole_mesh = dynamic_cast<UnstructuredMesh &>(**_hole_ptrs[hole_i]);
201 libMesh::MeshSerializer serial_hole(hole_mesh);
202 // Check the dimension of the hole mesh
203 // We do not need to worry about element order here as libMesh checks it
204 std::set<ElemType> hole_elem_types;
205 std::set<unsigned short> hole_elem_dims;
206 std::vector<std::pair<dof_id_type, unsigned int>> hole_elem_external_sides;
207 // Whether the hole mesh already carries mid-edge nodes (second order or higher). We
208 // check the element order directly: side_ptr() returns a first-order side even for a
209 // second-order element, so surface side types cannot be used to detect order.
210 bool hole_has_midpoints = false;
211 for (auto elem : hole_mesh.element_ptr_range())
212 {
213 hole_elem_dims.emplace(elem->dim());
214 if (elem->default_order() != libMesh::FIRST)
215 hole_has_midpoints = true;
216
217 // For a 3D element, we need to check the surface side element type instead of the element
218 // type. It is also a good opportunity to define the external boundary.
219 if (elem->dim() == 3)
220 for (auto s : make_range(elem->n_sides()))
221 {
222 // Note that the entire external boundary is used for defining the hole at this time
223 if (!elem->neighbor_ptr(s))
224 {
225 hole_elem_types.emplace(elem->side_ptr(s)->type());
226 hole_elem_external_sides.emplace_back(elem->id(), s);
227 }
228 }
229 // For a non-3D element, we just need to record the element type
230 else
231 hole_elem_types.emplace(elem->type());
232 }
233 if (hole_elem_dims.size() != 1 || *hole_elem_dims.begin() < 2)
235 "holes",
236 "All elements in a hole mesh must have the same dimension that is either 2D or 3D.");
237 else if (*hole_elem_dims.begin() == 3)
238 {
239 const bool stitching_this_hole = _stitch_holes.size() && _stitch_holes[hole_i];
240
241 // A stitched quadratic hole cannot conform to a first-order tetrahedralization.
242 if (!second_order_output && stitching_this_hole && hole_has_midpoints)
244 "tet_element_type",
245 "Cannot use first order elements with stitched quadratic element holes. Please "
246 "specify a higher-order tet_element_type or reduce the order of the hole inputs.");
247
248 // For 3D meshes, if there are non-TRI3 surface side elements
249 // (1) if no stitching is needed, we can just convert the whole mesh into TET to facilitate
250 // boundary identification (2) if stitching is needed, we can still convert and stitch, but
251 // that would modify the input hole mesh.
252 // Note a TET10/TET14 hole reports first-order (TRI3) surface sides here, so it is not
253 // converted; NetGen strips and restores its curved mid-edge nodes internally.
254 if (*hole_elem_types.begin() != ElemType::TRI3 || hole_elem_types.size() > 1)
255 {
256 if (stitching_this_hole && !_convert_holes_for_stitching)
257 paramError("holes",
258 "3D hole meshes with non-TRI3 surface elements cannot be stitched without "
259 "converting them to TET4. Consider setting convert_holes_for_stitching=true.");
260 else if (stitching_this_hole && _conversion_method == "SURFACE")
261 {
262 // collect the subdomain ids of the original mesh
263 std::set<subdomain_id_type> hole_mesh_original_subdomain_ids;
264 for (auto elem_it = hole_mesh.active_elements_begin();
265 elem_it != hole_mesh.active_elements_end();
266 elem_it++)
267 hole_mesh_original_subdomain_ids.emplace((*elem_it)->subdomain_id());
268
269 // Create a transition layer with triangle sides on the external boundary of the hole
270 BoundaryID temp_ext_bid = MooseMeshUtils::getNextFreeBoundaryID(hole_mesh);
271 for (const auto & hees : hole_elem_external_sides)
272 hole_mesh.get_boundary_info().add_side(hees.first, hees.second, temp_ext_bid);
274 hole_mesh, std::vector<BoundaryName>({std::to_string(temp_ext_bid)}), 1, false);
275 hole_mesh.get_boundary_info().remove_id(temp_ext_bid);
276
277 // Set subdomain names on converted elements
278 try
279 {
281 hole_mesh,
282 hole_mesh_original_subdomain_ids,
283 *hole_mesh_original_subdomain_ids.rbegin() + 1,
284 getParam<SubdomainName>("converted_tet_element_subdomain_name_suffix"),
285 getParam<SubdomainName>("converted_pyramid_element_subdomain_name_suffix"));
286 }
287 catch (const std::exception & e)
288 {
289 if (((std::string)e.what()).compare(26, 4, "TET4") == 0)
290 paramError("converted_tet_element_subdomain_name_suffix", e.what());
291 else // if (((std::string)e.what()).compare(26, 7, "PYRAMID5") == 0)
292 paramError("converted_pyramid_element_subdomain_name_suffix", e.what());
293 }
294
295 // MeshSerializer's destructor calls delete_remote_elements()
296 // For replicated meshes, we need to refresh the neighbor information
297 // Also, not doing prepare_for_use() causes an error in DEBUG mode
298 // in MeshTools::libmesh_assert_topology_consistent_procids()
299 hole_mesh.prepare_for_use();
300 }
301 else
302 MeshTools::Modification::all_tri(**_hole_ptrs[hole_i]);
303 }
304
305 // For second-order output, raise a stitched hole's order so its seam faces have
306 // matching mid-edge (and, for TET14, face-center) nodes. A hole that already carries
307 // the needed nodes keeps its inherited curvature. For TET14 output a TET10 hole must
308 // still be raised, because the added face-center node lies on the stitched face.
309 if (second_order_output && stitching_this_hole)
310 {
311 if (!hole_mesh.is_prepared())
312 hole_mesh.prepare_for_use();
313 // Re-read the current order; the hole may have been converted to TET4 above.
314 ElemType cur_type = libMesh::INVALID_ELEM;
315 for (const auto elem : hole_mesh.element_ptr_range())
316 {
317 cur_type = elem->type();
318 break;
319 }
320 if (tet_type == libMesh::TET10 && cur_type != libMesh::TET10 && cur_type != libMesh::TET14)
321 hole_mesh.all_second_order();
322 else if (tet_type == libMesh::TET14 && cur_type != libMesh::TET14)
323 hole_mesh.all_complete_order();
324 }
325 }
326 else // if (*hole_elem_dims.begin() == 2)
327 {
328 // There is no point to stitch a 2D hole mesh, so we throw an error if this is attempted
329 if (_stitch_holes.size() && _stitch_holes[hole_i])
330 paramError("holes",
331 "the hole mesh with index " + std::to_string(hole_i) +
332 " is a 2D mesh, for which stitching onto a 3D mesh does not make sense.");
333 }
334 }
335
336 std::unique_ptr<std::vector<std::unique_ptr<UnstructuredMesh>>> ngholes =
337 std::make_unique<std::vector<std::unique_ptr<UnstructuredMesh>>>();
338
339 // The libMesh Netgen interface will modify hole meshes in-place, so
340 // we make copies to pass in.
341 for (std::unique_ptr<MeshBase> * hole_ptr : _hole_ptrs)
342 {
343 // How did we never add a ReplicatedMesh(MeshBase&) constructor in
344 // libMesh?
345 const UnstructuredMesh & hole = dynamic_cast<UnstructuredMesh &>(**hole_ptr);
346 ngholes->push_back(std::make_unique<ReplicatedMesh>(hole));
347
348#ifdef LIBMESH_ENABLE_UNIQUE_ID
349 // That constructor inherits the source mesh's next-unique-id counter, and a
350 // DistributedMesh deliberately keeps a different counter on every rank. A
351 // ReplicatedMesh has to agree on it (see MeshBase::set_next_unique_id), or the
352 // elements libMesh's Netgen interface adds to this copy when it converts the hole
353 // to a surface mesh get different unique ids on different ranks -- which makes the
354 // Hilbert sort keys used by the partitioner's global-index lookup disagree.
355 UnstructuredMesh & hole_copy = *ngholes->back();
356 hole_copy.set_next_unique_id(hole_copy.parallel_max_unique_id());
357#endif
358 }
359
360 if (!_hole_ptrs.empty())
361 ngint.attach_hole_list(std::move(ngholes));
362
363 ngint.triangulate();
364
365 if (isParamValid("output_subdomain_name"))
366 {
367 auto output_subdomain_name = getParam<SubdomainName>("output_subdomain_name");
369
370 if (_output_subdomain_id == Elem::invalid_subdomain_id)
371 {
372 // We'll probably need to make a new ID, then
374
375 // But check the hole meshes for our output subdomain name too
376 for (auto & hole_ptr : _hole_ptrs)
377 {
378 auto possible_sbdid = MooseMeshUtils::getSubdomainID(output_subdomain_name, **hole_ptr);
379 // Huh, it was in one of them
380 if (possible_sbdid != Elem::invalid_subdomain_id)
381 {
382 _output_subdomain_id = possible_sbdid;
383 break;
384 }
387 }
388
389 mesh->set_subdomain_name(_output_subdomain_id, output_subdomain_name);
390 }
391 }
392
394 for (auto elem : mesh->element_ptr_range())
395 {
396 mooseAssert(elem->type() == tet_type, "Unexpected element type found in triangulation");
397
398 elem->subdomain_id() = _output_subdomain_id;
399
400 // I do not trust Laplacian mesh smoothing not to invert
401 // elements near reentrant corners. Eventually we'll add better
402 // smoothing options, but even those might have failure cases.
403 // Better to always do extra tests here than to ever let users
404 // try to run on a degenerate mesh.
405 if (elem->is_flipped())
406 {
407 if (_smooth_tri)
408 mooseError("Inverted element found in triangulation.\n"
409 "Laplacian smoothing can create these at reentrant corners; disable it?");
410 else
411 mooseError("Unexplained inverted element found in triangulation.\n");
412 }
413 }
414
415 // Setting subdomain_id()s invalidates subdomain caches
416 mesh->unset_has_cached_elem_data();
417
418 const bool use_binary_search = (_algorithm == "BINARY");
419
420 // The hole meshes are specified by the user, so they could have any
421 // BCID or no BCID or any combination of BCIDs on their outer
422 // boundary, so we'll have to set our own BCID to use for stitching
423 // there. We'll need to check all the holes for used BCIDs, if we
424 // want to pick a new ID on hole N that doesn't conflict with any
425 // IDs on hole M < N (or with the IDs on the new triangulation)
426
427 // The Netgen generated mesh does not have hole and output boundary ids,
428 // which is different from its 2D counterpart.
429 // So, we need to assign boundary ids to the hole boundaries and output boundary
430 // By default, we assign the hole boundary ids to be 1,2,..., N
431 // and the output boundary id to be 0
432 // We will need a different set of boundary ids if we are stitching holes
433
434 // end_bcid is one more than the maximum of default output boundary id and hole boundary ids
435 const boundary_id_type end_bcid = _hole_ptrs.size() + 1;
436
437 // For the hole meshes that need to be stitched, we would like to make sure the hole boundary ids
438 // and output boundary id are not conflicting with the existing boundary ids of the hole meshes to
439 // be stitched.
440 BoundaryID free_boundary_id = 0;
441 // We need to get a free boundary id that is larger than the existing boundary ids in any hole
442 // meshes
443 if (_stitch_holes.size())
444 {
445 for (auto hole_i : index_range(_hole_ptrs))
446 {
447 if (_stitch_holes[hole_i])
448 {
449 free_boundary_id =
450 std::max(free_boundary_id, MooseMeshUtils::getNextFreeBoundaryID(**_hole_ptrs[hole_i]));
451 (*_hole_ptrs[hole_i])->comm().max(free_boundary_id);
452 }
453 }
454 }
455 // new_hole_bcid is used to ensure the boundary ids used for stitching have no conflicts
456 // If we shift the default hole boundary ids and output boundary id by free_boundary_id, we need
457 // to have new_hole_bcid that is larger than any existing boundary ids in the hole meshes and the
458 // Netgen generated mesh
459 boundary_id_type new_hole_bcid = end_bcid + free_boundary_id;
460
461 auto hole_boundaries = isParamValid("hole_boundaries")
462 ? getParam<std::vector<BoundaryName>>("hole_boundaries")
463 : std::vector<BoundaryName>();
464 const BoundaryName output_boundary =
465 isParamValid("output_boundary") ? getParam<BoundaryName>("output_boundary") : BoundaryName();
466
467 // if outer boundary id is not specified by a numeric output_boundary, we just assign
468 // free_boundary_id to the output boundary
469 const std::vector<BoundaryID> output_boundary_id =
470 isParamValid("output_boundary")
471 ? (MooseUtils::isDigits(output_boundary)
472 ? std::vector<BoundaryID>(
473 1, MooseMeshUtils::getIDFromName<BoundaryName, BoundaryID>(output_boundary))
474 : std::vector<BoundaryID>(1, free_boundary_id))
475 : std::vector<BoundaryID>();
476 // Similarly, set the hole boundary ids one by one
477 std::vector<BoundaryID> hole_boundary_ids;
478 if (isParamValid("hole_boundaries"))
479 for (auto h : index_range(hole_boundaries))
480 hole_boundary_ids.push_back(
481 MooseUtils::isDigits(hole_boundaries[h])
482 ? MooseMeshUtils::getIDFromName<BoundaryName, BoundaryID>(hole_boundaries[h])
483 : h + 1 + free_boundary_id);
484
485 // if large ids are used in the hole boundaries or the output boundary,
486 // we need to make sure the new_hole_bcid is larger than them
487 if (hole_boundary_ids.size())
488 for (auto h : index_range(_hole_ptrs))
489 new_hole_bcid = std::max(new_hole_bcid, boundary_id_type(hole_boundary_ids[h] + 1));
490 if (output_boundary_id.size())
491 new_hole_bcid = std::max(new_hole_bcid, boundary_id_type(output_boundary_id[0] + 1));
492
493 bool doing_stitching = false;
494
495 for (auto hole_i : index_range(_hole_ptrs))
496 {
497 const MeshBase & hole_mesh = **_hole_ptrs[hole_i];
498 auto & hole_boundary_info = hole_mesh.get_boundary_info();
499 const std::set<boundary_id_type> & local_hole_bcids = hole_boundary_info.get_boundary_ids();
500
501 if (!local_hole_bcids.empty())
502 new_hole_bcid = std::max(new_hole_bcid, boundary_id_type(*local_hole_bcids.rbegin() + 1));
503 hole_mesh.comm().max(new_hole_bcid);
504
505 if (_stitch_holes.size() && _stitch_holes[hole_i])
506 doing_stitching = true;
507 }
508 // Now we can define boundaries to assist with stitching
509 const boundary_id_type inner_bcid = new_hole_bcid + 1;
510
511 // libMesh mesh stitching still requires a serialized mesh, and it's
512 // cheaper to do that once than to do it once-per-hole
513 libMesh::MeshSerializer serial(*mesh, doing_stitching);
514
515 // We'll be looking for any sides that match between hole meshes and
516 // the newly triangulated mesh, to apply bcids accordingly. We
517 // can't key on Elem::key() here, because that depends on node ids
518 // that differ from mesh to mesh. We can't use centroids here,
519 // because that depends on rounding error in order of operations
520 // that can differ from mesh to mesh. The node locations themselves
521 // should always match up exactly, though, so let's use (sorted!)
522 // tuples of those to map from nodes to elements and side numbers.
523 // Also added a Boolean to check if the face is already used
524 std::unordered_map<std::tuple<Point, Point, Point>,
525 std::pair<std::pair<Elem *, unsigned int>, bool>>
526 mesh_faces;
527
528 auto sorted_point_tuple = [](Elem & elem, unsigned int side)
529 {
530 // Key faces on their vertex corners only. For second-order tets (TET10/TET14)
531 // nodes_on_side() also returns mid-edge/face nodes, so we filter to vertices to
532 // keep a 3-corner key. For TET4 this filter is a no-op.
533 const std::vector<unsigned int> nodes_on_side = elem.nodes_on_side(side);
534 std::vector<Point> p;
535 p.reserve(3);
536 for (const auto n : nodes_on_side)
537 if (elem.is_vertex(n))
538 p.push_back(elem.point(n));
539 libmesh_assert_equal_to(p.size(), 3);
540 if (p[0] < p[1])
541 {
542 if (p[1] < p[2])
543 return std::make_tuple(p[0], p[1], p[2]);
544 else if (p[0] < p[2])
545 return std::make_tuple(p[0], p[2], p[1]);
546 else
547 return std::make_tuple(p[2], p[0], p[1]);
548 }
549 else
550 {
551 if (p[0] < p[2])
552 return std::make_tuple(p[1], p[0], p[2]);
553 else if (p[1] < p[2])
554 return std::make_tuple(p[1], p[2], p[0]);
555 else
556 return std::make_tuple(p[2], p[1], p[0]);
557 }
558 };
559
560 if (!_hole_ptrs.empty())
561 for (auto elem : mesh->element_ptr_range())
562 for (auto s : make_range(elem->n_sides()))
563 if (!elem->neighbor_ptr(s))
564 {
565 auto points = sorted_point_tuple(*elem, s);
566 libmesh_assert(!mesh_faces.count(points));
567 mesh_faces.emplace(points, std::make_pair(std::make_pair(elem, s), false));
568 }
569
570 auto & mesh_boundary_info = mesh->get_boundary_info();
571
572 // Define a reference map variable for subdomain map
573 auto & main_subdomain_map = mesh->set_subdomain_name_map();
574 for (auto hole_i : index_range(_hole_ptrs))
575 {
576 UnstructuredMesh & hole_mesh = dynamic_cast<UnstructuredMesh &>(**_hole_ptrs[hole_i]);
577 auto & hole_boundary_info = hole_mesh.get_boundary_info();
578
579 // Our algorithm here requires a serialized Mesh. To avoid
580 // redundant serialization and deserialization (libMesh
581 // MeshedHole and stitch_meshes still also require
582 // serialization) we'll do the serialization up front.
583 libMesh::MeshSerializer serial_hole(hole_mesh);
584
585 // We'll look for any sides that match between the hole mesh and
586 // the newly triangulated mesh, and apply bcids accordingly.
587 for (auto elem : hole_mesh.element_ptr_range())
588 for (auto s : make_range(elem->n_sides()))
589 if (!elem->neighbor_ptr(s))
590 {
591 auto points = sorted_point_tuple(*elem, s);
592 auto it = mesh_faces.find(points);
593
594 // I'd love to assert that we don't have any missing
595 // matches, but our holes might themselves have holes
596 if (it != mesh_faces.end())
597 {
598 auto [main_elem, main_side] = it->second.first;
599 if (_stitch_holes.size() && _stitch_holes[hole_i])
600 {
601 hole_boundary_info.add_side(elem, s, new_hole_bcid);
602 mesh_boundary_info.add_side(main_elem, main_side, inner_bcid);
603 }
604 // We would like to take this opportunity to identify the hole boundary
605 // We set the boundary id to hole_i + 1 at this stage (the default)
606 mesh_boundary_info.add_side(main_elem, main_side, hole_i + 1);
607 it->second.second = true;
608 }
609 }
610 }
611
612 // Any sides that do not match the hole surfaces belong to the external boundary
613 // We set the boundary id to 0 at this stage (the default)
614 for (auto & [points, elem_side] : mesh_faces)
615 if (!elem_side.second)
616 {
617 auto [main_elem, main_side] = elem_side.first;
618 mesh_boundary_info.add_side(main_elem, main_side, 0);
619 }
620
621 // Now hole boundary ids are 1,2,..., N
622 // Now external boundary id is 0
623 // We will find the upper bound of the boundary ids of the mesh so that we would not overwrite
624 // things during renumbering (because of the bcids used for stitching, this is different from
625 // end_bcid)
626 const auto temp_bcid_shift = MooseMeshUtils::getNextFreeBoundaryID(*mesh);
627 // If we stitch holes, we want to make sure the netgen mesh has no conflicting boundary ids
628 // with the hole meshes.
629 // OR, if we assign custom boundary ids, we want to make sure no overwriting occurs.
630 // So we shift all these boundary ids by temp_bcid_shift + free_boundary_id
631 // before we assign the new boundary ids
632 if (_stitch_holes.size() || output_boundary_id.size())
634 *mesh, 0, temp_bcid_shift + free_boundary_id);
635 if (_stitch_holes.size() || hole_boundary_ids.size())
636 for (auto hole_i : index_range(_hole_ptrs))
638 *mesh, hole_i + 1, hole_i + 1 + temp_bcid_shift + free_boundary_id);
639
640 // Now we can reassign the boundary ids
641 // If these ids are specified, we will use them
642 // Otherwise, we will use the default ones
643 if (output_boundary_id.size())
645 *mesh, temp_bcid_shift + free_boundary_id, output_boundary_id[0]);
646 else
648 *mesh, temp_bcid_shift + free_boundary_id, free_boundary_id);
649
650 if (hole_boundary_ids.size())
651 for (auto hole_i : index_range(_hole_ptrs))
653 *mesh, hole_i + 1 + temp_bcid_shift + free_boundary_id, hole_boundary_ids[hole_i]);
654 else
655 for (auto hole_i : index_range(_hole_ptrs))
657 *mesh, hole_i + 1 + temp_bcid_shift + free_boundary_id, hole_i + 1 + free_boundary_id);
658
659 for (auto hole_i : index_range(_hole_ptrs))
660 {
661 UnstructuredMesh & hole_mesh = dynamic_cast<UnstructuredMesh &>(**_hole_ptrs[hole_i]);
662
663 if (_stitch_holes.size() && _stitch_holes[hole_i])
664 {
665 // Retrieve subdomain name map from the mesh to be stitched and insert it into the main
666 // subdomain map
667 const auto & increment_subdomain_map = hole_mesh.get_subdomain_name_map();
668 main_subdomain_map.insert(increment_subdomain_map.begin(), increment_subdomain_map.end());
669
671 {
672 // The main mesh has been serialized early and will last through the end of this MG
673 // If the hole mesh to be stitched is not serialized, it will cause parallelization
674 // issues after combining when the number of processors is large
675 libMesh::MeshSerializer serial_hole(hole_mesh);
676 MooseMeshUtils::copyIntoMesh(*this, *mesh, hole_mesh, false, false, _communicator);
677 }
678 else
679 {
680 std::size_t n_nodes_stitched = mesh->stitch_meshes(hole_mesh,
681 inner_bcid,
682 new_hole_bcid,
683 TOLERANCE,
684 /*clear_stitched_bcids*/ true,
686 use_binary_search);
687
688 if (!n_nodes_stitched)
689 mooseError("Failed to stitch hole mesh ", hole_i, " to new tetrahedralization.");
690 }
691 }
692 }
693 if (doing_stitching && _combined_stitching)
694 {
695 std::size_t n_nodes_stitched = mesh->stitch_surfaces(inner_bcid,
696 new_hole_bcid,
697 TOLERANCE,
698 /*clear_stitched_bcids*/ true,
700 use_binary_search);
701 if (!n_nodes_stitched)
702 mooseError("Failed to stitch combined hole meshes to new tetrahedralization.");
703 }
704
705 // Add user-specified sideset names
706 if (hole_boundary_ids.size())
707 for (auto h : index_range(_hole_ptrs))
708 mesh->get_boundary_info().sideset_name(hole_boundary_ids[h]) = hole_boundaries[h];
709 if (output_boundary_id.size())
710 mesh->get_boundary_info().sideset_name(output_boundary_id[0]) = output_boundary;
711
712 // Check if one SubdomainName is shared by more than one subdomain ids
713 std::set<SubdomainName> main_subdomain_map_name_list;
714 for (auto const & id_name_pair : main_subdomain_map)
715 main_subdomain_map_name_list.emplace(id_name_pair.second);
716 if (main_subdomain_map.size() != main_subdomain_map_name_list.size())
717 paramError("holes", "The hole meshes contain subdomain name maps with conflicts.");
718
719 // We're done with the hole meshes now, and MeshGenerator doesn't
720 // want them anymore either.
721 for (auto & hole_ptr : _hole_ptrs)
722 hole_ptr->reset();
723
724 mesh->unset_is_prepared();
725 return mesh;
726#else
727 mooseError("Cannot use XYZDelaunayGenerator without NetGen-enabled libMesh.");
728 return std::unique_ptr<MeshBase>();
729#endif
730}
boundary_id_type BoundaryID
registerMooseObject("MooseApp", XYZDelaunayGenerator)
The main MOOSE class responsible for handling user-defined parameters in almost every MOOSE system.
void addParam(const std::string &name, const S &value, const std::string &doc_string)
These methods add an optional parameter and a documentation string to the InputParameters object.
void renameParam(const std::string &old_name, const std::string &new_name, const std::string &new_docstring)
Rename a parameter and provide a new documentation string.
void addRequiredParam(const std::string &name, const std::string &doc_string)
This method adds a parameter and documentation string to the InputParameters object that will be extr...
void addClassDescription(const std::string &doc_string)
This method adds a description of the class that will be displayed in the input file syntax dump.
void addRangeCheckedParam(const std::string &name, const T &value, const std::string &parsed_function, const std::string &doc_string)
MeshGenerators are objects that can modify or add to an existing mesh.
static InputParameters validParams()
void paramError(const std::string &param, Args... args) const
Emits an error prefixed with the file and line number of the given param (from the input file) along ...
Definition MooseBase.h:457
bool isParamSetByUser(const std::string &name) const
Test if the supplied parameter is set by a user, as opposed to not set or set to default.
Definition MooseBase.h:205
void mooseError(Args &&... args) const
Emits an error prefixed with object name and type and optionally a file path to the top-level block p...
Definition MooseBase.h:271
bool isParamValid(const std::string &name) const
Test if the supplied parameter is valid.
Definition MooseBase.h:199
This is a "smart" enum class intended to replace many of the shortcomings in the C++ enum type It sho...
Definition MooseEnum.h:55
void max(const T &r, T &o, Request &req) const
Generates a tetrahedral mesh, based on an input mesh defining the outer boundary and an optional set ...
const bool _verbose_stitching
Whether mesh stitching should have verbose output.
const bool _smooth_tri
Whether to do Laplacian mesh smoothing on the generated triangles.
const std::vector< bool > _stitch_holes
Whether to stitch to the mesh defining each hole.
const MooseEnum _algorithm
Type of algorithm used to find matching nodes (binary or exhaustive)
SubdomainID _output_subdomain_id
What subdomain_id to set on the generated tetrahedra.
const bool _combined_stitching
Whether to stitch all holes in one combined stitching step.
const Real _desired_volume
Desired volume of output tetrahedra.
const MooseEnum _conversion_method
Method to convert 3D hole meshes into compatible meshes.
const std::vector< std::unique_ptr< MeshBase > * > _hole_ptrs
Holds pointers to the pointers to input meshes defining holes.
std::unique_ptr< MeshBase > & _bdy_ptr
Input mesh defining the boundary to triangulate within.
std::unique_ptr< MeshBase > generate() override
Generate / modify the mesh.
const MooseEnum _tet_elem_type
Type of the tetrahedral elements to be generated (TET4, TET10, TET14, or DEFAULT)
static InputParameters validParams()
XYZDelaunayGenerator(const InputParameters &parameters)
const bool _convert_holes_for_stitching
Whether to convert 3D hole meshes with non-TRI3 surface elements into a compatible form.
void attach_hole_list(std::unique_ptr< std::vector< std::unique_ptr< UnstructuredMesh > > > holes)
virtual void triangulate() override
const Parallel::Communicator & _communicator
const Parallel::Communicator & comm() const
MeshBase & mesh
void transitionLayerGenerator(MeshBase &mesh, const std::vector< BoundaryName > &boundary_names, const unsigned int conversion_element_layer_number, const bool external_boundaries_checking)
Generate a transition layer of elements with TRI3 surfaces on the given boundaries.
void assignConvertedElementsSubdomainNameSuffix(MeshBase &mesh, const std::set< subdomain_id_type > &original_subdomain_ids, const subdomain_id_type sid_shift_base, const SubdomainName &tet_suffix, const SubdomainName &pyramid_suffix)
Assign a subdomain name suffix to the converted elements created during transition layer generation.
void copyIntoMesh(MeshGenerator &mg, UnstructuredMesh &destination, const UnstructuredMesh &source, const bool avoid_merging_subdomains, const bool avoid_merging_boundaries, const Parallel::Communicator &communicator)
Helper function for copying one mesh into another.
BoundaryID getNextFreeBoundaryID(MeshBase &input_mesh)
Checks input mesh and returns the largest boundary ID in the mesh plus one, which is a boundary ID in...
SubdomainID getNextFreeSubdomainID(MeshBase &input_mesh)
Checks input mesh and returns max(block ID) + 1, which represents a block ID that is not currently in...
SubdomainID getSubdomainID(const SubdomainName &subdomain_name, const MeshBase &mesh)
Gets the subdomain ID associated with the given SubdomainName.
void change_boundary_id(MeshBase &mesh, const boundary_id_type old_id, const boundary_id_type new_id)
void hash_combine(std::size_t &seed, const T &value)
std::size_t operator()(const std::tuple< libMesh::Point, libMesh::Point, libMesh::Point > &p) const