https://mooseframework.inl.gov
AutomaticMortarGeneration.C
Go to the documentation of this file.
1 //* This file is part of the MOOSE framework
2 //* https://mooseframework.inl.gov
3 //*
4 //* All rights reserved, see COPYRIGHT for full restrictions
5 //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6 //*
7 //* Licensed under LGPL 2.1, please see LICENSE for details
8 //* https://www.gnu.org/licenses/lgpl-2.1.html
9 
11 #include "MortarSegmentInfo.h"
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;
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 nodalQuadraturePointToSecondaryNodeMap(const Elem & secondary_elem,
76  const std::vector<Point> & q_points)
77 {
78  const auto n_nodes = secondary_elem.n_nodes();
79  if (q_points.size() != n_nodes)
80  mooseError("Nodal quadrature produced ",
81  q_points.size(),
82  " points for secondary mortar element ",
83  secondary_elem.id(),
84  " of type ",
85  libMesh::Utility::enum_to_string<ElemType>(secondary_elem.type()),
86  ", but the element has ",
87  n_nodes,
88  " nodes.");
89 
90  const auto invalid_node = std::numeric_limits<unsigned int>::max();
91  std::vector<unsigned int> qpoint_to_node(n_nodes, invalid_node);
92  std::vector<bool> node_used(n_nodes, false);
93 
94  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  const Real matching_tol = 100 * TOLERANCE * element_size;
107  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  for (const auto qp : make_range(q_points.size()))
113  {
114  unsigned int closest_node = invalid_node;
115  Real closest_dist_sq = std::numeric_limits<Real>::max();
116  Real second_closest_dist_sq = std::numeric_limits<Real>::max();
117 
118  for (const auto n : make_range(n_nodes))
119  {
120  if (node_used[n])
121  continue;
122 
123  const Real dist_sq = (q_points[qp] - secondary_elem.point(n)).norm_sq();
124  if (dist_sq < closest_dist_sq)
125  {
126  second_closest_dist_sq = closest_dist_sq;
127  closest_dist_sq = dist_sq;
128  closest_node = n;
129  }
130  else if (dist_sq < second_closest_dist_sq)
131  second_closest_dist_sq = dist_sq;
132  }
133 
134  if (closest_node == invalid_node || closest_dist_sq > matching_tol_sq)
135  mooseError("Could not match nodal quadrature point ",
136  qp,
137  " at ",
138  q_points[qp],
139  " to a node on secondary mortar element ",
140  secondary_elem.id(),
141  " of type ",
142  libMesh::Utility::enum_to_string<ElemType>(secondary_elem.type()),
143  ". The nearest unmatched node distance is ",
144  std::sqrt(closest_dist_sq),
145  ", which exceeds the tolerance ",
146  matching_tol,
147  ".");
148 
149  if (second_closest_dist_sq <= matching_tol_sq)
150  mooseError("Nodal quadrature point ",
151  qp,
152  " at ",
153  q_points[qp],
154  " does not map uniquely to secondary mortar element ",
155  secondary_elem.id(),
156  " of type ",
157  libMesh::Utility::enum_to_string<ElemType>(secondary_elem.type()),
158  ". Two unmatched nodes are within the matching tolerance ",
159  matching_tol,
160  ".");
161 
162  qpoint_to_node[qp] = closest_node;
163  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  return qpoint_to_node;
233 }
234 }
235 
237 {
238 public:
240  {
241  auto params = Output::validParams();
242  params.addPrivateParam<AutomaticMortarGeneration *>("_amg", nullptr);
243  params.addPrivateParam<MooseApp *>(MooseBase::app_param, nullptr);
244  params.set<std::string>(MooseBase::type_param) = "MortarNodalGeometryOutput";
245  return params;
246  };
247 
249  : Output(params), _amg(*getCheckedPointerParam<AutomaticMortarGeneration *>("_amg"))
250  {
251  }
252 
253  void output() override
254  {
255  // Must call compute_nodal_geometry first!
256  if (_amg._secondary_node_to_nodal_normal.empty() ||
257  _amg._secondary_node_to_hh_nodal_tangents.empty())
258  mooseError("No entries found in the secondary node -> nodal geometry map.");
259 
260  auto & problem = _app.feProblem();
261  auto & subproblem = _amg._on_displaced
262  ? static_cast<SubProblem &>(*problem.getDisplacedProblem())
263  : static_cast<SubProblem &>(problem);
264  auto & nodal_normals_es = subproblem.es();
265 
266  const std::string nodal_normals_sys_name = "nodal_normals";
267 
268  if (!_nodal_normals_system)
269  {
270  for (const auto s : make_range(nodal_normals_es.n_systems()))
271  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  return;
275 
276  _nodal_normals_system =
277  &nodal_normals_es.template add_system<ExplicitSystem>(nodal_normals_sys_name);
278  _nnx_var_num = _nodal_normals_system->add_variable("nodal_normal_x", FEType(FIRST, LAGRANGE)),
279  _nny_var_num = _nodal_normals_system->add_variable("nodal_normal_y", FEType(FIRST, LAGRANGE));
280  _nnz_var_num = _nodal_normals_system->add_variable("nodal_normal_z", FEType(FIRST, LAGRANGE));
281 
282  _t1x_var_num =
283  _nodal_normals_system->add_variable("nodal_tangent_1_x", FEType(FIRST, LAGRANGE)),
284  _t1y_var_num =
285  _nodal_normals_system->add_variable("nodal_tangent_1_y", FEType(FIRST, LAGRANGE));
286  _t1z_var_num =
287  _nodal_normals_system->add_variable("nodal_tangent_1_z", FEType(FIRST, LAGRANGE));
288 
289  _t2x_var_num =
290  _nodal_normals_system->add_variable("nodal_tangent_2_x", FEType(FIRST, LAGRANGE)),
291  _t2y_var_num =
292  _nodal_normals_system->add_variable("nodal_tangent_2_y", FEType(FIRST, LAGRANGE));
293  _t2z_var_num =
294  _nodal_normals_system->add_variable("nodal_tangent_2_z", FEType(FIRST, LAGRANGE));
295  nodal_normals_es.reinit();
296  }
297 
298  const DofMap & dof_map = _nodal_normals_system->get_dof_map();
299  std::vector<dof_id_type> dof_indices_nnx, dof_indices_nny, dof_indices_nnz;
300  std::vector<dof_id_type> dof_indices_t1x, dof_indices_t1y, dof_indices_t1z;
301  std::vector<dof_id_type> dof_indices_t2x, dof_indices_t2y, dof_indices_t2z;
302 
303  for (MeshBase::const_element_iterator el = _amg._mesh.elements_begin(),
304  end_el = _amg._mesh.elements_end();
305  el != end_el;
306  ++el)
307  {
308  const Elem * elem = *el;
309 
310  // Get the nodal dofs for this Elem.
311  dof_map.dof_indices(elem, dof_indices_nnx, _nnx_var_num);
312  dof_map.dof_indices(elem, dof_indices_nny, _nny_var_num);
313  dof_map.dof_indices(elem, dof_indices_nnz, _nnz_var_num);
314 
315  dof_map.dof_indices(elem, dof_indices_t1x, _t1x_var_num);
316  dof_map.dof_indices(elem, dof_indices_t1y, _t1y_var_num);
317  dof_map.dof_indices(elem, dof_indices_t1z, _t1z_var_num);
318 
319  dof_map.dof_indices(elem, dof_indices_t2x, _t2x_var_num);
320  dof_map.dof_indices(elem, dof_indices_t2y, _t2y_var_num);
321  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  for (MooseIndex(elem->n_vertices()) n = 0; n < elem->n_vertices(); ++n)
328  {
329  auto it = _amg._secondary_node_to_nodal_normal.find(elem->node_ptr(n));
330  if (it != _amg._secondary_node_to_nodal_normal.end())
331  {
332  _nodal_normals_system->solution->set(dof_indices_nnx[n], it->second(0));
333  _nodal_normals_system->solution->set(dof_indices_nny[n], it->second(1));
334  _nodal_normals_system->solution->set(dof_indices_nnz[n], it->second(2));
335  }
336 
337  auto it_tangent = _amg._secondary_node_to_hh_nodal_tangents.find(elem->node_ptr(n));
338  if (it_tangent != _amg._secondary_node_to_hh_nodal_tangents.end())
339  {
340  _nodal_normals_system->solution->set(dof_indices_t1x[n], it_tangent->second[0](0));
341  _nodal_normals_system->solution->set(dof_indices_t1y[n], it_tangent->second[0](1));
342  _nodal_normals_system->solution->set(dof_indices_t1z[n], it_tangent->second[0](2));
343 
344  _nodal_normals_system->solution->set(dof_indices_t2x[n], it_tangent->second[1](0));
345  _nodal_normals_system->solution->set(dof_indices_t2y[n], it_tangent->second[1](1));
346  _nodal_normals_system->solution->set(dof_indices_t2z[n], it_tangent->second[1](2));
347  }
348 
349  } // end loop over nodes
350  } // end loop over elems
351 
352  // Finish assembly.
353  _nodal_normals_system->solution->close();
354 
355  std::set<std::string> sys_names = {nodal_normals_sys_name};
356 
357  // Write the nodal normals to file
358  ExodusII_IO nodal_normals_writer(_amg._mesh);
359 
360  // Default to non-HDF5 output for wider compatibility
361  nodal_normals_writer.set_hdf5_writing(false);
362 
363  nodal_normals_writer.write_equation_systems(
364  "nodal_geometry_only.e", nodal_normals_es, &sys_names);
365  }
366 
367 private:
370 
372 
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;
386 };
387 
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  const bool triangulate_triangles)
400  : ConsoleStreamInterface(app),
401  _app(app),
402  _mesh(mesh_in),
403  _debug(debug),
404  _on_displaced(on_displaced),
405  _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  _distributed(_mesh.mesh_dimension() == 3 ? true : (!_on_displaced && !_mesh.is_replicated())),
410  _correct_edge_dropping(correct_edge_dropping),
411  _minimum_projection_angle(minimum_projection_angle),
412  _triangulation_mode(triangulation_mode),
413  _triangulate_triangles(triangulate_triangles)
414 {
415  _primary_secondary_boundary_id_pairs.push_back(boundary_key);
416  _primary_requested_boundary_ids.insert(boundary_key.first);
417  _secondary_requested_boundary_ids.insert(boundary_key.second);
418  _primary_secondary_subdomain_id_pairs.push_back(subdomain_key);
419  _primary_boundary_subdomain_ids.insert(subdomain_key.first);
420  _secondary_boundary_subdomain_ids.insert(subdomain_key.second);
421 
422  if (_distributed)
424  std::make_unique<DistributedMesh>(_mesh.comm(), _mesh.spatial_dimension());
425  else
427  std::make_unique<ReplicatedMesh>(_mesh.comm(), _mesh.spatial_dimension());
428 }
429 
430 std::string
432 {
433  std::vector<std::string> string_vec(_primary_secondary_boundary_id_pairs.size() * 2 + 1);
435  {
436  const auto [primary_bnd_id, secondary_bnd_id] = _primary_secondary_boundary_id_pairs[i];
437  string_vec[2 * i] = std::to_string(primary_bnd_id);
438  string_vec[2 * i + 1] = std::to_string(secondary_bnd_id);
439  }
440  string_vec.back() = _on_displaced ? "displaced" : "undisplaced";
441  return MooseUtils::join(string_vec, "_");
442 }
443 
444 void
446 {
447  if (!_debug)
448  return;
449 
450  _output_params = std::make_unique<InputParameters>(MortarNodalGeometryOutput::validParams());
451  _output_params->set<AutomaticMortarGeneration *>("_amg") = this;
452  _output_params->set<FEProblemBase *>("_fe_problem_base") = &_app.feProblem();
454  _output_params->set<std::string>(MooseBase::name_param) =
455  "mortar_nodal_geometry_" + mortarInterfaceName();
456  _output_params->finalize("MortarNodalGeometryOutput");
457  _app.getOutputWarehouse().addOutput(std::make_shared<MortarNodalGeometryOutput>(*_output_params));
458 }
459 
460 void
462 {
463  _mortar_segment_mesh->clear();
468  _msm_elem_to_info.clear();
469  _lower_elem_to_side_id.clear();
475  _secondary_ip_sub_ids.clear();
476  _primary_ip_sub_ids.clear();
479 }
480 
481 void
483 {
485  mooseError(
486  "Must specify secondary and primary boundary ids before building node-to-elem maps.");
487 
488  // Construct nodes_to_secondary_elem_map
489  for (const auto & secondary_elem :
490  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  if (!this->_secondary_boundary_subdomain_ids.count(secondary_elem->subdomain_id()))
494  continue;
495 
496  for (const auto & nd : secondary_elem->node_ref_range())
497  {
498  std::vector<const Elem *> & vec = _nodes_to_secondary_elem_map[nd.id()];
499  vec.push_back(secondary_elem);
500  }
501  }
502 
503  // Construct nodes_to_primary_elem_map
504  for (const auto & primary_elem :
505  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  if (!this->_primary_boundary_subdomain_ids.count(primary_elem->subdomain_id()))
509  continue;
510 
511  for (const auto & nd : primary_elem->node_ref_range())
512  {
513  std::vector<const Elem *> & vec = _nodes_to_primary_elem_map[nd.id()];
514  vec.push_back(primary_elem);
515  }
516  }
517 }
518 
519 std::vector<Point>
521 {
522  std::vector<Point> nodal_normals(secondary_elem.n_nodes());
523  for (const auto n : make_range(secondary_elem.n_nodes()))
524  nodal_normals[n] = _secondary_node_to_nodal_normal.at(secondary_elem.node_ptr(n));
525 
526  return nodal_normals;
527 }
528 
529 const Elem *
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  return _secondary_element_to_secondary_lowerd_element.at(secondary_elem_id);
537 }
538 
539 std::map<unsigned int, unsigned int>
541 {
542  std::map<unsigned int, unsigned int> secondary_ip_i_to_lower_secondary_i;
543  const Elem * const secondary_ip = lower_secondary_elem.interior_parent();
544  mooseAssert(secondary_ip, "This should be non-null");
545 
546  for (const auto i : make_range(lower_secondary_elem.n_nodes()))
547  {
548  const auto & nd = lower_secondary_elem.node_ref(i);
549  secondary_ip_i_to_lower_secondary_i[secondary_ip->get_node_index(&nd)] = i;
550  }
551 
552  return secondary_ip_i_to_lower_secondary_i;
553 }
554 
555 std::map<unsigned int, unsigned int>
557  const Elem & lower_primary_elem,
558  const Elem & primary_elem,
559  const Elem & /*lower_secondary_elem*/) const
560 {
561  std::map<unsigned int, unsigned int> primary_ip_i_to_lower_primary_i;
562 
563  for (const auto i : make_range(lower_primary_elem.n_nodes()))
564  {
565  const auto & nd = lower_primary_elem.node_ref(i);
566  primary_ip_i_to_lower_primary_i[primary_elem.get_node_index(&nd)] = i;
567  }
568 
569  return primary_ip_i_to_lower_primary_i;
570 }
571 
572 std::array<MooseUtils::SemidynamicVector<Point, 9>, 2>
574 {
575  // MetaPhysicL will check if we ran out of allocated space.
576  MooseUtils::SemidynamicVector<Point, 9> nodal_tangents_one(0);
577  MooseUtils::SemidynamicVector<Point, 9> nodal_tangents_two(0);
578 
579  for (const auto n : make_range(secondary_elem.n_nodes()))
580  {
581  const auto & tangent_vectors =
582  libmesh_map_find(_secondary_node_to_hh_nodal_tangents, secondary_elem.node_ptr(n));
583  nodal_tangents_one.push_back(tangent_vectors[0]);
584  nodal_tangents_two.push_back(tangent_vectors[1]);
585  }
586 
587  return {{nodal_tangents_one, nodal_tangents_two}};
588 }
589 
590 std::vector<Point>
592  const std::vector<Real> & oned_xi1_pts) const
593 {
594  std::vector<Point> xi1_pts(oned_xi1_pts.size());
595  for (const auto qp : index_range(oned_xi1_pts))
596  xi1_pts[qp] = oned_xi1_pts[qp];
597 
598  return getNormals(secondary_elem, xi1_pts);
599 }
600 
601 std::vector<Point>
603  const std::vector<Point> & xi1_pts) const
604 {
605  const auto mortar_dim = _mesh.mesh_dimension() - 1;
606  const auto num_qps = xi1_pts.size();
607  const auto nodal_normals = getNodalNormals(secondary_elem);
608  std::vector<Point> normals(num_qps);
609 
610  for (const auto n : make_range(secondary_elem.n_nodes()))
611  for (const auto qp : make_range(num_qps))
612  {
613  const auto phi =
614  (mortar_dim == 1)
615  ? Moose::fe_lagrange_1D_shape(secondary_elem.default_order(), n, xi1_pts[qp](0))
616  : Moose::fe_lagrange_2D_shape(secondary_elem.type(),
617  secondary_elem.default_order(),
618  n,
619  static_cast<const TypeVector<Real> &>(xi1_pts[qp]));
620  normals[qp] += phi * nodal_normals[n];
621  }
622 
623  if (_periodic)
624  for (auto & normal : normals)
625  normal *= -1;
626 
627  return normals;
628 }
629 
630 void
632 {
633  using std::abs;
634 
635  dof_id_type local_id_index = 0;
636  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  for (const auto & pr : _primary_secondary_boundary_id_pairs)
643  {
644  const auto primary_bnd_id = pr.first;
645  const auto secondary_bnd_id = pr.second;
646  const auto num_primary_nodes =
647  std::distance(_mesh.bid_nodes_begin(primary_bnd_id), _mesh.bid_nodes_end(primary_bnd_id));
648  const auto num_secondary_nodes = std::distance(_mesh.bid_nodes_begin(secondary_bnd_id),
649  _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  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  for (MeshBase::const_element_iterator el = _mesh.active_elements_begin(),
662  end_el = _mesh.active_elements_end();
663  el != end_el;
664  ++el)
665  {
666  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  if (!this->_secondary_boundary_subdomain_ids.count(secondary_elem->subdomain_id()))
670  continue;
671 
672  std::vector<Node *> new_nodes;
673  for (MooseIndex(secondary_elem->n_nodes()) n = 0; n < secondary_elem->n_nodes(); ++n)
674  {
675  new_nodes.push_back(_mortar_segment_mesh->add_point(
676  secondary_elem->point(n), secondary_elem->node_id(n), secondary_elem->processor_id()));
677  Node * const new_node = new_nodes.back();
678  new_node->set_unique_id(new_node->id() + node_unique_id_offset);
679  }
680 
681  std::unique_ptr<Elem> new_elem;
682  if (secondary_elem->default_order() == SECOND)
683  new_elem = std::make_unique<Edge3>();
684  else
685  new_elem = std::make_unique<Edge2>();
686 
687  new_elem->processor_id() = secondary_elem->processor_id();
688  new_elem->subdomain_id() = secondary_elem->subdomain_id();
689  new_elem->set_id(local_id_index++);
690  new_elem->set_unique_id(new_elem->id());
691 
692  for (MooseIndex(new_elem->n_nodes()) n = 0; n < new_elem->n_nodes(); ++n)
693  new_elem->set_node(n, new_nodes[n]);
694 
695  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  MortarSegmentInfo msinfo;
699  msinfo.xi1_a = -1;
700  msinfo.xi1_b = +1;
701  msinfo.secondary_elem = secondary_elem;
702 
703  auto new_container_it0 = _secondary_node_and_elem_to_xi2_primary_elem.find(
704  std::make_pair(secondary_elem->node_ptr(0), secondary_elem)),
705  new_container_it1 = _secondary_node_and_elem_to_xi2_primary_elem.find(
706  std::make_pair(secondary_elem->node_ptr(1), secondary_elem));
707 
708  bool new_container_node0_found =
709  (new_container_it0 != _secondary_node_and_elem_to_xi2_primary_elem.end()),
710  new_container_node1_found =
711  (new_container_it1 != _secondary_node_and_elem_to_xi2_primary_elem.end());
712 
713  const Elem * node0_primary_candidate = nullptr;
714  const Elem * node1_primary_candidate = nullptr;
715 
716  if (new_container_node0_found)
717  {
718  const auto & xi2_primary_elem_pair = new_container_it0->second;
719  msinfo.xi2_a = xi2_primary_elem_pair.first;
720  node0_primary_candidate = xi2_primary_elem_pair.second;
721  }
722 
723  if (new_container_node1_found)
724  {
725  const auto & xi2_primary_elem_pair = new_container_it1->second;
726  msinfo.xi2_b = xi2_primary_elem_pair.first;
727  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  if (node0_primary_candidate == node1_primary_candidate)
735  msinfo.primary_elem = node0_primary_candidate;
736 
737  // Associate this MSM elem with the MortarSegmentInfo.
738  _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  _secondary_elems_to_mortar_segments[secondary_elem->id()].insert(new_elem_ptr);
743  }
744 
745  // 2.) Insert new nodes from primary side and split mortar segments as necessary.
746  for (const auto & pr : _primary_node_and_elem_to_xi1_secondary_elem)
747  {
748  auto key = pr.first;
749  auto val = pr.second;
750 
751  const Node * primary_node = std::get<1>(key);
752  Real xi1 = val.first;
753  const Elem * secondary_elem = val.second;
754 
755  // If this is an aligned node, we don't need to do anything.
756  if (abs(abs(xi1) - 1.) < _xi_tolerance)
757  continue;
758 
759  auto && order = secondary_elem->default_order();
760 
761  // Determine physical location of new point to be inserted.
762  Point new_pt(0);
763  for (MooseIndex(secondary_elem->n_nodes()) n = 0; n < secondary_elem->n_nodes(); ++n)
764  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  auto & mortar_segment_set = _secondary_elems_to_mortar_segments[secondary_elem->id()];
768  Elem * current_mortar_segment = nullptr;
769  MortarSegmentInfo * info = nullptr;
770 
771  for (const auto & mortar_segment_candidate : mortar_segment_set)
772  {
773  try
774  {
775  info = &_msm_elem_to_info.at(mortar_segment_candidate);
776  }
777  catch (std::out_of_range &)
778  {
779  mooseError("MortarSegmentInfo not found for the mortar segment candidate");
780  }
781  if (info->xi1_a <= xi1 && xi1 <= info->xi1_b)
782  {
783  current_mortar_segment = mortar_segment_candidate;
784  break;
785  }
786  }
787 
788  // Make sure we found one.
789  if (current_mortar_segment == nullptr)
790  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  if (info->xi1_a == xi1 || xi1 == info->xi1_b)
798  continue;
799 
800  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  _mortar_segment_mesh->add_point(new_pt, new_id, secondary_elem->processor_id());
805  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  const Point normal = getNormals(*secondary_elem, std::vector<Real>({xi1}))[0];
811 
812  // Get the set of primary_node neighbors.
813  if (this->_nodes_to_primary_elem_map.find(primary_node->id()) ==
814  this->_nodes_to_primary_elem_map.end())
815  mooseError("We should already have built this primary node to elem pair!");
816  const std::vector<const Elem *> & primary_node_neighbors =
817  this->_nodes_to_primary_elem_map[primary_node->id()];
818 
819  // Sanity check
820  if (primary_node_neighbors.size() == 0 || primary_node_neighbors.size() > 2)
821  mooseError("We must have either 1 or 2 primary side nodal neighbors, but we had ",
822  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  const Elem * left_primary_elem = primary_node_neighbors[0];
830  const Elem * right_primary_elem =
831  (primary_node_neighbors.size() == 2) ? primary_node_neighbors[1] : nullptr;
832 
834 
835  // Storage for z-component of cross products for determining
836  // orientation.
837  std::array<Real, 2> secondary_node_cps;
838  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  for (unsigned int nid = 0; nid < 2; ++nid)
842  secondary_node_cps[nid] = normal.cross(secondary_elem->point(nid) - new_pt)(2);
843 
844  for (MooseIndex(primary_node_neighbors) mnn = 0; mnn < primary_node_neighbors.size(); ++mnn)
845  {
846  const Elem * primary_neigh = primary_node_neighbors[mnn];
847  Point opposite = (primary_neigh->node_ptr(0) == primary_node) ? primary_neigh->point(1)
848  : primary_neigh->point(0);
849  Point cp = normal.cross(opposite - new_pt);
850  primary_node_cps[mnn] = cp(2);
851  }
852 
853  // We will verify that only 1 orientation is actually valid.
854  bool orientation1_valid = false, orientation2_valid = false;
855 
856  if (primary_node_neighbors.size() == 2)
857  {
858  // 2 primary neighbor case
859  orientation1_valid = (secondary_node_cps[0] * primary_node_cps[0] > 0.) &&
860  (secondary_node_cps[1] * primary_node_cps[1] > 0.);
861 
862  orientation2_valid = (secondary_node_cps[0] * primary_node_cps[1] > 0.) &&
863  (secondary_node_cps[1] * primary_node_cps[0] > 0.);
864  }
865  else if (primary_node_neighbors.size() == 1)
866  {
867  // 1 primary neighbor case
868  orientation1_valid = (secondary_node_cps[0] * primary_node_cps[0] > 0.);
869  orientation2_valid = (secondary_node_cps[1] * primary_node_cps[0] > 0.);
870  }
871  else
872  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  if (orientation1_valid && orientation2_valid)
879  throw MooseException(
880  "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  if (!orientation1_valid && !orientation2_valid)
887  {
888  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  continue;
896  }
897 
898  // Make an Elem on the left
899  std::unique_ptr<Elem> new_elem_left;
900  if (order == SECOND)
901  new_elem_left = std::make_unique<Edge3>();
902  else
903  new_elem_left = std::make_unique<Edge2>();
904 
905  new_elem_left->processor_id() = current_mortar_segment->processor_id();
906  new_elem_left->subdomain_id() = current_mortar_segment->subdomain_id();
907  new_elem_left->set_id(local_id_index++);
908  new_elem_left->set_unique_id(new_elem_left->id());
909  new_elem_left->set_node(0, current_mortar_segment->node_ptr(0));
910  new_elem_left->set_node(1, new_node);
911 
912  // Make an Elem on the right
913  std::unique_ptr<Elem> new_elem_right;
914  if (order == SECOND)
915  new_elem_right = std::make_unique<Edge3>();
916  else
917  new_elem_right = std::make_unique<Edge2>();
918 
919  new_elem_right->processor_id() = current_mortar_segment->processor_id();
920  new_elem_right->subdomain_id() = current_mortar_segment->subdomain_id();
921  new_elem_right->set_id(local_id_index++);
922  new_elem_right->set_unique_id(new_elem_right->id());
923  new_elem_right->set_node(0, new_node);
924  new_elem_right->set_node(1, current_mortar_segment->node_ptr(1));
925 
926  if (order == SECOND)
927  {
928  // left
929  Point left_interior_point(0);
930  Real left_interior_xi = (xi1 + info->xi1_a) / 2;
931 
932  // This is eta for the current mortar segment that we're splitting
933  Real current_left_interior_eta =
934  (2. * left_interior_xi - info->xi1_a - info->xi1_b) / (info->xi1_b - info->xi1_a);
935 
936  for (MooseIndex(current_mortar_segment->n_nodes()) n = 0;
937  n < current_mortar_segment->n_nodes();
938  ++n)
939  left_interior_point += Moose::fe_lagrange_1D_shape(order, n, current_left_interior_eta) *
940  current_mortar_segment->point(n);
941 
942  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  Node * const new_interior_node_left = _mortar_segment_mesh->add_point(
946  left_interior_point, new_interior_left_id, new_elem_left->processor_id());
947  new_elem_left->set_node(2, new_interior_node_left);
948  new_interior_node_left->set_unique_id(new_interior_left_id + node_unique_id_offset);
949 
950  // right
951  Point right_interior_point(0);
952  Real right_interior_xi = (xi1 + info->xi1_b) / 2;
953  // This is eta for the current mortar segment that we're splitting
954  Real current_right_interior_eta =
955  (2. * right_interior_xi - info->xi1_a - info->xi1_b) / (info->xi1_b - info->xi1_a);
956 
957  for (MooseIndex(current_mortar_segment->n_nodes()) n = 0;
958  n < current_mortar_segment->n_nodes();
959  ++n)
960  right_interior_point += Moose::fe_lagrange_1D_shape(order, n, current_right_interior_eta) *
961  current_mortar_segment->point(n);
962 
963  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  Node * const new_interior_node_right = _mortar_segment_mesh->add_point(
967  right_interior_point, new_interior_id_right, new_elem_right->processor_id());
968  new_elem_right->set_node(2, new_interior_node_right);
969  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  if (orientation2_valid)
974  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  if (left_primary_elem)
979  left_xi2 = (primary_node == left_primary_elem->node_ptr(0)) ? -1 : +1;
980  if (right_primary_elem)
981  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  auto msm_it = _msm_elem_to_info.find(current_mortar_segment);
989  if (msm_it == _msm_elem_to_info.end())
990  mooseError("MortarSegmentInfo not found for current_mortar_segment.");
991  MortarSegmentInfo current_msinfo = msm_it->second;
992 
993  // add_left
994  {
995  Elem * msm_new_elem = _mortar_segment_mesh->add_elem(new_elem_left.release());
996 
997  // Create new MortarSegmentInfo objects for new_elem_left
998  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  new_msinfo_left.xi1_a = current_msinfo.xi1_a;
1004  new_msinfo_left.xi2_a = current_msinfo.xi2_a;
1005  new_msinfo_left.secondary_elem = secondary_elem;
1006  new_msinfo_left.xi1_b = xi1;
1007  new_msinfo_left.xi2_b = left_xi2;
1008  new_msinfo_left.primary_elem = left_primary_elem;
1009 
1010  // Add new msinfo objects to the map.
1011  _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  mortar_segment_set.insert(msm_new_elem);
1016  }
1017 
1018  // add_right
1019  {
1020  Elem * msm_new_elem = _mortar_segment_mesh->add_elem(new_elem_right.release());
1021 
1022  // Create new MortarSegmentInfo objects for new_elem_right
1023  MortarSegmentInfo new_msinfo_right;
1024 
1025  new_msinfo_right.xi1_b = current_msinfo.xi1_b;
1026  new_msinfo_right.xi2_b = current_msinfo.xi2_b;
1027  new_msinfo_right.secondary_elem = secondary_elem;
1028  new_msinfo_right.xi1_a = xi1;
1029  new_msinfo_right.xi2_a = right_xi2;
1030  new_msinfo_right.primary_elem = right_primary_elem;
1031 
1032  _msm_elem_to_info.emplace(msm_new_elem, new_msinfo_right);
1033 
1034  mortar_segment_set.insert(msm_new_elem);
1035  }
1036 
1037  // Erase the MortarSegmentInfo object for current_mortar_segment from the map.
1038  _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  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  _mortar_segment_mesh->delete_elem(current_mortar_segment);
1047  }
1048 
1049  // Remove all MSM elements without a primary contribution
1055  for (auto msm_elem : _mortar_segment_mesh->active_element_ptr_range())
1056  {
1057  MortarSegmentInfo & msinfo = libmesh_map_find(_msm_elem_to_info, msm_elem);
1058  Elem * primary_elem = const_cast<Elem *>(msinfo.primary_elem);
1059  if (primary_elem == nullptr || abs(msinfo.xi2_a) > 1.0 + TOLERANCE ||
1060  abs(msinfo.xi2_b) > 1.0 + TOLERANCE)
1061  {
1062  // Erase from secondary to msms map
1063  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  auto & msm_set = it->second;
1067  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  if (msm_set.empty())
1076 
1077  // Erase msinfo
1078  _msm_elem_to_info.erase(msm_elem);
1079 
1080  // Remove element from mortar segment mesh
1081  _mortar_segment_mesh->delete_elem(msm_elem);
1082  }
1083  else
1084  {
1087  }
1088  }
1089 
1090  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  for (const auto & element : _mortar_segment_mesh->element_ptr_range())
1095  for (auto & n : element->node_ref_range())
1096  msm_connected_nodes.insert(&n);
1097 
1098  for (const auto & node : _mortar_segment_mesh->node_ptr_range())
1099  if (!msm_connected_nodes.count(node))
1100  _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  _mortar_segment_mesh->cache_elem_data();
1114 
1115  // (Optionally) Write the mortar segment mesh to file for inspection
1116  if (_debug)
1117  outputMortarMesh();
1118 
1120 }
1121 
1122 void
1124 {
1125  ExodusII_IO mortar_segment_mesh_writer(*_mortar_segment_mesh);
1126 
1127  // Default to non-HDF5 output for wider compatibility
1128  mortar_segment_mesh_writer.set_hdf5_writing(false);
1129 
1130  std::array<std::string, 3> file_pieces = {
1131  _app.getOutputFileBase(/*for_non_moose_build_output=*/true),
1133  "mortar_segment_mesh.e"};
1134  mortar_segment_mesh_writer.write(MooseUtils::join(file_pieces, "_"));
1135 }
1136 
1137 void
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  auto secondary_sub_elem = _mortar_segment_mesh->add_elem_integer("secondary_sub_elem");
1143  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  if (!_msm_node_id_start.has_value())
1151  {
1152  dof_id_type local_secondary_sub_elems = 0, visible_primary_sub_elems = 0;
1153  for (const auto & [primary_sub_id, secondary_sub_id] : _primary_secondary_subdomain_id_pairs)
1154  {
1155  for (const auto * const el :
1157  local_secondary_sub_elems += el->n_sub_elem();
1158  for (const auto * const el : _mesh.active_subdomain_elements_ptr_range(primary_sub_id))
1159  visible_primary_sub_elems += el->n_sub_elem();
1160  }
1161  const dof_id_type per_rank_bound = local_secondary_sub_elems * visible_primary_sub_elems * 9;
1162  std::vector<dof_id_type> per_rank_bounds;
1163  _mesh.comm().allgather(per_rank_bound, per_rank_bounds);
1164  dof_id_type start = 0;
1165  for (const auto r : make_range(_mesh.processor_id()))
1166  start += per_rank_bounds[r];
1167  _msm_node_id_start = start;
1168  }
1169  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  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  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1177  {
1178  const auto primary_subd_id = pr.first;
1179  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  NanoflannMeshSubdomainAdaptor<3> mesh_adaptor(_mesh, primary_subd_id);
1183  subdomain_kd_tree_t kd_tree(
1184  3, mesh_adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(/*max leaf=*/10));
1185 
1186  // Construct the KD tree.
1187  kd_tree.buildIndex();
1188 
1189  // Define expression for getting sub-elements nodes (for sub-dividing secondary and primary
1190  // elements)
1191  auto get_sub_elem_nodes = [](const ElemType type,
1192  const unsigned int sub_elem) -> std::vector<unsigned int>
1193  {
1194  switch (type)
1195  {
1196  case TRI3:
1197  return {{0, 1, 2}};
1198  case QUAD4:
1199  return {{0, 1, 2, 3}};
1200  case TRI6:
1201  case TRI7:
1202  switch (sub_elem)
1203  {
1204  case 0:
1205  return {{0, 3, 5}};
1206  case 1:
1207  return {{3, 4, 5}};
1208  case 2:
1209  return {{3, 1, 4}};
1210  case 3:
1211  return {{5, 4, 2}};
1212  default:
1213  mooseError("get_sub_elem_nodes: Invalid sub_elem: ", sub_elem);
1214  }
1215  case QUAD8:
1216  switch (sub_elem)
1217  {
1218  case 0:
1219  return {{0, 4, 7}};
1220  case 1:
1221  return {{4, 1, 5}};
1222  case 2:
1223  return {{5, 2, 6}};
1224  case 3:
1225  return {{7, 6, 3}};
1226  case 4:
1227  return {{4, 5, 6, 7}};
1228  default:
1229  mooseError("get_sub_elem_nodes: Invalid sub_elem: ", sub_elem);
1230  }
1231  case QUAD9:
1232  switch (sub_elem)
1233  {
1234  case 0:
1235  return {{0, 4, 8, 7}};
1236  case 1:
1237  return {{4, 1, 5, 8}};
1238  case 2:
1239  return {{8, 5, 2, 6}};
1240  case 3:
1241  return {{7, 8, 6, 3}};
1242  default:
1243  mooseError("get_sub_elem_nodes: Invalid sub_elem: ", sub_elem);
1244  }
1245  default:
1246  mooseError("get_sub_elem_inds: Face element type: ",
1247  libMesh::Utility::enum_to_string<ElemType>(type),
1248  " invalid for 3D mortar");
1249  }
1250  };
1251 
1255  for (MeshBase::const_element_iterator el = _mesh.active_local_elements_begin(),
1256  end_el = _mesh.active_local_elements_end();
1257  el != end_el;
1258  ++el)
1259  {
1260  const Elem * secondary_side_elem = *el;
1261 
1262  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  if (secondary_side_elem->subdomain_id() != secondary_subd_id)
1266  continue;
1267 
1268  auto [secondary_elem_to_msm_map_it, insertion_happened] =
1269  _secondary_elems_to_mortar_segments.emplace(secondary_side_elem->id(),
1270  std::set<Elem *, CompareDofObjectsByID>{});
1271  libmesh_ignore(insertion_happened);
1272  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  secondary_side_elem->n_sub_elem());
1276  const auto nodal_normals = getNodalNormals(*secondary_side_elem);
1277 
1288  for (auto sel : make_range(secondary_side_elem->n_sub_elem()))
1289  {
1290  // Get indices of sub-element nodes in element
1291  auto sub_elem_nodes = get_sub_elem_nodes(secondary_side_elem->type(), sel);
1292 
1293  // Secondary sub-element center, normal, and nodes
1294  Point center;
1295  Point normal;
1296  std::vector<Point> nodes(sub_elem_nodes.size());
1297 
1298  // Loop through sub_element nodes, collect points and compute center and normal
1299  for (auto iv : make_range(sub_elem_nodes.size()))
1300  {
1301  const auto n = sub_elem_nodes[iv];
1302  nodes[iv] = secondary_side_elem->point(n);
1303  center += secondary_side_elem->point(n);
1304  normal += nodal_normals[n];
1305  }
1306  center /= sub_elem_nodes.size();
1307  normal = normal.unit();
1308 
1309  // Build and store linearized sub-elements for later use
1310  mortar_segment_helper[sel] = std::make_unique<MortarSegmentHelper>(
1311  nodes, center, normal, _triangulation_mode, _triangulate_triangles);
1312  }
1313 
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  Point center_point;
1324  switch (secondary_side_elem->type())
1325  {
1326  case TRI3:
1327  case QUAD4:
1328  center_point = mortar_segment_helper[0]->center();
1329  query_pt = {{center_point(0), center_point(1), center_point(2)}};
1330  break;
1331  case TRI6:
1332  case TRI7:
1333  center_point = mortar_segment_helper[1]->center();
1334  query_pt = {{center_point(0), center_point(1), center_point(2)}};
1335  break;
1336  case QUAD8:
1337  center_point = mortar_segment_helper[4]->center();
1338  query_pt = {{center_point(0), center_point(1), center_point(2)}};
1339  break;
1340  case QUAD9:
1341  center_point = secondary_side_elem->point(8);
1342  query_pt = {{center_point(0), center_point(1), center_point(2)}};
1343  break;
1344  default:
1345  mooseError(
1346  "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  const std::size_t num_results = 3;
1353 
1354  // Initialize result_set and do the search.
1355  std::vector<size_t> ret_index(num_results);
1356  std::vector<Real> out_dist_sqr(num_results);
1357  nanoflann::KNNResultSet<Real> result_set(num_results);
1358  result_set.init(&ret_index[0], &out_dist_sqr[0]);
1359  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  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  bool primary_elem_found = false;
1367  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  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  this->_nodes_to_primary_elem_map.at(static_cast<dof_id_type>(ret_index[r]));
1380 
1381  // Uniquely add elems to candidate set
1382  for (auto elem : node_elems)
1383  primary_elem_candidates.insert(elem);
1384  }
1385 
1393  while (!primary_elem_candidates.empty())
1394  {
1395  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  if (processed_primary_elems.count(primary_elem_candidate))
1399  continue;
1400 
1401  // Initialize set of nodes used to construct mortar segment elements
1402  std::vector<Point> nodal_points;
1403 
1404  // Initialize map from mortar segment elements to nodes
1405  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  std::vector<std::pair<unsigned int, unsigned int>> sub_elem_map;
1409 
1414  for (auto p_el : make_range(primary_elem_candidate->n_sub_elem()))
1415  {
1416  // Get nodes of primary sub-elements
1417  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  std::vector<Point> primary_sub_elem(sub_elem_nodes.size());
1421  for (auto iv : make_range(sub_elem_nodes.size()))
1422  {
1423  const auto n = sub_elem_nodes[iv];
1424  primary_sub_elem[iv] = primary_elem_candidate->point(n);
1425  }
1426 
1427  // Loop through secondary sub-elements
1428  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  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  for (auto i = sub_elem_map.size(); i < elem_to_node_map.size(); ++i)
1442  sub_elem_map.push_back(std::make_pair(s_el, p_el));
1443  }
1444  }
1445 
1446  // Mark primary element as processed and remove from candidate list
1447  processed_primary_elems.insert(primary_elem_candidate);
1448  primary_elem_candidates.erase(primary_elem_candidate);
1449 
1450  // If overlap of polygons was non-trivial (created mortar segment elements)
1451  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  if (!primary_elem_found)
1457  {
1458  primary_elem_found = true;
1459  primary_elem_candidates.clear();
1460  }
1461 
1462  // Add neighbors to candidate list
1463  for (auto neighbor : primary_elem_candidate->neighbor_ptr_range())
1464  {
1465  // If not valid or not on lower dimensional secondary subdomain, skip
1466  if (neighbor == nullptr || neighbor->subdomain_id() != primary_subd_id)
1467  continue;
1468  // If already processed, skip
1469  if (processed_primary_elems.count(neighbor))
1470  continue;
1471  // Otherwise, add to candidates
1472  primary_elem_candidates.insert(neighbor);
1473  }
1474 
1478  std::vector<Node *> new_nodes;
1479  for (auto pt : nodal_points)
1480  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  for (auto el : index_range(elem_to_node_map))
1485  {
1486  // Create new triangular element
1487  std::unique_ptr<Elem> new_elem;
1488  if (elem_to_node_map[el].size() == 3)
1489  new_elem = std::make_unique<Tri3>();
1490  else
1491  mooseError("Active mortar segments only supports TRI elements, 3 nodes expected "
1492  "but: ",
1493  elem_to_node_map[el].size(),
1494  " provided.");
1495 
1496  new_elem->processor_id() = secondary_side_elem->processor_id();
1497  new_elem->subdomain_id() = secondary_side_elem->subdomain_id();
1498  new_elem->set_id(next_elem_id++);
1499 
1500  // Attach newly created nodes
1501  for (auto i : index_range(elem_to_node_map[el]))
1502  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  if (new_elem->volume() / secondary_volume < TOLERANCE)
1506  continue;
1507 
1508  // Add elements to mortar segment mesh
1509  Elem * msm_new_elem = _mortar_segment_mesh->add_elem(new_elem.release());
1510 
1511  msm_new_elem->set_extra_integer(secondary_sub_elem, sub_elem_map[el].first);
1512  msm_new_elem->set_extra_integer(primary_sub_elem, sub_elem_map[el].second);
1513 
1514  // Fill out mortar segment info
1515  MortarSegmentInfo msinfo;
1516  msinfo.secondary_elem = secondary_side_elem;
1517  msinfo.primary_elem = primary_elem_candidate;
1518 
1519  // Associate this MSM elem with the MortarSegmentInfo.
1520  _msm_elem_to_info.emplace(msm_new_elem, msinfo);
1521 
1522  // Add this mortar segment to the secondary elem to mortar segment map
1523  secondary_to_msm_element_set.insert(msm_new_elem);
1524 
1526  // Unlike for 2D, we always have a primary when building the mortar mesh so we don't
1527  // have to check for null
1529  }
1530  }
1531  // End loop through primary element candidates
1532  }
1533 
1534  for (auto sel : make_range(secondary_side_elem->n_sub_elem()))
1535  {
1536  // Check if any segments failed to project
1537  if (mortar_segment_helper[sel]->remainder() == 1.0)
1538  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  if (secondary_to_msm_element_set.empty())
1546  _secondary_elems_to_mortar_segments.erase(secondary_elem_to_msm_map_it);
1547  } // End loop through secondary elements
1548  } // End loop through mortar constraint pairs
1549 
1550  _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  _mortar_segment_mesh->set_distributed();
1555 
1556  // Output mortar segment mesh
1557  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  for (const auto msm_el : _mortar_segment_mesh->active_local_element_ptr_range())
1562  if (msm_el->type() != TRI3)
1563  msm_el->subdomain_id()++;
1564 
1565  outputMortarMesh();
1566 
1567  // Undo increment
1568  for (const auto msm_el : _mortar_segment_mesh->active_local_element_ptr_range())
1569  if (msm_el->type() != TRI3)
1570  msm_el->subdomain_id()--;
1571  }
1572 
1574 
1575  // Print mortar segment mesh statistics
1576  if (_debug)
1577  {
1578  msmStatistics();
1579  }
1580 }
1581 
1582 void
1584 {
1585  std::unordered_map<processor_id_type, std::vector<std::pair<dof_id_type, dof_id_type>>>
1586  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  for (const auto & pr : _msm_elem_to_info)
1595  {
1596  const Elem * secondary_elem = pr.second.secondary_elem;
1597  const Elem * primary_elem = pr.second.primary_elem;
1598 
1599  // LowerSecondary
1600  coupling_info[secondary_elem->processor_id()].emplace_back(
1601  secondary_elem->id(), secondary_elem->interior_parent()->id());
1602  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  _mortar_interface_coupling[secondary_elem->id()].insert(
1606  secondary_elem->interior_parent()->id());
1607 
1608  // LowerPrimary
1609  coupling_info[secondary_elem->processor_id()].emplace_back(
1610  secondary_elem->id(), primary_elem->interior_parent()->id());
1611  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  _mortar_interface_coupling[secondary_elem->id()].insert(
1615  primary_elem->interior_parent()->id());
1616 
1617  // Lower-LowerDimensionalPrimary
1618  coupling_info[secondary_elem->processor_id()].emplace_back(secondary_elem->id(),
1619  primary_elem->id());
1620  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  _mortar_interface_coupling[secondary_elem->id()].insert(primary_elem->id());
1624 
1625  // SecondaryLower
1626  coupling_info[secondary_elem->interior_parent()->processor_id()].emplace_back(
1627  secondary_elem->interior_parent()->id(), secondary_elem->id());
1628 
1629  // SecondaryPrimary
1630  coupling_info[secondary_elem->interior_parent()->processor_id()].emplace_back(
1631  secondary_elem->interior_parent()->id(), primary_elem->interior_parent()->id());
1632 
1633  // PrimaryLower
1634  coupling_info[primary_elem->interior_parent()->processor_id()].emplace_back(
1635  primary_elem->interior_parent()->id(), secondary_elem->id());
1636 
1637  // PrimarySecondary
1638  coupling_info[primary_elem->interior_parent()->processor_id()].emplace_back(
1639  primary_elem->interior_parent()->id(), secondary_elem->interior_parent()->id());
1640  }
1641 
1642  // Push the coupling information
1643  auto action_functor =
1644  [this](processor_id_type,
1645  const std::vector<std::pair<dof_id_type, dof_id_type>> & coupling_info)
1646  {
1647  for (auto [i, j] : coupling_info)
1648  _mortar_interface_coupling[i].insert(j);
1649  };
1650  TIMPI::push_parallel_vector_data(_mesh.comm(), coupling_info, action_functor);
1651 }
1652 
1653 std::vector<AutomaticMortarGeneration::MsmSubdomainStats>
1655 {
1656  std::vector<MsmSubdomainStats> result;
1657  StatisticsVector<Real> primary;
1658  StatisticsVector<Real> secondary;
1660  std::unordered_map<dof_id_type, Real> primary_elems_to_volume;
1661 
1662  for (const auto & [primary_subd_id, secondary_subd_id] : _primary_secondary_subdomain_id_pairs)
1663  {
1664  for (const auto * const secondary_el :
1665  _mesh.active_local_subdomain_element_ptr_range(secondary_subd_id))
1666  {
1667  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  if (auto it = _secondary_elems_to_mortar_segments.find(secondary_el->id());
1672  for (const auto * const msm_elem : it->second)
1673  {
1674  msm.push_back(msm_elem->volume());
1675  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  if (msm_info.primary_elem)
1679  {
1680  if (msm_info.primary_elem->subdomain_id() != primary_subd_id)
1681  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  if (const auto [it, inserted] =
1686  primary_elems_to_volume.emplace(msm_info.primary_elem->id(), Real{});
1687  inserted)
1688  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  }
1696 
1697  _mesh.comm().set_union(primary_elems_to_volume);
1698  _mesh.comm().allgather(static_cast<std::vector<Real> &>(secondary));
1699  _mesh.comm().allgather(static_cast<std::vector<Real> &>(msm));
1700  primary.reserve(primary_elems_to_volume.size());
1701  for (const auto [_, volume] : primary_elems_to_volume)
1702  primary.push_back(volume);
1703 
1704  MsmSubdomainStats stats;
1705  stats.primary_subd_id = primary_subd_id;
1706  stats.secondary_subd_id = secondary_subd_id;
1707  stats.secondary_lower_n_elems = secondary.size();
1708  stats.secondary_lower_max_volume = secondary.maximum();
1709  stats.secondary_lower_min_volume = secondary.minimum();
1710  stats.secondary_lower_median_volume = secondary.median();
1711  stats.primary_lower_n_elems = primary.size();
1712  stats.primary_lower_max_volume = primary.maximum();
1713  stats.primary_lower_min_volume = primary.minimum();
1714  stats.primary_lower_median_volume = primary.median();
1715  stats.msm_n_elems = msm.size();
1716  stats.msm_max_volume = msm.maximum();
1717  stats.msm_min_volume = msm.minimum();
1718  stats.msm_median_volume = msm.median();
1719  result.push_back(stats);
1720 
1721  primary.clear();
1722  secondary.clear();
1723  msm.clear();
1724  primary_elems_to_volume.clear();
1725  }
1726 
1727  return result;
1728 }
1729 
1730 void
1732 {
1733  const auto all_stats = computeMsmStatistics();
1734 
1735  if (_mesh.processor_id() != 0)
1736  return;
1737 
1738  Moose::out << "Mortar Interface Statistics:" << std::endl;
1739  for (const auto & stats : all_stats)
1740  {
1741  std::vector<std::string> col_names = {"mesh", "n_elems", "max", "min", "median"};
1742  std::vector<std::string> subds = {"secondary_lower", "primary_lower", "mortar_segment"};
1743  std::vector<size_t> n_elems = {
1744  stats.secondary_lower_n_elems, stats.primary_lower_n_elems, stats.msm_n_elems};
1745  std::vector<Real> maxs = {
1746  stats.secondary_lower_max_volume, stats.primary_lower_max_volume, stats.msm_max_volume};
1747  std::vector<Real> mins = {
1748  stats.secondary_lower_min_volume, stats.primary_lower_min_volume, stats.msm_min_volume};
1749  std::vector<Real> medians = {stats.secondary_lower_median_volume,
1750  stats.primary_lower_median_volume,
1751  stats.msm_median_volume};
1752 
1753  FormattedTable table;
1754  table.clear();
1755  for (auto i : index_range(subds))
1756  {
1757  table.addRow(i);
1758  table.addData<std::string>(col_names[0], subds[i]);
1759  table.addData<size_t>(col_names[1], n_elems[i]);
1760  table.addData<Real>(col_names[2], maxs[i]);
1761  table.addData<Real>(col_names[3], mins[i]);
1762  table.addData<Real>(col_names[4], medians[i]);
1763  }
1764 
1765  Moose::out << "secondary subdomain: " << stats.secondary_subd_id
1766  << " \tprimary subdomain: " << stats.primary_subd_id << std::endl;
1767  table.printTable(Moose::out, subds.size());
1768  }
1769 }
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
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  const Real tol = (dim() == 3) ? 0.1 : TOLERANCE;
1784 
1785  std::unordered_map<processor_id_type, std::set<dof_id_type>> proc_to_inactive_nodes_set;
1786  const auto my_pid = _mesh.processor_id();
1787 
1788  // List of inactive nodes on local secondary elements
1789  std::unordered_set<dof_id_type> inactive_node_ids;
1790 
1791  std::unordered_map<const Elem *, Real> active_volume{};
1792 
1793  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1794  for (const auto el : _mesh.active_subdomain_elements_ptr_range(pr.second))
1795  active_volume[el] = 0.;
1796 
1797  // Compute fraction of elements with corresponding primary elements
1798  for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1799  {
1800  const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
1801  const Elem * secondary_elem = msinfo.secondary_elem;
1802 
1803  active_volume[secondary_elem] += msm_elem->volume();
1804  }
1805 
1806  // Mark all inactive local nodes
1807  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1808  // Loop through all elements on my processor
1809  for (const auto el : _mesh.active_local_subdomain_elements_ptr_range(pr.second))
1810  // If elem fully or partially dropped
1811  if (abs(active_volume[el] / el->volume() - 1.0) > tol)
1812  {
1813  // Add all nodes to list of inactive
1814  for (auto n : make_range(el->n_nodes()))
1815  inactive_node_ids.insert(el->node_id(n));
1816  }
1817 
1818  // Assemble list of procs that nodes contribute to
1819  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1820  {
1821  const auto secondary_subd_id = pr.second;
1822 
1823  // Loop through all elements not on my processor
1824  for (const auto el : _mesh.active_subdomain_elements_ptr_range(secondary_subd_id))
1825  {
1826  // Get processor_id
1827  const auto pid = el->processor_id();
1828 
1829  // If element is in my subdomain, skip
1830  if (pid == my_pid)
1831  continue;
1832 
1833  // If element on proc pid shares any of my inactive nodes, mark to send
1834  for (const auto n : make_range(el->n_nodes()))
1835  {
1836  const auto node_id = el->node_id(n);
1837  if (inactive_node_ids.find(node_id) != inactive_node_ids.end())
1838  proc_to_inactive_nodes_set[pid].insert(node_id);
1839  }
1840  }
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  std::unordered_map<processor_id_type, std::vector<dof_id_type>> proc_to_inactive_nodes_vector;
1847  for (const auto & proc_set : proc_to_inactive_nodes_set)
1848  proc_to_inactive_nodes_vector[proc_set.first].insert(
1849  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  auto action_functor = [this, &inactive_node_ids](const processor_id_type pid,
1855  const std::vector<dof_id_type> & sent_data)
1856  {
1857  if (pid == _mesh.processor_id())
1858  mooseError("Should not be communicating with self.");
1859  for (const auto pr : sent_data)
1860  inactive_node_ids.insert(pr);
1861  };
1862  TIMPI::push_parallel_vector_data(_mesh.comm(), proc_to_inactive_nodes_vector, action_functor);
1863  }
1864  _inactive_local_lm_nodes.clear();
1865  for (const auto node_id : inactive_node_ids)
1866  _inactive_local_lm_nodes.insert(_mesh.node_ptr(node_id));
1867 }
1868 
1869 void
1871 {
1873  {
1875  return;
1876  }
1877 
1878  std::unordered_map<processor_id_type, std::set<dof_id_type>> proc_to_active_nodes_set;
1879  const auto my_pid = _mesh.processor_id();
1880 
1881  // List of active nodes on local secondary elements
1882  std::unordered_set<dof_id_type> active_local_nodes;
1883 
1884  // Mark all active local nodes
1885  for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1886  {
1887  const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
1888  const Elem * secondary_elem = msinfo.secondary_elem;
1889 
1890  for (auto n : make_range(secondary_elem->n_nodes()))
1891  active_local_nodes.insert(secondary_elem->node_id(n));
1892  }
1893 
1894  // Assemble list of procs that nodes contribute to
1895  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1896  {
1897  const auto secondary_subd_id = pr.second;
1898 
1899  // Loop through all elements not on my processor
1900  for (const auto el : _mesh.active_subdomain_elements_ptr_range(secondary_subd_id))
1901  {
1902  // Get processor_id
1903  const auto pid = el->processor_id();
1904 
1905  // If element is in my subdomain, skip
1906  if (pid == my_pid)
1907  continue;
1908 
1909  // If element on proc pid shares any of my active nodes, mark to send
1910  for (const auto n : make_range(el->n_nodes()))
1911  {
1912  const auto node_id = el->node_id(n);
1913  if (active_local_nodes.find(node_id) != active_local_nodes.end())
1914  proc_to_active_nodes_set[pid].insert(node_id);
1915  }
1916  }
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  std::unordered_map<processor_id_type, std::vector<dof_id_type>> proc_to_active_nodes_vector;
1923  for (const auto & proc_set : proc_to_active_nodes_set)
1924  {
1925  proc_to_active_nodes_vector[proc_set.first].reserve(proc_to_active_nodes_set.size());
1926  for (const auto node_id : proc_set.second)
1927  proc_to_active_nodes_vector[proc_set.first].push_back(node_id);
1928  }
1929 
1930  // First push data
1931  auto action_functor = [this, &active_local_nodes](const processor_id_type pid,
1932  const std::vector<dof_id_type> & sent_data)
1933  {
1934  if (pid == _mesh.processor_id())
1935  mooseError("Should not be communicating with self.");
1936  active_local_nodes.insert(sent_data.begin(), sent_data.end());
1937  };
1938  TIMPI::push_parallel_vector_data(_mesh.comm(), proc_to_active_nodes_vector, action_functor);
1939  }
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  _inactive_local_lm_nodes.clear();
1944  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1945  for (const auto el : _mesh.active_local_subdomain_elements_ptr_range(
1946  /*secondary_subd_id*/ pr.second))
1947  for (const auto n : make_range(el->n_nodes()))
1948  if (active_local_nodes.find(el->node_id(n)) == active_local_nodes.end())
1949  _inactive_local_lm_nodes.insert(el->node_ptr(n));
1950 }
1951 
1952 // Note: could be combined with previous routine, keeping separate for clarity (for now)
1953 void
1955 {
1956  // Mark all active secondary elements
1957  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  const Real tol = (dim() == 3) ? 0.1 : TOLERANCE;
1964 
1965  std::unordered_map<const Elem *, Real> active_volume;
1966 
1967  // Compute fraction of elements with corresponding primary elements
1969  for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1970  {
1971  const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
1972  const Elem * secondary_elem = msinfo.secondary_elem;
1973 
1974  active_volume[secondary_elem] += msm_elem->volume();
1975  }
1976  //****
1977 
1978  for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1979  {
1980  const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
1981  const Elem * secondary_elem = msinfo.secondary_elem;
1982 
1983  //****
1985  if (abs(active_volume[secondary_elem] / secondary_elem->volume() - 1.0) > tol)
1986  continue;
1987  //****
1988 
1989  active_local_elems.insert(secondary_elem);
1990  }
1991 
1992  // Take complement of active elements in active local subdomain to get inactive local elements
1993  _inactive_local_lm_elems.clear();
1994  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1995  for (const auto el : _mesh.active_local_subdomain_elements_ptr_range(
1996  /*secondary_subd_id*/ pr.second))
1997  if (active_local_elems.find(el) == active_local_elems.end())
1998  _inactive_local_lm_elems.insert(el);
1999 }
2000 
2001 void
2003 {
2004  // The dimension according to Mesh::mesh_dimension().
2005  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  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  std::map<dof_id_type, std::vector<std::pair<Point, Real>>> node_to_normals_map;
2019 
2021  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  for (MeshBase::const_element_iterator el = _mesh.active_elements_begin(),
2027  end_el = _mesh.active_elements_end();
2028  el != end_el;
2029  ++el)
2030  {
2031  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  if (!_secondary_boundary_subdomain_ids.count(secondary_elem->subdomain_id()))
2035  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  FEType nnx_fe_type(secondary_elem->default_order(), LAGRANGE);
2040  std::unique_ptr<FEBase> nnx_fe_face(FEBase::build(dim, nnx_fe_type));
2041  nnx_fe_face->attach_quadrature_rule(&qface);
2042  const auto & face_normals = nnx_fe_face->get_normals();
2043  const auto & face_points = nnx_fe_face->get_xyz();
2044 
2045  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  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  _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  auto s = interior_parent->which_side_am_i(secondary_elem);
2062 
2063  // Reinit the face FE object on side s.
2064  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  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  for (const auto qp : make_range(face_points.size()))
2076  {
2077  const auto n = qpoint_to_secondary_node[qp];
2078  auto & normals_and_weights_vec = node_to_normals_map[secondary_elem->node_id(n)];
2079  normals_and_weights_vec.push_back(std::make_pair(sign * face_normals[qp], JxW[qp]));
2080  }
2081  }
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  for (const auto & pr : node_to_normals_map)
2087  {
2088  // Compute normal vector
2089  const auto & node_id = pr.first;
2090  const auto & normals_and_weights_vec = pr.second;
2091 
2092  Point nodal_normal;
2093  for (const auto & norm_and_weight : normals_and_weights_vec)
2094  nodal_normal += norm_and_weight.first * norm_and_weight.second;
2095  nodal_normal = nodal_normal.unit();
2096 
2097  _secondary_node_to_nodal_normal[_mesh.node_ptr(node_id)] = nodal_normal;
2098 
2099  Point nodal_tangent_one;
2100  Point nodal_tangent_two;
2101  householderOrthogolization(nodal_normal, nodal_tangent_one, nodal_tangent_two);
2102 
2103  _secondary_node_to_hh_nodal_tangents[_mesh.node_ptr(node_id)][0] = nodal_tangent_one;
2104  _secondary_node_to_hh_nodal_tangents[_mesh.node_ptr(node_id)][1] = nodal_tangent_two;
2105  }
2106 }
2107 
2108 void
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  const Real nx = nodal_normal(0);
2119  const Real ny = nodal_normal(1);
2120  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  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  if (abs(h_vector(0)) < TOLERANCE)
2131  {
2132  nodal_tangent_one(0) = 0;
2133  nodal_tangent_one(1) = 1;
2134  nodal_tangent_one(2) = 0;
2135 
2136  nodal_tangent_two(0) = 0;
2137  nodal_tangent_two(1) = 0;
2138  nodal_tangent_two(2) = -1;
2139 
2140  return;
2141  }
2142 
2143  const Real h = h_vector.norm();
2144 
2145  nodal_tangent_one(0) = -2.0 * h_vector(0) * h_vector(1) / (h * h);
2146  nodal_tangent_one(1) = 1.0 - 2.0 * h_vector(1) * h_vector(1) / (h * h);
2147  nodal_tangent_one(2) = -2.0 * h_vector(1) * h_vector(2) / (h * h);
2148 
2149  nodal_tangent_two(0) = -2.0 * h_vector(0) * h_vector(2) / (h * h);
2150  nodal_tangent_two(1) = -2.0 * h_vector(1) * h_vector(2) / (h * h);
2151  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
2158 {
2159  // For each primary/secondary boundary id pair, call the
2160  // project_secondary_nodes_single_pair() helper function.
2161  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
2162  projectSecondaryNodesSinglePair(pr.first, pr.second);
2163 }
2164 
2165 bool
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  if (!secondary_node_neighbors)
2176  secondary_node_neighbors = &libmesh_map_find(_nodes_to_secondary_elem_map, secondary_node.id());
2177  if (!primary_node_neighbors)
2178  primary_node_neighbors = &libmesh_map_find(_nodes_to_primary_elem_map, primary_node.id());
2179 
2180  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  for (const auto nn : index_range(*secondary_node_neighbors))
2211  {
2212  const Elem * const secondary_neigh = (*secondary_node_neighbors)[nn];
2213  const Point opposite = (secondary_neigh->node_ptr(0) == &secondary_node)
2214  ? secondary_neigh->point(1)
2215  : secondary_neigh->point(0);
2216  const Point cp = nodal_normal.cross(opposite - secondary_node);
2217  secondary_node_neighbor_cps[nn] = cp(2);
2218  }
2219 
2220  for (const auto nn : index_range(*primary_node_neighbors))
2221  {
2222  const Elem * const primary_neigh = (*primary_node_neighbors)[nn];
2223  const Point opposite = (primary_neigh->node_ptr(0) == &primary_node) ? primary_neigh->point(1)
2224  : primary_neigh->point(0);
2225  const Point cp = nodal_normal.cross(opposite - primary_node);
2226  primary_node_neighbor_cps[nn] = cp(2);
2227  }
2228 
2229  // Associate secondary/primary elems on matching sides.
2230  bool found_match = false;
2231  for (const auto snn : index_range(*secondary_node_neighbors))
2232  for (const auto mnn : index_range(*primary_node_neighbors))
2233  if (secondary_node_neighbor_cps[snn] * primary_node_neighbor_cps[mnn] > 0)
2234  {
2235  found_match = true;
2236  if (primary_elems_mapped[mnn])
2237  continue;
2238  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  const Real xi2 = (&primary_node == (*primary_node_neighbors)[mnn]->node_ptr(0)) ? -1 : +1;
2243  const auto secondary_key =
2244  std::make_pair(&secondary_node, (*secondary_node_neighbors)[snn]);
2245  const auto primary_val = std::make_pair(xi2, (*primary_node_neighbors)[mnn]);
2246  _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  (&secondary_node == (*secondary_node_neighbors)[snn]->node_ptr(0)) ? -1 : +1;
2251 
2252  const auto primary_key =
2253  std::make_tuple(primary_node.id(), &primary_node, (*primary_node_neighbors)[mnn]);
2254  const auto secondary_val = std::make_pair(xi1, (*secondary_node_neighbors)[snn]);
2255  _primary_node_and_elem_to_xi1_secondary_elem.emplace(primary_key, secondary_val);
2256  }
2257 
2258  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  rejected_elem_candidates.insert(&candidate_element);
2263  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  if (secondary_node_neighbors->size() == 1 && primary_node_neighbors->size() == 2)
2270  for (const auto i : index_range(primary_elems_mapped))
2271  if (!primary_elems_mapped[i])
2272  {
2274  std::make_tuple(primary_node.id(), &primary_node, (*primary_node_neighbors)[i]),
2275  std::make_pair(1, nullptr));
2276  }
2277 
2278  return found_match;
2279 }
2280 
2281 void
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  NanoflannMeshSubdomainAdaptor<3> mesh_adaptor(_mesh, lower_dimensional_primary_subdomain_id);
2290  subdomain_kd_tree_t kd_tree(
2291  3, mesh_adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(/*max leaf=*/10));
2292 
2293  // Construct the KD tree.
2294  kd_tree.buildIndex();
2295 
2296  for (MeshBase::const_element_iterator el = _mesh.active_elements_begin(),
2297  end_el = _mesh.active_elements_end();
2298  el != end_el;
2299  ++el)
2300  {
2301  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  if (secondary_side_elem->subdomain_id() != lower_dimensional_secondary_subdomain_id)
2305  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  for (MooseIndex(secondary_side_elem->n_vertices()) n = 0; n < secondary_side_elem->n_vertices();
2312  ++n)
2313  {
2314  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  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  bool is_mapped = true;
2324  for (MooseIndex(secondary_node_neighbors) snn = 0; snn < secondary_node_neighbors.size();
2325  ++snn)
2326  {
2327  auto secondary_key = std::make_pair(secondary_node, secondary_node_neighbors[snn]);
2328  if (!_secondary_node_and_elem_to_xi2_primary_elem.count(secondary_key))
2329  {
2330  is_mapped = false;
2331  break;
2332  }
2333  }
2334 
2335  // Go to the next node if this one has already been mapped.
2336  if (is_mapped)
2337  continue;
2338 
2339  // Look up the new nodal normal value in the local storage, error if not found.
2340  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  {(*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  const std::size_t num_results = 3;
2351 
2352  // Initialize result_set and do the search.
2353  std::vector<size_t> ret_index(num_results);
2354  std::vector<Real> out_dist_sqr(num_results);
2355  nanoflann::KNNResultSet<Real> result_set(num_results);
2356  result_set.init(&ret_index[0], &out_dist_sqr[0]);
2357  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  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  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  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  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  for (MooseIndex(primary_elem_candidates) e = 0; e < primary_elem_candidates.size(); ++e)
2384  {
2385  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  if (rejected_primary_elem_candidates.count(primary_elem_candidate))
2389  continue;
2390 
2391  // Now generically solve for xi2
2392  const auto order = primary_elem_candidate->default_order();
2393  DualNumber<Real> xi2_dn{0, 1};
2394  unsigned int current_iterate = 0, max_iterates = 10;
2395 
2396  // Newton loop
2397  do
2398  {
2400  for (MooseIndex(primary_elem_candidate->n_nodes()) n = 0;
2401  n < primary_elem_candidate->n_nodes();
2402  ++n)
2403  x2 +=
2404  Moose::fe_lagrange_1D_shape(order, n, xi2_dn) * primary_elem_candidate->point(n);
2405  const auto u = x2 - (*secondary_node);
2406  const auto F = u(0) * nodal_normal(1) - u(1) * nodal_normal(0);
2407 
2408  if (abs(F) < _newton_tolerance)
2409  break;
2410 
2411  if (F.derivatives())
2412  {
2413  Real dxi2 = -F.value() / F.derivatives();
2414 
2415  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  current_iterate = max_iterates;
2422  } while (++current_iterate < max_iterates);
2423 
2424  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  if ((current_iterate < max_iterates) && (std::abs(xi2) <= 1. + 5 * _xi_tolerance) &&
2438  (abs((primary_elem_candidate->point(0) - primary_elem_candidate->point(1)).unit() *
2439  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  if (abs(abs(xi2) - 1.) <= _xi_tolerance * 5.0)
2447  {
2448  const Node * primary_node = (xi2 < 0) ? primary_elem_candidate->node_ptr(0)
2449  : primary_elem_candidate->node_ptr(1);
2450  const bool created_mortar_segment =
2451  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  if (!created_mortar_segment)
2460  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  for (MooseIndex(secondary_node_neighbors) nn = 0;
2466  nn < secondary_node_neighbors.size();
2467  ++nn)
2468  {
2469  const Elem * neigh = secondary_node_neighbors[nn];
2470  for (MooseIndex(neigh->n_vertices()) nid = 0; nid < neigh->n_vertices(); ++nid)
2471  {
2472  const Node * neigh_node = neigh->node_ptr(nid);
2473  if (secondary_node == neigh_node)
2474  {
2475  auto key = std::make_pair(neigh_node, neigh);
2476  auto val = std::make_pair(xi2, primary_elem_candidate);
2478  }
2479  }
2480  }
2481  }
2482 
2483  projection_succeeded = true;
2484  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  rejected_primary_elem_candidates.insert(primary_elem_candidate);
2489  }
2490 
2491  if (projection_succeeded)
2492  break; // out of r-loop
2493  } // r-loop
2494 
2495  if (!projection_succeeded)
2496  {
2497  _failed_secondary_node_projections.insert(secondary_node->id());
2498  if (_debug)
2499  _console << "Failed to find primary Elem into which secondary node "
2500  << static_cast<const Point &>(*secondary_node) << ", id '"
2501  << secondary_node->id() << "', projects onto\n"
2502  << std::endl;
2503  }
2504  else if (_debug)
2505  _projected_secondary_nodes.insert(secondary_node->id());
2506  } // loop over side nodes
2507  } // end loop over lower-dimensional elements
2508 
2509  if (_distributed)
2510  {
2511  if (_debug)
2514  }
2515 
2516  if (_debug)
2517  _console << "\n"
2518  << _projected_secondary_nodes.size() << " out of "
2520  << " secondary nodes were successfully projected\n"
2521  << std::endl;
2522 }
2523 
2524 // Inverse map primary nodes onto their corresponding secondary elements for each primary/secondary
2525 // pair.
2526 void
2528 {
2529  // For each primary/secondary boundary id pair, call the
2530  // project_primary_nodes_single_pair() helper function.
2531  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
2532  projectPrimaryNodesSinglePair(pr.first, pr.second);
2533 }
2534 
2535 void
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  NanoflannMeshSubdomainAdaptor<3> mesh_adaptor(_mesh, lower_dimensional_secondary_subdomain_id);
2544  subdomain_kd_tree_t kd_tree(
2545  3, mesh_adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(/*max leaf=*/10));
2546 
2547  // Construct the KD tree for lower-dimensional elements in the volume mesh.
2548  kd_tree.buildIndex();
2549 
2550  std::unordered_set<dof_id_type> primary_nodes_visited;
2551 
2552  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  if (primary_side_elem->subdomain_id() != lower_dimensional_primary_subdomain_id)
2556  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  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  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  _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  std::make_tuple(primary_node->id(), primary_node, primary_node_neighbors[0]);
2576  if (!primary_nodes_visited.insert(primary_node->id()).second ||
2578  continue;
2579 
2580  // Data structure for performing Nanoflann searches.
2581  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  const size_t num_results = 3;
2588 
2589  // Initialize result_set and do the search.
2590  std::vector<size_t> ret_index(num_results);
2591  std::vector<Real> out_dist_sqr(num_results);
2592  nanoflann::KNNResultSet<Real> result_set(num_results);
2593  result_set.init(&ret_index[0], &out_dist_sqr[0]);
2594  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  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  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  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  _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  for (MooseIndex(secondary_elem_candidates) e = 0; e < secondary_elem_candidates.size(); ++e)
2620  {
2621  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  if (rejected_secondary_elem_candidates.count(secondary_elem_candidate))
2625  continue;
2626 
2627  std::vector<Point> nodal_normals(secondary_elem_candidate->n_nodes());
2628  for (const auto n : make_range(secondary_elem_candidate->n_nodes()))
2629  nodal_normals[n] =
2630  _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  DualNumber<Real> xi1_dn{0, 1}; // initial guess
2636  auto && order = secondary_elem_candidate->default_order();
2637  unsigned int current_iterate = 0, max_iterates = 10;
2638 
2639  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  {
2648  for (MooseIndex(secondary_elem_candidate->n_nodes()) n = 0;
2649  n < secondary_elem_candidate->n_nodes();
2650  ++n)
2651  {
2652  const auto phi = Moose::fe_lagrange_1D_shape(order, n, xi1_dn);
2653  x1 += phi * secondary_elem_candidate->point(n);
2654  normals += phi * nodal_normals[n];
2655  }
2656 
2657  const auto u = x1 - (*primary_node);
2658 
2659  const auto F = u(0) * normals(1) - u(1) * normals(0);
2660 
2661  if (abs(F) < _newton_tolerance)
2662  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  Real dxi1 = -F.value() / F.derivatives();
2668 
2669  xi1_dn += dxi1;
2670 
2671  normals = 0;
2672  } while (++current_iterate < max_iterates);
2673 
2674  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  if ((current_iterate < max_iterates) && (abs(xi1) <= 1. + _xi_tolerance) &&
2679  (abs((primary_side_elem->point(0) - primary_side_elem->point(1)).unit() *
2680  MetaPhysicL::raw_value(normals).unit()) <
2682  {
2683  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  const Node & secondary_node = (xi1 < 0) ? secondary_elem_candidate->node_ref(0)
2694  : secondary_elem_candidate->node_ref(1);
2695  bool created_mortar_segment = false;
2696 
2697  // If we have failed to project this secondary node, let's try again now
2698  if (_failed_secondary_node_projections.count(secondary_node.id()))
2699  created_mortar_segment = processAlignedNodes(secondary_node,
2700  *primary_node,
2701  nullptr,
2702  &primary_node_neighbors,
2703  MetaPhysicL::raw_value(normals),
2704  *secondary_elem_candidate,
2705  rejected_secondary_elem_candidates);
2706  else
2707  rejected_secondary_elem_candidates.insert(secondary_elem_candidate);
2708 
2709  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  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  const Elem * neigh = primary_node_neighbors[0];
2726  for (MooseIndex(neigh->n_vertices()) nid = 0; nid < neigh->n_vertices(); ++nid)
2727  {
2728  const Node * neigh_node = neigh->node_ptr(nid);
2729  if (primary_node == neigh_node)
2730  {
2731  auto key = std::make_tuple(neigh_node->id(), neigh_node, neigh);
2732  auto val = std::make_pair(xi1, secondary_elem_candidate);
2734  }
2735  }
2736  }
2737 
2738  projection_succeeded = true;
2739  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  rejected_secondary_elem_candidates.insert(secondary_elem_candidate);
2745  }
2746  } // end e-loop over candidate elems
2747 
2748  if (projection_succeeded)
2749  break; // out of r-loop
2750  } // r-loop
2751 
2752  if (!projection_succeeded && _debug)
2753  {
2754  _console << "\nFailed to find point from which primary node "
2755  << static_cast<const Point &>(*primary_node) << " was projected." << std::endl
2756  << std::endl;
2757  }
2758  } // loop over side nodes
2759  } // end loop over elements for finding where primary points would have projected from.
2760 }
2761 
2762 std::vector<AutomaticMortarGeneration::MortarFilterIter>
2764 {
2765  auto secondary_it = _nodes_to_secondary_elem_map.find(node.id());
2766  if (secondary_it == _nodes_to_secondary_elem_map.end())
2767  return {};
2768 
2769  const auto & secondary_elems = secondary_it->second;
2770  std::vector<MortarFilterIter> ret;
2771  ret.reserve(secondary_elems.size());
2772 
2773  for (const auto i : index_range(secondary_elems))
2774  {
2775  auto * const secondary_elem = secondary_elems[i];
2776  auto msm_it = _secondary_elems_to_mortar_segments.find(secondary_elem->id());
2777  if (msm_it == _secondary_elems_to_mortar_segments.end())
2778  // We may have removed this element key from this map
2779  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  ret.push_back(msm_it);
2788  }
2789 
2790  return ret;
2791 }
virtual T maximum() const
std::set< SubdomainID > _primary_boundary_subdomain_ids
MetaPhysicL::DualNumber< V, D, asd > abs(const MetaPhysicL::DualNumber< V, D, asd > &a)
Definition: EigenADReal.h:50
const Elem * getSecondaryLowerdElemFromSecondaryElem(dof_id_type secondary_elem_id) const
Return lower dimensional secondary element given its interior parent.
T fe_lagrange_2D_shape(const libMesh::ElemType type, const Order order, const unsigned int i, const VectorType< T > &p)
ElemType
std::unique_ptr< FEGenericBase< Real > > build(const unsigned int dim, const FEType &fet)
void allgather(const T &send_data, std::vector< T, A > &recv_data) const
std::unordered_set< const Elem * > _inactive_local_lm_elems
List of inactive lagrange multiplier nodes (for elemental variables)
static const std::string name_param
The name of the parameter that contains the object name.
Definition: MooseBase.h:55
static const std::string app_param
The name of the parameter that contains the MooseApp.
Definition: MooseBase.h:59
auto norm() const
QUAD8
unsigned int get_node_index(const Node *node_ptr) const
void computeInactiveLMNodes()
Get list of secondary nodes that don&#39;t contribute to interaction with any primary element...
std::vector< std::pair< BoundaryID, BoundaryID > > _primary_secondary_boundary_id_pairs
A list of primary/secondary boundary id pairs corresponding to each side of the mortar interface...
const Elem * interior_parent() const
static const std::string type_param
The name of the parameter that contains the object type.
Definition: MooseBase.h:53
MPI_Info info
void clear()
Clears the mortar segment mesh and accompanying data structures.
void dof_indices(const Elem *const elem, std::vector< dof_id_type > &di) const
std::set< SubdomainID > _secondary_boundary_subdomain_ids
The secondary/primary lower-dimensional boundary subdomain ids are the secondary/primary boundary ids...
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application...
Definition: MooseError.h:311
std::unordered_map< dof_id_type, std::set< Elem *, CompareDofObjectsByID > > _secondary_elems_to_mortar_segments
We maintain a mapping from lower-dimensional secondary elements in the original mesh to (sets of) ele...
std::map< unsigned int, unsigned int > getPrimaryIpToLowerElementMap(const Elem &primary_elem, const Elem &primary_elem_ip, const Elem &lower_secondary_elem) const
Compute on-the-fly mapping from primary interior parent nodes to its corresponding lower dimensional ...
auto norm_sq(const T &a)
static constexpr Real TOLERANCE
const bool _periodic
Whether this object will be generating a mortar segment mesh for periodic constraints.
void swap(std::vector< T > &data, const std::size_t idx0, const std::size_t idx1, const libMesh::Parallel::Communicator &comm)
Swap function for serial or distributed vector of data.
Definition: Shuffle.h:495
void addData(const std::string &name, const T &value)
Method for adding data to the output table.
unsigned int which_side_am_i(const Elem *e) const
void outputMortarMesh()
Write the mortar segment mesh to exodus.
Real _newton_tolerance
Newton solve tolerance for node projections.
void mooseWarning(Args &&... args)
Emit a warning message with the given stringified, concatenated args.
Definition: MooseError.h:345
void output() override
Overload this function with the desired output activities.
std::string getOutputFileBase(bool for_non_moose_build_output=false) const
Get the output file base name.
Definition: MooseApp.C:1531
MortarNodalGeometryOutput(const InputParameters &params)
auto raw_value(const Eigen::Map< T > &in)
Definition: EigenADReal.h:100
std::unordered_map< const Elem *, MortarSegmentInfo > _msm_elem_to_info
Map between Elems in the mortar segment mesh and their info structs.
const bool _distributed
Whether the mortar segment mesh is distributed.
Base class for MOOSE-based applications.
Definition: MooseApp.h:109
The main MOOSE class responsible for handling user-defined parameters in almost every MOOSE system...
void buildMortarSegmentMesh()
Builds the mortar segment mesh once the secondary and primary node projections have been completed...
const Parallel::Communicator & comm() const
void msmStatistics()
Prints mortar segment mesh statistics to console (calls computeMsmStatistics internally) ...
void buildCouplingInformation()
build the _mortar_interface_coupling data
std::vector< Point > getNodalNormals(const Elem &secondary_elem) const
Special adaptor that works with subdomains of the Mesh.
The following methods are specializations for using the libMesh::Parallel::packed_range_* routines fo...
static const Real invalid_xi
virtual Real hmax() const
SECOND
Specialization of SubProblem for solving nonlinear equations plus auxiliary equations.
FEProblemBase & feProblem() const
Definition: MooseApp.C:1851
void projectSecondaryNodes()
Project secondary nodes (find xi^(2) values) to the closest points on the primary surface...
auto max(const L &left, const R &right)
TRI3
const std::unordered_map< dof_id_type, std::set< Elem *, CompareDofObjectsByID > > & secondariesToMortarSegments() const
std::unordered_set< dof_id_type > _projected_secondary_nodes
Debugging container for printing information about fraction of successful projections for secondary n...
QUAD4
This class is a container/interface for the objects involved in automatic generation of mortar spaces...
const bool _debug
Whether to print debug output.
MortarSegmentTriangulationMode
Statistics for one primary-secondary subdomain pair.
Based class for output objects.
Definition: Output.h:43
const Elem * primary_elem
void push_parallel_vector_data(const Communicator &comm, MapToVectors &&data, const ActionFunctor &act_on_data)
uint8_t processor_id_type
const bool _triangulate_triangles
Whether already-triangular clipped polygons should still be centroid-subdivided.
std::set< SubdomainID > _secondary_ip_sub_ids
All the secondary interior parent subdomain IDs associated with the mortar mesh.
virtual libMesh::EquationSystems & es()=0
std::optional< dof_id_type > _msm_node_id_start
Cached per-rank starting ID for 3D MSM nodes/elements.
TypeVector< Real > unit() const
void libmesh_ignore(const Args &...)
const dof_id_type n_nodes
void projectPrimaryNodes()
(Inverse) project primary nodes to the points on the secondary surface where they would have come fro...
AutomaticMortarGeneration(MooseApp &app, MeshBase &mesh_in, const std::pair< BoundaryID, BoundaryID > &boundary_key, const std::pair< SubdomainID, SubdomainID > &subdomain_key, bool on_displaced, bool periodic, const bool debug, const bool correct_edge_dropping, const Real minimum_projection_angle, const MortarSegmentTriangulationMode triangulation_mode, const bool triangulate_triangles)
Must be constructed with a reference to the Mesh we are generating mortar spaces for.
CTSub CT_OPERATOR_BINARY CTMul CTCompareLess CTCompareGreater CTCompareEqual _arg template cos(_arg) *_arg.template D< dtag >()) CT_SIMPLE_UNARY_FUNCTION(cos
const Node & node_ref(const unsigned int i) const
dof_id_type id() const
virtual unsigned int n_nodes() const=0
void buildNodeToElemMaps()
Once the secondary_requested_boundary_ids and primary_requested_boundary_ids containers have been fil...
std::unordered_map< dof_id_type, const Elem * > _secondary_element_to_secondary_lowerd_element
Map from full dimensional secondary element id to lower dimensional secondary element.
virtual void write_equation_systems(const std::string &fname, const EquationSystems &es, const std::set< std::string > *system_names=nullptr) override
TRI6
std::map< std::tuple< dof_id_type, const Node *, const Elem * >, std::pair< Real, const Elem * > > _primary_node_and_elem_to_xi1_secondary_elem
Same type of container, but for mapping (Primary Node ID, Primary Node, Primary Elem) -> (xi^(1)...
void projectPrimaryNodesSinglePair(SubdomainID lower_dimensional_primary_subdomain_id, SubdomainID lower_dimensional_secondary_subdomain_id)
Helper function used internally by AutomaticMortarGeneration::project_primary_nodes().
std::unordered_map< dof_id_type, std::vector< const Elem * > > _nodes_to_primary_elem_map
SimpleRange< IndexType > as_range(const std::pair< IndexType, IndexType > &p)
const bool _correct_edge_dropping
Flag to enable regressed treatment of edge dropping where all LM DoFs on edge dropping element are st...
bool processAlignedNodes(const Node &secondary_node, const Node &primary_node, const std::vector< const Elem *> *secondary_node_neighbors, const std::vector< const Elem *> *primary_node_neighbors, const VectorValue< Real > &nodal_normal, const Elem &candidate_element, std::set< const Elem *> &rejected_element_candidates)
Process aligned nodes.
An inteface for the _console for outputting to the Console object.
std::array< MooseUtils::SemidynamicVector< Point, 9 >, 2 > getNodalTangents(const Elem &secondary_elem) const
Compute the two nodal tangents, which are built on-the-fly.
std::unique_ptr< InputParameters > _output_params
Storage for the input parameters used by the mortar nodal geometry output.
std::set< BoundaryID > _primary_requested_boundary_ids
The boundary ids corresponding to all the primary surfaces.
std::map< unsigned int, unsigned int > getSecondaryIpToLowerElementMap(const Elem &lower_secondary_elem) const
Compute on-the-fly mapping from secondary interior parent nodes to lower dimensional nodes...
TypeVector< typename CompareTypes< Real, T2 >::supertype > cross(const TypeVector< T2 > &v) const
nanoflann::KDTreeSingleIndexAdaptor< subdomain_adatper_t, NanoflannMeshSubdomainAdaptor< 3 >, 3 > subdomain_kd_tree_t
This class is used for building, formatting, and outputting tables of numbers.
Real volume(const MeshBase &mesh, unsigned int dim=libMesh::invalid_uint)
void computeNodalGeometry()
Computes and stores the nodal normal/tangent vectors in a local data structure instead of using the E...
Real _xi_tolerance
Tolerance for checking projection xi values.
static InputParameters validParams()
void computeIncorrectEdgeDroppingInactiveLMNodes()
Computes inactive secondary nodes when incorrect edge dropping behavior is enabled (any node touching...
std::unordered_set< dof_id_type > _failed_secondary_node_projections
Secondary nodes that failed to project.
void buildMortarSegmentMesh3d()
Builds the mortar segment mesh once the secondary and primary node projections have been completed...
const Elem * secondary_elem
void projectSecondaryNodesSinglePair(SubdomainID lower_dimensional_primary_subdomain_id, SubdomainID lower_dimensional_secondary_subdomain_id)
Helper function responsible for projecting secondary nodes onto primary elements for a single primary...
const MortarSegmentTriangulationMode _triangulation_mode
Triangulation mode used for clipped 3D mortar polygons.
Provides a way for users to bail out of the current solve.
void printTable(std::ostream &out, unsigned int last_n_entries=0)
Methods for dumping the table to the stream - either by filename or by stream handle.
void set_unique_id(unique_id_type new_id)
virtual unsigned int n_vertices() const=0
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
std::unordered_map< std::pair< const Node *, const Elem * >, std::pair< Real, const Elem * > > _secondary_node_and_elem_to_xi2_primary_elem
Similar to the map above, but associates a (Secondary Node, Secondary Elem) pair to a (xi^(2)...
Holds xi^(1), xi^(2), and other data for a given mortar segment.
Generic class for solving transient nonlinear problems.
Definition: SubProblem.h:78
virtual SimpleRange< element_iterator > active_local_subdomain_elements_ptr_range(subdomain_id_type sid)=0
subdomain_id_type subdomain_id() const
const bool _on_displaced
Whether this object is on the displaced mesh.
CTSub CT_OPERATOR_BINARY CTMul CTCompareLess CTCompareGreater CTCompareEqual _arg template * sqrt(_arg)) *_arg.template D< dtag >()) CT_SIMPLE_UNARY_FUNCTION(tanh
const Node * node_ptr(const unsigned int i) const
unsigned int spatial_dimension() const
virtual void write(const std::string &fname) override
std::vector< MsmSubdomainStats > computeMsmStatistics()
Computes mortar segment mesh statistics and returns one entry per subdomain pair. ...
void initOutput()
initialize mortar-mesh based output
void addOutput(std::shared_ptr< Output > output)
Adds an existing output object to the warehouse.
virtual Real volume() const
std::unordered_map< const Node *, std::array< Point, 2 > > _secondary_node_to_hh_nodal_tangents
Container for storing the nodal tangent/binormal vectors associated with each secondary node (Househo...
IntRange< T > make_range(T beg, T end)
KOKKOS_INLINE_FUNCTION T sign(T x)
Returns the sign of a value.
Definition: KokkosUtils.h:26
TRI7
unsigned int mesh_dimension() const
unsigned int level ElemType type std::set< subdomain_id_type > ss processor_id_type pid unsigned int level std::set< subdomain_id_type > virtual ss SimpleRange< element_iterator > active_subdomain_elements_ptr_range(subdomain_id_type sid)=0
virtual T minimum() const
T fe_lagrange_1D_shape(const Order order, const unsigned int i, const T &xi)
const Real _minimum_projection_angle
Parameter to control which angle (in degrees) is admissible for the creation of mortar segments...
QUAD9
MeshBase & _mesh
Reference to the mesh stored in equation_systems.
virtual const Point & point(const dof_id_type i) const=0
std::vector< std::pair< SubdomainID, SubdomainID > > _primary_secondary_subdomain_id_pairs
A list of primary/secondary subdomain id pairs corresponding to each side of the mortar interface...
const ConsoleStream _console
An instance of helper class to write streams to the Console objects.
SimpleRange< NeighborPtrIter > neighbor_ptr_range()
void addRow(Real time)
Force a new row in the table with the passed in time.
virtual unsigned int n_sub_elem() const=0
std::set< BoundaryID > _secondary_requested_boundary_ids
The boundary ids corresponding to all the secondary surfaces.
std::unordered_map< const Node *, Point > _secondary_node_to_nodal_normal
Container for storing the nodal normal vector associated with each secondary node.
virtual const Node * node_ptr(const dof_id_type i) const=0
std::unordered_set< const Node * > _inactive_local_lm_nodes
processor_id_type processor_id() const
virtual Order default_order() const=0
std::unordered_map< dof_id_type, std::unordered_set< dof_id_type > > _mortar_interface_coupling
Used by the AugmentSparsityOnInterface functor to determine whether a given Elem is coupled to any ot...
std::set< SubdomainID > _primary_ip_sub_ids
All the primary interior parent subdomain IDs associated with the mortar mesh.
SearchParams SearchParameters
AutomaticMortarGeneration & _amg
The mortar generation object that we will query for nodal normal and tangent information.
void set_hdf5_writing(bool write_hdf5)
processor_id_type processor_id() const
void householderOrthogolization(const Point &normal, Point &tangent_one, Point &tangent_two) const
Householder orthogonalization procedure to obtain proper basis for tangent and binormal vectors...
virtual ElemType type() const=0
dof_id_type node_id(const unsigned int i) const
std::vector< Point > getNormals(const Elem &secondary_elem, const std::vector< Point > &xi1_pts) const
Compute the normals at given reference points on a secondary element.
static InputParameters validParams()
Definition: Output.C:32
MooseApp & _app
The Moose app.
const Point & point(const unsigned int i) const
std::unique_ptr< MeshBase > _mortar_segment_mesh
1D Mesh of mortar segment elements which gets built by the call to build_mortar_segment_mesh().
auto index_range(const T &sizable)
void set_extra_integer(const unsigned int index, const dof_id_type value)
OutputWarehouse & getOutputWarehouse()
Get the OutputWarehouse objects.
Definition: MooseApp.C:2408
uint8_t dof_id_type
std::unordered_map< dof_id_type, std::vector< const Elem * > > _nodes_to_secondary_elem_map
Map from nodes to connected lower-dimensional elements on the secondary/primary subdomains.
std::unordered_map< const Elem *, unsigned int > _lower_elem_to_side_id
Keeps track of the mapping between lower-dimensional elements and the side_id of the interior_parent ...
const Real pi
void computeInactiveLMElems()
Get list of secondary elems without any corresponding primary elements.
void set_union(T &data, const unsigned int root_id) const