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