https://mooseframework.inl.gov
MeshTriangulationUtils.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 "MeshTriangulationUtils.h"
11 #include "MooseMeshUtils.h"
12 #include "CastUniquePointer.h"
13 
14 #include "libmesh/elem.h"
15 #include "libmesh/boundary_info.h"
16 #include "libmesh/int_range.h"
17 #include "libmesh/enum_to_string.h"
18 #include "libmesh/mesh_modification.h"
19 #include "libmesh/mesh_serializer.h"
20 #include "libmesh/mesh_triangle_holes.h"
21 #include "libmesh/parsed_function.h"
22 #include "libmesh/poly2tri_triangulator.h"
23 #include "libmesh/unstructured_mesh.h"
24 
25 using namespace libMesh;
26 
27 namespace MeshTriangulationUtils
28 {
29 
30 std::unique_ptr<MeshBase>
32  std::unique_ptr<MeshBase> boundary_mesh,
33  std::vector<std::unique_ptr<MeshBase>> hole_meshes,
34  const XYDelaunayOptions & xyd_opts)
35 {
36  // Put the boundary mesh in a local pointer
37  std::unique_ptr<UnstructuredMesh> mesh =
38  dynamic_pointer_cast<UnstructuredMesh>(std::move(boundary_mesh));
39 
40  // Get ready to triangulate the line segments we extract from it
42  poly2tri.triangulation_type() = libMesh::TriangulatorInterface::PSLG;
43 
44  // If we're using a user-requested subset of boundaries on that
45  // mesh, get their ids.
46  std::set<std::size_t> bdy_ids;
47 
48  if (!xyd_opts.input_boundary_names.empty())
49  {
50  if (!xyd_opts.input_subdomain_names.empty())
51  mg.paramError(
52  "input_subdomain_names",
53  "input_boundary_names and input_subdomain_names cannot both specify an outer boundary.");
54 
55  for (const auto & name : xyd_opts.input_boundary_names)
56  {
58  if (bcid == BoundaryInfo::invalid_id)
59  mg.paramError("input_boundary_names", name, " is not a boundary name in the input mesh");
60 
61  bdy_ids.insert(bcid);
62  }
63  }
64 
65  if (!xyd_opts.input_subdomain_names.empty())
66  {
67  // Make sure subdomain info caches are up to date
70 
71  const auto subdomain_ids =
73 
74  // Check that the requested subdomains exist in the mesh
75  std::set<SubdomainID> subdomains;
76  mesh->subdomain_ids(subdomains);
77 
78  for (auto i : index_range(subdomain_ids))
79  {
80  if (subdomain_ids[i] == Moose::INVALID_BLOCK_ID || !subdomains.count(subdomain_ids[i]))
81  mg.paramError("input_subdomain_names",
82  xyd_opts.input_subdomain_names[i],
83  " was not found in the boundary mesh");
84 
85  bdy_ids.insert(subdomain_ids[i]);
86  }
87  }
88 
89  if (!bdy_ids.empty())
90  poly2tri.set_outer_boundary_ids(bdy_ids);
91 
92  poly2tri.set_interpolate_boundary_points(xyd_opts.add_nodes_per_boundary_segment);
93  poly2tri.set_refine_boundary_allowed(xyd_opts.refine_bdy);
94  poly2tri.set_verify_hole_boundaries(xyd_opts.verify_holes);
95 
96  poly2tri.desired_area() = xyd_opts.desired_area;
97  poly2tri.minimum_angle() = 0; // Not yet supported
98  poly2tri.smooth_after_generating() = xyd_opts.smooth_tri;
99 
100  std::vector<libMesh::TriangulatorInterface::MeshedHole> meshed_holes;
101  std::vector<libMesh::TriangulatorInterface::Hole *> triangulator_hole_ptrs(hole_meshes.size());
102  // This tells us the element orders of the hole meshes
103  // For the boundary meshes, it can be access through poly2tri.segment_midpoints.
104  std::vector<bool> holes_with_midpoints(hole_meshes.size());
105  bool stitch_second_order_holes(false);
106 
107  // Make sure pointers here aren't invalidated by a resize
108  meshed_holes.reserve(hole_meshes.size());
109  for (auto hole_i : index_range(hole_meshes))
110  {
111  if (!hole_meshes[hole_i]->is_prepared())
112  hole_meshes[hole_i]->prepare_for_use();
113  if (hole_i < xyd_opts.hole_boundary_id_filters.size() &&
114  !xyd_opts.hole_boundary_id_filters[hole_i].empty())
115  meshed_holes.emplace_back(*hole_meshes[hole_i], xyd_opts.hole_boundary_id_filters[hole_i]);
116  else
117  meshed_holes.emplace_back(*hole_meshes[hole_i]);
118  holes_with_midpoints[hole_i] = meshed_holes.back().n_midpoints();
119  stitch_second_order_holes =
120  xyd_opts.stitch_holes.empty()
121  ? false
122  : ((holes_with_midpoints[hole_i] && xyd_opts.stitch_holes[hole_i]) ||
123  stitch_second_order_holes);
124  if (hole_i < xyd_opts.refine_holes.size())
125  meshed_holes.back().set_refine_boundary_allowed(xyd_opts.refine_holes[hole_i]);
126 
127  triangulator_hole_ptrs[hole_i] = &meshed_holes.back();
128  }
129  if (stitch_second_order_holes &&
130  (xyd_opts.tri_elem_type == "TRI3" || xyd_opts.tri_elem_type == "DEFAULT"))
131  mg.paramError(
132  "tri_element_type",
133  "Cannot use first order elements with stitched quadratic element holes. Please try "
134  "to specify a higher-order tri_element_type or reduce the order of the hole inputs.");
135 
136  if (!triangulator_hole_ptrs.empty())
137  poly2tri.attach_hole_list(&triangulator_hole_ptrs);
138 
139  if (xyd_opts.desired_area_func != "")
140  {
141  // poly2tri will clone this so it's fine going out of scope
143  poly2tri.set_desired_area_function(&area_func);
144  }
145  else if (xyd_opts.use_auto_area_func)
146  {
147  poly2tri.set_auto_area_function(
148  mg.comm(),
150  xyd_opts.auto_area_function_power,
151  xyd_opts.auto_area_func_default_size > 0.0 ? xyd_opts.auto_area_func_default_size : 0.0,
153  : -1.0);
154  }
155 
156  if (xyd_opts.tri_elem_type == "TRI6")
157  poly2tri.elem_type() = libMesh::ElemType::TRI6;
158  else if (xyd_opts.tri_elem_type == "TRI7")
159  poly2tri.elem_type() = libMesh::ElemType::TRI7;
160  // Add interior points before triangulating. Only points inside the boundaries
161  // will be meshed.
162  for (const auto & point : xyd_opts.interior_points)
163  mesh->add_point(point);
164 
165  poly2tri.triangulate();
166 
167  SubdomainID output_subdomain_id =
168  xyd_opts.has_output_subdomain_id ? xyd_opts.output_subdomain_id : 0;
169 
170  if (xyd_opts.has_output_subdomain_name)
171  {
173 
174  if (id == Elem::invalid_subdomain_id)
175  {
176  if (!xyd_opts.has_output_subdomain_id)
177  {
178  // We'll probably need to make a new ID, then
179  output_subdomain_id = MooseMeshUtils::getNextFreeSubdomainID(*mesh);
180 
181  // But check the hole meshes for our output subdomain name too
182  for (auto & hole_ptr : hole_meshes)
183  {
184  auto possible_sbdid =
186  // Huh, it was in one of them
187  if (possible_sbdid != Elem::invalid_subdomain_id)
188  {
189  output_subdomain_id = possible_sbdid;
190  break;
191  }
192  output_subdomain_id =
193  std::max(output_subdomain_id, MooseMeshUtils::getNextFreeSubdomainID(*hole_ptr));
194  }
195  }
196  }
197  else
198  {
199  if (xyd_opts.has_output_subdomain_id)
200  {
201  if (id != output_subdomain_id)
202  mg.paramError("output_subdomain_name",
203  "name has been used by the input meshes and the corresponding id is not "
204  "equal to 'output_subdomain_id'");
205  }
206  else
207  output_subdomain_id = id;
208  }
209  // We do not want to set an empty subdomain name
210  if (xyd_opts.output_subdomain_name.size())
211  mesh->subdomain_name(output_subdomain_id) = xyd_opts.output_subdomain_name;
212  }
213 
214  if (xyd_opts.smooth_tri || output_subdomain_id)
215  for (auto elem : mesh->element_ptr_range())
216  {
217  mooseAssert(elem->type() == (xyd_opts.tri_elem_type == "TRI6"
218  ? TRI6
219  : (xyd_opts.tri_elem_type == "TRI7" ? TRI7 : TRI3)),
220  "Unexpected element type " << libMesh::Utility::enum_to_string(elem->type())
221  << " found in triangulation");
222 
223  elem->subdomain_id() = output_subdomain_id;
224 
225  // I do not trust Laplacian mesh smoothing not to invert
226  // elements near reentrant corners. Eventually we'll add better
227  // smoothing options, but even those might have failure cases.
228  // Better to always do extra tests here than to ever let users
229  // try to run on a degenerate mesh.
230  if (xyd_opts.smooth_tri)
231  {
232  auto cross_prod = (elem->point(1) - elem->point(0)).cross(elem->point(2) - elem->point(0));
233 
234  if (cross_prod(2) <= 0)
235  mooseError("Inverted element found in triangulation.\n"
236  "Laplacian smoothing can create these at reentrant corners; disable it?");
237  }
238  }
239 
240  // The hole meshes are specified by the user, so they could have any
241  // BCID or no BCID or any combination of BCIDs on their outer
242  // boundary, so we'll have to set our own BCID to use for stitching
243  // there. We'll need to check all the holes for used BCIDs, if we
244  // want to pick a new ID on hole N that doesn't conflict with any
245  // IDs on hole M < N (or with the IDs on the new triangulation)
246 
247  // The new triangulation by default assigns BCID i+1 to hole i ...
248  // but we can't even use this for mesh stitching, because we can't
249  // be sure it isn't also already in use on the hole's mesh and so we
250  // won't be able to safely clear it afterwards.
251  const boundary_id_type end_bcid = hole_meshes.size() + 1;
252 
253  // If a hole has its boundary layer mesh, we need to move the hole bcid to the "real" hole
254  // boundary in the boundary layer mesh. So we need to record them here.
255  std::vector<BoundaryID> hole_boundary_rec(hole_meshes.size());
256  std::iota(hole_boundary_rec.begin(), hole_boundary_rec.end(), 1);
257 
258  // For the hole meshes that need to be stitched, we would like to make sure the hole boundary ids
259  // and output boundary id are not conflicting with the existing boundary ids of the hole meshes to
260  // be stitched.
261  BoundaryID free_boundary_id = 0;
262  if (xyd_opts.stitch_holes.size())
263  {
264  for (auto hole_i : index_range(hole_meshes))
265  {
266  if (xyd_opts.stitch_holes[hole_i])
267  {
268  free_boundary_id =
269  std::max(free_boundary_id, MooseMeshUtils::getNextFreeBoundaryID(*hole_meshes[hole_i]));
270  hole_meshes[hole_i]->comm().max(free_boundary_id);
271  }
272  }
273  for (auto h : index_range(hole_meshes))
274  {
275  libMesh::MeshTools::Modification::change_boundary_id(*mesh, h + 1, h + 1 + free_boundary_id);
276  hole_boundary_rec[h] = h + 1 + free_boundary_id;
277  }
278  }
279  boundary_id_type new_hole_bcid = end_bcid + free_boundary_id;
280 
281  // We might be overriding the default bcid numbers. We have to be
282  // careful about how we renumber, though. We pick unused temporary
283  // numbers because e.g. "0->2, 2->0" is impossible to do
284  // sequentially, but "0->N, 2->N+2, N->2, N+2->0" works.
286  *mesh, 0, (xyd_opts.has_output_boundary ? end_bcid : 0) + free_boundary_id);
287 
288  if (!xyd_opts.hole_boundaries.empty())
289  {
290  auto hole_boundary_ids = MooseMeshUtils::getBoundaryIDs(*mesh, xyd_opts.hole_boundaries, true);
291 
292  for (auto h : index_range(hole_meshes))
294  *mesh, h + 1 + free_boundary_id, h + 1 + free_boundary_id + end_bcid);
295 
296  for (auto h : index_range(hole_meshes))
297  {
299  *mesh, h + 1 + free_boundary_id + end_bcid, hole_boundary_ids[h]);
300  hole_boundary_rec[h] = hole_boundary_ids[h];
301  mesh->get_boundary_info().sideset_name(hole_boundary_ids[h]) = xyd_opts.hole_boundaries[h];
302  new_hole_bcid = std::max(new_hole_bcid, boundary_id_type(hole_boundary_ids[h] + 1));
303  }
304  }
305 
306  if (xyd_opts.has_output_boundary)
307  {
308  const std::vector<BoundaryID> output_boundary_id =
310 
312  *mesh, end_bcid + free_boundary_id, output_boundary_id[0]);
313  mesh->get_boundary_info().sideset_name(output_boundary_id[0]) = xyd_opts.output_boundary;
314 
315  new_hole_bcid = std::max(new_hole_bcid, boundary_id_type(output_boundary_id[0] + 1));
316  }
317 
318  bool doing_stitching = false;
319 
320  for (auto hole_i : index_range(hole_meshes))
321  {
322  const MeshBase & hole_mesh = *hole_meshes[hole_i];
323  auto & hole_boundary_info = hole_mesh.get_boundary_info();
324  const std::set<boundary_id_type> & local_hole_bcids = hole_boundary_info.get_boundary_ids();
325 
326  if (!local_hole_bcids.empty())
327  new_hole_bcid = std::max(new_hole_bcid, boundary_id_type(*local_hole_bcids.rbegin() + 1));
328  hole_mesh.comm().max(new_hole_bcid);
329 
330  if (hole_i < xyd_opts.stitch_holes.size() && xyd_opts.stitch_holes[hole_i])
331  doing_stitching = true;
332  }
333 
334  const boundary_id_type inner_bcid = new_hole_bcid + 1;
335 
336  // libMesh mesh stitching still requires a serialized mesh, and it's
337  // cheaper to do that once than to do it once-per-hole
338  libMesh::MeshSerializer serial(*mesh, doing_stitching);
339 
340  // Define a reference map variable for subdomain map
341  auto & main_subdomain_map = mesh->set_subdomain_name_map();
342  for (auto hole_i : index_range(hole_meshes))
343  {
344  if (hole_i < xyd_opts.stitch_holes.size() && xyd_opts.stitch_holes[hole_i])
345  {
346  UnstructuredMesh & hole_mesh = dynamic_cast<UnstructuredMesh &>(*hole_meshes[hole_i]);
347  // increase hole mesh order if the triangulation mesh has higher order
348  if (!holes_with_midpoints[hole_i])
349  {
350  if (xyd_opts.tri_elem_type == "TRI6")
351  hole_mesh.all_second_order();
352  else if (xyd_opts.tri_elem_type == "TRI7")
353  hole_mesh.all_complete_order();
354  }
355  auto & hole_boundary_info = hole_mesh.get_boundary_info();
356 
357  // Our algorithm here requires a serialized Mesh. To avoid
358  // redundant serialization and deserialization (libMesh
359  // MeshedHole and stitch_meshes still also require
360  // serialization) we'll do the serialization up front.
361  libMesh::MeshSerializer serial_hole(hole_mesh);
362 
363  // It would have been nicer for MeshedHole to add the BCID
364  // itself, but we want MeshedHole to work with a const mesh.
365  // We'll still use MeshedHole, for its code distinguishing
366  // outer boundaries from inner boundaries on a
367  // hole-with-holes.
368  const auto & hole_bdy_id_filter = (hole_i < xyd_opts.hole_boundary_id_filters.size())
369  ? xyd_opts.hole_boundary_id_filters[hole_i]
370  : std::set<std::size_t>();
371  libMesh::TriangulatorInterface::MeshedHole mh{hole_mesh, hole_bdy_id_filter};
372 
373  // We have to translate from MeshedHole points to mesh
374  // sides.
375  std::unordered_map<Point, Point> next_hole_boundary_point;
376  const int np = mh.n_points();
377  for (auto pi : make_range(1, np))
378  next_hole_boundary_point[mh.point(pi - 1)] = mh.point(pi);
379  next_hole_boundary_point[mh.point(np - 1)] = mh.point(0);
380 
381 #ifndef NDEBUG
382  int found_hole_sides = 0;
383 #endif
384  for (auto elem : hole_mesh.element_ptr_range())
385  {
386  if (elem->dim() != 2)
387  mooseError("Non 2-D element found in hole; stitching is not supported.");
388 
389  auto ns = elem->n_sides();
390  for (auto s : make_range(ns))
391  {
392  auto it_s = next_hole_boundary_point.find(elem->point(s));
393  if (it_s != next_hole_boundary_point.end())
394  if (it_s->second == elem->point((s + 1) % ns))
395  {
396  hole_boundary_info.add_side(elem, s, new_hole_bcid);
397 #ifndef NDEBUG
398  ++found_hole_sides;
399 #endif
400  }
401  }
402  }
403  mooseAssert(found_hole_sides == np, "Failed to find full outer boundary of meshed hole");
404 
405  auto & mesh_boundary_info = mesh->get_boundary_info();
406 #ifndef NDEBUG
407  int found_inner_sides = 0;
408 #endif
409  for (auto elem : mesh->element_ptr_range())
410  {
411  auto ns = elem->n_sides();
412  for (auto s : make_range(ns))
413  {
414  auto it_s = next_hole_boundary_point.find(elem->point((s + 1) % ns));
415  if (it_s != next_hole_boundary_point.end())
416  if (it_s->second == elem->point(s))
417  {
418  mesh_boundary_info.add_side(elem, s, inner_bcid);
419 #ifndef NDEBUG
420  ++found_inner_sides;
421 #endif
422  }
423  }
424  }
425  mooseAssert(found_inner_sides == np, "Failed to find full boundary around meshed hole");
426 
427  // Retrieve subdomain name map from the mesh to be stitched and insert it into the main
428  // subdomain map
429  const auto & increment_subdomain_map = hole_mesh.get_subdomain_name_map();
430  main_subdomain_map.insert(increment_subdomain_map.begin(), increment_subdomain_map.end());
431 
432  // We do not need the hole_bdy_id_filter anymore
433  for (const auto & bcid : hole_bdy_id_filter)
434  hole_boundary_info.remove_id(bcid);
435  // If we are stitching a hole boundary layer mesh, we need to reassign the bcid
436  if (hole_bdy_id_filter.size())
437  {
439  hole_mesh,
440  xyd_opts.hole_boundary_inner_id_defaults[hole_i].empty()
441  ? 1
442  : *xyd_opts.hole_boundary_inner_id_defaults[hole_i].begin(),
443  hole_boundary_rec[hole_i],
444  true);
445  hole_mesh.get_boundary_info().sideset_name(hole_boundary_rec[hole_i]) =
446  mesh->get_boundary_info().sideset_name(hole_boundary_rec[hole_i]);
447  mesh->get_boundary_info().remove_id(hole_boundary_rec[hole_i]);
448  }
449 
450  mesh->stitch_meshes(hole_mesh,
451  inner_bcid,
452  new_hole_bcid,
453  TOLERANCE,
454  /*clear_stitched_bcids*/ true,
455  xyd_opts.verbose_stitching,
456  xyd_opts.use_binary_search);
457  }
458  }
459  // Check if one SubdomainName is shared by more than one subdomain ids
460  std::set<SubdomainName> main_subdomain_map_name_list;
461  for (auto const & id_name_pair : main_subdomain_map)
462  main_subdomain_map_name_list.emplace(id_name_pair.second);
463  if (main_subdomain_map.size() != main_subdomain_map_name_list.size())
464  mg.paramError("holes", "The hole meshes contain subdomain name maps with conflicts.");
465 
467  return mesh;
468 }
469 }
std::string name(const ElemQuality q)
void remove_id(boundary_id_type id, bool global=false)
Bundle of inputs for triangulateWithDelaunay.
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:467
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application...
Definition: MooseError.h:311
static constexpr Real TOLERANCE
MeshBase & mesh
const Parallel::Communicator & comm() const
std::unique_ptr< T_DEST, T_DELETER > dynamic_pointer_cast(std::unique_ptr< T_SRC, T_DELETER > &src)
These are reworked from https://stackoverflow.com/a/11003103.
std::vector< subdomain_id_type > getSubdomainIDs(const libMesh::MeshBase &mesh, const std::vector< SubdomainName > &subdomain_name)
Get the associated subdomainIDs for the subdomain names that are passed in.
The following methods are specializations for using the libMesh::Parallel::packed_range_* routines fo...
const BoundaryInfo & get_boundary_info() const
Preparation preparation() const
virtual Node * add_point(const Point &p, const dof_id_type id=DofObject::invalid_id, const processor_id_type proc_id=DofObject::invalid_processor_id)=0
std::vector< BoundaryName > input_boundary_names
SubdomainID getSubdomainID(const SubdomainName &subdomain_name, const MeshBase &mesh)
Gets the subdomain ID associated with the given SubdomainName.
auto max(const L &left, const R &right)
const SubdomainID INVALID_BLOCK_ID
Definition: MooseTypes.C:20
BoundaryID getBoundaryID(const BoundaryName &boundary_name, const MeshBase &mesh)
Gets the boundary ID associated with the given BoundaryName.
std::vector< SubdomainName > input_subdomain_names
int8_t boundary_id_type
static const boundary_id_type invalid_id
const std::map< subdomain_id_type, std::string > & get_subdomain_name_map() const
boundary_id_type BoundaryID
void change_boundary_id(MeshBase &mesh, const boundary_id_type old_id, const boundary_id_type new_id)
std::vector< std::set< std::size_t > > hole_boundary_id_filters
void subdomain_ids(std::set< subdomain_id_type > &ids, const bool global=true) const
std::vector< BoundaryID > getBoundaryIDs(const libMesh::MeshBase &mesh, const std::vector< BoundaryName > &boundary_name, bool generate_unknown, const std::set< BoundaryID > &mesh_boundary_ids)
Gets the boundary IDs with their names.
std::string & subdomain_name(subdomain_id_type id)
std::unique_ptr< MeshBase > triangulateWithDelaunay(MeshGenerator &mg, std::unique_ptr< MeshBase > boundary_mesh, std::vector< std::unique_ptr< MeshBase >> hole_meshes, const XYDelaunayOptions &xyd_opts)
std::string & sideset_name(boundary_id_type id)
std::string enum_to_string(const T e)
const std::set< boundary_id_type > & get_boundary_ids() const
void max(const T &r, T &o, Request &req) const
virtual void all_complete_order()
void add_side(const dof_id_type elem, const unsigned short int side, const boundary_id_type id)
IntRange< T > make_range(T beg, T end)
void all_second_order(const bool full_ordered=true)
virtual const Point & point(const dof_id_type i) const=0
std::vector< std::set< BoundaryID > > hole_boundary_inner_id_defaults
void changeBoundaryId(MeshBase &mesh, const libMesh::boundary_id_type old_id, const libMesh::boundary_id_type new_id, bool delete_prev)
Changes the old boundary ID to a new ID in the mesh.
SubdomainID getNextFreeSubdomainID(MeshBase &input_mesh)
Checks input mesh and returns max(block ID) + 1, which represents a block ID that is not currently in...
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...
static constexpr subdomain_id_type invalid_subdomain_id
const DofMap &dof_map LIBMESH_COMMA unsigned int std::string & set_subdomain_name_map()
MeshGenerators are objects that can modify or add to an existing mesh.
Definition: MeshGenerator.h:33
void unset_is_prepared()
auto index_range(const T &sizable)
const Real pi