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