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

Generated by: LCOV version 1.14