LCOV - code coverage report
Current view: top level - src/meshgenerators - MeshDiagnosticsGenerator.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: #31761 (28487c) with base 701993 Lines: 814 906 89.8 %
Date: 2025-11-11 13:51:07 Functions: 16 16 100.0 %
Legend: Lines: hit not hit

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

Generated by: LCOV version 1.14