https://mooseframework.inl.gov
MeshDiagnosticsGenerator.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 #include "MooseMeshUtils.h"
12 #include "CastUniquePointer.h"
13 #include "MeshCoarseningUtils.h"
15 
16 #include "libmesh/mesh_tools.h"
17 #include "libmesh/mesh_refinement.h"
18 #include "libmesh/fe.h"
19 #include "libmesh/quadrature_gauss.h"
20 #include "libmesh/face_tri3.h"
21 #include "libmesh/cell_tet4.h"
22 #include "libmesh/face_quad4.h"
23 #include "libmesh/cell_hex8.h"
24 #include "libmesh/string_to_enum.h"
25 #include "libmesh/enum_point_locator_type.h"
26 
28 
31 {
32 
34 
35  params.addRequiredParam<MeshGeneratorName>("input", "The mesh we want to diagnose");
36  params.addClassDescription("Runs a series of diagnostics on the mesh to detect potential issues "
37  "such as unsupported features");
38 
39  // Options for the output level
40  MooseEnum chk_option("NO_CHECK INFO WARNING ERROR", "NO_CHECK");
41 
42  params.addParam<MooseEnum>(
43  "examine_sidesets_orientation",
44  chk_option,
45  "whether to check that sidesets are consistently oriented using neighbor subdomains. If a "
46  "sideset is inconsistently oriented within a subdomain, this will not be detected");
47  params.addParam<MooseEnum>(
48  "check_for_watertight_sidesets",
49  chk_option,
50  "whether to check for external sides that are not assigned to any sidesets");
51  params.addParam<MooseEnum>(
52  "check_for_watertight_nodesets",
53  chk_option,
54  "whether to check for external nodes that are not assigned to any nodeset");
55  params.addParam<std::vector<BoundaryName>>(
56  "boundaries_to_check",
57  {},
58  "Names boundaries that should form a watertight envelope around the mesh. Defaults to all "
59  "the boundaries combined.");
60  params.addParam<MooseEnum>(
61  "examine_element_volumes", chk_option, "whether to examine volume of the elements");
62  params.addParam<Real>("minimum_element_volumes", 1e-16, "minimum size for element volume");
63  params.addParam<Real>("maximum_element_volumes", 1e16, "Maximum size for element volume");
64 
65  params.addParam<MooseEnum>("examine_element_types",
66  chk_option,
67  "whether to look for multiple element types in the same sub-domain");
68  params.addParam<MooseEnum>(
69  "examine_element_overlap", chk_option, "whether to find overlapping elements");
70  params.addParam<MooseEnum>(
71  "examine_nonplanar_sides", chk_option, "whether to check element sides are planar");
72  params.addParam<MooseEnum>("examine_non_conformality",
73  chk_option,
74  "whether to examine the conformality of elements in the mesh");
75  params.addParam<MooseEnum>("examine_non_matching_edges",
76  chk_option,
77  "Whether to check if there are any intersecting edges");
78  params.addParam<Real>("intersection_tol", TOLERANCE, "tolerence for intersecting edges");
79  params.addParam<Real>("nonconformal_tol", TOLERANCE, "tolerance for element non-conformality");
80  params.addParam<MooseEnum>(
81  "search_for_adaptivity_nonconformality",
82  chk_option,
83  "whether to check for non-conformality arising from adaptive mesh refinement");
84  params.addParam<MooseEnum>("check_local_jacobian",
85  chk_option,
86  "whether to check the local Jacobian for negative values");
87  params.addParam<unsigned int>(
88  "log_length_limit",
89  10,
90  "How many problematic element/nodes/sides/etc are explicitly reported on by each check");
91  return params;
92 }
93 
95  : MeshGenerator(parameters),
96  _input(getMesh("input")),
97  _check_sidesets_orientation(getParam<MooseEnum>("examine_sidesets_orientation")),
98  _check_watertight_sidesets(getParam<MooseEnum>("check_for_watertight_sidesets")),
99  _check_watertight_nodesets(getParam<MooseEnum>("check_for_watertight_nodesets")),
100  _watertight_boundary_names(getParam<std::vector<BoundaryName>>("boundaries_to_check")),
101  _check_element_volumes(getParam<MooseEnum>("examine_element_volumes")),
102  _min_volume(getParam<Real>("minimum_element_volumes")),
103  _max_volume(getParam<Real>("maximum_element_volumes")),
104  _check_element_types(getParam<MooseEnum>("examine_element_types")),
105  _check_element_overlap(getParam<MooseEnum>("examine_element_overlap")),
106  _check_non_planar_sides(getParam<MooseEnum>("examine_nonplanar_sides")),
107  _check_non_conformal_mesh(getParam<MooseEnum>("examine_non_conformality")),
108  _non_conformality_tol(getParam<Real>("nonconformal_tol")),
109  _check_non_matching_edges(getParam<MooseEnum>("examine_non_matching_edges")),
110  _non_matching_edge_tol(getParam<Real>("intersection_tol")),
111  _check_adaptivity_non_conformality(
112  getParam<MooseEnum>("search_for_adaptivity_nonconformality")),
113  _check_local_jacobian(getParam<MooseEnum>("check_local_jacobian")),
114  _num_outputs(getParam<unsigned int>("log_length_limit"))
115 {
116  // Check that no secondary parameters have been passed with the main check disabled
117  if ((isParamSetByUser("minimum_element_volumes") ||
118  isParamSetByUser("maximum_element_volumes")) &&
119  _check_element_volumes == "NO_CHECK")
120  paramError("examine_element_volumes",
121  "You must set this parameter to true to trigger element size checks");
122  if (isParamSetByUser("nonconformal_tol") && _check_non_conformal_mesh == "NO_CHECK")
123  paramError("examine_non_conformality",
124  "You must set this parameter to true to trigger mesh conformality check");
125  if (_check_sidesets_orientation == "NO_CHECK" && _check_watertight_sidesets == "NO_CHECK" &&
126  _check_watertight_nodesets == "NO_CHECK" && _check_element_volumes == "NO_CHECK" &&
127  _check_element_types == "NO_CHECK" && _check_element_overlap == "NO_CHECK" &&
128  _check_non_planar_sides == "NO_CHECK" && _check_non_conformal_mesh == "NO_CHECK" &&
129  _check_adaptivity_non_conformality == "NO_CHECK" && _check_local_jacobian == "NO_CHECK" &&
130  _check_non_matching_edges == "NO_CHECK")
131  mooseError("You need to turn on at least one diagnostic. Did you misspell a parameter?");
132 }
133 
134 std::unique_ptr<MeshBase>
136 {
137  std::unique_ptr<MeshBase> mesh = std::move(_input);
138 
139  // Most of the checks assume we have the full mesh
140  if (!mesh->is_serial())
141  mooseError("Only serialized meshes are supported");
142 
143  // We prepare for use at the beginning to facilitate diagnosis
144  // This deliberately does not trust the mesh to know whether it's already prepared or not
145  mesh->prepare_for_use();
146 
147  // check that specified boundary is valid, convert BoundaryNames to BoundaryIDs, and sort
148  for (const auto & boundary_name : _watertight_boundary_names)
149  {
150  if (!MooseMeshUtils::hasBoundaryName(*mesh, boundary_name))
151  mooseError("User specified boundary_to_check \'", boundary_name, "\' does not exist");
152  }
154  std::sort(_watertight_boundaries.begin(), _watertight_boundaries.end());
155 
156  if (_check_sidesets_orientation != "NO_CHECK")
158 
159  if (_check_watertight_sidesets != "NO_CHECK")
161 
162  if (_check_watertight_nodesets != "NO_CHECK")
164 
165  if (_check_element_volumes != "NO_CHECK")
167 
168  if (_check_element_types != "NO_CHECK")
170 
171  if (_check_element_overlap != "NO_CHECK")
173 
174  if (_check_non_planar_sides != "NO_CHECK")
176 
177  if (_check_non_conformal_mesh != "NO_CHECK")
179 
180  if (_check_adaptivity_non_conformality != "NO_CHECK")
182 
183  if (_check_local_jacobian != "NO_CHECK")
185 
186  if (_check_non_matching_edges != "NO_CHECK")
188 
189  return dynamic_pointer_cast<MeshBase>(mesh);
190 }
191 
192 void
193 MeshDiagnosticsGenerator::checkSidesetsOrientation(const std::unique_ptr<MeshBase> & mesh) const
194 {
195  auto & boundary_info = mesh->get_boundary_info();
196  auto side_tuples = boundary_info.build_side_list();
197 
198  for (const auto bid : boundary_info.get_boundary_ids())
199  {
200  // This check only looks at subdomains on both sides of the sideset
201  // it wont pick up if the sideset is changing orientations while inside of a subdomain
202  std::set<std::pair<subdomain_id_type, subdomain_id_type>> block_neighbors;
203  for (const auto index : index_range(side_tuples))
204  {
205  if (std::get<2>(side_tuples[index]) != bid)
206  continue;
207  const auto elem_ptr = mesh->elem_ptr(std::get<0>(side_tuples[index]));
208  if (elem_ptr->neighbor_ptr(std::get<1>(side_tuples[index])))
209  block_neighbors.insert(std::make_pair(
210  elem_ptr->subdomain_id(),
211  elem_ptr->neighbor_ptr(std::get<1>(side_tuples[index]))->subdomain_id()));
212  }
213 
214  // Check that there is no flipped pair
215  std::set<std::pair<subdomain_id_type, subdomain_id_type>> flipped_pairs;
216  for (const auto & block_pair_1 : block_neighbors)
217  for (const auto & block_pair_2 : block_neighbors)
218  if (block_pair_1 != block_pair_2)
219  if (block_pair_1.first == block_pair_2.second &&
220  block_pair_1.second == block_pair_2.first)
221  flipped_pairs.insert(block_pair_1);
222 
223  std::string message;
224  const std::string sideset_full_name =
225  boundary_info.sideset_name(bid) + " (" + std::to_string(bid) + ")";
226  if (!flipped_pairs.empty())
227  {
228  std::string block_pairs_string = "";
229  for (const auto & pair : flipped_pairs)
230  block_pairs_string +=
231  " [" + mesh->subdomain_name(pair.first) + " (" + std::to_string(pair.first) + "), " +
232  mesh->subdomain_name(pair.second) + " (" + std::to_string(pair.second) + ")]";
233  message = "Inconsistent orientation of sideset " + sideset_full_name +
234  " with regards to subdomain pairs" + block_pairs_string;
235  }
236  else
237  message = "Sideset " + sideset_full_name +
238  " is consistently oriented with regards to the blocks it neighbors";
239 
240  diagnosticsLog(message, _check_sidesets_orientation, flipped_pairs.size());
241 
242  // Now check that there is no sideset radically flipping from one side's normal to another
243  // side next to it, in the same sideset
244  // We'll consider pi / 2 to be the most steep angle we'll pass
245  unsigned int num_normals_flipping = 0;
246  Real steepest_side_angles = 0;
247  for (const auto & [elem_id, side_id, side_bid] : side_tuples)
248  {
249  if (side_bid != bid)
250  continue;
251  const auto & elem_ptr = mesh->elem_ptr(elem_id);
252 
253  // Get side normal
254  const std::unique_ptr<const Elem> face = elem_ptr->build_side_ptr(side_id);
255  std::unique_ptr<libMesh::FEBase> fe(
256  libMesh::FEBase::build(elem_ptr->dim(), libMesh::FEType(elem_ptr->default_order())));
257  libMesh::QGauss qface(elem_ptr->dim() - 1, CONSTANT);
258  fe->attach_quadrature_rule(&qface);
259  const auto & normals = fe->get_normals();
260  fe->reinit(elem_ptr, side_id);
261  mooseAssert(normals.size() == 1, "We expected only one normal here");
262  const auto & side_normal = normals[0];
263 
264  // Compare to the sideset normals of neighbor sides in that sideset
265  for (const auto neighbor : elem_ptr->neighbor_ptr_range())
266  if (neighbor)
267  for (const auto neigh_side_index : neighbor->side_index_range())
268  {
269  // Check that the neighbor side is also in the sideset being examined
270  if (!boundary_info.has_boundary_id(neighbor, neigh_side_index, bid))
271  continue;
272 
273  // We re-init everything for the neighbor in case it's a different dimension
274  std::unique_ptr<libMesh::FEBase> fe_neighbor(libMesh::FEBase::build(
275  neighbor->dim(), libMesh::FEType(neighbor->default_order())));
276  libMesh::QGauss qface(neighbor->dim() - 1, CONSTANT);
277  fe_neighbor->attach_quadrature_rule(&qface);
278  const auto & neigh_normals = fe_neighbor->get_normals();
279  fe_neighbor->reinit(neighbor, neigh_side_index);
280  mooseAssert(neigh_normals.size() == 1, "We expected only one normal here");
281  const auto & neigh_side_normal = neigh_normals[0];
282 
283  // Check the angle by computing the dot product
284  if (neigh_side_normal * side_normal <= 0)
285  {
286  num_normals_flipping++;
287  steepest_side_angles =
288  std::max(std::acos(neigh_side_normal * side_normal), steepest_side_angles);
289  if (num_normals_flipping <= _num_outputs)
290  _console << "Side normals changed by more than pi/2 for sideset "
291  << sideset_full_name << " between side " << side_id << " of element "
292  << elem_ptr->id() << " and side " << neigh_side_index
293  << " of neighbor element " << neighbor->id() << std::endl;
294  else if (num_normals_flipping == _num_outputs + 1)
295  _console << "Maximum output reached for sideset normal flipping check. Silencing "
296  "output from now on"
297  << std::endl;
298  }
299  }
300  }
301 
302  if (num_normals_flipping)
303  message = "Sideset " + sideset_full_name +
304  " has two neighboring sides with a very large angle. Largest angle detected: " +
305  std::to_string(steepest_side_angles) + " rad (" +
306  std::to_string(steepest_side_angles * 180 / libMesh::pi) + " degrees).";
307  else
308  message = "Sideset " + sideset_full_name +
309  " does not appear to have side-to-neighbor-side orientation flips. All neighbor "
310  "sides normal differ by less than pi/2";
311 
312  diagnosticsLog(message, _check_sidesets_orientation, num_normals_flipping);
313  }
314 }
315 
316 void
317 MeshDiagnosticsGenerator::checkWatertightSidesets(const std::unique_ptr<MeshBase> & mesh) const
318 {
319  /*
320  Algorithm Overview:
321  1) Loop through all elements
322  2) For each element loop through all its sides
323  3) If it has no neighbors it's an external side
324  4) If external check if it's part of a sideset
325  */
326  if (mesh->mesh_dimension() < 2)
327  mooseError("The sideset check only works for 2D and 3D meshes");
328  auto & boundary_info = mesh->get_boundary_info();
329  boundary_info.build_side_list();
330  const auto sideset_map = boundary_info.get_sideset_map();
331  unsigned int num_faces_without_sideset = 0;
332 
333  for (const auto elem : mesh->active_element_ptr_range())
334  {
335  for (auto i : elem->side_index_range())
336  {
337  // Check if side is external
338  if (elem->neighbor_ptr(i) == nullptr)
339  {
340  // If external get the boundary ids associated with this side
341  std::vector<boundary_id_type> boundary_ids;
342  auto side_range = sideset_map.equal_range(elem);
343  for (const auto & itr : as_range(side_range))
344  if (itr.second.first == i)
345  boundary_ids.push_back(i);
346  // get intersection of boundary_ids and _watertight_boundaries
347  std::vector<boundary_id_type> intersections =
349 
350  bool no_specified_ids = boundary_ids.empty();
351  bool specified_ids = !_watertight_boundaries.empty() && intersections.empty();
352  std::string message;
353  if (mesh->mesh_dimension() == 3)
354  message = "Element " + std::to_string(elem->id()) +
355  " contains an external face which has not been assigned to ";
356  else
357  message = "Element " + std::to_string(elem->id()) +
358  " contains an external edge which has not been assigned to ";
359  if (no_specified_ids)
360  message = message + "a sideset";
361  else if (specified_ids)
362  message = message + "one of the specified sidesets";
363  if ((no_specified_ids || specified_ids) && num_faces_without_sideset < _num_outputs)
364  {
365  _console << message << std::endl;
366  num_faces_without_sideset++;
367  }
368  }
369  }
370  }
371  std::string message;
372  if (mesh->mesh_dimension() == 3)
373  message = "Number of external element faces that have not been assigned to a sideset: " +
374  std::to_string(num_faces_without_sideset);
375  else
376  message = "Number of external element edges that have not been assigned to a sideset: " +
377  std::to_string(num_faces_without_sideset);
378  diagnosticsLog(message, _check_watertight_sidesets, num_faces_without_sideset);
379 }
380 
381 void
382 MeshDiagnosticsGenerator::checkWatertightNodesets(const std::unique_ptr<MeshBase> & mesh) const
383 {
384  /*
385  Diagnostic Overview:
386  1) Mesh precheck
387  2) Loop through all elements
388  3) Loop through all sides of that element
389  4) If side is external loop through its nodes
390  5) If node is not associated with any nodeset add to list
391  6) Print out node id
392  */
393  if (mesh->mesh_dimension() < 2)
394  mooseError("The nodeset check only works for 2D and 3D meshes");
395  auto & boundary_info = mesh->get_boundary_info();
396  unsigned int num_nodes_without_nodeset = 0;
397  std::set<dof_id_type> checked_nodes_id;
398 
399  for (const auto elem : mesh->active_element_ptr_range())
400  {
401  for (const auto i : elem->side_index_range())
402  {
403  // Check if side is external
404  if (elem->neighbor_ptr(i) == nullptr)
405  {
406  // Side is external, now check nodes
407  auto side = elem->side_ptr(i);
408  const auto & node_list = side->get_nodes();
409  for (unsigned int j = 0; j < side->n_nodes(); j++)
410  {
411  const auto node = node_list[j];
412  if (checked_nodes_id.count(node->id()))
413  continue;
414  // get vector of node's boundaries (in most cases it will only have one)
415  std::vector<boundary_id_type> boundary_ids;
416  boundary_info.boundary_ids(node, boundary_ids);
417  std::vector<boundary_id_type> intersection =
419 
420  bool no_specified_ids = boundary_info.n_boundary_ids(node) == 0;
421  bool specified_ids = !_watertight_boundaries.empty() && intersection.empty();
422  std::string message =
423  "Node " + std::to_string(node->id()) +
424  " is on an external boundary of the mesh, but has not been assigned to ";
425  if (no_specified_ids)
426  message = message + "a nodeset";
427  else if (specified_ids)
428  message = message + "one of the specified nodesets";
429  if ((no_specified_ids || specified_ids) && num_nodes_without_nodeset < _num_outputs)
430  {
431  checked_nodes_id.insert(node->id());
432  num_nodes_without_nodeset++;
433  _console << message << std::endl;
434  }
435  }
436  }
437  }
438  }
439  std::string message;
440  message = "Number of external nodes that have not been assigned to a nodeset: " +
441  std::to_string(num_nodes_without_nodeset);
442  diagnosticsLog(message, _check_watertight_nodesets, num_nodes_without_nodeset);
443 }
444 
445 std::vector<boundary_id_type>
447  const std::vector<boundary_id_type> & watertight_boundaries,
448  std::vector<boundary_id_type> & boundary_ids) const
449 {
450  // Only the boundary_ids vector is sorted here. watertight_boundaries has to be sorted beforehand
451  // Returns their intersection (elements that they share)
452  std::sort(boundary_ids.begin(), boundary_ids.end());
453  std::vector<boundary_id_type> boundary_intersection;
454  std::set_intersection(watertight_boundaries.begin(),
455  watertight_boundaries.end(),
456  boundary_ids.begin(),
457  boundary_ids.end(),
458  std::back_inserter(boundary_intersection));
459  return boundary_intersection;
460 }
461 
462 void
463 MeshDiagnosticsGenerator::checkElementVolumes(const std::unique_ptr<MeshBase> & mesh) const
464 {
465  unsigned int num_tiny_elems = 0;
466  unsigned int num_negative_elems = 0;
467  unsigned int num_big_elems = 0;
468  // loop elements within the mesh (assumes replicated)
469  for (auto & elem : mesh->active_element_ptr_range())
470  {
471  if (elem->volume() <= _min_volume)
472  {
473  if (num_tiny_elems < _num_outputs)
474  _console << "Element with volume below threshold detected : \n"
475  << "id " << elem->id() << " near point " << elem->vertex_average() << std::endl;
476  else if (num_tiny_elems == _num_outputs)
477  _console << "Maximum output reached, log is silenced" << std::endl;
478  num_tiny_elems++;
479  }
480  if (elem->volume() >= _max_volume)
481  {
482  if (num_big_elems < _num_outputs)
483  _console << "Element with volume above threshold detected : \n"
484  << elem->get_info() << std::endl;
485  else if (num_big_elems == _num_outputs)
486  _console << "Maximum output reached, log is silenced" << std::endl;
487  num_big_elems++;
488  }
489  }
490  diagnosticsLog("Number of elements below prescribed minimum volume : " +
491  std::to_string(num_tiny_elems),
493  num_tiny_elems);
494  diagnosticsLog("Number of elements with negative volume : " + std::to_string(num_negative_elems),
496  num_negative_elems);
497  diagnosticsLog("Number of elements above prescribed maximum volume : " +
498  std::to_string(num_big_elems),
500  num_big_elems);
501 }
502 
503 void
504 MeshDiagnosticsGenerator::checkElementTypes(const std::unique_ptr<MeshBase> & mesh) const
505 {
506  std::set<subdomain_id_type> ids;
507  mesh->subdomain_ids(ids);
508  // loop on sub-domain
509  for (auto & id : ids)
510  {
511  // ElemType defines an enum for geometric element types
512  std::set<ElemType> types;
513  // loop on elements within this sub-domain
514  for (auto & elem : mesh->active_subdomain_elements_ptr_range(id))
515  types.insert(elem->type());
516 
517  std::string elem_type_names = "";
518  for (auto & elem_type : types)
519  elem_type_names += " " + Moose::stringify(elem_type);
520 
521  _console << "Element type in subdomain " + mesh->subdomain_name(id) + " (" +
522  std::to_string(id) + ") :" + elem_type_names
523  << std::endl;
524  if (types.size() > 1)
525  diagnosticsLog("Two different element types in subdomain " + std::to_string(id),
527  true);
528  }
529 }
530 
531 void
532 MeshDiagnosticsGenerator::checkElementOverlap(const std::unique_ptr<MeshBase> & mesh) const
533 {
534  {
535  unsigned int num_elem_overlaps = 0;
536  auto pl = mesh->sub_point_locator();
537  // loop on nodes, assumed replicated mesh
538  for (auto & node : mesh->node_ptr_range())
539  {
540  // find all the elements around this node
541  std::set<const Elem *> elements;
542  (*pl)(*node, elements);
543 
544  for (auto & elem : elements)
545  {
546  if (!elem->contains_point(*node))
547  continue;
548 
549  // not overlapping inside the element if part of its nodes
550  bool found = false;
551  for (auto & elem_node : elem->node_ref_range())
552  if (*node == elem_node)
553  {
554  found = true;
555  break;
556  }
557  // not overlapping inside the element if right on its side
558  bool on_a_side = false;
559  for (const auto & elem_side_index : elem->side_index_range())
560  if (elem->side_ptr(elem_side_index)->contains_point(*node, _non_conformality_tol))
561  on_a_side = true;
562  if (!found && !on_a_side)
563  {
564  num_elem_overlaps++;
565  if (num_elem_overlaps < _num_outputs)
566  _console << "Element overlap detected at : " << *node << std::endl;
567  else if (num_elem_overlaps == _num_outputs)
568  _console << "Maximum output reached, log is silenced" << std::endl;
569  }
570  }
571  }
572 
573  diagnosticsLog("Number of elements overlapping (node-based heuristics): " +
574  Moose::stringify(num_elem_overlaps),
576  num_elem_overlaps);
577  num_elem_overlaps = 0;
578 
579  // loop on all elements in mesh: assumes a replicated mesh
580  for (auto & elem : mesh->active_element_ptr_range())
581  {
582  // find all the elements around the centroid of this element
583  std::set<const Elem *> overlaps;
584  (*pl)(elem->vertex_average(), overlaps);
585 
586  if (overlaps.size() > 1)
587  {
588  num_elem_overlaps++;
589  if (num_elem_overlaps < _num_outputs)
590  _console << "Element overlap detected with element : " << elem->id() << " near point "
591  << elem->vertex_average() << std::endl;
592  else if (num_elem_overlaps == _num_outputs)
593  _console << "Maximum output reached, log is silenced" << std::endl;
594  }
595  }
596  diagnosticsLog("Number of elements overlapping (centroid-based heuristics): " +
597  Moose::stringify(num_elem_overlaps),
599  num_elem_overlaps);
600  }
601 }
602 
603 void
604 MeshDiagnosticsGenerator::checkNonPlanarSides(const std::unique_ptr<MeshBase> & mesh) const
605 {
606  unsigned int sides_non_planar = 0;
607  // loop on all elements in mesh: assumes a replicated mesh
608  for (auto & elem : mesh->active_element_ptr_range())
609  {
610  for (auto i : make_range(elem->n_sides()))
611  {
612  auto side = elem->side_ptr(i);
613  std::vector<const Point *> nodes;
614  for (auto & node : side->node_ref_range())
615  nodes.emplace_back(&node);
616 
617  if (nodes.size() <= 3)
618  continue;
619  // First vector of the base
620  const RealVectorValue v1 = *nodes[0] - *nodes[1];
621 
622  // Find another node so that we can form a basis. It should just be node 0, 1, 2
623  // to form two independent vectors, but degenerate elements can make them aligned
624  bool aligned = true;
625  unsigned int third_node_index = 2;
626  RealVectorValue v2;
627  while (aligned && third_node_index < nodes.size())
628  {
629  v2 = *nodes[0] - *nodes[third_node_index++];
630  aligned = MooseUtils::absoluteFuzzyEqual(v1 * v2 - v1.norm() * v2.norm(), 0);
631  }
632 
633  // Degenerate element, could not find a third node that is not aligned
634  if (aligned)
635  continue;
636 
637  bool found_non_planar = false;
638 
639  for (auto node_offset : make_range(nodes.size() - 3))
640  {
641  RealVectorValue v3 = *nodes[0] - *nodes[node_offset + 3];
642  bool planar = MooseUtils::absoluteFuzzyEqual(v2.cross(v1) * v3, 0);
643  if (!planar)
644  found_non_planar = true;
645  }
646 
647  if (found_non_planar)
648  {
649  sides_non_planar++;
650  if (sides_non_planar < _num_outputs)
651  _console << "Nonplanar side detected for side " << i
652  << " of element :" << elem->get_info() << std::endl;
653  else if (sides_non_planar == _num_outputs)
654  _console << "Maximum output reached, log is silenced" << std::endl;
655  }
656  }
657  }
658  diagnosticsLog("Number of non-planar element sides detected: " +
659  Moose::stringify(sides_non_planar),
661  sides_non_planar);
662 }
663 
664 void
665 MeshDiagnosticsGenerator::checkNonConformalMesh(const std::unique_ptr<MeshBase> & mesh) const
666 {
667  unsigned int num_nonconformal_nodes = 0;
669  mesh, _console, _num_outputs, _non_conformality_tol, num_nonconformal_nodes);
670  diagnosticsLog("Number of non-conformal nodes: " + Moose::stringify(num_nonconformal_nodes),
672  num_nonconformal_nodes);
673 }
674 
675 void
677  const std::unique_ptr<MeshBase> & mesh) const
678 {
679  unsigned int num_likely_AMR_created_nonconformality = 0;
680  auto pl = mesh->sub_point_locator();
681  pl->set_close_to_point_tol(_non_conformality_tol);
682 
683  // We have to make a copy because adding the new parent element to the mesh
684  // will modify the mesh for the analysis of the next nodes
685  // Make a copy of the mesh, add this element
686  auto mesh_copy = mesh->clone();
687  libMesh::MeshRefinement mesh_refiner(*mesh_copy);
688 
689  // loop on nodes, assumes a replicated mesh
690  for (auto & node : mesh->node_ptr_range())
691  {
692  // find all the elements around this node
693  std::set<const Elem *> elements_around;
694  (*pl)(*node, elements_around);
695 
696  // Keep track of the refined elements and the coarse element
697  std::set<const Elem *> fine_elements;
698  std::set<const Elem *> coarse_elements;
699 
700  // loop through the set of elements near this node
701  for (auto elem : elements_around)
702  {
703  // If the node is not part of this element's nodes, it is a
704  // case of non-conformality
705  bool node_on_elem = false;
706 
707  if (elem->get_node_index(node) != libMesh::invalid_uint)
708  {
709  node_on_elem = true;
710  // non-vertex nodes are not cause for the kind of non-conformality we are looking for
711  if (!elem->is_vertex(elem->get_node_index(node)))
712  continue;
713  }
714 
715  // Keep track of all the elements this node is a part of. They are potentially the
716  // 'fine' (refined) elements next to a coarser element
717  if (node_on_elem)
718  fine_elements.insert(elem);
719  // Else, the node is not part of the element considered, so if the element had been part
720  // of an AMR-created non-conformality, this element is on the coarse side
721  if (!node_on_elem)
722  coarse_elements.insert(elem);
723  }
724 
725  // all the elements around contained the node as one of their nodes
726  // if the coarse and refined sides are not stitched together, this check can fail,
727  // as nodes that are physically near one element are not part of it because of the lack of
728  // stitching (overlapping nodes)
729  if (fine_elements.size() == elements_around.size())
730  continue;
731 
732  if (fine_elements.empty())
733  continue;
734 
735  // Depending on the type of element, we already know the number of elements we expect
736  // to be part of this set of likely refined candidates for a given non-conformal node to
737  // examine. We can only decide if it was born out of AMR if it's the center node of the face
738  // of a coarse element near refined elements
739  const auto elem_type = (*fine_elements.begin())->type();
740  if ((elem_type == QUAD4 || elem_type == QUAD8 || elem_type == QUAD9) &&
741  fine_elements.size() != 2)
742  continue;
743  else if ((elem_type == HEX8 || elem_type == HEX20 || elem_type == HEX27) &&
744  fine_elements.size() != 4)
745  continue;
746  else if ((elem_type == TRI3 || elem_type == TRI6 || elem_type == TRI7) &&
747  fine_elements.size() != 3)
748  continue;
749  else if ((elem_type == TET4 || elem_type == TET10 || elem_type == TET14) &&
750  (fine_elements.size() % 2 != 0))
751  continue;
752 
753  // only one coarse element in front of refined elements except for tets. Whatever we're
754  // looking at is not the interface between coarse and refined elements
755  // Tets are split on their edges (rather than the middle of a face) so there could be any
756  // number of coarse elements in front of the node non-conformality created by refinement
757  if (elem_type != TET4 && elem_type != TET10 && elem_type != TET14 && coarse_elements.size() > 1)
758  continue;
759 
760  // There exists non-conformality, the node should have been a node of all the elements
761  // that are close enough to the node, and it is not
762 
763  // Nodes of the tentative parent element
764  std::vector<const Node *> tentative_coarse_nodes;
765 
766  // For quads and hexes, there is one (quad) or four (hexes) sides that are tied to this node
767  // at the non-conformal interface between the refined elements and a coarse element
768  if (elem_type == QUAD4 || elem_type == QUAD8 || elem_type == QUAD9 || elem_type == HEX8 ||
769  elem_type == HEX20 || elem_type == HEX27)
770  {
771  const auto elem = *fine_elements.begin();
772 
773  // Find which sides (of the elements) the node considered is part of
774  std::vector<Elem *> node_on_sides;
775  unsigned int side_inside_parent = std::numeric_limits<unsigned int>::max();
776  for (auto i : make_range(elem->n_sides()))
777  {
778  const auto side = elem->side_ptr(i);
779  std::vector<const Node *> other_nodes_on_side;
780  bool node_on_side = false;
781  for (const auto & elem_node : side->node_ref_range())
782  {
783  if (*node == elem_node)
784  node_on_side = true;
785  else
786  other_nodes_on_side.emplace_back(&elem_node);
787  }
788  // node is on the side, but is it the side that goes away from the coarse element?
789  if (node_on_side)
790  {
791  // if all the other nodes on this side are in one of the other potentially refined
792  // elements, it's one of the side(s) (4 sides in a 3D hex for example) inside the
793  // parent
794  bool all_side_nodes_are_shared = true;
795  for (const auto & other_node : other_nodes_on_side)
796  {
797  bool shared_with_a_fine_elem = false;
798  for (const auto & other_elem : fine_elements)
799  if (other_elem != elem &&
800  other_elem->get_node_index(other_node) != libMesh::invalid_uint)
801  shared_with_a_fine_elem = true;
802 
803  if (!shared_with_a_fine_elem)
804  all_side_nodes_are_shared = false;
805  }
806  if (all_side_nodes_are_shared)
807  {
808  side_inside_parent = i;
809  // We stop examining sides, it does not matter which side we pick inside the parent
810  break;
811  }
812  }
813  }
814  if (side_inside_parent == std::numeric_limits<unsigned int>::max())
815  continue;
816 
817  // Gather the other potential elements in the refined element:
818  // they are point neighbors of the node that is shared between all the elements flagged
819  // for the non-conformality
820  // Find shared node
821  const auto interior_side = elem->side_ptr(side_inside_parent);
822  const Node * interior_node = nullptr;
823  for (const auto & other_node : interior_side->node_ref_range())
824  {
825  if (other_node == *node)
826  continue;
827  bool in_all_node_neighbor_elements = true;
828  for (auto other_elem : fine_elements)
829  {
830  if (other_elem->get_node_index(&other_node) == libMesh::invalid_uint)
831  in_all_node_neighbor_elements = false;
832  }
833  if (in_all_node_neighbor_elements)
834  {
835  interior_node = &other_node;
836  break;
837  }
838  }
839  // Did not find interior node. Probably not AMR
840  if (!interior_node)
841  continue;
842 
843  // Add point neighbors of interior node to list of potentially refined elements
844  std::set<const Elem *> all_elements;
845  elem->find_point_neighbors(*interior_node, all_elements);
846 
847  if (elem_type == QUAD4 || elem_type == QUAD8 || elem_type == QUAD9)
848  {
850  *interior_node, *node, *elem, tentative_coarse_nodes, fine_elements);
851  if (!success)
852  continue;
853  }
854  // For hexes we first look at the fine-neighbors of the non-conformality
855  // then the fine elements neighbors of the center 'node' of the potential parent
856  else
857  {
858  // Get the coarse neighbor side to be able to recognize nodes that should become part of
859  // the coarse parent
860  const auto & coarse_elem = *coarse_elements.begin();
861  unsigned short coarse_side_i = 0;
862  for (const auto & coarse_side_index : coarse_elem->side_index_range())
863  {
864  const auto coarse_side_ptr = coarse_elem->side_ptr(coarse_side_index);
865  // The side of interest is the side that contains the non-conformality
866  if (!coarse_side_ptr->close_to_point(*node, 10 * _non_conformality_tol))
867  continue;
868  else
869  {
870  coarse_side_i = coarse_side_index;
871  break;
872  }
873  }
874  const auto coarse_side = coarse_elem->side_ptr(coarse_side_i);
875 
876  // We did not find the side of the coarse neighbor near the refined elements
877  // Try again at another node
878  if (!coarse_side)
879  continue;
880 
881  // We cant directly use the coarse neighbor nodes
882  // - The user might be passing a disjoint mesh
883  // - There could two levels of refinement separating the coarse neighbor and its refined
884  // counterparts
885  // We use the fine element nodes
886  unsigned int i = 0;
887  tentative_coarse_nodes.resize(4);
888  for (const auto & elem_1 : fine_elements)
889  for (const auto & coarse_node : elem_1->node_ref_range())
890  {
891  bool node_shared = false;
892  for (const auto & elem_2 : fine_elements)
893  {
894  if (elem_2 != elem_1)
895  if (elem_2->get_node_index(&coarse_node) != libMesh::invalid_uint)
896  node_shared = true;
897  }
898  // A node for the coarse parent will appear in only one fine neighbor (not shared)
899  // and will lay on the side of the coarse neighbor
900  // We only care about the coarse neighbor vertex nodes
901  if (!node_shared && coarse_side->close_to_point(coarse_node, _non_conformality_tol) &&
902  elem_1->is_vertex(elem_1->get_node_index(&coarse_node)))
903  tentative_coarse_nodes[i++] = &coarse_node;
904  mooseAssert(i <= 5, "We went too far in this index");
905  }
906 
907  // We did not find 4 coarse nodes. Mesh might be disjoint and the coarse element does not
908  // contain the fine elements nodes we found
909  if (i != 4)
910  continue;
911 
912  // Need to order these nodes to form a valid quad / base of an hex
913  // We go around the axis formed by the node and the interior node
914  Point axis = *interior_node - *node;
915  const auto start_circle = elem->vertex_average();
917  tentative_coarse_nodes, *interior_node, start_circle, axis);
918  tentative_coarse_nodes.resize(8);
919 
920  // Use the neighbors of the fine elements that contain these nodes to get the vertex
921  // nodes
922  for (const auto & elem : fine_elements)
923  {
924  // Find the index of the coarse node for the starting element
925  unsigned int node_index = 0;
926  for (const auto & coarse_node : tentative_coarse_nodes)
927  {
928  if (elem->get_node_index(coarse_node) != libMesh::invalid_uint)
929  break;
930  node_index++;
931  }
932 
933  // Get the neighbor element that is part of the fine elements to coarsen together
934  for (const auto & neighbor : elem->neighbor_ptr_range())
935  if (all_elements.count(neighbor) && !fine_elements.count(neighbor))
936  {
937  // Find the coarse node for the neighbor
938  const Node * coarse_elem_node = nullptr;
939  for (const auto & fine_node : neighbor->node_ref_range())
940  {
941  if (!neighbor->is_vertex(neighbor->get_node_index(&fine_node)))
942  continue;
943  bool node_shared = false;
944  for (const auto & elem_2 : all_elements)
945  if (elem_2 != neighbor &&
946  elem_2->get_node_index(&fine_node) != libMesh::invalid_uint)
947  node_shared = true;
948  if (!node_shared)
949  {
950  coarse_elem_node = &fine_node;
951  break;
952  }
953  }
954  // Insert the coarse node at the right place
955  tentative_coarse_nodes[node_index + 4] = coarse_elem_node;
956  mooseAssert(node_index + 4 < tentative_coarse_nodes.size(), "Indexed too far");
957  mooseAssert(coarse_elem_node, "Did not find last coarse element node");
958  }
959  }
960  }
961 
962  // No need to separate fine elements near the non-conformal node and away from it
963  fine_elements = all_elements;
964  }
965  // For TRI elements, we use the fine triangle element at the center of the potential
966  // coarse triangle element
967  else if (elem_type == TRI3 || elem_type == TRI6 || elem_type == TRI7)
968  {
969  // Find the center element
970  // It's the only element that shares a side with both of the other elements near the node
971  // considered
972  const Elem * center_elem = nullptr;
973  for (const auto refined_elem_1 : fine_elements)
974  {
975  unsigned int num_neighbors = 0;
976  for (const auto refined_elem_2 : fine_elements)
977  {
978  if (refined_elem_1 == refined_elem_2)
979  continue;
980  if (refined_elem_1->has_neighbor(refined_elem_2))
981  num_neighbors++;
982  }
983  if (num_neighbors >= 2)
984  center_elem = refined_elem_1;
985  }
986  // Did not find the center fine element, probably not AMR
987  if (!center_elem)
988  continue;
989  // Now get the tentative coarse element nodes
990  for (const auto refined_elem : fine_elements)
991  {
992  if (refined_elem == center_elem)
993  continue;
994  for (const auto & other_node : refined_elem->node_ref_range())
995  if (center_elem->get_node_index(&other_node) == libMesh::invalid_uint &&
996  refined_elem->is_vertex(refined_elem->get_node_index(&other_node)))
997  tentative_coarse_nodes.push_back(&other_node);
998  }
999 
1000  // Get the final tentative new coarse element node, on the other side of the center
1001  // element from the non-conformality
1002  unsigned int center_side_opposite_node = std::numeric_limits<unsigned int>::max();
1003  for (auto side_index : center_elem->side_index_range())
1004  if (center_elem->side_ptr(side_index)->get_node_index(node) == libMesh::invalid_uint)
1005  center_side_opposite_node = side_index;
1006  const auto neighbor_on_other_side_of_opposite_center_side =
1007  center_elem->neighbor_ptr(center_side_opposite_node);
1008 
1009  // Element is on a boundary, cannot form a coarse element
1010  if (!neighbor_on_other_side_of_opposite_center_side)
1011  continue;
1012 
1013  fine_elements.insert(neighbor_on_other_side_of_opposite_center_side);
1014  for (const auto & tri_node : neighbor_on_other_side_of_opposite_center_side->node_ref_range())
1015  if (neighbor_on_other_side_of_opposite_center_side->is_vertex(
1016  neighbor_on_other_side_of_opposite_center_side->get_node_index(&tri_node)) &&
1017  center_elem->side_ptr(center_side_opposite_node)->get_node_index(&tri_node) ==
1019  tentative_coarse_nodes.push_back(&tri_node);
1020 
1021  mooseAssert(center_side_opposite_node != std::numeric_limits<unsigned int>::max(),
1022  "Did not find the side opposite the non-conformality");
1023  mooseAssert(tentative_coarse_nodes.size() == 3,
1024  "We are forming a coarsened triangle element");
1025  }
1026  // For TET elements, it's very different because the non-conformality does not happen inside
1027  // of a face, but on an edge of one or more coarse elements
1028  else if (elem_type == TET4 || elem_type == TET10 || elem_type == TET14)
1029  {
1030  // There are 4 tets on the tips of the coarsened tet and 4 tets inside
1031  // let's identify all of them
1032  std::set<const Elem *> tips_tets;
1033  std::set<const Elem *> inside_tets;
1034 
1035  // pick a coarse element and work with its fine neighbors
1036  const Elem * coarse_elem = nullptr;
1037  std::set<const Elem *> fine_tets;
1038  for (auto & coarse_one : coarse_elements)
1039  {
1040  for (const auto & elem : fine_elements)
1041  // for two levels of refinement across, this is not working
1042  // we would need a "has_face_embedded_in_this_other_ones_face" routine
1043  if (elem->has_neighbor(coarse_one))
1044  fine_tets.insert(elem);
1045 
1046  if (fine_tets.size())
1047  {
1048  coarse_elem = coarse_one;
1049  break;
1050  }
1051  }
1052  // There's no coarse element neighbor to a group of finer tets, not AMR
1053  if (!coarse_elem)
1054  continue;
1055 
1056  // There is one last point neighbor of the node that is sandwiched between two neighbors
1057  for (const auto & elem : fine_elements)
1058  {
1059  int num_face_neighbors = 0;
1060  for (const auto & tet : fine_tets)
1061  if (tet->has_neighbor(elem))
1062  num_face_neighbors++;
1063  if (num_face_neighbors == 2)
1064  {
1065  fine_tets.insert(elem);
1066  break;
1067  }
1068  }
1069 
1070  // There should be two other nodes with non-conformality near this coarse element
1071  // Find both, as they will be nodes of the rest of the elements to add to the potential
1072  // fine tet list. They are shared by two of the fine tets we have already found
1073  std::set<const Node *> other_nodes;
1074  for (const auto & tet_1 : fine_tets)
1075  {
1076  for (const auto & node_1 : tet_1->node_ref_range())
1077  {
1078  if (&node_1 == node)
1079  continue;
1080  if (!tet_1->is_vertex(tet_1->get_node_index(&node_1)))
1081  continue;
1082  for (const auto & tet_2 : fine_tets)
1083  {
1084  if (tet_2 == tet_1)
1085  continue;
1086  if (tet_2->get_node_index(&node_1) != libMesh::invalid_uint)
1087  // check that it's near the coarse element as well
1088  if (coarse_elem->close_to_point(node_1, 10 * _non_conformality_tol))
1089  other_nodes.insert(&node_1);
1090  }
1091  }
1092  }
1093  mooseAssert(other_nodes.size() == 2,
1094  "Should find only two extra non-conformal nodes near the coarse element");
1095 
1096  // Now we can go towards this tip element next to two non-conformalities
1097  for (const auto & tet_1 : fine_tets)
1098  {
1099  for (const auto & neighbor : tet_1->neighbor_ptr_range())
1100  if (neighbor->get_node_index(*other_nodes.begin()) != libMesh::invalid_uint &&
1101  neighbor->is_vertex(neighbor->get_node_index(*other_nodes.begin())) &&
1102  neighbor->get_node_index(*other_nodes.rbegin()) != libMesh::invalid_uint &&
1103  neighbor->is_vertex(neighbor->get_node_index(*other_nodes.rbegin())))
1104  fine_tets.insert(neighbor);
1105  }
1106  // Now that the element next to the time is in the fine_tets, we can get the tip
1107  for (const auto & tet_1 : fine_tets)
1108  {
1109  for (const auto & neighbor : tet_1->neighbor_ptr_range())
1110  if (neighbor->get_node_index(*other_nodes.begin()) != libMesh::invalid_uint &&
1111  neighbor->is_vertex(neighbor->get_node_index(*other_nodes.begin())) &&
1112  neighbor->get_node_index(*other_nodes.rbegin()) != libMesh::invalid_uint &&
1113  neighbor->is_vertex(neighbor->get_node_index(*other_nodes.rbegin())))
1114  fine_tets.insert(neighbor);
1115  }
1116 
1117  // Get the sandwiched tets between the tets we already found
1118  for (const auto & tet_1 : fine_tets)
1119  for (const auto & neighbor : tet_1->neighbor_ptr_range())
1120  for (const auto & tet_2 : fine_tets)
1121  if (tet_1 != tet_2 && tet_2->has_neighbor(neighbor) && neighbor != coarse_elem)
1122  fine_tets.insert(neighbor);
1123 
1124  // tips tests are the only ones to have a node that is shared by no other tet in the group
1125  for (const auto & tet_1 : fine_tets)
1126  {
1127  unsigned int unshared_nodes = 0;
1128  for (const auto & other_node : tet_1->node_ref_range())
1129  {
1130  if (!tet_1->is_vertex(tet_1->get_node_index(&other_node)))
1131  continue;
1132  bool node_shared = false;
1133  for (const auto & tet_2 : fine_tets)
1134  if (tet_2 != tet_1 && tet_2->get_node_index(&other_node) != libMesh::invalid_uint)
1135  node_shared = true;
1136  if (!node_shared)
1137  unshared_nodes++;
1138  }
1139  if (unshared_nodes == 1)
1140  tips_tets.insert(tet_1);
1141  else if (unshared_nodes == 0)
1142  inside_tets.insert(tet_1);
1143  else
1144  mooseError("Did not expect a tet to have two unshared vertex nodes here");
1145  }
1146 
1147  // Finally grab the last tip of the tentative coarse tet. It shares:
1148  // - 3 nodes with the other tips, only one with each
1149  // - 1 face with only one tet of the fine tet group
1150  // - it has a node that no other fine tet shares (the tip node)
1151  for (const auto & tet : inside_tets)
1152  {
1153  for (const auto & neighbor : tet->neighbor_ptr_range())
1154  {
1155  // Check that it shares a face with no other potential fine tet
1156  bool shared_with_another_tet = false;
1157  for (const auto & tet_2 : fine_tets)
1158  {
1159  if (tet_2 == tet)
1160  continue;
1161  if (tet_2->has_neighbor(neighbor))
1162  shared_with_another_tet = true;
1163  }
1164  if (shared_with_another_tet)
1165  continue;
1166 
1167  // Used to count the nodes shared with tip tets. Can only be 1 per tip tet
1168  std::vector<const Node *> tip_nodes_shared;
1169  unsigned int unshared_nodes = 0;
1170  for (const auto & other_node : neighbor->node_ref_range())
1171  {
1172  if (!neighbor->is_vertex(neighbor->get_node_index(&other_node)))
1173  continue;
1174 
1175  // Check for being a node-neighbor of the 3 other tip tets
1176  for (const auto & tip_tet : tips_tets)
1177  {
1178  if (neighbor == tip_tet)
1179  continue;
1180 
1181  // we could break here but we want to check that no other tip shares that node
1182  if (tip_tet->get_node_index(&other_node) != libMesh::invalid_uint)
1183  tip_nodes_shared.push_back(&other_node);
1184  }
1185  // Check for having a node shared with no other tet
1186  bool node_shared = false;
1187  for (const auto & tet_2 : fine_tets)
1188  if (tet_2 != neighbor && tet_2->get_node_index(&other_node) != libMesh::invalid_uint)
1189  node_shared = true;
1190  if (!node_shared)
1191  unshared_nodes++;
1192  }
1193  if (tip_nodes_shared.size() == 3 && unshared_nodes == 1)
1194  tips_tets.insert(neighbor);
1195  }
1196  }
1197 
1198  // append the missing fine tets (inside the coarse element, away from the node considered)
1199  // into the fine elements set for the check on "did it refine the tentative coarse tet
1200  // onto the same fine tets"
1201  fine_elements.clear();
1202  for (const auto & elem : tips_tets)
1203  fine_elements.insert(elem);
1204  for (const auto & elem : inside_tets)
1205  fine_elements.insert(elem);
1206 
1207  // get the vertex of the coarse element from the tip tets
1208  for (const auto & tip : tips_tets)
1209  {
1210  for (const auto & node : tip->node_ref_range())
1211  {
1212  bool outside = true;
1213 
1214  const auto id = tip->get_node_index(&node);
1215  if (!tip->is_vertex(id))
1216  continue;
1217  for (const auto & tet : inside_tets)
1218  if (tet->get_node_index(&node) != libMesh::invalid_uint)
1219  outside = false;
1220  if (outside)
1221  {
1222  tentative_coarse_nodes.push_back(&node);
1223  // only one tip node per tip tet
1224  break;
1225  }
1226  }
1227  }
1228 
1229  std::sort(tentative_coarse_nodes.begin(), tentative_coarse_nodes.end());
1230  tentative_coarse_nodes.erase(
1231  std::unique(tentative_coarse_nodes.begin(), tentative_coarse_nodes.end()),
1232  tentative_coarse_nodes.end());
1233 
1234  // The group of fine elements ended up having less or more than 4 tips, so it's clearly
1235  // not forming a coarse tetrahedral
1236  if (tentative_coarse_nodes.size() != 4)
1237  continue;
1238  }
1239  else
1240  {
1241  mooseInfo("Unsupported element type ",
1242  elem_type,
1243  ". Skipping detection for this node and all future nodes near only this "
1244  "element type");
1245  continue;
1246  }
1247 
1248  // Check the fine element types: if not all the same then it's not uniform AMR
1249  for (auto elem : fine_elements)
1250  if (elem->type() != elem_type)
1251  continue;
1252 
1253  // Check the number of coarse element nodes gathered
1254  for (const auto & check_node : tentative_coarse_nodes)
1255  if (check_node == nullptr)
1256  continue;
1257 
1258  // Form a parent, of a low order type as we only have the extreme vertex nodes
1259  std::unique_ptr<Elem> parent = Elem::build(Elem::first_order_equivalent_type(elem_type));
1260  auto parent_ptr = mesh_copy->add_elem(parent.release());
1261 
1262  // Set the nodes to the coarse element
1263  for (auto i : index_range(tentative_coarse_nodes))
1264  parent_ptr->set_node(i, mesh_copy->node_ptr(tentative_coarse_nodes[i]->id()));
1265 
1266  // Refine this parent
1267  parent_ptr->set_refinement_flag(Elem::REFINE);
1268  parent_ptr->refine(mesh_refiner);
1269  const auto num_children = parent_ptr->n_children();
1270 
1271  // Compare with the original set of elements
1272  // We already know the child share the exterior node. If they share the same vertex
1273  // average as the group of unrefined elements we will call this good enough for now
1274  // For tetrahedral elements we cannot rely on the children all matching as the choice in
1275  // the diagonal selection can be made differently. We'll just say 4 matching children is
1276  // good enough for the heuristic
1277  unsigned int num_children_match = 0;
1278  for (const auto & child : parent_ptr->child_ref_range())
1279  {
1280  for (const auto & potential_children : fine_elements)
1281  if (MooseUtils::absoluteFuzzyEqual(child.vertex_average()(0),
1282  potential_children->vertex_average()(0),
1284  MooseUtils::absoluteFuzzyEqual(child.vertex_average()(1),
1285  potential_children->vertex_average()(1),
1287  MooseUtils::absoluteFuzzyEqual(child.vertex_average()(2),
1288  potential_children->vertex_average()(2),
1290  {
1291  num_children_match++;
1292  break;
1293  }
1294  }
1295 
1296  if (num_children_match == num_children ||
1297  ((elem_type == TET4 || elem_type == TET10 || elem_type == TET14) &&
1298  num_children_match == 4))
1299  {
1300  num_likely_AMR_created_nonconformality++;
1301  if (num_likely_AMR_created_nonconformality < _num_outputs)
1302  {
1303  _console << "Detected non-conformality likely created by AMR near" << *node
1304  << Moose::stringify(elem_type)
1305  << " elements that could be merged into a coarse element:" << std::endl;
1306  for (const auto & elem : fine_elements)
1307  _console << elem->id() << " ";
1308  _console << std::endl;
1309  }
1310  else if (num_likely_AMR_created_nonconformality == _num_outputs)
1311  _console << "Maximum log output reached, silencing output" << std::endl;
1312  }
1313  }
1314 
1316  "Number of non-conformal nodes likely due to mesh refinement detected by heuristic: " +
1317  Moose::stringify(num_likely_AMR_created_nonconformality),
1319  num_likely_AMR_created_nonconformality);
1320  pl->unset_close_to_point_tol();
1321 }
1322 
1323 void
1324 MeshDiagnosticsGenerator::checkLocalJacobians(const std::unique_ptr<MeshBase> & mesh) const
1325 {
1326  unsigned int num_negative_elem_qp_jacobians = 0;
1327  // Get a high-ish order quadrature
1328  auto qrule_dimension = mesh->mesh_dimension();
1329  libMesh::QGauss qrule(qrule_dimension, FIFTH);
1330 
1331  // Use a constant monomial
1332  const libMesh::FEType fe_type(CONSTANT, libMesh::MONOMIAL);
1333 
1334  // Initialize a basic constant monomial shape function everywhere
1335  std::unique_ptr<libMesh::FEBase> fe_elem;
1336  if (mesh->mesh_dimension() == 1)
1337  fe_elem = std::make_unique<libMesh::FEMonomial<1>>(fe_type);
1338  if (mesh->mesh_dimension() == 2)
1339  fe_elem = std::make_unique<libMesh::FEMonomial<2>>(fe_type);
1340  else
1341  fe_elem = std::make_unique<libMesh::FEMonomial<3>>(fe_type);
1342 
1343  fe_elem->get_JxW();
1344  fe_elem->attach_quadrature_rule(&qrule);
1345 
1346  // Check elements (assumes serialized mesh)
1347  for (const auto & elem : mesh->element_ptr_range())
1348  {
1349  // Handle mixed-dimensional meshes
1350  if (qrule_dimension != elem->dim())
1351  {
1352  // Re-initialize a quadrature
1353  qrule_dimension = elem->dim();
1354  qrule = libMesh::QGauss(qrule_dimension, FIFTH);
1355 
1356  // Re-initialize a monomial FE
1357  if (elem->dim() == 1)
1358  fe_elem = std::make_unique<libMesh::FEMonomial<1>>(fe_type);
1359  if (elem->dim() == 2)
1360  fe_elem = std::make_unique<libMesh::FEMonomial<2>>(fe_type);
1361  else
1362  fe_elem = std::make_unique<libMesh::FEMonomial<3>>(fe_type);
1363 
1364  fe_elem->get_JxW();
1365  fe_elem->attach_quadrature_rule(&qrule);
1366  }
1367 
1368  try
1369  {
1370  fe_elem->reinit(elem);
1371  }
1372  catch (libMesh::LogicError & e)
1373  {
1374  num_negative_elem_qp_jacobians++;
1375  const auto msg = std::string(e.what());
1376  if (msg.find("negative Jacobian") != std::string::npos)
1377  {
1378  if (num_negative_elem_qp_jacobians < _num_outputs)
1379  _console << "Negative Jacobian found in element " << elem->id() << " near point "
1380  << elem->vertex_average() << std::endl;
1381  else if (num_negative_elem_qp_jacobians == _num_outputs)
1382  _console << "Maximum log output reached, silencing output" << std::endl;
1383  }
1384  else
1385  _console << e.what() << std::endl;
1386  }
1387  }
1388  diagnosticsLog("Number of elements with a negative Jacobian: " +
1389  Moose::stringify(num_negative_elem_qp_jacobians),
1391  num_negative_elem_qp_jacobians);
1392 
1393  unsigned int num_negative_side_qp_jacobians = 0;
1394  // Get a high-ish order side quadrature
1395  auto qrule_side_dimension = mesh->mesh_dimension() - 1;
1396  libMesh::QGauss qrule_side(qrule_side_dimension, FIFTH);
1397 
1398  // Use the side quadrature now
1399  fe_elem->attach_quadrature_rule(&qrule_side);
1400 
1401  // Check element sides
1402  for (const auto & elem : mesh->element_ptr_range())
1403  {
1404  // Handle mixed-dimensional meshes
1405  if (int(qrule_side_dimension) != elem->dim() - 1)
1406  {
1407  qrule_side_dimension = elem->dim() - 1;
1408  qrule_side = libMesh::QGauss(qrule_side_dimension, FIFTH);
1409 
1410  // Re-initialize a side FE
1411  if (elem->dim() == 1)
1412  fe_elem = std::make_unique<libMesh::FEMonomial<1>>(fe_type);
1413  if (elem->dim() == 2)
1414  fe_elem = std::make_unique<libMesh::FEMonomial<2>>(fe_type);
1415  else
1416  fe_elem = std::make_unique<libMesh::FEMonomial<3>>(fe_type);
1417 
1418  fe_elem->get_JxW();
1419  fe_elem->attach_quadrature_rule(&qrule_side);
1420  }
1421 
1422  for (const auto & side : elem->side_index_range())
1423  {
1424  try
1425  {
1426  fe_elem->reinit(elem, side);
1427  }
1428  catch (libMesh::LogicError & e)
1429  {
1430  const auto msg = std::string(e.what());
1431  if (msg.find("negative Jacobian") != std::string::npos)
1432  {
1433  num_negative_side_qp_jacobians++;
1434  if (num_negative_side_qp_jacobians < _num_outputs)
1435  _console << "Negative Jacobian found in side " << side << " of element" << elem->id()
1436  << " near point " << elem->vertex_average() << std::endl;
1437  else if (num_negative_side_qp_jacobians == _num_outputs)
1438  _console << "Maximum log output reached, silencing output" << std::endl;
1439  }
1440  else
1441  _console << e.what() << std::endl;
1442  }
1443  }
1444  }
1445  diagnosticsLog("Number of element sides with negative Jacobians: " +
1446  Moose::stringify(num_negative_side_qp_jacobians),
1448  num_negative_side_qp_jacobians);
1449 }
1450 
1451 void
1452 MeshDiagnosticsGenerator::checkNonMatchingEdges(const std::unique_ptr<MeshBase> & mesh) const
1453 {
1454  /*Algorithm Overview
1455  1)Prechecks
1456  a)This algorithm only works for 3D so check for that first
1457  2)Loop
1458  a)Loop through every element
1459  b)For each element get the edges associated with it
1460  c)For each edge check overlap with any edges of nearby elements
1461  d)Have check to make sure the same pair of edges are not being tested twice for overlap
1462  3)Overlap check
1463  a)Shortest line that connects both lines is perpendicular to both lines
1464  b)A good overview of the math for finding intersecting lines can be found
1465  here->paulbourke.net/geometry/pointlineplane/
1466  */
1467  if (mesh->mesh_dimension() != 3)
1468  {
1469  mooseWarning("The edge intersection algorithm only works with 3D meshes. "
1470  "'examine_non_matching_edges' is skipped");
1471  return;
1472  }
1473  if (!mesh->is_serial())
1474  mooseError("Only serialized/replicated meshes are supported");
1475  unsigned int num_intersecting_edges = 0;
1476 
1477  // Create map of element to bounding box to avoing reinitializing the same bounding box multiple
1478  // times
1479  std::unordered_map<Elem *, BoundingBox> bounding_box_map;
1480  for (const auto elem : mesh->active_element_ptr_range())
1481  {
1482  const auto boundingBox = elem->loose_bounding_box();
1483  bounding_box_map.insert({elem, boundingBox});
1484  }
1485 
1486  std::unique_ptr<PointLocatorBase> point_locator = mesh->sub_point_locator();
1487  std::set<std::array<dof_id_type, 4>> overlapping_edges_nodes;
1488  for (const auto elem : mesh->active_element_ptr_range())
1489  {
1490  // loop through elem's nodes and find nearby elements with it
1491  std::set<const Elem *> candidate_elems;
1492  std::set<const Elem *> nearby_elems;
1493  for (unsigned int i = 0; i < elem->n_nodes(); i++)
1494  {
1495  (*point_locator)(elem->point(i), candidate_elems);
1496  nearby_elems.insert(candidate_elems.begin(), candidate_elems.end());
1497  }
1498  std::vector<std::unique_ptr<const Elem>> elem_edges(elem->n_edges());
1499  for (auto i : elem->edge_index_range())
1500  elem_edges[i] = elem->build_edge_ptr(i);
1501  for (const auto other_elem : nearby_elems)
1502  {
1503  // If they're the same element then there's no need to check for overlap
1504  if (elem->id() >= other_elem->id())
1505  continue;
1506 
1507  std::vector<std::unique_ptr<const Elem>> other_edges(other_elem->n_edges());
1508  for (auto j : other_elem->edge_index_range())
1509  other_edges[j] = other_elem->build_edge_ptr(j);
1510  for (auto & edge : elem_edges)
1511  {
1512  for (auto & other_edge : other_edges)
1513  {
1514  // Get nodes from edges
1515  const Node * n1 = edge->get_nodes()[0];
1516  const Node * n2 = edge->get_nodes()[1];
1517  const Node * n3 = other_edge->get_nodes()[0];
1518  const Node * n4 = other_edge->get_nodes()[1];
1519 
1520  // Create array<dof_id_type, 4> to check against set
1521  std::array<dof_id_type, 4> node_id_array = {n1->id(), n2->id(), n3->id(), n4->id()};
1522  std::sort(node_id_array.begin(), node_id_array.end());
1523 
1524  // Check if the edges have already been added to our check_edges list
1525  if (overlapping_edges_nodes.count(node_id_array))
1526  {
1527  continue;
1528  }
1529 
1530  // Check element/edge type
1531  if (edge->type() != EDGE2)
1532  {
1533  std::string element_message = "Edge of type " + Utility::enum_to_string(edge->type()) +
1534  " was found in cell " + std::to_string(elem->id()) +
1535  " which is of type " +
1536  Utility::enum_to_string(elem->type()) + '\n' +
1537  "The edge intersection check only works for EDGE2 "
1538  "elements.\nThis message will not be output again";
1539  mooseDoOnce(_console << element_message << std::endl);
1540  continue;
1541  }
1542  if (other_edge->type() != EDGE2)
1543  continue;
1544 
1545  // Now compare edge with other_edge
1546  Point intersection_coords;
1548  *edge, *other_edge, intersection_coords, _non_matching_edge_tol);
1549  if (overlap)
1550  {
1551  // Add the nodes that make up the 2 edges to the vector overlapping_edges_nodes
1552  overlapping_edges_nodes.insert(node_id_array);
1553  num_intersecting_edges += 2;
1554  if (num_intersecting_edges < _num_outputs)
1555  {
1556  // Print error message
1557  std::string elem_id = std::to_string(elem->id());
1558  std::string other_elem_id = std::to_string(other_elem->id());
1559  std::string x_coord = std::to_string(intersection_coords(0));
1560  std::string y_coord = std::to_string(intersection_coords(1));
1561  std::string z_coord = std::to_string(intersection_coords(2));
1562  std::string message = "Intersecting edges found between elements " + elem_id +
1563  " and " + other_elem_id + " near point (" + x_coord + ", " +
1564  y_coord + ", " + z_coord + ")";
1565  _console << message << std::endl;
1566  }
1567  }
1568  }
1569  }
1570  }
1571  }
1572  diagnosticsLog("Number of intersecting element edges: " +
1573  Moose::stringify(num_intersecting_edges),
1575  num_intersecting_edges);
1576 }
1577 
1578 void
1580  const MooseEnum & log_level,
1581  bool problem_detected) const
1582 {
1583  mooseAssert(log_level != "NO_CHECK",
1584  "We should not be outputting logs if the check had been disabled");
1585  if (log_level == "INFO" || !problem_detected)
1586  mooseInfoRepeated(msg);
1587  else if (log_level == "WARNING")
1588  mooseWarning(msg);
1589  else if (log_level == "ERROR")
1590  mooseError(msg);
1591  else
1592  mooseError("Should not reach here");
1593 }
void checkNonMatchingEdges(const std::unique_ptr< MeshBase > &mesh) const
std::unique_ptr< FEGenericBase< Real > > build(const unsigned int dim, const FEType &fet)
void checkWatertightNodesets(const std::unique_ptr< MeshBase > &mesh) const
auto norm() const -> decltype(std::norm(Real()))
bool absoluteFuzzyEqual(const T &var1, const T2 &var2, const T3 &tol=libMesh::TOLERANCE *libMesh::TOLERANCE)
Function to check whether two variables are equal within an absolute tolerance.
Definition: MooseUtils.h:380
QUAD8
const unsigned int invalid_uint
HEX8
bool hasBoundaryName(const MeshBase &input_mesh, const BoundaryName &name)
Whether a particular boundary name exists in the mesh.
std::unique_ptr< MeshBase > & _input
the input mesh to be diagnosed
const MooseEnum _check_sidesets_orientation
whether to check that sidesets are consistently oriented using neighbor subdomains ...
void mooseInfo(Args &&... args) const
TET10
const MooseEnum _check_element_types
whether to check different element types in the same sub-domain
bool checkFirstOrderEdgeOverlap(const Elem &edge1, const Elem &edge2, Point &intersection_point, const Real intersection_tol)
const boundary_id_type side_id
MeshBase & mesh
std::unique_ptr< MeshBase > generate() override
Generate / modify the mesh.
const unsigned int _num_outputs
number of logs to output at most for each check
void mooseInfoRepeated(Args &&... args)
Emit an informational message with the given stringified, concatenated args.
Definition: MooseError.h:377
The main MOOSE class responsible for handling user-defined parameters in almost every MOOSE system...
const MooseEnum _check_non_conformal_mesh
whether to check for non-conformal meshes
HEX20
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< BoundaryID > _watertight_boundaries
IDs of boundaries to be checked in watertight checks.
void checkNonConformalMeshFromAdaptivity(const std::unique_ptr< MeshBase > &mesh) const
Routine to check whether a mesh presents non-conformality born from adaptivity.
MeshDiagnosticsGenerator(const InputParameters &parameters)
const MooseEnum _check_local_jacobian
whether to check for negative jacobians in the domain
static InputParameters validParams()
bool getFineElementsFromInteriorNode(const libMesh::Node &interior_node, const libMesh::Node &reference_node, const libMesh::Elem &elem, std::vector< const libMesh::Node *> &tentative_coarse_nodes, std::set< const libMesh::Elem *> &fine_elements)
Utility routine to gather vertex nodes for, and elements contained in, for a coarse QUAD or HEX eleme...
void mooseWarning(Args &&... args) const
Emits a warning prefixed with object name and type.
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...
auto max(const L &left, const R &right)
TRI3
const Real _non_conformality_tol
tolerance for detecting when meshes are not conformal
QUAD4
FIFTH
registerMooseObject("MooseApp", MeshDiagnosticsGenerator)
CONSTANT
std::vector< BoundaryName > _watertight_boundary_names
Names of boundaries to be checked in watertight checks.
void checkLocalJacobians(const std::unique_ptr< MeshBase > &mesh) const
Routine to check whether the Jacobians (elem and side) are not negative.
void checkNonConformalMesh(const std::unique_ptr< MeshBase > &mesh) const
Routine to check whether a mesh presents non-conformality.
void checkElementTypes(const std::unique_ptr< MeshBase > &mesh) const
Routine to check the element types in each subdomain.
TET4
TRI6
const MooseEnum _check_adaptivity_non_conformality
whether to check for the adaptivity of non-conformal meshes
SimpleRange< IndexType > as_range(const std::pair< IndexType, IndexType > &p)
const Real _min_volume
minimum size for element volume to be counted as a tiny element
const std::string & type() const
Get the type of this class.
Definition: MooseBase.h:51
virtual_for_inffe const std::vector< Real > & get_JxW() const
This is a "smart" enum class intended to replace many of the shortcomings in the C++ enum type It sho...
Definition: MooseEnum.h:33
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.
HEX27
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 ...
TypeVector< typename CompareTypes< Real, T2 >::supertype > cross(const TypeVector< T2 > &v) const
const Real _max_volume
maximum size for element volume to be counted as a big element
void checkElementOverlap(const std::unique_ptr< MeshBase > &mesh) const
Routine to check whether elements overlap in the mesh.
TET14
static InputParameters validParams()
Definition: MeshGenerator.C:23
std::string stringify(const T &t)
conversion to string
Definition: Conversion.h:64
const MooseEnum _check_element_volumes
whether to check element volumes
void checkElementVolumes(const std::unique_ptr< MeshBase > &mesh) const
Routine to check the element volumes.
const MooseEnum _check_watertight_nodesets
whether to check that each external node is assigned to a nodeset
const MooseEnum _check_non_planar_sides
whether to check for elements in different planes (non_planar)
bool isParamSetByUser(const std::string &nm) const
Test if the supplied parameter is set by a user, as opposed to not set or set to default.
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
EDGE2
void diagnosticsLog(std::string msg, const MooseEnum &log_level, bool problem_detected) const
Utility routine to output the final diagnostics level in the desired mode.
std::vector< boundary_id_type > findBoundaryOverlap(const std::vector< boundary_id_type > &watertight_boundaries, std::vector< boundary_id_type > &boundary_ids) const
Helper function that finds the intersection between the given vectors.
IntRange< T > make_range(T beg, T end)
const MooseEnum _check_watertight_sidesets
whether to check that each external side is assigned to a sideset
TRI7
void mooseError(Args &&... args) const
Emits an error prefixed with object name and type.
void checkSidesetsOrientation(const std::unique_ptr< MeshBase > &mesh) const
Routine to check sideset orientation near subdomains.
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 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...
QUAD9
const ConsoleStream _console
An instance of helper class to write streams to the Console objects.
const MooseEnum _check_element_overlap
whether to check for intersecting elements
void reorderNodes(std::vector< const libMesh::Node *> &nodes, const libMesh::Point &origin, const libMesh::Point &clock_start, libMesh::Point &axis)
Utility routine to re-order a vector of nodes so that they can form a valid quad element.
void checkNonConformalMesh(const std::unique_ptr< libMesh::MeshBase > &mesh, const ConsoleStream &console, const unsigned int num_outputs, const Real conformality_tol, unsigned int &num_nonconformal_nodes)
void checkNonPlanarSides(const std::unique_ptr< MeshBase > &mesh) const
Routine to check whether there are non-planar sides in the mesh.
MeshGenerators are objects that can modify or add to an existing mesh.
Definition: MeshGenerator.h:32
void checkWatertightSidesets(const std::unique_ptr< MeshBase > &mesh) const
void ErrorVector unsigned int
auto index_range(const T &sizable)
const Real pi