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