https://mooseframework.inl.gov
Loading...
Searching...
No Matches
AutomaticMortarGeneration.C
Go to the documentation of this file.
1//* This file is part of the MOOSE framework
2//* https://mooseframework.inl.gov
3//*
4//* All rights reserved, see COPYRIGHT for full restrictions
5//* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6//*
7//* Licensed under LGPL 2.1, please see LICENSE for details
8//* https://www.gnu.org/licenses/lgpl-2.1.html
9
11#include "MortarSegmentInfo.h"
13#include "MooseError.h"
14#include "MooseTypes.h"
16#include "MortarSegmentHelper.h"
17#include "MortarUtils.h"
18#include "FormattedTable.h"
19#include "FEProblemBase.h"
20#include "DisplacedProblem.h"
21#include "Output.h"
22#include "SolutionInvalidity.h"
23
24#include "libmesh/mesh_tools.h"
25#include "libmesh/explicit_system.h"
26#include "libmesh/numeric_vector.h"
27#include "libmesh/elem.h"
28#include "libmesh/node.h"
29#include "libmesh/dof_map.h"
30#include "libmesh/edge_edge2.h"
31#include "libmesh/edge_edge3.h"
32#include "libmesh/face_tri3.h"
33#include "libmesh/face_tri6.h"
34#include "libmesh/face_tri7.h"
35#include "libmesh/face_quad4.h"
36#include "libmesh/face_quad8.h"
37#include "libmesh/face_quad9.h"
38#include "libmesh/exodusII_io.h"
39#include "libmesh/quadrature_gauss.h"
40#include "libmesh/quadrature_nodal.h"
41#include "libmesh/distributed_mesh.h"
42#include "libmesh/replicated_mesh.h"
43#include "libmesh/enum_to_string.h"
44#include "libmesh/statistics.h"
45#include "libmesh/equation_systems.h"
46#include "libmesh/fe_map.h"
47
48#include "metaphysicl/dualnumber.h"
49
50#include "timpi/communicator.h"
51#include "timpi/parallel_sync.h"
52
53#include <array>
54#include <algorithm>
55#include <cmath>
56#include <limits>
57
59
60// Make newer nanoflann API spelling compatible with older nanoflann
61// versions
62#if NANOFLANN_VERSION < 0x150
63namespace nanoflann
64{
65typedef SearchParams SearchParameters;
66}
67#endif
68
69namespace
70{
71// QNodal on a parent side returns normals, weights, and physical points in the
72// parent-side quadrature ordering. That ordering is not guaranteed to match the
73// node ordering of the generated lower-dimensional secondary element, especially
74// for higher-order faces. Build the association geometrically so each
75// quadrature value is attached to the secondary node at the same physical point.
76std::vector<unsigned int>
77nodalQuadraturePointToSecondaryNodeMap(const Elem & secondary_elem,
78 const std::vector<Point> & q_points)
79{
80 const auto n_nodes = secondary_elem.n_nodes();
81 if (q_points.size() != n_nodes)
82 mooseError("Nodal quadrature produced ",
83 q_points.size(),
84 " points for secondary mortar element ",
85 secondary_elem.id(),
86 " of type ",
87 libMesh::Utility::enum_to_string<ElemType>(secondary_elem.type()),
88 ", but the element has ",
89 n_nodes,
90 " nodes.");
91
92 const auto invalid_node = std::numeric_limits<unsigned int>::max();
93 std::vector<unsigned int> qpoint_to_node(n_nodes, invalid_node);
94 std::vector<bool> node_used(n_nodes, false);
95
96 const Real element_size = secondary_elem.hmax();
97 mooseAssert(element_size > 0,
98 "Secondary mortar element "
99 << secondary_elem.id() << " of type "
100 << libMesh::Utility::enum_to_string<ElemType>(secondary_elem.type())
101 << " has a non-positive hmax and cannot be used for nodal quadrature point "
102 "matching.");
103
104 // The nodal quadrature locations and the generated secondary nodes are two floating-point
105 // reconstructions of the same physical points. Scale the tolerance by element size so the
106 // matching is insensitive to coordinate magnitude; the 100*TOLERANCE factor allows roundoff
107 // from FE reinitialization and mesh generation while remaining far below a valid node spacing.
108 const Real matching_tol = 100 * TOLERANCE * element_size;
109 const Real matching_tol_sq = matching_tol * matching_tol;
110
111 // Each nodal quadrature point should coincide with exactly one still-unused
112 // secondary node. The unused-node search makes the mapping one-to-one and
113 // avoids silently assigning two quadrature entries to the same node.
114 for (const auto qp : make_range(q_points.size()))
115 {
116 unsigned int closest_node = invalid_node;
117 Real closest_dist_sq = std::numeric_limits<Real>::max();
118 Real second_closest_dist_sq = std::numeric_limits<Real>::max();
119
120 for (const auto n : make_range(n_nodes))
121 {
122 if (node_used[n])
123 continue;
124
125 const Real dist_sq = (q_points[qp] - secondary_elem.point(n)).norm_sq();
126 if (dist_sq < closest_dist_sq)
127 {
128 second_closest_dist_sq = closest_dist_sq;
129 closest_dist_sq = dist_sq;
130 closest_node = n;
131 }
132 else if (dist_sq < second_closest_dist_sq)
133 second_closest_dist_sq = dist_sq;
134 }
135
136 if (closest_node == invalid_node || closest_dist_sq > matching_tol_sq)
137 mooseError("Could not match nodal quadrature point ",
138 qp,
139 " at ",
140 q_points[qp],
141 " to a node on secondary mortar element ",
142 secondary_elem.id(),
143 " of type ",
144 libMesh::Utility::enum_to_string<ElemType>(secondary_elem.type()),
145 ". The nearest unmatched node distance is ",
146 std::sqrt(closest_dist_sq),
147 ", which exceeds the tolerance ",
148 matching_tol,
149 ".");
150
151 if (second_closest_dist_sq <= matching_tol_sq)
152 mooseError("Nodal quadrature point ",
153 qp,
154 " at ",
155 q_points[qp],
156 " does not map uniquely to secondary mortar element ",
157 secondary_elem.id(),
158 " of type ",
159 libMesh::Utility::enum_to_string<ElemType>(secondary_elem.type()),
160 ". Two unmatched nodes are within the matching tolerance ",
161 matching_tol,
162 ".");
163
164 qpoint_to_node[qp] = closest_node;
165 node_used[closest_node] = true;
166 }
167
168#ifdef DEBUG
169 // In optimized builds the mapping above skips already matched nodes for speed. In debug builds,
170 // audit the full candidate set to catch ambiguous geometry or accidental many-to-one matches.
171 std::vector<unsigned int> node_to_qpoint(n_nodes, invalid_node);
172 for (const auto qp : make_range(q_points.size()))
173 {
174 const auto mapped_node = qpoint_to_node[qp];
175 mooseAssert(mapped_node != invalid_node && mapped_node < n_nodes,
176 "Invalid secondary node mapping for nodal quadrature point " << qp << ".");
177 mooseAssert(node_to_qpoint[mapped_node] == invalid_node,
178 "Secondary node " << mapped_node << " on mortar element " << secondary_elem.id()
179 << " was matched to both nodal quadrature point "
180 << node_to_qpoint[mapped_node] << " and " << qp << ".");
181 node_to_qpoint[mapped_node] = qp;
182
183 // Check the qp -> node direction without excluding nodes already matched by previous qps.
184 unsigned int candidate_count = 0;
185 unsigned int candidate_node = invalid_node;
186 for (const auto n : make_range(n_nodes))
187 if ((q_points[qp] - secondary_elem.point(n)).norm_sq() <= matching_tol_sq)
188 {
189 ++candidate_count;
190 candidate_node = n;
191 }
192
193 mooseAssert(candidate_count == 1,
194 "Nodal quadrature point " << qp << " on mortar element " << secondary_elem.id()
195 << " has " << candidate_count
196 << " secondary node candidates within tolerance "
197 << matching_tol << ".");
198 mooseAssert(candidate_node == mapped_node,
199 "Nodal quadrature point " << qp << " on mortar element " << secondary_elem.id()
200 << " was matched to node " << mapped_node
201 << ", but the full candidate search found node "
202 << candidate_node << ".");
203 }
204
205 for (const auto n : make_range(n_nodes))
206 {
207 mooseAssert(node_to_qpoint[n] != invalid_node,
208 "Secondary node " << n << " on mortar element " << secondary_elem.id()
209 << " was not matched to a nodal quadrature point.");
210
211 // Check the node -> qp direction so every secondary node is also uniquely represented.
212 unsigned int candidate_count = 0;
213 unsigned int candidate_qp = invalid_node;
214 for (const auto qp : make_range(q_points.size()))
215 if ((q_points[qp] - secondary_elem.point(n)).norm_sq() <= matching_tol_sq)
216 {
217 ++candidate_count;
218 candidate_qp = qp;
219 }
220
221 mooseAssert(candidate_count == 1,
222 "Secondary node " << n << " on mortar element " << secondary_elem.id() << " has "
223 << candidate_count
224 << " nodal quadrature point candidates within tolerance "
225 << matching_tol << ".");
226 mooseAssert(candidate_qp == node_to_qpoint[n],
227 "Secondary node " << n << " on mortar element " << secondary_elem.id()
228 << " was matched to nodal quadrature point " << node_to_qpoint[n]
229 << ", but the full candidate search found point " << candidate_qp
230 << ".");
231 }
232#endif
233
234 return qpoint_to_node;
235}
236}
237
239{
240public:
242 {
243 auto params = Output::validParams();
244 params.addPrivateParam<AutomaticMortarGeneration *>("_amg", nullptr);
245 params.addPrivateParam<MooseApp *>(MooseBase::app_param, nullptr);
246 params.set<std::string>(MooseBase::type_param) = "MortarNodalGeometryOutput";
247 return params;
248 };
249
254
255 void output() override
256 {
257 // Must call compute_nodal_geometry first!
260 mooseError("No entries found in the secondary node -> nodal geometry map.");
261
262 auto & problem = _app.feProblem();
263 auto & subproblem = _amg._on_displaced ? cast_ref<SubProblem &>(*problem.getDisplacedProblem())
264 : cast_ref<SubProblem &>(problem);
265 auto & nodal_normals_es = subproblem.es();
266
267 const std::string nodal_normals_sys_name = "nodal_normals";
268
270 {
271 for (const auto s : make_range(nodal_normals_es.n_systems()))
272 if (!nodal_normals_es.get_system(s).is_initialized())
273 // This is really early on in the simulation and the systems have not been initialized. We
274 // thus need to avoid calling reinit on systems that haven't even had their first init yet
275 return;
276
278 &nodal_normals_es.template add_system<ExplicitSystem>(nodal_normals_sys_name);
279 _nnx_var_num = _nodal_normals_system->add_variable("nodal_normal_x", FEType(FIRST, LAGRANGE)),
280 _nny_var_num = _nodal_normals_system->add_variable("nodal_normal_y", FEType(FIRST, LAGRANGE));
281 _nnz_var_num = _nodal_normals_system->add_variable("nodal_normal_z", FEType(FIRST, LAGRANGE));
282
284 _nodal_normals_system->add_variable("nodal_tangent_1_x", FEType(FIRST, LAGRANGE)),
286 _nodal_normals_system->add_variable("nodal_tangent_1_y", FEType(FIRST, LAGRANGE));
288 _nodal_normals_system->add_variable("nodal_tangent_1_z", FEType(FIRST, LAGRANGE));
289
291 _nodal_normals_system->add_variable("nodal_tangent_2_x", FEType(FIRST, LAGRANGE)),
293 _nodal_normals_system->add_variable("nodal_tangent_2_y", FEType(FIRST, LAGRANGE));
295 _nodal_normals_system->add_variable("nodal_tangent_2_z", FEType(FIRST, LAGRANGE));
296 nodal_normals_es.reinit();
297 }
298
299 const DofMap & dof_map = _nodal_normals_system->get_dof_map();
300 std::vector<dof_id_type> dof_indices_nnx, dof_indices_nny, dof_indices_nnz;
301 std::vector<dof_id_type> dof_indices_t1x, dof_indices_t1y, dof_indices_t1z;
302 std::vector<dof_id_type> dof_indices_t2x, dof_indices_t2y, dof_indices_t2z;
303
304 for (MeshBase::const_element_iterator el = _amg._mesh.elements_begin(),
305 end_el = _amg._mesh.elements_end();
306 el != end_el;
307 ++el)
308 {
309 const Elem * elem = *el;
310
311 // Get the nodal dofs for this Elem.
312 dof_map.dof_indices(elem, dof_indices_nnx, _nnx_var_num);
313 dof_map.dof_indices(elem, dof_indices_nny, _nny_var_num);
314 dof_map.dof_indices(elem, dof_indices_nnz, _nnz_var_num);
315
316 dof_map.dof_indices(elem, dof_indices_t1x, _t1x_var_num);
317 dof_map.dof_indices(elem, dof_indices_t1y, _t1y_var_num);
318 dof_map.dof_indices(elem, dof_indices_t1z, _t1z_var_num);
319
320 dof_map.dof_indices(elem, dof_indices_t2x, _t2x_var_num);
321 dof_map.dof_indices(elem, dof_indices_t2y, _t2y_var_num);
322 dof_map.dof_indices(elem, dof_indices_t2z, _t2z_var_num);
323
324 //
325
326 // For each node of the Elem, if it is in the secondary_node_to_nodal_normal
327 // container, set the corresponding nodal normal dof values.
328 for (MooseIndex(elem->n_vertices()) n = 0; n < elem->n_vertices(); ++n)
329 {
330 auto it = _amg._secondary_node_to_nodal_normal.find(elem->node_ptr(n));
332 {
333 _nodal_normals_system->solution->set(dof_indices_nnx[n], it->second(0));
334 _nodal_normals_system->solution->set(dof_indices_nny[n], it->second(1));
335 _nodal_normals_system->solution->set(dof_indices_nnz[n], it->second(2));
336 }
337
338 auto it_tangent = _amg._secondary_node_to_hh_nodal_tangents.find(elem->node_ptr(n));
339 if (it_tangent != _amg._secondary_node_to_hh_nodal_tangents.end())
340 {
341 _nodal_normals_system->solution->set(dof_indices_t1x[n], it_tangent->second[0](0));
342 _nodal_normals_system->solution->set(dof_indices_t1y[n], it_tangent->second[0](1));
343 _nodal_normals_system->solution->set(dof_indices_t1z[n], it_tangent->second[0](2));
344
345 _nodal_normals_system->solution->set(dof_indices_t2x[n], it_tangent->second[1](0));
346 _nodal_normals_system->solution->set(dof_indices_t2y[n], it_tangent->second[1](1));
347 _nodal_normals_system->solution->set(dof_indices_t2z[n], it_tangent->second[1](2));
348 }
349
350 } // end loop over nodes
351 } // end loop over elems
352
353 // Finish assembly.
355
356 std::set<std::string> sys_names = {nodal_normals_sys_name};
357
358 // Write the nodal normals to file
359 ExodusII_IO nodal_normals_writer(_amg._mesh);
360
361 // Default to non-HDF5 output for wider compatibility
362 nodal_normals_writer.set_hdf5_writing(false);
363
364 nodal_normals_writer.write_equation_systems(
365 "nodal_geometry_only.e", nodal_normals_es, &sys_names);
366 }
367
368private:
371
373
375 unsigned int _nnx_var_num;
376 unsigned int _nny_var_num;
377 unsigned int _nnz_var_num;
378
379 unsigned int _t1x_var_num;
380 unsigned int _t1y_var_num;
381 unsigned int _t1z_var_num;
382
383 unsigned int _t2x_var_num;
384 unsigned int _t2y_var_num;
385 unsigned int _t2z_var_num;
387};
388
390 MooseApp & app,
391 MeshBase & mesh_in,
392 const std::pair<BoundaryID, BoundaryID> & boundary_key,
393 const std::pair<SubdomainID, SubdomainID> & subdomain_key,
394 bool on_displaced,
395 bool periodic,
396 const bool debug,
397 const bool correct_edge_dropping,
398 const Real minimum_projection_angle,
399 const Mortar3DSubpatchPlane mortar_3d_subpatch_plane,
400 const MortarSegmentTriangulationMode triangulation_mode,
401 const bool triangulate_triangles,
402 const Mortar3DQuadraturePointMapping mortar_3d_qp_mapping)
404 _app(app),
405 _mesh(mesh_in),
406 _debug(debug),
407 _on_displaced(on_displaced),
408 _periodic(periodic),
409 // 3D mortar always builds the mortar segment mesh distributedly (each rank adds only its local
410 // secondary elements). For 2D, we ghost the entire mortar interface when displaced, so
411 // displaced meshes are always replicated; otherwise follow the parent mesh.
412 _distributed(_mesh.mesh_dimension() == 3 ? true : (!_on_displaced && !_mesh.is_replicated())),
413 _correct_edge_dropping(correct_edge_dropping),
414 _minimum_projection_angle(minimum_projection_angle),
415 _mortar_3d_subpatch_plane(mortar_3d_subpatch_plane),
416 _triangulation_mode(triangulation_mode),
417 _triangulate_triangles(triangulate_triangles),
418 _mortar_3d_qp_mapping(mortar_3d_qp_mapping)
419{
420 _primary_secondary_boundary_id_pairs.push_back(boundary_key);
421 _primary_requested_boundary_ids.insert(boundary_key.first);
422 _secondary_requested_boundary_ids.insert(boundary_key.second);
423 _primary_secondary_subdomain_id_pairs.push_back(subdomain_key);
424 _primary_boundary_subdomain_ids.insert(subdomain_key.first);
425 _secondary_boundary_subdomain_ids.insert(subdomain_key.second);
426
427 if (_distributed)
429 std::make_unique<DistributedMesh>(_mesh.comm(), _mesh.spatial_dimension());
430 else
432 std::make_unique<ReplicatedMesh>(_mesh.comm(), _mesh.spatial_dimension());
433}
434
435std::string
437{
438 std::vector<std::string> string_vec(_primary_secondary_boundary_id_pairs.size() * 2 + 1);
439 for (const auto i : index_range(_primary_secondary_boundary_id_pairs))
440 {
441 const auto [primary_bnd_id, secondary_bnd_id] = _primary_secondary_boundary_id_pairs[i];
442 string_vec[2 * i] = std::to_string(primary_bnd_id);
443 string_vec[2 * i + 1] = std::to_string(secondary_bnd_id);
444 }
445 string_vec.back() = _on_displaced ? "displaced" : "undisplaced";
446 return MooseUtils::join(string_vec, "_");
447}
448
449void
451{
452 if (!_debug)
453 return;
454
455 _output_params = std::make_unique<InputParameters>(MortarNodalGeometryOutput::validParams());
456 _output_params->set<AutomaticMortarGeneration *>("_amg") = this;
457 _output_params->set<FEProblemBase *>("_fe_problem_base") = &_app.feProblem();
459 _output_params->set<std::string>(MooseBase::name_param) =
460 "mortar_nodal_geometry_" + mortarInterfaceName();
461 _output_params->finalize("MortarNodalGeometryOutput");
462 _app.getOutputWarehouse().addOutput(std::make_shared<MortarNodalGeometryOutput>(*_output_params));
463}
464
465void
486
488AutomaticMortarGeneration::mortarSegmentReferencePoints(const Elem & mortar_segment_elem) const
489{
490 if (_mortar_3d_qp_mapping != Mortar3DQuadraturePointMapping::REFERENCE_INTERPOLATION)
491 mooseError("Mortar segment reference points were requested for mortar segment element ",
492 mortar_segment_elem.id(),
493 ", but the reference-interpolation mapping mode is not enabled.");
494
495 const auto reference_points_it = _msm_elem_to_reference_points.find(&mortar_segment_elem);
496 if (reference_points_it == _msm_elem_to_reference_points.end())
497 mooseError("No reference-point record was found for mortar segment element ",
498 mortar_segment_elem.id(),
499 ". The mortar segment info and reference-point maps are not aligned.");
500
501 return reference_points_it->second;
502}
503
504void
506{
509 "Must specify secondary and primary boundary ids before building node-to-elem maps.");
510
511 // Construct nodes_to_secondary_elem_map
512 for (const auto & secondary_elem :
513 as_range(_mesh.active_elements_begin(), _mesh.active_elements_end()))
514 {
515 // If this is not one of the lower-dimensional secondary side elements, go on to the next one.
516 if (!this->_secondary_boundary_subdomain_ids.count(secondary_elem->subdomain_id()))
517 continue;
518
519 for (const auto & nd : secondary_elem->node_ref_range())
520 {
521 std::vector<const Elem *> & vec = _nodes_to_secondary_elem_map[nd.id()];
522 vec.push_back(secondary_elem);
523 }
524 }
525
526 // Construct nodes_to_primary_elem_map
527 for (const auto & primary_elem :
528 as_range(_mesh.active_elements_begin(), _mesh.active_elements_end()))
529 {
530 // If this is not one of the lower-dimensional primary side elements, go on to the next one.
531 if (!this->_primary_boundary_subdomain_ids.count(primary_elem->subdomain_id()))
532 continue;
533
534 for (const auto & nd : primary_elem->node_ref_range())
535 {
536 std::vector<const Elem *> & vec = _nodes_to_primary_elem_map[nd.id()];
537 vec.push_back(primary_elem);
538 }
539 }
540}
541
542std::vector<Point>
543AutomaticMortarGeneration::getNodalNormals(const Elem & secondary_elem) const
544{
545 std::vector<Point> nodal_normals(secondary_elem.n_nodes());
546 for (const auto n : make_range(secondary_elem.n_nodes()))
547 nodal_normals[n] = _secondary_node_to_nodal_normal.at(secondary_elem.node_ptr(n));
548
549 return nodal_normals;
550}
551
552const Elem *
554 dof_id_type secondary_elem_id) const
555{
556 mooseAssert(_secondary_element_to_secondary_lowerd_element.count(secondary_elem_id),
557 "Map should locate secondary element");
558
559 return _secondary_element_to_secondary_lowerd_element.at(secondary_elem_id);
560}
561
562std::map<unsigned int, unsigned int>
564{
565 std::map<unsigned int, unsigned int> secondary_ip_i_to_lower_secondary_i;
566 const Elem * const secondary_ip = lower_secondary_elem.interior_parent();
567 mooseAssert(secondary_ip, "This should be non-null");
568
569 for (const auto i : make_range(lower_secondary_elem.n_nodes()))
570 {
571 const auto & nd = lower_secondary_elem.node_ref(i);
572 secondary_ip_i_to_lower_secondary_i[secondary_ip->get_node_index(&nd)] = i;
573 }
574
575 return secondary_ip_i_to_lower_secondary_i;
576}
577
578std::map<unsigned int, unsigned int>
580 const Elem & lower_primary_elem,
581 const Elem & primary_elem,
582 const Elem & /*lower_secondary_elem*/) const
583{
584 std::map<unsigned int, unsigned int> primary_ip_i_to_lower_primary_i;
585
586 for (const auto i : make_range(lower_primary_elem.n_nodes()))
587 {
588 const auto & nd = lower_primary_elem.node_ref(i);
589 primary_ip_i_to_lower_primary_i[primary_elem.get_node_index(&nd)] = i;
590 }
591
592 return primary_ip_i_to_lower_primary_i;
593}
594
595std::array<MooseUtils::SemidynamicVector<Point, 9>, 2>
596AutomaticMortarGeneration::getNodalTangents(const Elem & secondary_elem) const
597{
598 // MetaPhysicL will check if we ran out of allocated space.
599 MooseUtils::SemidynamicVector<Point, 9> nodal_tangents_one(0);
600 MooseUtils::SemidynamicVector<Point, 9> nodal_tangents_two(0);
601
602 for (const auto n : make_range(secondary_elem.n_nodes()))
603 {
604 const auto & tangent_vectors =
605 libmesh_map_find(_secondary_node_to_hh_nodal_tangents, secondary_elem.node_ptr(n));
606 nodal_tangents_one.push_back(tangent_vectors[0]);
607 nodal_tangents_two.push_back(tangent_vectors[1]);
608 }
609
610 return {{nodal_tangents_one, nodal_tangents_two}};
611}
612
613std::vector<Point>
614AutomaticMortarGeneration::getNormals(const Elem & secondary_elem,
615 const std::vector<Real> & oned_xi1_pts) const
616{
617 std::vector<Point> xi1_pts(oned_xi1_pts.size());
618 for (const auto qp : index_range(oned_xi1_pts))
619 xi1_pts[qp] = oned_xi1_pts[qp];
620
621 return getNormals(secondary_elem, xi1_pts);
622}
623
624std::vector<Point>
625AutomaticMortarGeneration::getNormals(const Elem & secondary_elem,
626 const std::vector<Point> & xi1_pts) const
627{
628 const auto mortar_dim = _mesh.mesh_dimension() - 1;
629 const auto num_qps = xi1_pts.size();
630 const auto nodal_normals = getNodalNormals(secondary_elem);
631 std::vector<Point> normals(num_qps);
632
633 for (const auto n : make_range(secondary_elem.n_nodes()))
634 for (const auto qp : make_range(num_qps))
635 {
636 const auto phi =
637 (mortar_dim == 1)
638 ? Moose::fe_lagrange_1D_shape(secondary_elem.default_order(), n, xi1_pts[qp](0))
639 : Moose::fe_lagrange_2D_shape(secondary_elem.type(),
640 secondary_elem.default_order(),
641 n,
642 cast_ref<const TypeVector<Real> &>(xi1_pts[qp]));
643 normals[qp] += phi * nodal_normals[n];
644 }
645
646 if (_periodic)
647 for (auto & normal : normals)
648 normal *= -1;
649
650 return normals;
651}
652
653void
655{
656 using std::abs;
657
658 dof_id_type local_id_index = 0;
659 std::size_t node_unique_id_offset = 0;
660
661 // Create an offset by the maximum number of mortar segment elements that can be created *plus*
662 // the number of lower-dimensional secondary subdomain elements. Recall that the number of mortar
663 // segments created is a function of node projection, *and* that if we split elems we will delete
664 // that elem which has already taken a unique id
665 for (const auto & pr : _primary_secondary_boundary_id_pairs)
666 {
667 const auto primary_bnd_id = pr.first;
668 const auto secondary_bnd_id = pr.second;
669 const auto num_primary_nodes =
670 std::distance(_mesh.bid_nodes_begin(primary_bnd_id), _mesh.bid_nodes_end(primary_bnd_id));
671 const auto num_secondary_nodes = std::distance(_mesh.bid_nodes_begin(secondary_bnd_id),
672 _mesh.bid_nodes_end(secondary_bnd_id));
673 mooseAssert(num_primary_nodes,
674 "There are no primary nodes on boundary ID "
675 << primary_bnd_id << ". Does that bondary ID even exist on the mesh?");
676 mooseAssert(num_secondary_nodes,
677 "There are no secondary nodes on boundary ID "
678 << secondary_bnd_id << ". Does that bondary ID even exist on the mesh?");
679
680 node_unique_id_offset += num_primary_nodes + 2 * num_secondary_nodes;
681 }
682
683 // 1.) Add all lower-dimensional secondary side elements as the "initial" mortar segments.
684 for (MeshBase::const_element_iterator el = _mesh.active_elements_begin(),
685 end_el = _mesh.active_elements_end();
686 el != end_el;
687 ++el)
688 {
689 const Elem * secondary_elem = *el;
690
691 // If this is not one of the lower-dimensional secondary side elements, go on to the next one.
692 if (!this->_secondary_boundary_subdomain_ids.count(secondary_elem->subdomain_id()))
693 continue;
694
695 std::vector<Node *> new_nodes;
696 for (MooseIndex(secondary_elem->n_nodes()) n = 0; n < secondary_elem->n_nodes(); ++n)
697 {
698 new_nodes.push_back(_mortar_segment_mesh->add_point(
699 secondary_elem->point(n), secondary_elem->node_id(n), secondary_elem->processor_id()));
700 Node * const new_node = new_nodes.back();
701 new_node->set_unique_id(new_node->id() + node_unique_id_offset);
702 }
703
704 std::unique_ptr<Elem> new_elem;
705 if (secondary_elem->default_order() == SECOND)
706 new_elem = std::make_unique<Edge3>();
707 else
708 new_elem = std::make_unique<Edge2>();
709
710 new_elem->processor_id() = secondary_elem->processor_id();
711 new_elem->subdomain_id() = secondary_elem->subdomain_id();
712 new_elem->set_id(local_id_index++);
713 new_elem->set_unique_id(new_elem->id());
714
715 for (MooseIndex(new_elem->n_nodes()) n = 0; n < new_elem->n_nodes(); ++n)
716 new_elem->set_node(n, new_nodes[n]);
717
718 Elem * new_elem_ptr = _mortar_segment_mesh->add_elem(new_elem.release());
719
720 // The xi^(1) values for this mortar segment are initially -1 and 1.
721 MortarSegmentInfo msinfo;
722 msinfo.xi1_a = -1;
723 msinfo.xi1_b = +1;
724 msinfo.secondary_elem = secondary_elem;
725
726 auto new_container_it0 = _secondary_node_and_elem_to_xi2_primary_elem.find(
727 std::make_pair(secondary_elem->node_ptr(0), secondary_elem)),
728 new_container_it1 = _secondary_node_and_elem_to_xi2_primary_elem.find(
729 std::make_pair(secondary_elem->node_ptr(1), secondary_elem));
730
731 bool new_container_node0_found =
732 (new_container_it0 != _secondary_node_and_elem_to_xi2_primary_elem.end()),
733 new_container_node1_found =
734 (new_container_it1 != _secondary_node_and_elem_to_xi2_primary_elem.end());
735
736 const Elem * node0_primary_candidate = nullptr;
737 const Elem * node1_primary_candidate = nullptr;
738
739 if (new_container_node0_found)
740 {
741 const auto & xi2_primary_elem_pair = new_container_it0->second;
742 msinfo.xi2_a = xi2_primary_elem_pair.first;
743 node0_primary_candidate = xi2_primary_elem_pair.second;
744 }
745
746 if (new_container_node1_found)
747 {
748 const auto & xi2_primary_elem_pair = new_container_it1->second;
749 msinfo.xi2_b = xi2_primary_elem_pair.first;
750 node1_primary_candidate = xi2_primary_elem_pair.second;
751 }
752
753 // If both node0 and node1 agree on the primary element they are
754 // projected into, then this mortar segment fits entirely within
755 // a single primary element, and we can go ahead and set the
756 // msinfo.primary_elem pointer now.
757 if (node0_primary_candidate == node1_primary_candidate)
758 msinfo.primary_elem = node0_primary_candidate;
759
760 // Associate this MSM elem with the MortarSegmentInfo.
761 _msm_elem_to_info.emplace(new_elem_ptr, msinfo);
762
763 // Maintain the mapping between secondary elems and mortar segment elems contained within them.
764 // Initially, only the original secondary_elem is present.
765 _secondary_elems_to_mortar_segments[secondary_elem->id()].insert(new_elem_ptr);
766 }
767
768 // 2.) Insert new nodes from primary side and split mortar segments as necessary.
770 {
771 auto key = pr.first;
772 auto val = pr.second;
773
774 const Node * primary_node = std::get<1>(key);
775 Real xi1 = val.first;
776 const Elem * secondary_elem = val.second;
777
778 // If this is an aligned node, we don't need to do anything.
779 if (abs(abs(xi1) - 1.) < _xi_tolerance)
780 continue;
781
782 auto && order = secondary_elem->default_order();
783
784 // Determine physical location of new point to be inserted.
785 Point new_pt(0);
786 for (MooseIndex(secondary_elem->n_nodes()) n = 0; n < secondary_elem->n_nodes(); ++n)
787 new_pt += Moose::fe_lagrange_1D_shape(order, n, xi1) * secondary_elem->point(n);
788
789 // Find the current mortar segment that will have to be split.
790 auto & mortar_segment_set = _secondary_elems_to_mortar_segments[secondary_elem->id()];
791 Elem * current_mortar_segment = nullptr;
792 MortarSegmentInfo * info = nullptr;
793
794 for (const auto & mortar_segment_candidate : mortar_segment_set)
795 {
796 try
797 {
798 info = &_msm_elem_to_info.at(mortar_segment_candidate);
799 }
800 catch (std::out_of_range &)
801 {
802 mooseError("MortarSegmentInfo not found for the mortar segment candidate");
803 }
804 if (info->xi1_a <= xi1 && xi1 <= info->xi1_b)
805 {
806 current_mortar_segment = mortar_segment_candidate;
807 break;
808 }
809 }
810
811 // Make sure we found one.
812 if (current_mortar_segment == nullptr)
813 mooseError("Unable to find appropriate mortar segment during linear search!");
814
815 // If node lands on endpoint of segment, don't split.
816 // Jacob: This condition was getting missed by the < comparison a few lines above. To fix it I
817 // just made it <= and put this condition in to handle equality different. It probably could be
818 // done with a tolerance but the the toleranced equality is already handled later when we drop
819 // segments with small volume.
820 if (info->xi1_a == xi1 || xi1 == info->xi1_b)
821 continue;
822
823 const auto new_id = _mortar_segment_mesh->max_node_id();
824 mooseAssert(_mortar_segment_mesh->comm().verify(new_id),
825 "new_id must be the same on all processes");
826 Node * const new_node =
827 _mortar_segment_mesh->add_point(new_pt, new_id, secondary_elem->processor_id());
828 new_node->set_unique_id(new_id + node_unique_id_offset);
829
830 // Reconstruct the nodal normal at xi1. This will help us
831 // determine the orientation of the primary elems relative to the
832 // new mortar segments.
833 const Point normal = getNormals(*secondary_elem, std::vector<Real>({xi1}))[0];
834
835 // Get the set of primary_node neighbors.
836 if (this->_nodes_to_primary_elem_map.find(primary_node->id()) ==
837 this->_nodes_to_primary_elem_map.end())
838 mooseError("We should already have built this primary node to elem pair!");
839 const std::vector<const Elem *> & primary_node_neighbors =
840 this->_nodes_to_primary_elem_map[primary_node->id()];
841
842 // Sanity check
843 if (primary_node_neighbors.size() == 0 || primary_node_neighbors.size() > 2)
844 mooseError("We must have either 1 or 2 primary side nodal neighbors, but we had ",
845 primary_node_neighbors.size());
846
847 // Primary Elem pointers which we will eventually assign to the
848 // mortar segments being created. We start by assuming
849 // primary_node_neighbor[0] is on the "left" and
850 // primary_node_neighbor[1]/"nothing" is on the "right" and then
851 // swap them if that's not the case.
852 const Elem * left_primary_elem = primary_node_neighbors[0];
853 const Elem * right_primary_elem =
854 (primary_node_neighbors.size() == 2) ? primary_node_neighbors[1] : nullptr;
855
857
858 // Storage for z-component of cross products for determining
859 // orientation.
860 std::array<Real, 2> secondary_node_cps;
861 std::vector<Real> primary_node_cps(primary_node_neighbors.size());
862
863 // Store z-component of left and right secondary node cross products with the nodal normal.
864 for (unsigned int nid = 0; nid < 2; ++nid)
865 secondary_node_cps[nid] = normal.cross(secondary_elem->point(nid) - new_pt)(2);
866
867 for (MooseIndex(primary_node_neighbors) mnn = 0; mnn < primary_node_neighbors.size(); ++mnn)
868 {
869 const Elem * primary_neigh = primary_node_neighbors[mnn];
870 Point opposite = (primary_neigh->node_ptr(0) == primary_node) ? primary_neigh->point(1)
871 : primary_neigh->point(0);
872 Point cp = normal.cross(opposite - new_pt);
873 primary_node_cps[mnn] = cp(2);
874 }
875
876 // We will verify that only 1 orientation is actually valid.
877 bool orientation1_valid = false, orientation2_valid = false;
878
879 if (primary_node_neighbors.size() == 2)
880 {
881 // 2 primary neighbor case
882 orientation1_valid = (secondary_node_cps[0] * primary_node_cps[0] > 0.) &&
883 (secondary_node_cps[1] * primary_node_cps[1] > 0.);
884
885 orientation2_valid = (secondary_node_cps[0] * primary_node_cps[1] > 0.) &&
886 (secondary_node_cps[1] * primary_node_cps[0] > 0.);
887 }
888 else if (primary_node_neighbors.size() == 1)
889 {
890 // 1 primary neighbor case
891 orientation1_valid = (secondary_node_cps[0] * primary_node_cps[0] > 0.);
892 orientation2_valid = (secondary_node_cps[1] * primary_node_cps[0] > 0.);
893 }
894 else
895 mooseError("Invalid primary node neighbors size ", primary_node_neighbors.size());
896
897 // Verify that both orientations are not simultaneously valid/invalid. If they are not, then we
898 // are going to throw an exception instead of erroring out since we can easily reach this point
899 // if we have one bad linear solve. It's better in general to catch the error and then try a
900 // smaller time-step
901 if (orientation1_valid && orientation2_valid)
902 throw MooseException(
903 "AutomaticMortarGeneration: Both orientations cannot simultaneously be valid.");
904
905 // We are going to treat the case where both orientations are invalid as a case in which we
906 // should not be splitting the mortar mesh to incorporate primary mesh elements.
907 // In practice, this case has appeared for very oblique projections, so we assume these cases
908 // will not be considered in mortar thermomechanical contact.
909 if (!orientation1_valid && !orientation2_valid)
910 {
911 static const auto invalid_orientation_id =
913 "AutomaticMortarGeneration",
914 "Unable to determine valid secondary-primary orientation. Consequently we will "
915 "consider projection of the primary node invalid and not split the mortar segment. "
916 "This situation can indicate there are very oblique projections between primary "
917 "(mortar) and secondary (non-mortar) surfaces for a good problem set up. It can also "
918 "mean your time step is too large.",
919 true);
920 _app.solutionInvalidity().flagInvalidSolutionInternal(invalid_orientation_id);
921 continue;
922 }
923
924 // Make an Elem on the left
925 std::unique_ptr<Elem> new_elem_left;
926 if (order == SECOND)
927 new_elem_left = std::make_unique<Edge3>();
928 else
929 new_elem_left = std::make_unique<Edge2>();
930
931 new_elem_left->processor_id() = current_mortar_segment->processor_id();
932 new_elem_left->subdomain_id() = current_mortar_segment->subdomain_id();
933 new_elem_left->set_id(local_id_index++);
934 new_elem_left->set_unique_id(new_elem_left->id());
935 new_elem_left->set_node(0, current_mortar_segment->node_ptr(0));
936 new_elem_left->set_node(1, new_node);
937
938 // Make an Elem on the right
939 std::unique_ptr<Elem> new_elem_right;
940 if (order == SECOND)
941 new_elem_right = std::make_unique<Edge3>();
942 else
943 new_elem_right = std::make_unique<Edge2>();
944
945 new_elem_right->processor_id() = current_mortar_segment->processor_id();
946 new_elem_right->subdomain_id() = current_mortar_segment->subdomain_id();
947 new_elem_right->set_id(local_id_index++);
948 new_elem_right->set_unique_id(new_elem_right->id());
949 new_elem_right->set_node(0, new_node);
950 new_elem_right->set_node(1, current_mortar_segment->node_ptr(1));
951
952 if (order == SECOND)
953 {
954 // left
955 Point left_interior_point(0);
956 Real left_interior_xi = (xi1 + info->xi1_a) / 2;
957
958 // This is eta for the current mortar segment that we're splitting
959 Real current_left_interior_eta =
960 (2. * left_interior_xi - info->xi1_a - info->xi1_b) / (info->xi1_b - info->xi1_a);
961
962 for (MooseIndex(current_mortar_segment->n_nodes()) n = 0;
963 n < current_mortar_segment->n_nodes();
964 ++n)
965 left_interior_point += Moose::fe_lagrange_1D_shape(order, n, current_left_interior_eta) *
966 current_mortar_segment->point(n);
967
968 const auto new_interior_left_id = _mortar_segment_mesh->max_node_id();
969 mooseAssert(_mortar_segment_mesh->comm().verify(new_interior_left_id),
970 "new_id must be the same on all processes");
971 Node * const new_interior_node_left = _mortar_segment_mesh->add_point(
972 left_interior_point, new_interior_left_id, new_elem_left->processor_id());
973 new_elem_left->set_node(2, new_interior_node_left);
974 new_interior_node_left->set_unique_id(new_interior_left_id + node_unique_id_offset);
975
976 // right
977 Point right_interior_point(0);
978 Real right_interior_xi = (xi1 + info->xi1_b) / 2;
979 // This is eta for the current mortar segment that we're splitting
980 Real current_right_interior_eta =
981 (2. * right_interior_xi - info->xi1_a - info->xi1_b) / (info->xi1_b - info->xi1_a);
982
983 for (MooseIndex(current_mortar_segment->n_nodes()) n = 0;
984 n < current_mortar_segment->n_nodes();
985 ++n)
986 right_interior_point += Moose::fe_lagrange_1D_shape(order, n, current_right_interior_eta) *
987 current_mortar_segment->point(n);
988
989 const auto new_interior_id_right = _mortar_segment_mesh->max_node_id();
990 mooseAssert(_mortar_segment_mesh->comm().verify(new_interior_id_right),
991 "new_id must be the same on all processes");
992 Node * const new_interior_node_right = _mortar_segment_mesh->add_point(
993 right_interior_point, new_interior_id_right, new_elem_right->processor_id());
994 new_elem_right->set_node(2, new_interior_node_right);
995 new_interior_node_right->set_unique_id(new_interior_id_right + node_unique_id_offset);
996 }
997
998 // If orientation 2 was valid, swap the left and right primaries.
999 if (orientation2_valid)
1000 std::swap(left_primary_elem, right_primary_elem);
1001
1002 // Now that we know left_primary_elem and right_primary_elem, we can determine left_xi2 and
1003 // right_xi2.
1004 if (left_primary_elem)
1005 left_xi2 = (primary_node == left_primary_elem->node_ptr(0)) ? -1 : +1;
1006 if (right_primary_elem)
1007 right_xi2 = (primary_node == right_primary_elem->node_ptr(0)) ? -1 : +1;
1008
1009 // Grab the MortarSegmentInfo object associated with this
1010 // segment. We can use "at()" here since we want this to fail if
1011 // current_mortar_segment is not found... Since we're going to
1012 // erase this entry from the map momentarily, we make an actual
1013 // copy rather than grabbing a reference.
1014 auto msm_it = _msm_elem_to_info.find(current_mortar_segment);
1015 if (msm_it == _msm_elem_to_info.end())
1016 mooseError("MortarSegmentInfo not found for current_mortar_segment.");
1017 MortarSegmentInfo current_msinfo = msm_it->second;
1018
1019 // add_left
1020 {
1021 Elem * msm_new_elem = _mortar_segment_mesh->add_elem(new_elem_left.release());
1022
1023 // Create new MortarSegmentInfo objects for new_elem_left
1024 MortarSegmentInfo new_msinfo_left;
1025
1026 // The new MortarSegmentInfo info objects inherit their "outer"
1027 // information from current_msinfo and the rest is determined by
1028 // the Node being inserted.
1029 new_msinfo_left.xi1_a = current_msinfo.xi1_a;
1030 new_msinfo_left.xi2_a = current_msinfo.xi2_a;
1031 new_msinfo_left.secondary_elem = secondary_elem;
1032 new_msinfo_left.xi1_b = xi1;
1033 new_msinfo_left.xi2_b = left_xi2;
1034 new_msinfo_left.primary_elem = left_primary_elem;
1035
1036 // Add new msinfo objects to the map.
1037 _msm_elem_to_info.emplace(msm_new_elem, new_msinfo_left);
1038
1039 // We need to insert new_elem_left in
1040 // the mortar_segment_set for this secondary_elem.
1041 mortar_segment_set.insert(msm_new_elem);
1042 }
1043
1044 // add_right
1045 {
1046 Elem * msm_new_elem = _mortar_segment_mesh->add_elem(new_elem_right.release());
1047
1048 // Create new MortarSegmentInfo objects for new_elem_right
1049 MortarSegmentInfo new_msinfo_right;
1050
1051 new_msinfo_right.xi1_b = current_msinfo.xi1_b;
1052 new_msinfo_right.xi2_b = current_msinfo.xi2_b;
1053 new_msinfo_right.secondary_elem = secondary_elem;
1054 new_msinfo_right.xi1_a = xi1;
1055 new_msinfo_right.xi2_a = right_xi2;
1056 new_msinfo_right.primary_elem = right_primary_elem;
1057
1058 _msm_elem_to_info.emplace(msm_new_elem, new_msinfo_right);
1059
1060 mortar_segment_set.insert(msm_new_elem);
1061 }
1062
1063 // Erase the MortarSegmentInfo object for current_mortar_segment from the map.
1064 _msm_elem_to_info.erase(msm_it);
1065
1066 // current_mortar_segment must be erased from the
1067 // mortar_segment_set since it has now been split.
1068 mortar_segment_set.erase(current_mortar_segment);
1069
1070 // The original mortar segment has been split, so erase it from
1071 // the mortar segment mesh.
1072 _mortar_segment_mesh->delete_elem(current_mortar_segment);
1073 }
1074
1075 // Remove all MSM elements without a primary contribution
1081 for (auto msm_elem : _mortar_segment_mesh->active_element_ptr_range())
1082 {
1083 MortarSegmentInfo & msinfo = libmesh_map_find(_msm_elem_to_info, msm_elem);
1084 Elem * primary_elem = const_cast<Elem *>(msinfo.primary_elem);
1085 if (primary_elem == nullptr || abs(msinfo.xi2_a) > 1.0 + TOLERANCE ||
1086 abs(msinfo.xi2_b) > 1.0 + TOLERANCE)
1087 {
1088 // Erase from secondary to msms map
1089 auto it = _secondary_elems_to_mortar_segments.find(msinfo.secondary_elem->id());
1090 mooseAssert(it != _secondary_elems_to_mortar_segments.end(),
1091 "We should have found the element");
1092 auto & msm_set = it->second;
1093 msm_set.erase(msm_elem);
1094 // We may be creating nodes with only one element neighbor where before this removal there
1095 // were two. But the nodal normal used in computations will reflect the two-neighbor geometry.
1096 // For a lower-d secondary mesh corner, that will imply the corner node will have a tilted
1097 // normal vector (same for tangents) despite the mortar segment mesh not including its
1098 // vertical neighboring element. It is the secondary element neighbors (not mortar segment
1099 // mesh neighbors) that determine the nodal normal field.
1100 if (msm_set.empty())
1102
1103 // Erase msinfo
1104 _msm_elem_to_info.erase(msm_elem);
1105
1106 // Remove element from mortar segment mesh
1107 _mortar_segment_mesh->delete_elem(msm_elem);
1108 }
1109 else
1110 {
1111 _secondary_ip_sub_ids.insert(msinfo.secondary_elem->interior_parent()->subdomain_id());
1112 _primary_ip_sub_ids.insert(msinfo.primary_elem->interior_parent()->subdomain_id());
1113 }
1114 }
1115
1116 std::unordered_set<Node *> msm_connected_nodes;
1117
1118 // Deleting elements may produce isolated nodes.
1119 // Loops for identifying and removing such nodes from mortar segment mesh.
1120 for (const auto & element : _mortar_segment_mesh->element_ptr_range())
1121 for (auto & n : element->node_ref_range())
1122 msm_connected_nodes.insert(&n);
1123
1124 for (const auto & node : _mortar_segment_mesh->node_ptr_range())
1125 if (!msm_connected_nodes.count(node))
1126 _mortar_segment_mesh->delete_node(node);
1127
1128#ifdef DEBUG
1129 // Verify that all segments without primary contribution have been deleted
1130 for (auto msm_elem : _mortar_segment_mesh->active_element_ptr_range())
1131 {
1132 const MortarSegmentInfo & msinfo = libmesh_map_find(_msm_elem_to_info, msm_elem);
1133 mooseAssert(msinfo.primary_elem != nullptr,
1134 "All mortar segment elements should have valid "
1135 "primary element.");
1136 }
1137#endif
1138
1139 _mortar_segment_mesh->cache_elem_data();
1140
1141 // (Optionally) Write the mortar segment mesh to file for inspection
1142 if (_debug)
1144
1146}
1147
1148void
1150{
1151 ExodusII_IO mortar_segment_mesh_writer(*_mortar_segment_mesh);
1152
1153 // Default to non-HDF5 output for wider compatibility
1154 mortar_segment_mesh_writer.set_hdf5_writing(false);
1155
1156 std::array<std::string, 3> file_pieces = {
1157 _app.getOutputFileBase(/*for_non_moose_build_output=*/true),
1159 "mortar_segment_mesh.e"};
1160 mortar_segment_mesh_writer.write(MooseUtils::join(file_pieces, "_"));
1161}
1162
1163void
1165{
1166 const bool use_reference_interpolation =
1167 _mortar_3d_qp_mapping == Mortar3DQuadraturePointMapping::REFERENCE_INTERPOLATION;
1168
1169 // Add an integer flag to mortar segment mesh to keep track of which subelem
1170 // of second order primal elements mortar segments correspond to
1171 auto secondary_sub_elem = _mortar_segment_mesh->add_elem_integer("secondary_sub_elem");
1172 auto primary_sub_elem = _mortar_segment_mesh->add_elem_integer("primary_sub_elem");
1173
1174 // Assign globally unique node/element IDs via an exclusive prefix scan: each rank's bound is
1175 // local_secondary_sub_elems * visible_primary_sub_elems * 9, where 9 is the maximum nodes a
1176 // single secondary/primary sub-element pair can produce (8-vertex clipped polygon + center).
1177 // The result is cached and invalidated by meshChanged(), so the allgather only runs on topology
1178 // changes, not on every displaced-mesh residual update.
1179 if (!_msm_node_id_start.has_value())
1180 {
1181 dof_id_type local_secondary_sub_elems = 0, visible_primary_sub_elems = 0;
1182 for (const auto & [primary_sub_id, secondary_sub_id] : _primary_secondary_subdomain_id_pairs)
1183 {
1184 for (const auto * const el :
1185 _mesh.active_local_subdomain_elements_ptr_range(secondary_sub_id))
1186 local_secondary_sub_elems += el->n_sub_elem();
1187 for (const auto * const el : _mesh.active_subdomain_elements_ptr_range(primary_sub_id))
1188 visible_primary_sub_elems += el->n_sub_elem();
1189 }
1190 const dof_id_type per_rank_bound = local_secondary_sub_elems * visible_primary_sub_elems * 9;
1191 std::vector<dof_id_type> per_rank_bounds;
1192 _mesh.comm().allgather(per_rank_bound, per_rank_bounds);
1193 dof_id_type start = 0;
1194 for (const auto r : make_range(_mesh.processor_id()))
1195 start += per_rank_bounds[r];
1196 _msm_node_id_start = start;
1197 }
1198 dof_id_type next_node_id = *_msm_node_id_start;
1199 // Element IDs use the same starting offset: node and element IDs are separately numbered, and
1200 // element count per clip (n triangles) is always <= node count (n+1), so per_rank_bound covers
1201 // both.
1202 dof_id_type next_elem_id = next_node_id;
1203
1204 // Loop through mortar secondary and primary pairs to create mortar segment mesh between each
1205 for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1206 {
1207 const auto primary_subd_id = pr.first;
1208 const auto secondary_subd_id = pr.second;
1209
1210 // Build k-d tree for use in Step 1.2 for primary interface coarse screening
1211 NanoflannMeshSubdomainAdaptor<3> mesh_adaptor(_mesh, primary_subd_id);
1212 subdomain_kd_tree_t kd_tree(
1213 3, mesh_adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(/*max leaf=*/10));
1214
1215 // Construct the KD tree.
1216 kd_tree.buildIndex();
1217
1218 // Return the unoriented geometric normal of a linearized subpatch. These expressions are the
1219 // TRI3 and QUAD4 mapping tangents evaluated at the reference center, equivalent to evaluating
1220 // the first-order finite-element normal there without constructing a temporary element.
1221 auto get_sub_elem_geometric_normal = [](const std::vector<Point> & nodes)
1222 {
1223 Point dxdxi;
1224 Point dxdeta;
1225 if (nodes.size() == 3)
1226 {
1227 dxdxi = nodes[1] - nodes[0];
1228 dxdeta = nodes[2] - nodes[0];
1229 }
1230 else if (nodes.size() == 4)
1231 {
1232 // Bilinear center tangents define one normal for the full quad instead of selecting one of
1233 // the two diagonal triangle normals.
1234 dxdxi = 0.25 * (nodes[1] + nodes[2] - nodes[0] - nodes[3]);
1235 dxdeta = 0.25 * (nodes[2] + nodes[3] - nodes[0] - nodes[1]);
1236 }
1237 else
1238 mooseError("GEOMETRIC_NORMAL 3D mortar subpatch plane construction only supports "
1239 "triangular and quadrilateral subpatches, but received ",
1240 nodes.size(),
1241 " nodes.");
1242
1243 Point geometric_normal = dxdxi.cross(dxdeta);
1244 const auto normal_norm = geometric_normal.norm();
1245 // The cross product has units of area, so compare it with the product of tangent lengths.
1246 // Their ratio is the sine of the included angle and is independent of the mesh length scale.
1247 if (normal_norm <= TOLERANCE * dxdxi.norm() * dxdeta.norm())
1248 mooseError("GEOMETRIC_NORMAL 3D mortar subpatch plane construction encountered a "
1249 "degenerate subpatch.");
1250
1251 geometric_normal /= normal_norm;
1252 return geometric_normal;
1253 };
1254
1258 for (MeshBase::const_element_iterator el = _mesh.active_local_elements_begin(),
1259 end_el = _mesh.active_local_elements_end();
1260 el != end_el;
1261 ++el)
1262 {
1263 const Elem * secondary_side_elem = *el;
1264
1265 const Real secondary_volume = secondary_side_elem->volume();
1266
1267 // If this Elem is not in the current secondary subdomain, go on to the next one.
1268 if (secondary_side_elem->subdomain_id() != secondary_subd_id)
1269 continue;
1270
1271 auto [secondary_elem_to_msm_map_it, insertion_happened] =
1272 _secondary_elems_to_mortar_segments.emplace(secondary_side_elem->id(),
1273 std::set<Elem *, CompareDofObjectsByID>{});
1274 libmesh_ignore(insertion_happened);
1275 auto & secondary_to_msm_element_set = secondary_elem_to_msm_map_it->second;
1276
1277 std::vector<std::unique_ptr<MortarSegmentHelper>> mortar_segment_helper(
1278 secondary_side_elem->n_sub_elem());
1279 const auto nodal_normals = getNodalNormals(*secondary_side_elem);
1280
1291 for (auto sel : make_range(secondary_side_elem->n_sub_elem()))
1292 {
1293 // Get indices of sub-element nodes in element
1294 const auto sub_elem_nodes =
1295 Moose::Mortar::getMortarSubElementNodeIndices(*secondary_side_elem, sel);
1296
1297 // Secondary sub-element center, normal, and nodes
1298 Point center;
1299 Point normal;
1300 std::vector<Point> nodes(sub_elem_nodes.size());
1301
1302 // Collect the sub-element points and evaluate its center and averaged nodal normal.
1303 for (auto iv : make_range(sub_elem_nodes.size()))
1304 {
1305 const auto n = sub_elem_nodes[iv];
1306 nodes[iv] = secondary_side_elem->point(n);
1307 center += secondary_side_elem->point(n);
1308 normal += nodal_normals[n];
1309 }
1310 center /= sub_elem_nodes.size();
1311 normal = normal.unit();
1312
1313 if (_mortar_3d_subpatch_plane == Mortar3DSubpatchPlane::GEOMETRIC_NORMAL)
1314 {
1315 const Point averaged_normal = normal;
1316 normal = get_sub_elem_geometric_normal(nodes);
1317 if (normal * averaged_normal < 0)
1318 normal *= -1;
1319 }
1320
1321 if (use_reference_interpolation)
1322 {
1323 std::vector<Point> sub_elem_reference_points;
1324 sub_elem_reference_points.reserve(sub_elem_nodes.size());
1325 for (const auto node_index : sub_elem_nodes)
1326 sub_elem_reference_points.push_back(secondary_side_elem->master_point(node_index));
1327
1328 mortar_segment_helper[sel] =
1329 std::make_unique<MortarSegmentHelper>(std::move(nodes),
1330 std::move(sub_elem_reference_points),
1331 center,
1332 normal,
1335 }
1336 else
1337 mortar_segment_helper[sel] = std::make_unique<MortarSegmentHelper>(
1338 std::move(nodes), center, normal, _triangulation_mode, _triangulate_triangles);
1339 }
1340
1346 // Search point for performing Nanoflann (k-d tree) searches.
1347 // In each case we use the center point of the original element (not sub-elements for second
1348 // order elements). This is to do search for all sub-elements simultaneously
1349 std::array<Real, 3> query_pt;
1350 Point center_point;
1351 switch (secondary_side_elem->type())
1352 {
1353 case TRI3:
1354 case QUAD4:
1355 center_point = mortar_segment_helper[0]->center();
1356 query_pt = {{center_point(0), center_point(1), center_point(2)}};
1357 break;
1358 case TRI6:
1359 case TRI7:
1360 center_point = mortar_segment_helper[1]->center();
1361 query_pt = {{center_point(0), center_point(1), center_point(2)}};
1362 break;
1363 case QUAD8:
1364 center_point = mortar_segment_helper[4]->center();
1365 query_pt = {{center_point(0), center_point(1), center_point(2)}};
1366 break;
1367 case QUAD9:
1368 center_point = secondary_side_elem->point(8);
1369 query_pt = {{center_point(0), center_point(1), center_point(2)}};
1370 break;
1371 default:
1372 mooseError(
1373 "Face element type: ", secondary_side_elem->type(), "not supported for 3D mortar");
1374 }
1375
1376 // The number of results we want to get. These results will only be used to find
1377 // a single element with non-trivial overlap, after an element is identified a breadth
1378 // first search is done on neighbors
1379 const std::size_t num_results = 3;
1380
1381 // Initialize result_set and do the search.
1382 std::vector<size_t> ret_index(num_results);
1383 std::vector<Real> out_dist_sqr(num_results);
1384 nanoflann::KNNResultSet<Real> result_set(num_results);
1385 result_set.init(&ret_index[0], &out_dist_sqr[0]);
1386 kd_tree.findNeighbors(result_set, &query_pt[0], nanoflann::SearchParameters());
1387
1388 // Initialize list of processed primary elements, we don't want to revisit processed elements
1389 std::set<const Elem *, CompareDofObjectsByID> processed_primary_elems;
1390
1391 // Initialize candidate set and flag for switching between coarse screening and breadth-first
1392 // search
1393 bool primary_elem_found = false;
1394 std::set<const Elem *, CompareDofObjectsByID> primary_elem_candidates;
1395 const bool use_geometric_subpatch_normals =
1396 _mortar_3d_subpatch_plane == Mortar3DSubpatchPlane::GEOMETRIC_NORMAL;
1397 // In geometric mode the projection-angle cutoff also rejects near-orthogonal subpatch pairs.
1398 // The absolute dot product below keeps opposing primary/secondary orientations admissible.
1399 const Real minimum_subpatch_normal_alignment =
1400 use_geometric_subpatch_normals ? std::sin(_minimum_projection_angle * libMesh::pi / 180.0)
1401 : 0.0;
1402
1403 // Loop candidate nodes (returned by Nanoflann) and add all adjoining elems to candidate set
1404 for (auto r : make_range(result_set.size()))
1405 {
1406 // Verify that the squared distance we compute is the same as nanoflann's
1407 mooseAssert(abs((_mesh.point(ret_index[r]) - center_point).norm_sq() - out_dist_sqr[r]) <=
1408 TOLERANCE,
1409 "Lower-dimensional element squared distance verification failed.");
1410
1411 // Get list of elems connected to node
1412 std::vector<const Elem *> & node_elems =
1413 this->_nodes_to_primary_elem_map.at(static_cast<dof_id_type>(ret_index[r]));
1414
1415 // Uniquely add elems to candidate set
1416 for (auto elem : node_elems)
1417 primary_elem_candidates.insert(elem);
1418 }
1419
1427 while (!primary_elem_candidates.empty())
1428 {
1429 const Elem * primary_elem_candidate = *primary_elem_candidates.begin();
1430
1431 // If we've already processed this candidate, we don't need to check it again.
1432 if (processed_primary_elems.count(primary_elem_candidate))
1433 {
1434 primary_elem_candidates.erase(primary_elem_candidate);
1435 continue;
1436 }
1437
1438 // Initialize set of nodes used to construct mortar segment elements
1439 std::vector<Point> nodal_points;
1440
1441 // Initialize map from mortar segment elements to nodes
1442 std::vector<std::vector<unsigned int>> elem_to_node_map;
1443
1444 // Initialize list of secondary and primary sub-elements that formed each mortar segment
1445 std::vector<std::pair<unsigned int, unsigned int>> sub_elem_map;
1446 std::vector<std::array<Point, 3>> elem_to_secondary_reference_points;
1447 std::vector<std::array<Point, 3>> elem_to_primary_reference_points;
1448
1453 for (auto p_el : make_range(primary_elem_candidate->n_sub_elem()))
1454 {
1455 // Get nodes of primary sub-elements
1456 const auto sub_elem_nodes =
1457 Moose::Mortar::getMortarSubElementNodeIndices(*primary_elem_candidate, p_el);
1458
1459 // Get list of primary sub-element vertex nodes
1460 std::vector<Point> primary_sub_elem(sub_elem_nodes.size());
1461 for (auto iv : make_range(sub_elem_nodes.size()))
1462 {
1463 const auto n = sub_elem_nodes[iv];
1464 primary_sub_elem[iv] = primary_elem_candidate->point(n);
1465 }
1466 Point primary_sub_elem_normal;
1467 if (use_geometric_subpatch_normals)
1468 primary_sub_elem_normal = get_sub_elem_geometric_normal(primary_sub_elem);
1469
1470 std::vector<Point> sub_elem_reference_points;
1471 if (use_reference_interpolation)
1472 {
1473 sub_elem_reference_points.reserve(sub_elem_nodes.size());
1474 for (const auto node_index : sub_elem_nodes)
1475 sub_elem_reference_points.push_back(primary_elem_candidate->master_point(node_index));
1476 }
1477
1478 // Loop through secondary sub-elements
1479 for (auto s_el : make_range(secondary_side_elem->n_sub_elem()))
1480 {
1481 // Nearby primary candidates can include adjacent corner faces. Those faces may clip to
1482 // numerical slivers, which we do not consider valid face-to-face mortar pairs for this
1483 // search.
1484 if (use_geometric_subpatch_normals &&
1485 std::abs(primary_sub_elem_normal * mortar_segment_helper[s_el]->normal()) <
1486 minimum_subpatch_normal_alignment)
1487 continue;
1488
1489 // Mortar segment helpers were defined for each secondary sub-element, they will:
1490 // 1. Project primary sub-element onto linearized secondary sub-element
1491 // 2. Clip projected primary sub-element against secondary sub-element
1492 // 3. Triangulate clipped polygon to form mortar segments
1493 //
1494 // Mortar segment helpers append a list of mortar segment nodes and connectivities that
1495 // can be directly used to build mortar segments
1496 const auto segments_before_helper = elem_to_node_map.size();
1497 if (use_reference_interpolation)
1498 mortar_segment_helper[s_el]->getMortarSegments(primary_sub_elem,
1499 sub_elem_reference_points,
1500 nodal_points,
1501 elem_to_node_map,
1502 elem_to_secondary_reference_points,
1503 elem_to_primary_reference_points,
1504 TOLERANCE * secondary_volume);
1505 else
1506 mortar_segment_helper[s_el]->getMortarSegments(
1507 primary_sub_elem, nodal_points, elem_to_node_map);
1508
1509 // Keep track of which secondary and primary sub-elements created segment
1510 for (auto i = segments_before_helper; i < elem_to_node_map.size(); ++i)
1511 sub_elem_map.push_back(std::make_pair(s_el, p_el));
1512 }
1513 }
1514
1515 // Mark primary element as processed and remove from candidate list
1516 processed_primary_elems.insert(primary_elem_candidate);
1517 primary_elem_candidates.erase(primary_elem_candidate);
1518
1519 // If overlap of polygons was non-trivial (created mortar segment elements)
1520 if (!elem_to_node_map.empty())
1521 {
1522 if (sub_elem_map.size() != elem_to_node_map.size())
1523 mooseError("The mortar segment subpatch map is not aligned with the mortar segment "
1524 "connectivity map.");
1525 if (use_reference_interpolation &&
1526 (elem_to_secondary_reference_points.size() != elem_to_node_map.size() ||
1527 elem_to_primary_reference_points.size() != elem_to_node_map.size()))
1528 mooseError("The mortar segment reference-point maps are not aligned with the mortar "
1529 "segment connectivity map.");
1530
1531 // Only overlap polygons large enough to become mortar segments may switch the candidate
1532 // search to breadth first.
1533 bool seed_breadth_first_search = false;
1534 std::vector<bool> retained_mortar_segments(elem_to_node_map.size(), false);
1535 for (const auto el : index_range(elem_to_node_map))
1536 {
1537 const auto & node_map = elem_to_node_map[el];
1538 if (node_map.size() != 3)
1539 mooseError(
1540 "Active mortar segments only supports TRI elements, 3 nodes expected but: ",
1541 node_map.size(),
1542 " provided.");
1543
1544 const Point e1 = nodal_points[node_map[1]] - nodal_points[node_map[0]];
1545 const Point e2 = nodal_points[node_map[2]] - nodal_points[node_map[0]];
1546 retained_mortar_segments[el] =
1547 0.5 * e1.cross(e2).norm() / secondary_volume >= TOLERANCE;
1548 seed_breadth_first_search = seed_breadth_first_search || retained_mortar_segments[el];
1549 }
1550
1551 if (seed_breadth_first_search)
1552 {
1553 // If this is the first element with a qualifying overlap, set flag. Candidates will
1554 // now be neighbors of elements that had qualifying overlap.
1555 if (!primary_elem_found)
1556 {
1557 primary_elem_found = true;
1558 primary_elem_candidates.clear();
1559 }
1560
1561 // Add neighbors to candidate list
1562 for (auto neighbor : primary_elem_candidate->neighbor_ptr_range())
1563 {
1564 // If not valid or not on lower dimensional secondary subdomain, skip
1565 if (neighbor == nullptr || neighbor->subdomain_id() != primary_subd_id)
1566 continue;
1567 // If already processed, skip
1568 if (processed_primary_elems.count(neighbor))
1569 continue;
1570 // Otherwise, add to candidates
1571 primary_elem_candidates.insert(neighbor);
1572 }
1573 }
1574
1578 std::vector<Node *> new_nodes;
1579 // Clipping can append points for triangles later rejected by the area tolerance. Add only
1580 // points referenced by retained triangles so the mortar mesh has no orphan nodes.
1581 std::vector<bool> retained_nodes(nodal_points.size(), false);
1582 for (const auto el : index_range(elem_to_node_map))
1583 if (retained_mortar_segments[el])
1584 for (const auto node : elem_to_node_map[el])
1585 retained_nodes[node] = true;
1586
1587 new_nodes.resize(nodal_points.size(), nullptr);
1588 for (const auto node : index_range(nodal_points))
1589 if (retained_nodes[node])
1590 new_nodes[node] = _mortar_segment_mesh->add_point(
1591 nodal_points[node], next_node_id++, secondary_side_elem->processor_id());
1592
1593 // Loop through triangular elements in map
1594 for (auto el : index_range(elem_to_node_map))
1595 {
1596 if (!retained_mortar_segments[el])
1597 continue;
1598
1599 std::unique_ptr<Elem> new_elem;
1600 if (elem_to_node_map[el].size() == 3)
1601 new_elem = std::make_unique<Tri3>();
1602 else
1603 mooseError("Active mortar segments only supports TRI elements, 3 nodes expected "
1604 "but: ",
1605 elem_to_node_map[el].size(),
1606 " provided.");
1607
1608 new_elem->processor_id() = secondary_side_elem->processor_id();
1609 new_elem->subdomain_id() = secondary_side_elem->subdomain_id();
1610 new_elem->set_id(next_elem_id++);
1611
1612 // Attach newly created nodes
1613 for (auto i : index_range(elem_to_node_map[el]))
1614 new_elem->set_node(i, new_nodes[elem_to_node_map[el][i]]);
1615
1616 // If element is smaller than tolerance, don't add to msm
1617 if (new_elem->volume() / secondary_volume < TOLERANCE)
1618 continue;
1619
1620 // Add elements to mortar segment mesh
1621 Elem * msm_new_elem = _mortar_segment_mesh->add_elem(new_elem.release());
1622
1623 msm_new_elem->set_extra_integer(secondary_sub_elem, sub_elem_map[el].first);
1624 msm_new_elem->set_extra_integer(primary_sub_elem, sub_elem_map[el].second);
1625
1626 // Fill out mortar segment info
1627 MortarSegmentInfo msinfo;
1628 msinfo.secondary_elem = secondary_side_elem;
1629 msinfo.primary_elem = primary_elem_candidate;
1630
1631 // Associate this MSM elem with the MortarSegmentInfo.
1632 _msm_elem_to_info.emplace(msm_new_elem, msinfo);
1633
1634 // Store reference data only for retained segments.
1635 if (use_reference_interpolation)
1636 {
1637 MortarSegmentReferencePoints reference_points{elem_to_secondary_reference_points[el],
1638 elem_to_primary_reference_points[el]};
1639 _msm_elem_to_reference_points.emplace(msm_new_elem, reference_points);
1640 }
1641
1642 // Add this mortar segment to the secondary elem to mortar segment map
1643 secondary_to_msm_element_set.insert(msm_new_elem);
1644
1645 _secondary_ip_sub_ids.insert(msinfo.secondary_elem->interior_parent()->subdomain_id());
1646 // Unlike for 2D, we always have a primary when building the mortar mesh so we don't
1647 // have to check for null
1648 _primary_ip_sub_ids.insert(msinfo.primary_elem->interior_parent()->subdomain_id());
1649 }
1650 }
1651 // End loop through primary element candidates
1652 }
1653
1654 if (use_geometric_subpatch_normals)
1655 {
1656 // A geometric corner filter may intentionally leave individual subpatches uncovered. Warn
1657 // only when the complete secondary element failed to produce a retained segment.
1658 if (secondary_to_msm_element_set.empty())
1659 mooseDoOnce(
1660 mooseWarning("Some secondary elements on mortar interface were unable to identify"
1661 " a corresponding primary element; this may be expected depending on"
1662 " problem geometry but may indicate a failure of the element search"
1663 " or projection"));
1664 }
1665 else
1666 for (auto sel : make_range(secondary_side_elem->n_sub_elem()))
1667 if (mortar_segment_helper[sel]->remainder() == 1.0)
1668 mooseDoOnce(
1669 mooseWarning("Some secondary elements on mortar interface were unable to identify"
1670 " a corresponding primary element; this may be expected depending on"
1671 " problem geometry but may indicate a failure of the element search"
1672 " or projection"));
1673
1674 if (secondary_to_msm_element_set.empty())
1675 _secondary_elems_to_mortar_segments.erase(secondary_elem_to_msm_map_it);
1676 } // End loop through secondary elements
1677 } // End loop through mortar constraint pairs
1678
1679 mooseAssert(!use_reference_interpolation ||
1681 "Mortar segment info and reference-point maps must remain aligned.");
1682
1683 _mortar_segment_mesh->cache_elem_data();
1684
1685 // The mesh was built distributedly (each rank owns only its local elements), so mark it
1686 // as such so MeshSerializer correctly gathers it to proc 0 for Exodus output.
1687 _mortar_segment_mesh->set_distributed();
1688
1689 // Output mortar segment mesh
1690 if (_debug)
1691 {
1692 // If element is not triangular, increment subdomain id
1693 // (ExodusII does not support mixed element types in a single subdomain)
1694 for (const auto msm_el : _mortar_segment_mesh->active_local_element_ptr_range())
1695 if (msm_el->type() != TRI3)
1696 msm_el->subdomain_id()++;
1697
1699
1700 // Undo increment
1701 for (const auto msm_el : _mortar_segment_mesh->active_local_element_ptr_range())
1702 if (msm_el->type() != TRI3)
1703 msm_el->subdomain_id()--;
1704 }
1705
1707
1708 // Print mortar segment mesh statistics
1709 if (_debug)
1710 {
1711 msmStatistics();
1712 }
1713}
1714
1715void
1717{
1718 std::unordered_map<processor_id_type, std::vector<std::pair<dof_id_type, dof_id_type>>>
1719 coupling_info;
1720
1721 // Loop over the msm_elem_to_info object and build a bi-directional
1722 // multimap from secondary elements to the primary Elems which they are
1723 // coupled to and vice-versa. This is used in the
1724 // AugmentSparsityOnInterface functor to determine whether a given
1725 // secondary Elem is coupled across the mortar interface to a primary
1726 // element.
1727 for (const auto & pr : _msm_elem_to_info)
1728 {
1729 const Elem * secondary_elem = pr.second.secondary_elem;
1730 const Elem * primary_elem = pr.second.primary_elem;
1731
1732 // Every process owning a row on a secondary node or either volume parent needs the complete
1733 // secondary face-star coupling, including when that process also owns the lower-dimensional
1734 // face.
1735 std::set<processor_id_type> secondary_face_coupling_processors{
1736 secondary_elem->processor_id(),
1737 secondary_elem->interior_parent()->processor_id(),
1738 primary_elem->interior_parent()->processor_id()};
1739 for (const auto n : secondary_elem->node_index_range())
1740 secondary_face_coupling_processors.insert(secondary_elem->node_ptr(n)->processor_id());
1741
1742 // LowerSecondary
1743 for (const auto processor : secondary_face_coupling_processors)
1744 coupling_info[processor].emplace_back(secondary_elem->id(),
1745 secondary_elem->interior_parent()->id());
1746
1747 // LowerPrimary
1748 for (const auto processor : secondary_face_coupling_processors)
1749 coupling_info[processor].emplace_back(secondary_elem->id(),
1750 primary_elem->interior_parent()->id());
1751
1752 // Lower-LowerDimensionalPrimary
1753 for (const auto processor : secondary_face_coupling_processors)
1754 coupling_info[processor].emplace_back(secondary_elem->id(), primary_elem->id());
1755
1756 // SecondaryLower
1757 coupling_info[secondary_elem->interior_parent()->processor_id()].emplace_back(
1758 secondary_elem->interior_parent()->id(), secondary_elem->id());
1759
1760 // SecondaryPrimary
1761 coupling_info[secondary_elem->interior_parent()->processor_id()].emplace_back(
1762 secondary_elem->interior_parent()->id(), primary_elem->interior_parent()->id());
1763
1764 // PrimaryLower
1765 coupling_info[primary_elem->interior_parent()->processor_id()].emplace_back(
1766 primary_elem->interior_parent()->id(), secondary_elem->id());
1767
1768 // PrimarySecondary
1769 coupling_info[primary_elem->interior_parent()->processor_id()].emplace_back(
1770 primary_elem->interior_parent()->id(), secondary_elem->interior_parent()->id());
1771 }
1772
1773 // A face pair may produce several mortar segments. Remove repeated entries before communication.
1774 for (auto & [_, couplings] : coupling_info)
1775 {
1776 std::sort(couplings.begin(), couplings.end());
1777 couplings.erase(std::unique(couplings.begin(), couplings.end()), couplings.end());
1778 }
1779
1780 // Push the coupling information
1781 auto action_functor =
1782 [this](processor_id_type,
1783 const std::vector<std::pair<dof_id_type, dof_id_type>> & coupling_info)
1784 {
1785 for (auto [i, j] : coupling_info)
1786 _mortar_interface_coupling[i].insert(j);
1787 };
1788 TIMPI::push_parallel_vector_data(_mesh.comm(), coupling_info, action_functor);
1789}
1790
1791std::vector<AutomaticMortarGeneration::MsmSubdomainStats>
1793{
1794 std::vector<MsmSubdomainStats> result;
1798 std::unordered_map<dof_id_type, Real> primary_elems_to_volume;
1799
1800 for (const auto & [primary_subd_id, secondary_subd_id] : _primary_secondary_subdomain_id_pairs)
1801 {
1802 for (const auto * const secondary_el :
1803 _mesh.active_local_subdomain_element_ptr_range(secondary_subd_id))
1804 {
1805 secondary.push_back(secondary_el->volume());
1806 // We may not have projected onto a primary face in which case we may not have created mortar
1807 // segments
1808 if (auto it = _secondary_elems_to_mortar_segments.find(secondary_el->id());
1810 for (const auto * const msm_elem : it->second)
1811 {
1812 msm.push_back(msm_elem->volume());
1813 const auto & msm_info = libmesh_map_find(_msm_elem_to_info, msm_elem);
1814 // Now it's also possible that we didn't project onto a primary face and we *did* create
1815 // mortar segments
1816 if (msm_info.primary_elem)
1817 {
1818 if (msm_info.primary_elem->subdomain_id() != primary_subd_id)
1819 mooseError("Unhandled primary-secondary pairing when computing mortar segment "
1820 "statistics. This could happen if you have the same secondary "
1821 "lower-dimensional subdomain ID paired with multiple lower-dimensional "
1822 "primary subdomain IDs. Contact a MOOSE developer for help.");
1823 if (const auto [it, inserted] =
1824 primary_elems_to_volume.emplace(msm_info.primary_elem->id(), Real{});
1825 inserted)
1826 it->second = msm_info.primary_elem->volume();
1827 else
1828 mooseAssert(
1829 MooseUtils::absoluteFuzzyEqual(it->second, msm_info.primary_elem->volume()),
1830 "Volumes should be consistent");
1831 }
1832 }
1833 }
1834
1835 _mesh.comm().set_union(primary_elems_to_volume);
1836 _mesh.comm().allgather(cast_ref<std::vector<Real> &>(secondary));
1837 _mesh.comm().allgather(cast_ref<std::vector<Real> &>(msm));
1838 primary.reserve(primary_elems_to_volume.size());
1839 for (const auto [_, volume] : primary_elems_to_volume)
1840 primary.push_back(volume);
1841
1842 // An interface on which no mortar segments formed has empty primary and segment lists, for
1843 // which libMesh's maximum() and minimum() are undefined. Report 0, as median() does.
1844 const auto max_volume = [](const auto & volumes)
1845 { return volumes.empty() ? Real(0) : volumes.maximum(); };
1846 const auto min_volume = [](const auto & volumes)
1847 { return volumes.empty() ? Real(0) : volumes.minimum(); };
1848
1849 MsmSubdomainStats stats;
1850 stats.primary_subd_id = primary_subd_id;
1851 stats.secondary_subd_id = secondary_subd_id;
1852 stats.secondary_lower_n_elems = secondary.size();
1853 stats.secondary_lower_max_volume = max_volume(secondary);
1854 stats.secondary_lower_min_volume = min_volume(secondary);
1855 stats.secondary_lower_median_volume = secondary.median();
1856 stats.primary_lower_n_elems = primary.size();
1857 stats.primary_lower_max_volume = max_volume(primary);
1858 stats.primary_lower_min_volume = min_volume(primary);
1859 stats.primary_lower_median_volume = primary.median();
1860 stats.msm_n_elems = msm.size();
1861 stats.msm_max_volume = max_volume(msm);
1862 stats.msm_min_volume = min_volume(msm);
1863 stats.msm_median_volume = msm.median();
1864 result.push_back(stats);
1865
1866 primary.clear();
1867 secondary.clear();
1868 msm.clear();
1869 primary_elems_to_volume.clear();
1870 }
1871
1872 return result;
1873}
1874
1875void
1877{
1878 const auto all_stats = computeMsmStatistics();
1879
1880 if (_mesh.processor_id() != 0)
1881 return;
1882
1883 Moose::out << "Mortar Interface Statistics:" << std::endl;
1884 for (const auto & stats : all_stats)
1885 {
1886 std::vector<std::string> col_names = {"mesh", "n_elems", "max", "min", "median"};
1887 std::vector<std::string> subds = {"secondary_lower", "primary_lower", "mortar_segment"};
1888 std::vector<size_t> n_elems = {
1889 stats.secondary_lower_n_elems, stats.primary_lower_n_elems, stats.msm_n_elems};
1890 std::vector<Real> maxs = {
1891 stats.secondary_lower_max_volume, stats.primary_lower_max_volume, stats.msm_max_volume};
1892 std::vector<Real> mins = {
1893 stats.secondary_lower_min_volume, stats.primary_lower_min_volume, stats.msm_min_volume};
1894 std::vector<Real> medians = {stats.secondary_lower_median_volume,
1895 stats.primary_lower_median_volume,
1896 stats.msm_median_volume};
1897
1898 FormattedTable table;
1899 table.clear();
1900 for (auto i : index_range(subds))
1901 {
1902 table.addRow(i);
1903 table.addData<std::string>(col_names[0], subds[i]);
1904 table.addData<size_t>(col_names[1], n_elems[i]);
1905 table.addData<Real>(col_names[2], maxs[i]);
1906 table.addData<Real>(col_names[3], mins[i]);
1907 table.addData<Real>(col_names[4], medians[i]);
1908 }
1909
1910 Moose::out << "secondary subdomain: " << stats.secondary_subd_id
1911 << " \tprimary subdomain: " << stats.primary_subd_id << std::endl;
1912 table.printTable(Moose::out, subds.size());
1913 }
1914}
1915
1916// The blocks marked with **** are for regressing edge dropping treatment and should be removed
1917// eventually.
1918//****
1919// Compute inactve nodes when the old (incorrect) edge dropping treatemnt is enabled
1920void
1922{
1923 using std::abs;
1924
1925 // Note that in 3D our trick to check whether an element has edge dropping needs loose tolerances
1926 // since the mortar segments are on the linearized element and comparing the volume of the
1927 // linearized element does not have the same volume as the warped element
1928 const Real tol = (dim() == 3) ? 0.1 : TOLERANCE;
1929
1930 // List of inactive nodes on local secondary elements
1931 std::unordered_set<dof_id_type> inactive_node_ids;
1932
1933 std::unordered_map<const Elem *, Real> active_volume{};
1934
1935 for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1936 for (const auto el : _mesh.active_subdomain_elements_ptr_range(pr.second))
1937 active_volume[el] = 0.;
1938
1939 // Compute fraction of elements with corresponding primary elements
1940 for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1941 {
1942 const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
1943 const Elem * secondary_elem = msinfo.secondary_elem;
1944
1945 active_volume[secondary_elem] += msm_elem->volume();
1946 }
1947
1948 // Mark all inactive local nodes
1949 for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1950 // Loop through all elements on my processor
1951 for (const auto el : _mesh.active_local_subdomain_elements_ptr_range(pr.second))
1952 // If elem fully or partially dropped
1953 if (abs(active_volume[el] / el->volume() - 1.0) > tol)
1954 {
1955 // Add all nodes to list of inactive
1956 for (auto n : make_range(el->n_nodes()))
1957 inactive_node_ids.insert(el->node_id(n));
1958 }
1959
1960 // Only the owner of a node zeroes its LM DoFs, but the owner need not own (or even ghost) any
1961 // secondary lower-dimensional element touching it, so send each inactive node to its owner,
1962 // which takes the union
1963 std::unordered_map<processor_id_type, std::vector<dof_id_type>> proc_to_inactive_nodes;
1964 for (const auto node_id : inactive_node_ids)
1965 proc_to_inactive_nodes[_mesh.node_ptr(node_id)->processor_id()].push_back(node_id);
1966
1968 auto action_functor = [this](const processor_id_type, const std::vector<dof_id_type> & sent_data)
1969 {
1970 for (const auto node_id : sent_data)
1971 _inactive_local_lm_nodes.insert(_mesh.node_ptr(node_id));
1972 };
1973 TIMPI::push_parallel_vector_data(_mesh.comm(), proc_to_inactive_nodes, action_functor);
1974}
1975
1976void
1978{
1980 {
1982 return;
1983 }
1984
1985 // Only the owner of a node zeroes its LM DoFs, but the owner need not own (or even ghost) any
1986 // secondary lower-dimensional element touching it. Each processor therefore sends every node of
1987 // its local secondary elements to the node's owner, flagged as active if a local mortar segment
1988 // touches it. A node is inactive if no processor flags it as active.
1989
1990 // List of active nodes on local secondary elements
1991 std::unordered_set<dof_id_type> active_local_nodes;
1992
1993 // Mark all active local nodes
1994 for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1995 {
1996 const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
1997 const Elem * secondary_elem = msinfo.secondary_elem;
1998
1999 for (auto n : make_range(secondary_elem->n_nodes()))
2000 active_local_nodes.insert(secondary_elem->node_id(n));
2001 }
2002
2003 // List of all nodes on local secondary elements
2004 std::unordered_set<dof_id_type> local_nodes;
2005 for (const auto & pr : _primary_secondary_subdomain_id_pairs)
2006 for (const auto el : _mesh.active_local_subdomain_elements_ptr_range(
2007 /*secondary_subd_id*/ pr.second))
2008 for (const auto n : make_range(el->n_nodes()))
2009 local_nodes.insert(el->node_id(n));
2010
2011 // Pairs of (node id, 1 if active on the sending processor and 0 otherwise)
2012 std::unordered_map<processor_id_type, std::vector<std::pair<dof_id_type, unsigned char>>>
2013 proc_to_nodes;
2014 for (const auto node_id : local_nodes)
2015 proc_to_nodes[_mesh.node_ptr(node_id)->processor_id()].emplace_back(
2016 node_id, active_local_nodes.count(node_id));
2017
2018 std::unordered_set<dof_id_type> candidate_nodes, active_nodes;
2019 auto action_functor = [&candidate_nodes, &active_nodes](
2020 const processor_id_type,
2021 const std::vector<std::pair<dof_id_type, unsigned char>> & sent_data)
2022 {
2023 for (const auto & [node_id, active] : sent_data)
2024 {
2025 candidate_nodes.insert(node_id);
2026 if (active)
2027 active_nodes.insert(node_id);
2028 }
2029 };
2030 TIMPI::push_parallel_vector_data(_mesh.comm(), proc_to_nodes, action_functor);
2031
2032 // Store the owned inactive nodes to use later to zero LM DoFs on inactive nodes
2034 for (const auto node_id : candidate_nodes)
2035 if (!active_nodes.count(node_id))
2036 _inactive_local_lm_nodes.insert(_mesh.node_ptr(node_id));
2037}
2038
2039// Note: could be combined with previous routine, keeping separate for clarity (for now)
2040void
2042{
2043 // Mark all active secondary elements
2044 std::unordered_set<const Elem *> active_local_elems;
2045
2046 //****
2047 // Note that in 3D our trick to check whether an element has edge dropping needs loose tolerances
2048 // since the mortar segments are on the linearized element and comparing the volume of the
2049 // linearized element does not have the same volume as the warped element
2050 const Real tol = (dim() == 3) ? 0.1 : TOLERANCE;
2051
2052 std::unordered_map<const Elem *, Real> active_volume;
2053
2054 // Compute fraction of elements with corresponding primary elements
2056 for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
2057 {
2058 const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
2059 const Elem * secondary_elem = msinfo.secondary_elem;
2060
2061 active_volume[secondary_elem] += msm_elem->volume();
2062 }
2063 //****
2064
2065 for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
2066 {
2067 const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
2068 const Elem * secondary_elem = msinfo.secondary_elem;
2069
2070 //****
2072 if (abs(active_volume[secondary_elem] / secondary_elem->volume() - 1.0) > tol)
2073 continue;
2074 //****
2075
2076 active_local_elems.insert(secondary_elem);
2077 }
2078
2079 // Take complement of active elements in active local subdomain to get inactive local elements
2081 for (const auto & pr : _primary_secondary_subdomain_id_pairs)
2082 for (const auto el : _mesh.active_local_subdomain_elements_ptr_range(
2083 /*secondary_subd_id*/ pr.second))
2084 if (active_local_elems.find(el) == active_local_elems.end())
2085 _inactive_local_lm_elems.insert(el);
2086}
2087
2088void
2090{
2091 // The dimension according to Mesh::mesh_dimension().
2092 const auto dim = _mesh.mesh_dimension();
2093
2094 mooseAssert(dim == 2 || dim == 3,
2095 "AutomaticMortarGeneration::computeNodalGeometry() is only valid for "
2096 "mortar constraints on 2D or 3D meshes.");
2097 // A nodal lower-dimensional nodal quadrature rule to be used on faces.
2098 libMesh::QNodal qface(dim - 1);
2099
2100 // A map from the node id to the attached elemental normals/weights evaluated at the node. Th
2101 // length of the vector will correspond to the number of elements attached to the node. If it is a
2102 // vertex node, for a 1D mortar mesh, the vector length will be two. If it is an interior node,
2103 // the vector will be length 1. The first member of the pair is that element's normal at the node.
2104 // The second member is that element's JxW at the node
2105 std::map<dof_id_type, std::vector<std::pair<Point, Real>>> node_to_normals_map;
2106
2108 Real sign = _periodic ? -1 : 1;
2109
2110 // First loop over lower-dimensional secondary side elements and compute/save the outward normal
2111 // for each one. We loop over all active elements currently, but this procedure could be
2112 // parallelized as well.
2113 for (MeshBase::const_element_iterator el = _mesh.active_elements_begin(),
2114 end_el = _mesh.active_elements_end();
2115 el != end_el;
2116 ++el)
2117 {
2118 const Elem * secondary_elem = *el;
2119
2120 // If this is not one of the lower-dimensional secondary side elements, go on to the next one.
2121 if (!_secondary_boundary_subdomain_ids.count(secondary_elem->subdomain_id()))
2122 continue;
2123
2124 // We will create an FE object and attach the nodal quadrature rule such that we can get out the
2125 // normals at the element nodes
2126 FEType nnx_fe_type(secondary_elem->default_order(), LAGRANGE);
2127 std::unique_ptr<FEBase> nnx_fe_face(FEBase::build(dim, nnx_fe_type));
2128 nnx_fe_face->attach_quadrature_rule(&qface);
2129 const auto & face_normals = nnx_fe_face->get_normals();
2130 const auto & face_points = nnx_fe_face->get_xyz();
2131
2132 const auto & JxW = nnx_fe_face->get_JxW();
2133
2134 // Which side of the parent are we? We need to know this to know
2135 // which side to reinit.
2136 const Elem * interior_parent = secondary_elem->interior_parent();
2137 mooseAssert(interior_parent,
2138 "No interior parent exists for element "
2139 << secondary_elem->id()
2140 << ". There may be a problem with your sideset set-up.");
2141
2142 // Map to get lower dimensional element from interior parent on secondary surface
2143 // This map can be used to provide a handle to methods in this class that need to
2144 // operate on lower dimensional elements.
2145 _secondary_element_to_secondary_lowerd_element.emplace(interior_parent->id(), secondary_elem);
2146
2147 // Look up which side of the interior parent secondary_elem is.
2148 auto s = interior_parent->which_side_am_i(secondary_elem);
2149
2150 // Reinit the face FE object on side s.
2151 nnx_fe_face->reinit(interior_parent, s);
2152
2153 // Match by physical location instead of assuming that parent-side nodal
2154 // quadrature ordering and lower-dimensional side-element node ordering are
2155 // identical.
2156 const auto qpoint_to_secondary_node =
2157 nodalQuadraturePointToSecondaryNodeMap(*secondary_elem, face_points);
2158
2159 mooseAssert(face_normals.size() == face_points.size() && JxW.size() == face_points.size(),
2160 "Face nodal geometry vectors must have the same size.");
2161
2162 for (const auto qp : make_range(face_points.size()))
2163 {
2164 const auto n = qpoint_to_secondary_node[qp];
2165 auto & normals_and_weights_vec = node_to_normals_map[secondary_elem->node_id(n)];
2166 normals_and_weights_vec.push_back(std::make_pair(sign * face_normals[qp], JxW[qp]));
2167 }
2168 }
2169
2170 // Normalize the JxW-weighted sum of incident face normals at each secondary node.
2171 for (const auto & pr : node_to_normals_map)
2172 {
2173 // Compute normal vector
2174 const auto & node_id = pr.first;
2175 const auto & normals_and_weights_vec = pr.second;
2176
2177 Point nodal_normal;
2178 for (const auto & norm_and_weight : normals_and_weights_vec)
2179 nodal_normal += norm_and_weight.first * norm_and_weight.second;
2180 nodal_normal = nodal_normal.unit();
2181
2182 _secondary_node_to_nodal_normal[_mesh.node_ptr(node_id)] = nodal_normal;
2183 _secondary_node_to_hh_nodal_tangents[_mesh.node_ptr(node_id)] =
2185 }
2186}
2187
2188void
2190 const std::function<ADPoint(const Node &)> & coordinate,
2191 std::unordered_map<const Node *, ADRealVectorValue> & nodal_normals) const
2192{
2193 const auto dim = _mesh.mesh_dimension();
2194 mooseAssert(dim == 2 || dim == 3, "AD nodal normals are only valid for 2D or 3D mortar.");
2195
2196 libMesh::QNodal qface(dim - 1);
2197 const auto & qweights = qface.get_weights();
2198 const Real sign = _periodic ? -1 : 1;
2199
2200 std::unordered_map<const Node *, ADRealVectorValue> weighted_area_vectors;
2201 std::unordered_map<const Node *, Real> weighted_area_magnitudes;
2202
2203 // One face FE object per secondary element order, reused across elements of that order
2204 std::map<Order, std::unique_ptr<FEBase>> fe_faces;
2205
2206 // AD propagates coordinate derivatives through the parent-side area vectors and normalization.
2207 for (const auto secondary_subdomain_id : _secondary_boundary_subdomain_ids)
2208 for (const Elem * const secondary_elem :
2209 as_range(_mesh.active_subdomain_elements_begin(secondary_subdomain_id),
2210 _mesh.active_subdomain_elements_end(secondary_subdomain_id)))
2211 {
2212 const Elem * const interior_parent = secondary_elem->interior_parent();
2213 mooseAssert(interior_parent,
2214 "No interior parent exists for element "
2215 << secondary_elem->id()
2216 << ". There may be a problem with your sideset set-up.");
2217
2218 const Real element_size = secondary_elem->hmax();
2219 if (element_size <= 0)
2220 mooseError("Cannot compute an AD nodal normal for secondary mortar element ",
2221 secondary_elem->id(),
2222 " because it has a non-positive characteristic length.");
2223
2224 const auto side = interior_parent->which_side_am_i(secondary_elem);
2225 auto & fe_face = fe_faces[secondary_elem->default_order()];
2226 if (!fe_face)
2227 {
2228 fe_face = FEBase::build(dim, FEType(secondary_elem->default_order(), LAGRANGE));
2229 fe_face->attach_quadrature_rule(&qface);
2230 }
2231
2232 const auto & face_normals = fe_face->get_normals();
2233 const auto & face_points = fe_face->get_xyz();
2234 const auto & map_dpsidxi = fe_face->get_fe_map().get_dpsidxi();
2235 const auto * const map_dpsideta = dim == 3 ? &fe_face->get_fe_map().get_dpsideta() : nullptr;
2236
2237 fe_face->reinit(interior_parent, side);
2238
2239 const auto qpoint_to_secondary_node =
2240 nodalQuadraturePointToSecondaryNodeMap(*secondary_elem, face_points);
2241
2242 mooseAssert(face_normals.size() == face_points.size() &&
2243 qweights.size() == face_points.size(),
2244 "Face nodal geometry vectors and quadrature weights must have the same size.");
2245
2246 // The parent-side map includes enriched face mappings such as TET14-to-TRI7.
2247 auto parent_side_elem = interior_parent->build_side_ptr(side);
2248 const unsigned int n_mapping_shape_functions =
2249 dim == 2 ? libMesh::FE<2, LAGRANGE>::n_dofs(parent_side_elem.get(),
2250 parent_side_elem->default_order())
2251 : libMesh::FE<3, LAGRANGE>::n_dofs(parent_side_elem.get(),
2252 parent_side_elem->default_order());
2253 if (map_dpsidxi.size() != n_mapping_shape_functions ||
2254 (map_dpsideta && map_dpsideta->size() != n_mapping_shape_functions))
2255 mooseError("The parent-side geometric map for secondary mortar element ",
2256 secondary_elem->id(),
2257 " of type ",
2258 libMesh::Utility::enum_to_string<ElemType>(secondary_elem->type()),
2259 " does not have one mapping shape function per element node.");
2260 mooseAssert(n_mapping_shape_functions <= parent_side_elem->n_nodes(),
2261 "The mapping basis must not index past the side element's nodes.");
2262
2263 std::vector<ADPoint> side_points;
2264 side_points.reserve(n_mapping_shape_functions);
2265 for (const auto side_node_index : make_range(n_mapping_shape_functions))
2266 side_points.push_back(coordinate(parent_side_elem->node_ref(side_node_index)));
2267
2268 for (const auto qp : index_range(face_points))
2269 {
2270 const auto secondary_node_index = qpoint_to_secondary_node[qp];
2271 const Node * const secondary_node = secondary_elem->node_ptr(secondary_node_index);
2272
2273 ADRealVectorValue tangent_xi;
2274 ADRealVectorValue tangent_eta;
2275 for (const auto side_node_index : make_range(n_mapping_shape_functions))
2276 {
2277 tangent_xi.add_scaled(side_points[side_node_index], map_dpsidxi[side_node_index][qp]);
2278 if (dim == 3)
2279 tangent_eta.add_scaled(side_points[side_node_index],
2280 (*map_dpsideta)[side_node_index][qp]);
2281 }
2282
2283 auto area_vector = dim == 2 ? ADRealVectorValue(tangent_xi(1), -tangent_xi(0), 0)
2284 : tangent_xi.cross(tangent_eta);
2285 const Real area = MetaPhysicL::raw_value(area_vector.norm());
2286 const Real degeneracy_tolerance =
2287 dim == 2 ? TOLERANCE * element_size : TOLERANCE * element_size * element_size;
2288 if (area <= degeneracy_tolerance)
2289 mooseError("Cannot compute an AD nodal normal on degenerate secondary mortar element ",
2290 secondary_elem->id(),
2291 ".");
2292
2293 // qweight * area_vector is equivalent to JxW * face_normal. Fix its orientation to the
2294 // Real-valued face normal because the incident-face set and orientation are not
2295 // differentiated.
2296 const Real orientation =
2297 MetaPhysicL::raw_value(area_vector) * face_normals[qp] < 0 ? -1 : 1;
2298
2299 weighted_area_vectors[secondary_node] += sign * orientation * qweights[qp] * area_vector;
2300 weighted_area_magnitudes[secondary_node] += std::abs(qweights[qp]) * area;
2301 }
2302 }
2303
2304 nodal_normals.clear();
2305 nodal_normals.reserve(weighted_area_vectors.size());
2306 for (auto & [secondary_node, weighted_area_vector] : weighted_area_vectors)
2307 {
2308 const Real weighted_area_vector_norm = MetaPhysicL::raw_value(weighted_area_vector.norm());
2309 const Real weighted_area_magnitude = libmesh_map_find(weighted_area_magnitudes, secondary_node);
2310 if (weighted_area_vector_norm <= TOLERANCE * weighted_area_magnitude)
2311 mooseError("Cannot compute a normalized AD nodal normal for secondary node ",
2312 secondary_node->id(),
2313 " because its incident face contributions cancel to a near-zero weighted normal.");
2314
2315 auto nodal_normal = weighted_area_vector.unit();
2316 const auto & stored_normal = libmesh_map_find(_secondary_node_to_nodal_normal, secondary_node);
2317 // Equivalent FE and AD evaluations of the same unit normal may differ through operation
2318 // ordering. The tolerance admits roundoff while detecting inconsistent geometry states.
2319 mooseAssert((MetaPhysicL::raw_value(nodal_normal) - stored_normal).norm() < 1e-10,
2320 "The stored and AD secondary nodal normals must use the same geometry state.");
2321 // Set the raw value from the stored contact direction while retaining the derivatives produced
2322 // by the equivalent AD area-vector calculation.
2323 for (const auto component : make_range(Moose::dim))
2324 nodal_normal(component).value() = stored_normal(component);
2325 nodal_normals.emplace(secondary_node, std::move(nodal_normal));
2326 }
2327}
2328
2329// Project secondary nodes onto their corresponding primary elements for each primary/secondary
2330// pair.
2331void
2333{
2334 // For each primary/secondary boundary id pair, call the
2335 // project_secondary_nodes_single_pair() helper function.
2336 for (const auto & pr : _primary_secondary_subdomain_id_pairs)
2337 projectSecondaryNodesSinglePair(pr.first, pr.second);
2338}
2339
2340bool
2342 const Node & secondary_node,
2343 const Node & primary_node,
2344 const std::vector<const Elem *> * secondary_node_neighbors,
2345 const std::vector<const Elem *> * primary_node_neighbors,
2346 const VectorValue<Real> & nodal_normal,
2347 const Elem & candidate_element,
2348 std::set<const Elem *> & rejected_elem_candidates)
2349{
2350 if (!secondary_node_neighbors)
2351 secondary_node_neighbors = &libmesh_map_find(_nodes_to_secondary_elem_map, secondary_node.id());
2352 if (!primary_node_neighbors)
2353 primary_node_neighbors = &libmesh_map_find(_nodes_to_primary_elem_map, primary_node.id());
2354
2355 std::vector<bool> primary_elems_mapped(primary_node_neighbors->size(), false);
2356
2357 // Add entries to secondary_node_and_elem_to_xi2_primary_elem container.
2358 //
2359 // First, determine "on left" vs. "on right" orientation of the nodal neighbors.
2360 // There can be a max of 2 nodal neighbors, and we want to make sure that the
2361 // secondary nodal neighbor on the "left" is associated with the primary nodal
2362 // neighbor on the "left" and similarly for the "right". We use cross products to determine
2363 // alignment. In the below diagram, 'x' denotes a node, and connected '|' are lower dimensional
2364 // elements.
2365 // x
2366 // x |
2367 // | |
2368 // secondary x ----> x primary
2369 // | |
2370 // | x
2371 // x
2372 //
2373 // Looking at the aligned nodes, the secondary node first, if we pick the top secondary lower
2374 // dimensional element, then the cross product as written a few lines below points out of the
2375 // screen towards you. (Point in the direction of the secondary nodal normal, and then curl your
2376 // hand towards the secondary element's opposite node, then the thumb points in the direction of
2377 // the cross product). Doing the same with the aligned primary node, if we pick the top primary
2378 // element, then the cross product also points out of the screen. Because the cross products
2379 // point in the same direction (positive dot product), then we know to associate the
2380 // secondary-primary element pair. If we had picked the bottom primary element whose cross
2381 // product points into the screen, then clearly the cross products point in the opposite
2382 // direction and we don't have a match
2383 std::array<Real, 2> secondary_node_neighbor_cps, primary_node_neighbor_cps;
2384
2385 for (const auto nn : index_range(*secondary_node_neighbors))
2386 {
2387 const Elem * const secondary_neigh = (*secondary_node_neighbors)[nn];
2388 const Point opposite = (secondary_neigh->node_ptr(0) == &secondary_node)
2389 ? secondary_neigh->point(1)
2390 : secondary_neigh->point(0);
2391 const Point cp = nodal_normal.cross(opposite - secondary_node);
2392 secondary_node_neighbor_cps[nn] = cp(2);
2393 }
2394
2395 for (const auto nn : index_range(*primary_node_neighbors))
2396 {
2397 const Elem * const primary_neigh = (*primary_node_neighbors)[nn];
2398 const Point opposite = (primary_neigh->node_ptr(0) == &primary_node) ? primary_neigh->point(1)
2399 : primary_neigh->point(0);
2400 const Point cp = nodal_normal.cross(opposite - primary_node);
2401 primary_node_neighbor_cps[nn] = cp(2);
2402 }
2403
2404 // Associate secondary/primary elems on matching sides.
2405 bool found_match = false;
2406 for (const auto snn : index_range(*secondary_node_neighbors))
2407 for (const auto mnn : index_range(*primary_node_neighbors))
2408 if (secondary_node_neighbor_cps[snn] * primary_node_neighbor_cps[mnn] > 0)
2409 {
2410 found_match = true;
2411 if (primary_elems_mapped[mnn])
2412 continue;
2413 primary_elems_mapped[mnn] = true;
2414
2415 // Figure out xi^(2) value by looking at which node primary_node is
2416 // of the current primary node neighbor.
2417 const Real xi2 = (&primary_node == (*primary_node_neighbors)[mnn]->node_ptr(0)) ? -1 : +1;
2418 const auto secondary_key =
2419 std::make_pair(&secondary_node, (*secondary_node_neighbors)[snn]);
2420 const auto primary_val = std::make_pair(xi2, (*primary_node_neighbors)[mnn]);
2421 _secondary_node_and_elem_to_xi2_primary_elem.emplace(secondary_key, primary_val);
2422
2423 // Also map in the other direction.
2424 const Real xi1 =
2425 (&secondary_node == (*secondary_node_neighbors)[snn]->node_ptr(0)) ? -1 : +1;
2426
2427 const auto primary_key =
2428 std::make_tuple(primary_node.id(), &primary_node, (*primary_node_neighbors)[mnn]);
2429 const auto secondary_val = std::make_pair(xi1, (*secondary_node_neighbors)[snn]);
2430 _primary_node_and_elem_to_xi1_secondary_elem.emplace(primary_key, secondary_val);
2431 }
2432
2433 if (!found_match)
2434 {
2435 // There could be coincident nodes and this might be a bad primary candidate (see
2436 // issue #21680). Instead of giving up, let's try continuing
2437 rejected_elem_candidates.insert(&candidate_element);
2438 return false;
2439 }
2440
2441 // We need to handle the case where we've exactly projected a secondary node onto a
2442 // primary node, but our secondary node is at one of the secondary boundary face endpoints and
2443 // our primary node is not.
2444 if (secondary_node_neighbors->size() == 1 && primary_node_neighbors->size() == 2)
2445 for (const auto i : index_range(primary_elems_mapped))
2446 if (!primary_elems_mapped[i])
2447 {
2449 std::make_tuple(primary_node.id(), &primary_node, (*primary_node_neighbors)[i]),
2450 std::make_pair(1, nullptr));
2451 }
2452
2453 return found_match;
2454}
2455
2456void
2458 SubdomainID lower_dimensional_primary_subdomain_id,
2459 SubdomainID lower_dimensional_secondary_subdomain_id)
2460{
2461 using std::abs;
2462
2463 // Build the "subdomain" adaptor based KD Tree.
2464 NanoflannMeshSubdomainAdaptor<3> mesh_adaptor(_mesh, lower_dimensional_primary_subdomain_id);
2465 subdomain_kd_tree_t kd_tree(
2466 3, mesh_adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(/*max leaf=*/10));
2467
2468 // Construct the KD tree.
2469 kd_tree.buildIndex();
2470
2471 for (MeshBase::const_element_iterator el = _mesh.active_elements_begin(),
2472 end_el = _mesh.active_elements_end();
2473 el != end_el;
2474 ++el)
2475 {
2476 const Elem * secondary_side_elem = *el;
2477
2478 // If this Elem is not in the current secondary subdomain, go on to the next one.
2479 if (secondary_side_elem->subdomain_id() != lower_dimensional_secondary_subdomain_id)
2480 continue;
2481
2482 // For each node on the lower-dimensional element, find the nearest
2483 // node on the primary side using the KDTree, then
2484 // search in nearby elements for where it projects
2485 // along the nodal normal direction.
2486 for (MooseIndex(secondary_side_elem->n_vertices()) n = 0; n < secondary_side_elem->n_vertices();
2487 ++n)
2488 {
2489 const Node * secondary_node = secondary_side_elem->node_ptr(n);
2490
2491 // Get the nodal neighbors for secondary_node, so we can check whether we've
2492 // already successfully projected it.
2493 const std::vector<const Elem *> & secondary_node_neighbors =
2494 this->_nodes_to_secondary_elem_map.at(secondary_node->id());
2495
2496 // Check whether we've already mapped this secondary node
2497 // successfully for all of its nodal neighbors.
2498 bool is_mapped = true;
2499 for (MooseIndex(secondary_node_neighbors) snn = 0; snn < secondary_node_neighbors.size();
2500 ++snn)
2501 {
2502 auto secondary_key = std::make_pair(secondary_node, secondary_node_neighbors[snn]);
2503 if (!_secondary_node_and_elem_to_xi2_primary_elem.count(secondary_key))
2504 {
2505 is_mapped = false;
2506 break;
2507 }
2508 }
2509
2510 // Go to the next node if this one has already been mapped.
2511 if (is_mapped)
2512 continue;
2513
2514 // Look up the new nodal normal value in the local storage, error if not found.
2515 Point nodal_normal = _secondary_node_to_nodal_normal.at(secondary_node);
2516
2517 // Data structure for performing Nanoflann searches.
2518 std::array<Real, 3> query_pt = {
2519 {(*secondary_node)(0), (*secondary_node)(1), (*secondary_node)(2)}};
2520
2521 // The number of results we want to get. We'll look for a
2522 // "few" nearest nodes, hopefully that is enough to let us
2523 // figure out which lower-dimensional Elem on the primary
2524 // side we are across from.
2525 const std::size_t num_results = 3;
2526
2527 // Initialize result_set and do the search.
2528 std::vector<size_t> ret_index(num_results);
2529 std::vector<Real> out_dist_sqr(num_results);
2530 nanoflann::KNNResultSet<Real> result_set(num_results);
2531 result_set.init(&ret_index[0], &out_dist_sqr[0]);
2532 kd_tree.findNeighbors(result_set, &query_pt[0], nanoflann::SearchParameters());
2533
2534 // If this flag gets set in the loop below, we can break out of the outer r-loop as well.
2535 bool projection_succeeded = false;
2536
2537 // Once we've rejected a candidate for a given secondary_node,
2538 // there's no reason to check it again.
2539 std::set<const Elem *> rejected_primary_elem_candidates;
2540
2541 // Loop over the closest nodes, check whether
2542 // the secondary node successfully projects into
2543 // either of the closest neighbors, stop when
2544 // the projection succeeds.
2545 for (MooseIndex(result_set) r = 0; r < result_set.size(); ++r)
2546 {
2547 // Verify that the squared distance we compute is the same as nanoflann'sFss
2548 mooseAssert(abs((_mesh.point(ret_index[r]) - *secondary_node).norm_sq() -
2549 out_dist_sqr[r]) <= TOLERANCE,
2550 "Lower-dimensional element squared distance verification failed.");
2551
2552 // Get a reference to the vector of lower dimensional elements from the
2553 // nodes_to_primary_elem_map.
2554 std::vector<const Elem *> & primary_elem_candidates =
2555 this->_nodes_to_primary_elem_map.at(static_cast<dof_id_type>(ret_index[r]));
2556
2557 // Search the Elems connected to this node on the primary mesh side.
2558 for (MooseIndex(primary_elem_candidates) e = 0; e < primary_elem_candidates.size(); ++e)
2559 {
2560 const Elem * primary_elem_candidate = primary_elem_candidates[e];
2561
2562 // If we've already rejected this candidate, we don't need to check it again.
2563 if (rejected_primary_elem_candidates.count(primary_elem_candidate))
2564 continue;
2565
2566 // Now generically solve for xi2
2567 const auto order = primary_elem_candidate->default_order();
2568 DualNumber<Real> xi2_dn{0, 1};
2569 unsigned int current_iterate = 0, max_iterates = 10;
2570
2571 // Newton loop
2572 do
2573 {
2574 VectorValue<DualNumber<Real>> x2(0);
2575 for (MooseIndex(primary_elem_candidate->n_nodes()) n = 0;
2576 n < primary_elem_candidate->n_nodes();
2577 ++n)
2578 x2 +=
2579 Moose::fe_lagrange_1D_shape(order, n, xi2_dn) * primary_elem_candidate->point(n);
2580 const auto u = x2 - (*secondary_node);
2581 const auto F = u(0) * nodal_normal(1) - u(1) * nodal_normal(0);
2582
2583 if (abs(F) < _newton_tolerance)
2584 break;
2585
2586 if (F.derivatives())
2587 {
2588 Real dxi2 = -F.value() / F.derivatives();
2589
2590 xi2_dn += dxi2;
2591 }
2592 else
2593 // It's possible that the secondary surface nodal normal is completely orthogonal to
2594 // the primary surface normal, in which case the derivative is 0. We know in this case
2595 // that the projection should be a failure
2596 current_iterate = max_iterates;
2597 } while (++current_iterate < max_iterates);
2598
2599 Real xi2 = xi2_dn.value();
2600
2601 // Check whether the projection worked. The last condition checks for obliqueness of the
2602 // projection
2603 //
2604 // We are projecting on one side first and the other side second. If we make the
2605 // tolerance bigger and remove the (5) factor we are going to continue to miss the
2606 // second projection and fall into the exception message in
2607 // projectPrimaryNodesSinglePair. What makes this modification to not fall in the
2608 // exception is that we are projecting on one side more xi than in the other. There
2609 // should be a better way of doing this by using actual distances and not parametric
2610 // coordinates. But I believe making the tolerance uniformly larger or smaller won't do
2611 // the trick here.
2612 if ((current_iterate < max_iterates) && (std::abs(xi2) <= 1. + 5 * _xi_tolerance) &&
2613 (abs((primary_elem_candidate->point(0) - primary_elem_candidate->point(1)).unit() *
2614 nodal_normal) < std::cos(_minimum_projection_angle * libMesh::pi / 180)))
2615 {
2616 // If xi2 == +1 or -1 then this secondary node mapped directly to a node on the primary
2617 // surface. This isn't as unlikely as you might think, it will happen if the meshes
2618 // on the interface start off being perfectly aligned. In this situation, we need to
2619 // associate the secondary node with two different elements (and two corresponding
2620 // xi^(2) values.
2621 if (abs(abs(xi2) - 1.) <= _xi_tolerance * 5.0)
2622 {
2623 const Node * primary_node = (xi2 < 0) ? primary_elem_candidate->node_ptr(0)
2624 : primary_elem_candidate->node_ptr(1);
2625 const bool created_mortar_segment =
2626 processAlignedNodes(*secondary_node,
2627 *primary_node,
2628 &secondary_node_neighbors,
2629 nullptr,
2630 nodal_normal,
2631 *primary_elem_candidate,
2632 rejected_primary_elem_candidates);
2633
2634 if (!created_mortar_segment)
2635 continue;
2636 }
2637 else // Point falls somewhere in the middle of the Elem.
2638 {
2639 // Add two entries to secondary_node_and_elem_to_xi2_primary_elem.
2640 for (MooseIndex(secondary_node_neighbors) nn = 0;
2641 nn < secondary_node_neighbors.size();
2642 ++nn)
2643 {
2644 const Elem * neigh = secondary_node_neighbors[nn];
2645 for (MooseIndex(neigh->n_vertices()) nid = 0; nid < neigh->n_vertices(); ++nid)
2646 {
2647 const Node * neigh_node = neigh->node_ptr(nid);
2648 if (secondary_node == neigh_node)
2649 {
2650 auto key = std::make_pair(neigh_node, neigh);
2651 auto val = std::make_pair(xi2, primary_elem_candidate);
2653 }
2654 }
2655 }
2656 }
2657
2658 projection_succeeded = true;
2659 break; // out of e-loop
2660 }
2661 else
2662 // The current secondary_node is not in this Elem, so keep track of the rejects.
2663 rejected_primary_elem_candidates.insert(primary_elem_candidate);
2664 }
2665
2666 if (projection_succeeded)
2667 break; // out of r-loop
2668 } // r-loop
2669
2670 if (!projection_succeeded)
2671 {
2672 _failed_secondary_node_projections.insert(secondary_node->id());
2673 if (_debug)
2674 _console << "Failed to find primary Elem into which secondary node "
2675 << cast_ref<const Point &>(*secondary_node) << ", id '" << secondary_node->id()
2676 << "', projects onto\n"
2677 << std::endl;
2678 }
2679 else if (_debug)
2680 _projected_secondary_nodes.insert(secondary_node->id());
2681 } // loop over side nodes
2682 } // end loop over lower-dimensional elements
2683
2684 if (_distributed)
2685 {
2686 if (_debug)
2687 _mesh.comm().set_union(_projected_secondary_nodes);
2688 _mesh.comm().set_union(_failed_secondary_node_projections);
2689 }
2690
2691 if (_debug)
2692 _console << "\n"
2693 << _projected_secondary_nodes.size() << " out of "
2695 << " secondary nodes were successfully projected\n"
2696 << std::endl;
2697}
2698
2699// Inverse map primary nodes onto their corresponding secondary elements for each primary/secondary
2700// pair.
2701void
2703{
2704 // For each primary/secondary boundary id pair, call the
2705 // project_primary_nodes_single_pair() helper function.
2706 for (const auto & pr : _primary_secondary_subdomain_id_pairs)
2707 projectPrimaryNodesSinglePair(pr.first, pr.second);
2708}
2709
2710void
2712 SubdomainID lower_dimensional_primary_subdomain_id,
2713 SubdomainID lower_dimensional_secondary_subdomain_id)
2714{
2715 using std::abs;
2716
2717 // Build a Nanoflann object on the lower-dimensional secondary elements of the Mesh.
2718 NanoflannMeshSubdomainAdaptor<3> mesh_adaptor(_mesh, lower_dimensional_secondary_subdomain_id);
2719 subdomain_kd_tree_t kd_tree(
2720 3, mesh_adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(/*max leaf=*/10));
2721
2722 // Construct the KD tree for lower-dimensional elements in the volume mesh.
2723 kd_tree.buildIndex();
2724
2725 std::unordered_set<dof_id_type> primary_nodes_visited;
2726
2727 for (const auto & primary_side_elem : _mesh.active_element_ptr_range())
2728 {
2729 // If this is not one of the lower-dimensional primary side elements, go on to the next one.
2730 if (primary_side_elem->subdomain_id() != lower_dimensional_primary_subdomain_id)
2731 continue;
2732
2733 // For each node on this side, find the nearest node on the secondary side using the KDTree,
2734 // then search in nearby elements for where it projects along the nodal normal direction.
2735 for (MooseIndex(primary_side_elem->n_vertices()) n = 0; n < primary_side_elem->n_vertices();
2736 ++n)
2737 {
2738 // Get a pointer to this node.
2739 const Node * primary_node = primary_side_elem->node_ptr(n);
2740
2741 // Get the nodal neighbors connected to this primary node.
2742 const std::vector<const Elem *> & primary_node_neighbors =
2743 _nodes_to_primary_elem_map.at(primary_node->id());
2744
2745 // Check whether we have already successfully inverse mapped this primary node (whether during
2746 // secondary node projection or now during primary node projection) or we have already failed
2747 // to inverse map this primary node (now during primary node projection), and then skip if
2748 // either of those things is true
2749 auto primary_key =
2750 std::make_tuple(primary_node->id(), primary_node, primary_node_neighbors[0]);
2751 if (!primary_nodes_visited.insert(primary_node->id()).second ||
2753 continue;
2754
2755 // Data structure for performing Nanoflann searches.
2756 Real query_pt[3] = {(*primary_node)(0), (*primary_node)(1), (*primary_node)(2)};
2757
2758 // The number of results we want to get. We'll look for a
2759 // "few" nearest nodes, hopefully that is enough to let us
2760 // figure out which lower-dimensional Elem on the secondary side
2761 // we are across from.
2762 const size_t num_results = 3;
2763
2764 // Initialize result_set and do the search.
2765 std::vector<size_t> ret_index(num_results);
2766 std::vector<Real> out_dist_sqr(num_results);
2767 nanoflann::KNNResultSet<Real> result_set(num_results);
2768 result_set.init(&ret_index[0], &out_dist_sqr[0]);
2769 kd_tree.findNeighbors(result_set, &query_pt[0], nanoflann::SearchParameters());
2770
2771 // If this flag gets set in the loop below, we can break out of the outer r-loop as well.
2772 bool projection_succeeded = false;
2773
2774 // Once we've rejected a candidate for a given
2775 // primary_node, there's no reason to check it
2776 // again.
2777 std::set<const Elem *> rejected_secondary_elem_candidates;
2778
2779 // Loop over the closest nodes, check whether the secondary node successfully projects into
2780 // either of the closest neighbors, stop when the projection succeeds.
2781 for (MooseIndex(result_set) r = 0; r < result_set.size(); ++r)
2782 {
2783 // Verify that the squared distance we compute is the same as nanoflann's
2784 mooseAssert(abs((_mesh.point(ret_index[r]) - *primary_node).norm_sq() - out_dist_sqr[r]) <=
2785 TOLERANCE,
2786 "Lower-dimensional element squared distance verification failed.");
2787
2788 // Get a reference to the vector of lower dimensional elements from the
2789 // nodes_to_secondary_elem_map.
2790 const std::vector<const Elem *> & secondary_elem_candidates =
2791 _nodes_to_secondary_elem_map.at(static_cast<dof_id_type>(ret_index[r]));
2792
2793 // Print the Elems connected to this node on the secondary mesh side.
2794 for (MooseIndex(secondary_elem_candidates) e = 0; e < secondary_elem_candidates.size(); ++e)
2795 {
2796 const Elem * secondary_elem_candidate = secondary_elem_candidates[e];
2797
2798 // If we've already rejected this candidate, we don't need to check it again.
2799 if (rejected_secondary_elem_candidates.count(secondary_elem_candidate))
2800 continue;
2801
2802 std::vector<Point> nodal_normals(secondary_elem_candidate->n_nodes());
2803 for (const auto n : make_range(secondary_elem_candidate->n_nodes()))
2804 nodal_normals[n] =
2805 _secondary_node_to_nodal_normal.at(secondary_elem_candidate->node_ptr(n));
2806
2807 // Use equation 2.4.6 from Bin Yang's dissertation to try and solve for
2808 // the position on the secondary element where this primary came from. This
2809 // requires a Newton iteration in general.
2810 DualNumber<Real> xi1_dn{0, 1}; // initial guess
2811 auto && order = secondary_elem_candidate->default_order();
2812 unsigned int current_iterate = 0, max_iterates = 10;
2813
2814 VectorValue<DualNumber<Real>> normals(0);
2815
2816 // Newton iteration loop - this to converge in 1 iteration when it
2817 // succeeds, and possibly two iterations when it converges to a
2818 // xi outside the reference element. I don't know any reason why it should
2819 // only take 1 iteration -- the Jacobian is not constant in general...
2820 do
2821 {
2822 VectorValue<DualNumber<Real>> x1(0);
2823 for (MooseIndex(secondary_elem_candidate->n_nodes()) n = 0;
2824 n < secondary_elem_candidate->n_nodes();
2825 ++n)
2826 {
2827 const auto phi = Moose::fe_lagrange_1D_shape(order, n, xi1_dn);
2828 x1 += phi * secondary_elem_candidate->point(n);
2829 normals += phi * nodal_normals[n];
2830 }
2831
2832 const auto u = x1 - (*primary_node);
2833
2834 const auto F = u(0) * normals(1) - u(1) * normals(0);
2835
2836 if (abs(F) < _newton_tolerance)
2837 break;
2838
2839 // Unlike for projection of nodal normals onto primary surfaces, we should never have a
2840 // case where the nodal normal is completely orthogonal to the secondary surface, so we
2841 // do not have to guard against F.derivatives() == 0 here
2842 Real dxi1 = -F.value() / F.derivatives();
2843
2844 xi1_dn += dxi1;
2845
2846 normals = 0;
2847 } while (++current_iterate < max_iterates);
2848
2849 Real xi1 = xi1_dn.value();
2850
2851 // Check for convergence to a valid solution... The last condition checks for obliqueness
2852 // of the projection
2853 if ((current_iterate < max_iterates) && (abs(xi1) <= 1. + _xi_tolerance) &&
2854 (abs((primary_side_elem->point(0) - primary_side_elem->point(1)).unit() *
2855 MetaPhysicL::raw_value(normals).unit()) <
2856 std::cos(_minimum_projection_angle * libMesh::pi / 180.0)))
2857 {
2858 if (abs(abs(xi1) - 1.) < _xi_tolerance)
2859 {
2860 // Special case: xi1=+/-1.
2861 // It is unlikely that we get here, because this primary node should already
2862 // have been mapped during the project_secondary_nodes() routine, but
2863 // there is still a chance since the tolerances are applied to
2864 // the xi coordinate and that value may be different on a primary element and a
2865 // secondary element since they may have different sizes. It's also possible that we
2866 // may reach this point if the solve has yielded a non-physical configuration such as
2867 // one block being pushed way out into space
2868 const Node & secondary_node = (xi1 < 0) ? secondary_elem_candidate->node_ref(0)
2869 : secondary_elem_candidate->node_ref(1);
2870 bool created_mortar_segment = false;
2871
2872 // If we have failed to project this secondary node, let's try again now
2873 if (_failed_secondary_node_projections.count(secondary_node.id()))
2874 created_mortar_segment = processAlignedNodes(secondary_node,
2875 *primary_node,
2876 nullptr,
2877 &primary_node_neighbors,
2878 MetaPhysicL::raw_value(normals),
2879 *secondary_elem_candidate,
2880 rejected_secondary_elem_candidates);
2881 else
2882 rejected_secondary_elem_candidates.insert(secondary_elem_candidate);
2883
2884 if (!created_mortar_segment)
2885 // We used to throw an exception in this scope but now that we support processing
2886 // aligned nodes within this primary node projection method, I don't see any harm in
2887 // simply rejecting the secondary element candidate in the case of failure and
2888 // continuing just as we do when projecting secondary nodes
2889 continue;
2890 }
2891 else // somewhere in the middle of the Elem
2892 {
2893 // Add entry to primary_node_and_elem_to_xi1_secondary_elem
2894 //
2895 // Note: we originally duplicated the map values for the keys (node, left_neighbor)
2896 // and (node, right_neighbor) but I don't think that should be necessary. Instead we
2897 // just do it for neighbor 0, but really maybe we don't even need to do that since
2898 // we can always look up the neighbors later given the Node... keeping it like this
2899 // helps to maintain the "symmetry" of the two containers.
2900 const Elem * neigh = primary_node_neighbors[0];
2901 for (MooseIndex(neigh->n_vertices()) nid = 0; nid < neigh->n_vertices(); ++nid)
2902 {
2903 const Node * neigh_node = neigh->node_ptr(nid);
2904 if (primary_node == neigh_node)
2905 {
2906 auto key = std::make_tuple(neigh_node->id(), neigh_node, neigh);
2907 auto val = std::make_pair(xi1, secondary_elem_candidate);
2909 }
2910 }
2911 }
2912
2913 projection_succeeded = true;
2914 break; // out of e-loop
2915 }
2916 else
2917 {
2918 // The current primary_point is not in this Elem, so keep track of the rejects.
2919 rejected_secondary_elem_candidates.insert(secondary_elem_candidate);
2920 }
2921 } // end e-loop over candidate elems
2922
2923 if (projection_succeeded)
2924 break; // out of r-loop
2925 } // r-loop
2926
2927 if (!projection_succeeded && _debug)
2928 {
2929 _console << "\nFailed to find point from which primary node "
2930 << cast_ref<const Point &>(*primary_node) << " was projected." << std::endl
2931 << std::endl;
2932 }
2933 } // loop over side nodes
2934 } // end loop over elements for finding where primary points would have projected from.
2935}
2936
2937std::vector<AutomaticMortarGeneration::MortarFilterIter>
2939{
2940 auto secondary_it = _nodes_to_secondary_elem_map.find(node.id());
2941 if (secondary_it == _nodes_to_secondary_elem_map.end())
2942 return {};
2943
2944 const auto & secondary_elems = secondary_it->second;
2945 std::vector<MortarFilterIter> ret;
2946 ret.reserve(secondary_elems.size());
2947
2948 for (const auto i : index_range(secondary_elems))
2949 {
2950 auto * const secondary_elem = secondary_elems[i];
2951 auto msm_it = _secondary_elems_to_mortar_segments.find(secondary_elem->id());
2952 if (msm_it == _secondary_elems_to_mortar_segments.end())
2953 // We may have removed this element key from this map
2954 continue;
2955
2956 mooseAssert(secondary_elem->active(),
2957 "We loop over active elements when building the mortar segment mesh, so we golly "
2958 "well hope this is active.");
2959 mooseAssert(!msm_it->second.empty(),
2960 "We should have removed all secondaries from this map if they do not have any "
2961 "mortar segments associated with them.");
2962 ret.push_back(msm_it);
2963 }
2964
2965 return ret;
2966}
subdomain_id_type SubdomainID
void mooseWarning(Args &&... args)
Emit a warning message with the given stringified, concatenated args.
Definition MooseError.h:345
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application.
Definition MooseError.h:311
libMesh::VectorValue< ADReal > ADRealVectorValue
AD Array typedefs.
Definition MooseTypes.h:411
libMesh::VectorValue< ADReal > ADPoint
Definition MooseTypes.h:413
MortarSegmentTriangulationMode
Point center
Definition MortarUtils.C:58
nanoflann::KDTreeSingleIndexAdaptor< subdomain_adatper_t, NanoflannMeshSubdomainAdaptor< 3 >, 3 > subdomain_kd_tree_t
This class is a container/interface for the objects involved in automatic generation of mortar spaces...
const Elem * getSecondaryLowerdElemFromSecondaryElem(dof_id_type secondary_elem_id) const
Return lower dimensional secondary element given its interior parent.
Real _newton_tolerance
Newton solve tolerance for node projections.
std::unordered_map< const Node *, Point > _secondary_node_to_nodal_normal
Container for storing the nodal normal vector associated with each secondary node.
const MortarSegmentReferencePoints & mortarSegmentReferencePoints(const Elem &mortar_segment_elem) const
Return the parent-face reference coordinates for a mortar segment.
std::unordered_map< const Elem *, unsigned int > _lower_elem_to_side_id
Keeps track of the mapping between lower-dimensional elements and the side_id of the interior_parent ...
std::unordered_map< const Elem *, MortarSegmentReferencePoints > _msm_elem_to_reference_points
Reference-coordinate data used only by the reference-interpolation mapping mode.
std::unordered_set< dof_id_type > _projected_secondary_nodes
Debugging container for printing information about fraction of successful projections for secondary n...
std::unordered_map< dof_id_type, std::vector< const Elem * > > _nodes_to_secondary_elem_map
Map from nodes to connected lower-dimensional elements on the secondary/primary subdomains.
std::set< BoundaryID > _secondary_requested_boundary_ids
The boundary ids corresponding to all the secondary surfaces.
std::unordered_map< dof_id_type, std::vector< const Elem * > > _nodes_to_primary_elem_map
Real _xi_tolerance
Tolerance for checking projection xi values.
void outputMortarMesh()
Write the mortar segment mesh to exodus.
void computeNodalGeometry()
Computes and stores the nodal normal/tangent vectors in a local data structure instead of using the E...
const Mortar3DQuadraturePointMapping _mortar_3d_qp_mapping
Method used to map 3D mortar segment quadrature points to primary and secondary faces.
void msmStatistics()
Prints mortar segment mesh statistics to console (calls computeMsmStatistics internally)
std::vector< std::pair< BoundaryID, BoundaryID > > _primary_secondary_boundary_id_pairs
A list of primary/secondary boundary id pairs corresponding to each side of the mortar interface.
std::unique_ptr< InputParameters > _output_params
Storage for the input parameters used by the mortar nodal geometry output.
void buildMortarSegmentMesh()
Builds the mortar segment mesh once the secondary and primary node projections have been completed.
std::unordered_set< const Node * > _inactive_local_lm_nodes
List of inactive lagrange multiplier nodes owned by this processor (for nodal variables)
const bool _periodic
Whether this object will be generating a mortar segment mesh for periodic constraints.
void projectSecondaryNodes()
Project secondary nodes (find xi^(2) values) to the closest points on the primary surface.
void buildMortarSegmentMesh3d()
Builds the mortar segment mesh once the secondary and primary node projections have been completed.
void computeInactiveLMElems()
Get list of secondary elems without any corresponding primary elements.
std::set< SubdomainID > _secondary_boundary_subdomain_ids
The secondary/primary lower-dimensional boundary subdomain ids are the secondary/primary boundary ids...
const bool _correct_edge_dropping
Flag to enable regressed treatment of edge dropping where all LM DoFs on edge dropping element are st...
void buildCouplingInformation()
build the _mortar_interface_coupling data
const Mortar3DSubpatchPlane _mortar_3d_subpatch_plane
Method used to define the local projection planes for 3D secondary subpatches.
std::vector< Point > getNormals(const Elem &secondary_elem, const std::vector< Point > &xi1_pts) const
Compute the normals at given reference points on a secondary element.
std::set< SubdomainID > _secondary_ip_sub_ids
All the secondary interior parent subdomain IDs associated with the mortar mesh.
void initOutput()
initialize mortar-mesh based output
std::unordered_map< const Node *, std::array< Point, 2 > > _secondary_node_to_hh_nodal_tangents
Container for storing the nodal tangent/binormal vectors associated with each secondary node (Househo...
bool processAlignedNodes(const Node &secondary_node, const Node &primary_node, const std::vector< const Elem * > *secondary_node_neighbors, const std::vector< const Elem * > *primary_node_neighbors, const VectorValue< Real > &nodal_normal, const Elem &candidate_element, std::set< const Elem * > &rejected_element_candidates)
Process aligned nodes.
void projectSecondaryNodesSinglePair(SubdomainID lower_dimensional_primary_subdomain_id, SubdomainID lower_dimensional_secondary_subdomain_id)
Helper function responsible for projecting secondary nodes onto primary elements for a single primary...
std::vector< MsmSubdomainStats > computeMsmStatistics()
Computes mortar segment mesh statistics and returns one entry per subdomain pair.
MeshBase & _mesh
Reference to the mesh stored in equation_systems.
const bool _debug
Whether to print debug output.
std::vector< std::pair< SubdomainID, SubdomainID > > _primary_secondary_subdomain_id_pairs
A list of primary/secondary subdomain id pairs corresponding to each side of the mortar interface.
std::map< unsigned int, unsigned int > getSecondaryIpToLowerElementMap(const Elem &lower_secondary_elem) const
Compute on-the-fly mapping from secondary interior parent nodes to lower dimensional nodes.
void buildNodeToElemMaps()
Once the secondary_requested_boundary_ids and primary_requested_boundary_ids containers have been fil...
std::unique_ptr< MeshBase > _mortar_segment_mesh
1D Mesh of mortar segment elements which gets built by the call to build_mortar_segment_mesh().
void computeADNodalNormals(const std::function< ADPoint(const Node &)> &coordinate, std::unordered_map< const Node *, ADRealVectorValue > &nodal_normals) const
Build the normalized JxW-weighted secondary nodal normals from AD nodal coordinates.
std::optional< dof_id_type > _msm_node_id_start
Cached per-rank starting ID for 3D MSM nodes/elements.
void clear()
Clears the mortar segment mesh and accompanying data structures.
std::unordered_map< dof_id_type, std::unordered_set< dof_id_type > > _mortar_interface_coupling
Used by the AugmentSparsityOnInterface functor to determine whether a given Elem is coupled to any ot...
std::map< unsigned int, unsigned int > getPrimaryIpToLowerElementMap(const Elem &primary_elem, const Elem &primary_elem_ip, const Elem &lower_secondary_elem) const
Compute on-the-fly mapping from primary interior parent nodes to its corresponding lower dimensional ...
std::unordered_map< std::pair< const Node *, const Elem * >, std::pair< Real, const Elem * > > _secondary_node_and_elem_to_xi2_primary_elem
Similar to the map above, but associates a (Secondary Node, Secondary Elem) pair to a (xi^(2),...
std::unordered_map< const Elem *, MortarSegmentInfo > _msm_elem_to_info
Map between Elems in the mortar segment mesh and their info structs.
AutomaticMortarGeneration(MooseApp &app, MeshBase &mesh_in, const std::pair< BoundaryID, BoundaryID > &boundary_key, const std::pair< SubdomainID, SubdomainID > &subdomain_key, bool on_displaced, bool periodic, const bool debug, const bool correct_edge_dropping, const Real minimum_projection_angle, const Mortar3DSubpatchPlane mortar_3d_subpatch_plane, const MortarSegmentTriangulationMode triangulation_mode, const bool triangulate_triangles, const Mortar3DQuadraturePointMapping mortar_3d_qp_mapping=Mortar3DQuadraturePointMapping::NORMAL_PROJECTION)
Must be constructed with a reference to the Mesh we are generating mortar spaces for.
std::map< std::tuple< dof_id_type, const Node *, const Elem * >, std::pair< Real, const Elem * > > _primary_node_and_elem_to_xi1_secondary_elem
Same type of container, but for mapping (Primary Node ID, Primary Node, Primary Elem) -> (xi^(1),...
const bool _on_displaced
Whether this object is on the displaced mesh.
std::unordered_set< const Elem * > _inactive_local_lm_elems
List of inactive lagrange multiplier nodes (for elemental variables)
void computeIncorrectEdgeDroppingInactiveLMNodes()
Computes inactive secondary nodes when incorrect edge dropping behavior is enabled (any node touching...
const bool _distributed
Whether the mortar segment mesh is distributed.
std::array< MooseUtils::SemidynamicVector< Point, 9 >, 2 > getNodalTangents(const Elem &secondary_elem) const
Compute the two nodal tangents, which are built on-the-fly.
std::set< SubdomainID > _primary_ip_sub_ids
All the primary interior parent subdomain IDs associated with the mortar mesh.
std::unordered_map< dof_id_type, const Elem * > _secondary_element_to_secondary_lowerd_element
Map from full dimensional secondary element id to lower dimensional secondary element.
const bool _triangulate_triangles
Whether already-triangular clipped polygons should still be centroid-subdivided.
std::set< SubdomainID > _primary_boundary_subdomain_ids
void projectPrimaryNodesSinglePair(SubdomainID lower_dimensional_primary_subdomain_id, SubdomainID lower_dimensional_secondary_subdomain_id)
Helper function used internally by AutomaticMortarGeneration::project_primary_nodes().
const MortarSegmentTriangulationMode _triangulation_mode
Triangulation mode used for clipped 3D mortar polygons.
std::set< BoundaryID > _primary_requested_boundary_ids
The boundary ids corresponding to all the primary surfaces.
std::unordered_set< dof_id_type > _failed_secondary_node_projections
Secondary nodes that failed to project.
std::vector< Point > getNodalNormals(const Elem &secondary_elem) const
std::unordered_map< dof_id_type, std::set< Elem *, CompareDofObjectsByID > > _secondary_elems_to_mortar_segments
We maintain a mapping from lower-dimensional secondary elements in the original mesh to (sets of) ele...
const std::unordered_map< dof_id_type, std::set< Elem *, CompareDofObjectsByID > > & secondariesToMortarSegments() const
void projectPrimaryNodes()
(Inverse) project primary nodes to the points on the secondary surface where they would have come fro...
void computeInactiveLMNodes()
Get list of secondary nodes that don't contribute to interaction with any primary element.
const Real _minimum_projection_angle
Parameter to control which angle (in degrees) is admissible for the creation of mortar segments.
An inteface for the _console for outputting to the Console object.
const ConsoleStream _console
An instance of helper class to write streams to the Console objects.
Specialization of SubProblem for solving nonlinear equations plus auxiliary equations.
This class is used for building, formatting, and outputting tables of numbers.
void printTable(std::ostream &out, unsigned int last_n_entries=0)
Methods for dumping the table to the stream - either by filename or by stream handle.
void addData(const std::string &name, const T &value)
Method for adding data to the output table.
void addRow(Real time)
Force a new row in the table with the passed in time.
The main MOOSE class responsible for handling user-defined parameters in almost every MOOSE system.
Base class for MOOSE-based applications.
Definition MooseApp.h:110
std::string getOutputFileBase(bool for_non_moose_build_output=false) const
Get the output file base name.
Definition MooseApp.C:1541
OutputWarehouse & getOutputWarehouse()
Get the OutputWarehouse objects.
Definition MooseApp.C:2418
FEProblemBase & feProblem() const
Definition MooseApp.C:1861
SolutionInvalidity & solutionInvalidity()
Get the SolutionInvalidity for this app.
Definition MooseApp.h:182
static const std::string name_param
The name of the parameter that contains the object name.
Definition MooseBase.h:55
static const std::string type_param
The name of the parameter that contains the object type.
Definition MooseBase.h:53
void mooseError(Args &&... args) const
Emits an error prefixed with object name and type and optionally a file path to the top-level block p...
Definition MooseBase.h:271
T getCheckedPointerParam(const std::string &name, const std::string &error_string="") const
Verifies that the requested parameter exists and is not NULL and returns it to the caller.
Definition MooseBase.h:450
static const std::string app_param
The name of the parameter that contains the MooseApp.
Definition MooseBase.h:59
Provides a way for users to bail out of the current solve.
MooseApp & _app
The MOOSE application this is associated with.
Definition MooseBase.h:375
MortarNodalGeometryOutput(const InputParameters &params)
static InputParameters validParams()
AutomaticMortarGeneration & _amg
The mortar generation object that we will query for nodal normal and tangent information.
libMesh::System * _nodal_normals_system
Member variables for geometry debug output.
void output() override
Overload this function with the desired output activities.
Special adaptor that works with subdomains of the Mesh.
void addOutput(std::shared_ptr< Output > output)
Adds an existing output object to the warehouse.
Based class for output objects.
Definition Output.h:52
static InputParameters validParams()
Definition Output.C:32
void flagInvalidSolutionInternal(const InvalidSolutionID _invalid_solution_id)
Increments solution invalid occurrences for each solution id.
void dof_indices(const Elem *const elem, std::vector< dof_id_type > &di) const
unsigned int n_dofs(const ElemType t, const Order o)
const std::vector< Real > & get_weights() const
unsigned int add_variable(std::string_view var, const FEType &type, const std::set< subdomain_id_type > *const active_subdomains=nullptr)
std::unique_ptr< NumericVector< Number > > solution
const DofMap & get_dof_map() const
TypeVector< typename CompareTypes< T, T2 >::supertype > cross(const TypeVector< T2 > &v) const
void add_scaled(const TypeVector< T2 > &, const T &)
InvalidSolutionID registerInvalidity(const std::string &object_type, const std::string &message, const bool warning)
Call to register an invalid calculation.
auto raw_value(const Eigen::Map< T > &in)
std::vector< unsigned int > getMortarSubElementNodeIndices(const Elem &parent_elem, unsigned int sub_elem)
Return the node indices for a first-order sub-element of a parent face.
Definition MortarUtils.C:67
std::array< Vector, 2 > householderTangents(const Vector &normal)
Construct the two tangent vectors used by mortar from a unit normal.
T fe_lagrange_2D_shape(const libMesh::ElemType type, const Order order, const unsigned int i, const VectorType< T > &p)
static constexpr std::size_t dim
This is the dimension of all vector and tensor datastructures used in MOOSE.
Definition Moose.h:175
T fe_lagrange_1D_shape(const Order order, const unsigned int i, const T &xi)
void push_parallel_vector_data(const Communicator &comm, MapToVectors &&data, const ActionFunctor &act_on_data)
const Real pi
SolutionInvalidityRegistry & getSolutionInvalidityRegistry()
Get the global SolutionInvalidityRegistry singleton.
SearchParams SearchParameters
Statistics for one primary-secondary subdomain pair.
Holds xi^(1), xi^(2), and other data for a given mortar segment.
const Elem * primary_elem
static const Real invalid_xi
const Elem * secondary_elem
Parent-face reference coordinates associated with the vertices of one triangular mortar segment.
const dof_id_type n_nodes